> ## Documentation Index
> Fetch the complete documentation index at: https://code.storage/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage Product Files

> Store product files in a managed repository, validate each complete change, and apply one exact revision.

Use Code Storage as the version control system for a product's files. Your application uses the SDK
to store each change as a commit, to read one exact version, and to show history. A user edits the
files through your product interface and never uses Git. Your application validates the complete
file set, saves it, applies that exact version, and records it as the active revision. The active
revision is the exact commit that the product currently uses.

## Provision the managed repository

For each product, create one managed repository in Code Storage. Save the repository name on your
product record, so a later request finds the right repository. The examples use `products` and
`productFiles` for services that you own. This code gives their interfaces:

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
// A file set maps each file path to its contents.
type FileSet = Record<string, string>;

interface Product {
  id: string;
  name: string;
  repoName: string;
  activeRevision: string;
  headRevision: string;
  files: FileSet;
}

// Your product records.
interface ProductStore {
  create(fields: { name: string }): Promise<Product>;
  find(id: string): Promise<Product>;
  update(id: string, fields: Partial<Product>): Promise<void>;
}

// Your service that builds, validates, and applies the files of a product.
interface ProductFileService {
  buildInitial(productId: string): Promise<FileSet>;
  buildCandidate(current: Product, edit: unknown): Promise<FileSet>;
  deletedPaths(currentFiles: FileSet, candidateFiles: FileSet): string[];
  apply(archive: ReadableStream<Uint8Array>, revision: string): Promise<void>;
}

declare const products: ProductStore;
declare const productFiles: ProductFileService;
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from dataclasses import dataclass
from typing import Protocol

# A file set maps each file path to its contents.
FileSet = dict[str, str]


@dataclass
class Product:
    id: str
    name: str
    repo_name: str
    head_revision: str
    files: FileSet


class ProductStore(Protocol):
    """Your product records."""

    def create(self, name: str) -> Product: ...

    def find(self, id: str) -> Product: ...

    def update(self, id: str, fields: dict[str, str]) -> None: ...


class ProductFileService(Protocol):
    """Your service that builds, validates, and applies the files of a product."""

    def build_initial(self, product_id: str) -> FileSet: ...

    def build_candidate(self, current: Product, edit: object) -> FileSet: ...

    def deleted_paths(self, current_files: FileSet, candidate_files: FileSet) -> list[str]: ...

    def apply(self, archive: bytes, revision: str) -> None: ...


products: ProductStore
product_files: ProductFileService
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import (
	"context"
	"io"
)

type FileSet map[string]string // each file path and its contents

type Product struct {
	ID           string
	Name         string
	RepoName     string
	HeadRevision string
	Files        FileSet
}

type ProductUpdate struct {
	RepoName       string
	ActiveRevision string
	HeadRevision   string
}

// ProductStore holds your product records.
type ProductStore interface {
	Create(ctx context.Context, name string) (Product, error)
	Find(ctx context.Context, id string) (Product, error)
	Update(ctx context.Context, id string, update ProductUpdate) error
}

// ProductFileService builds, validates, and applies the files of a product.
type ProductFileService interface {
	BuildInitial(ctx context.Context, productID string) (FileSet, error)
	BuildCandidate(ctx context.Context, current Product, edit any) (FileSet, error)
	DeletedPaths(currentFiles, candidateFiles FileSet) []string
	Apply(ctx context.Context, archive io.Reader, revision string) error
}

var (
	products     ProductStore
	productFiles ProductFileService
)
```

Set `PIERRE_ORG` and `PIERRE_PRIVATE_KEY` as shown in the
[Quick Start](https://code.storage/docs/getting-started/quick-start.md).

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { Pierre } from '@pierre/sdk';

const pierre = new Pierre(); // reads PIERRE_ORG and PIERRE_PRIVATE_KEY

const product = await products.create({ name: 'Support agent' });
const repo = await pierre.repos.create({ repo_name: `products/${product.id}` });

await products.update(product.id, { repoName: repo.repo_name });
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pierre import Pierre

pierre = Pierre()  # reads PIERRE_ORG and PIERRE_PRIVATE_KEY

product = products.create(name="Support agent")
repo = pierre.repos.create(repo_name=f"products/{product.id}")

products.update(product.id, {"repo_name": repo["repo_name"]})
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import (
	"context"
	"fmt"

	pierre "github.com/pierrecomputer/sdk/go"
)

ctx := context.Background()
client, err := pierre.NewClient() // reads PIERRE_ORG and PIERRE_PRIVATE_KEY
if err != nil {
	return err
}

product, err := products.Create(ctx, "Support agent")
if err != nil {
	return err
}
repo, err := client.Repos.Create(ctx, pierre.ReposCreateParams{
	RepoName: pierre.String(fmt.Sprintf("products/%s", product.ID)),
})
if err != nil {
	return err
}

if err := products.Update(ctx, product.ID, ProductUpdate{RepoName: repo.RepoName}); err != nil {
	return err
}
```

## Create the first active revision

Build and validate the complete file set before you save it. Save the files as one commit. Apply
that exact commit, then record it as the active revision.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const initialFiles = await productFiles.buildInitial(product.id);

const committed = await pierre.repos.commits.create({
  repo_name: repo.repo_name,
  author: { name: 'Product Service', email: 'product-service@example.com' },
  commit_message: 'Create product definition',
  files: Object.entries(initialFiles).map(([path, contents]) => ({ path, content: contents })),
  target_branch: 'main',
});
const archive = await pierre.repos.archive.get({
  repo_name: repo.repo_name,
  ref: committed.commit.commit_sha,
});

await productFiles.apply(archive.body!, committed.commit.commit_sha);
await products.update(product.id, {
  activeRevision: committed.commit.commit_sha,
  headRevision: committed.commit.commit_sha,
});
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
initial_files = product_files.build_initial(product.id)

committed = pierre.repos.commits.create(
    repo_name=repo["repo_name"],
    author={"name": "Product Service", "email": "product-service@example.com"},
    commit_message="Create product definition",
    files=[{"path": path, "content": contents} for path, contents in initial_files.items()],
    target_branch="main",
)
archive = pierre.repos.archive.get(
    repo_name=repo["repo_name"],
    ref=committed["commit"]["commit_sha"],
)
archive_bytes = archive.read()
archive.close()

product_files.apply(archive_bytes, committed["commit"]["commit_sha"])
products.update(product.id, {
    "active_revision": committed["commit"]["commit_sha"],
    "head_revision": committed["commit"]["commit_sha"],
})
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import "strings"

initialFiles, err := productFiles.BuildInitial(ctx, product.ID)
if err != nil {
	return err
}

var files []pierre.CommitFile
for path, contents := range initialFiles {
	files = append(files, pierre.CommitFile{Path: path, Content: strings.NewReader(contents)})
}

committed, err := client.Repos.Commits.Create(ctx, pierre.ReposCommitsCreateParams{
	RepoName:      repo.RepoName,
	Author:        pierre.GitIdentity{Name: "Product Service", Email: "product-service@example.com"},
	CommitMessage: "Create product definition",
	Files:         files,
	TargetBranch:  "main",
})
if err != nil {
	return err
}
archive, err := client.Repos.Archive.Get(ctx, pierre.ReposArchiveGetParams{
	RepoName: repo.RepoName,
	Ref:      pierre.String(committed.Commit.CommitSHA),
})
if err != nil {
	return err
}
defer archive.Close()

if err := productFiles.Apply(ctx, archive, committed.Commit.CommitSHA); err != nil {
	return err
}
if err := products.Update(ctx, product.ID, ProductUpdate{
	ActiveRevision: committed.Commit.CommitSHA,
	HeadRevision:   committed.Commit.CommitSHA,
}); err != nil {
	return err
}
```

Update `activeRevision` only after the apply step succeeds.

## Validate and save a user edit

Build the complete file set that would result from the edit. Validate all files before you save any
of them. If validation fails, return the errors and keep the active revision unchanged.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import {
  RefGuardError,
  RefUpdateError,
  type CommitFile,
  type CommitWriteResponse,
  type GitIdentity,
} from '@pierre/sdk';

async function saveEdit(productId: string, submittedEdit: unknown, editor: GitIdentity) {
  const current = await products.find(productId);
  const candidateFiles = await productFiles.buildCandidate(current, submittedEdit);

  const files: CommitFile[] = [];

  for (const [path, contents] of Object.entries(candidateFiles)) {
    files.push({ path, content: contents });
  }

  for (const path of productFiles.deletedPaths(current.files, candidateFiles)) {
    files.push({ path, operation: 'delete' });
  }

  let committed: CommitWriteResponse;

  try {
    committed = await pierre.repos.commits.create({
      repo_name: current.repoName,
      author: editor,
      commit_message: `Update ${current.name}`,
      expected_target_sha: current.headRevision,
      files,
      target_branch: 'main',
    });
  } catch (error) {
    if (error instanceof RefGuardError) {
      return { ok: false, reason: 'conflict' };
    }
    if (error instanceof RefUpdateError && error.result.status === 'precondition_failed') {
      return { ok: true, reason: 'no_changes' };
    }

    throw error;
  }

  return { ok: true, revision: committed.commit.commit_sha };
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pierre import RefGuardError, RefUpdateError
from pierre.types import CommitFile, GitIdentity


def save_edit(product_id: str, submitted_edit: object, editor: GitIdentity) -> dict[str, object]:
    current = products.find(product_id)
    candidate_files = product_files.build_candidate(current, submitted_edit)

    files: list[CommitFile] = []

    for path, contents in candidate_files.items():
        files.append({"path": path, "content": contents})

    for path in product_files.deleted_paths(current.files, candidate_files):
        files.append({"path": path, "operation": "delete"})

    try:
        committed = pierre.repos.commits.create(
            repo_name=current.repo_name,
            author=editor,
            commit_message=f"Update {current.name}",
            expected_target_sha=current.head_revision,
            files=files,
            target_branch="main",
        )
    except RefGuardError:
        return {"ok": False, "reason": "conflict"}
    except RefUpdateError as error:
        if error.result["status"] == "precondition_failed":
            return {"ok": True, "reason": "no_changes"}
        raise

    return {"ok": True, "revision": committed["commit"]["commit_sha"]}
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import "errors"

type EditResult struct {
	OK       bool
	Reason   string
	Revision string
}

func saveEdit(
	ctx context.Context,
	client *pierre.Client,
	productID string,
	submittedEdit any,
	editor pierre.GitIdentity,
) (EditResult, error) {
	current, err := products.Find(ctx, productID)
	if err != nil {
		return EditResult{}, err
	}
	candidateFiles, err := productFiles.BuildCandidate(ctx, current, submittedEdit)
	if err != nil {
		return EditResult{}, err
	}

	var files []pierre.CommitFile
	for path, contents := range candidateFiles {
		files = append(files, pierre.CommitFile{Path: path, Content: strings.NewReader(contents)})
	}
	for _, path := range productFiles.DeletedPaths(current.Files, candidateFiles) {
		files = append(files, pierre.CommitFile{Path: path, Operation: "delete"})
	}

	committed, err := client.Repos.Commits.Create(ctx, pierre.ReposCommitsCreateParams{
		RepoName:          current.RepoName,
		Author:            editor,
		CommitMessage:     fmt.Sprintf("Update %s", current.Name),
		ExpectedTargetSHA: pierre.String(current.HeadRevision),
		Files:             files,
		TargetBranch:      "main",
	})
	if err != nil {
		var guardErr *pierre.RefGuardError
		if errors.As(err, &guardErr) {
			return EditResult{OK: false, Reason: "conflict"}, nil
		}
		var refErr *pierre.RefUpdateError
		if errors.As(err, &refErr) {
			if refErr.Result.Status == pierre.RefUpdateResultStatusPreconditionFailed {
				return EditResult{OK: true, Reason: "no_changes"}, nil
			}
		}
		return EditResult{}, err
	}

	return EditResult{OK: true, Revision: committed.Commit.CommitSHA}, nil
}
```

A conflict means that another editor saved first. Reload the current files and ask the editor to
resolve the conflict. Do not save the old file set again without review.

An identical candidate file set returns `precondition_failed`; treat it as success with no new
revision.

After a timeout, the commit can still exist. Do not resend the same request. Read the current head
again. Rebuild the candidate file set from the current product state. Send a new commit with the new
head as `expected_target_sha`.

## Apply the saved version

After a successful save, read the complete file set by its commit SHA. Apply only those files. Mark
the commit as active only after the apply step succeeds.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
async function applySavedVersion(product: Product, revision: string) {
  const archive = await pierre.repos.archive.get({ repo_name: product.repoName, ref: revision });

  try {
    await productFiles.apply(archive.body!, revision);
    await products.update(product.id, {
      activeRevision: revision,
      headRevision: revision,
    });
  } catch (error) {
    await products.update(product.id, { headRevision: revision });
    throw error;
  }
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
def apply_saved_version(product: Product, revision: str) -> None:
    archive = pierre.repos.archive.get(repo_name=product.repo_name, ref=revision)

    try:
        archive_bytes = archive.read()
        product_files.apply(archive_bytes, revision)
        products.update(product.id, {
            "active_revision": revision,
            "head_revision": revision,
        })
    except Exception:
        products.update(product.id, {"head_revision": revision})
        raise
    finally:
        archive.close()
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
func applySavedVersion(
	ctx context.Context,
	client *pierre.Client,
	product Product,
	revision string,
) error {
	archive, err := client.Repos.Archive.Get(ctx, pierre.ReposArchiveGetParams{
		RepoName: product.RepoName,
		Ref:      pierre.String(revision),
	})
	if err != nil {
		return err
	}
	defer archive.Close()

	if err := productFiles.Apply(ctx, archive, revision); err != nil {
		if updateErr := products.Update(ctx, product.ID, ProductUpdate{
			HeadRevision: revision,
		}); updateErr != nil {
			return updateErr
		}
		return err
	}

	return products.Update(ctx, product.ID, ProductUpdate{
		ActiveRevision: revision,
		HeadRevision:   revision,
	})
}
```

Do not apply files that you have not saved. If the apply step fails, keep the prior active revision.
Fix the apply service and retry the same commit SHA.

## Show history and undo a change

Keep repository details behind your product interface. Use
[`repos.commits.list`](https://code.storage/docs/api/repos/commits-list) and [`repos.diff.get`](https://code.storage/docs/api/repos/diff-get) to show
who changed the product and what changed.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const history = await pierre.repos.commits.list({
  repo_name: repo.repo_name,
  ref: 'main',
  limit: 20,
});
const selected = history.data[0];
const diff = await pierre.repos.diff.get({ repo_name: repo.repo_name, ref: selected.sha });

console.log(selected.author_name, diff.stats);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
history = pierre.repos.commits.list(repo_name=repo["repo_name"], ref="main", limit=20)
selected = history.data[0]
diff = pierre.repos.diff.get(repo_name=repo["repo_name"], ref=selected["sha"])

print(selected["author_name"], diff["stats"])
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
history, err := client.Repos.Commits.List(ctx, pierre.ReposCommitsListParams{
	RepoName: repo.RepoName,
	Ref:      pierre.String("main"),
	Limit:    pierre.Int(20),
})
if err != nil {
	return err
}
selected := history.Data[0]
diff, err := client.Repos.Diff.Get(ctx, pierre.ReposDiffGetParams{
	RepoName: repo.RepoName,
	Ref:      pierre.String(selected.SHA),
})
if err != nil {
	return err
}

fmt.Println(selected.AuthorName, diff.Stats)
```

Use [`repos.commits.restore`](https://code.storage/docs/api/repos/commit-restore) to create a new commit from an earlier
version. Apply and record the new commit, not the old commit that supplied its files.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
async function undoChange(productId: string, selectedSafeRevision: string, editor: GitIdentity) {
  const current = await products.find(productId);

  const restored = await pierre.repos.commits.restore({
    repo_name: current.repoName,
    author: editor,
    base_ref: selectedSafeRevision,
    commit_message: `Restore ${selectedSafeRevision.slice(0, 7)}`,
    expected_target_sha: current.headRevision,
    target_branch: 'main',
  });

  await applySavedVersion(current, restored.commit.commit_sha);
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
def undo_change(product_id: str, selected_safe_revision: str, editor: GitIdentity) -> None:
    current = products.find(product_id)

    restored = pierre.repos.commits.restore(
        repo_name=current.repo_name,
        author=editor,
        base_ref=selected_safe_revision,
        commit_message=f"Restore {selected_safe_revision[:7]}",
        expected_target_sha=current.head_revision,
        target_branch="main",
    )

    apply_saved_version(current, restored["commit"]["commit_sha"])
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
func undoChange(
	ctx context.Context,
	client *pierre.Client,
	productID string,
	selectedSafeRevision string,
	editor pierre.GitIdentity,
) error {
	current, err := products.Find(ctx, productID)
	if err != nil {
		return err
	}

	restored, err := client.Repos.Commits.Restore(ctx, pierre.ReposCommitsRestoreParams{
		RepoName:          current.RepoName,
		Author:            editor,
		BaseRef:           pierre.String(selectedSafeRevision),
		CommitMessage:     pierre.String(fmt.Sprintf("Restore %s", selectedSafeRevision[:7])),
		ExpectedTargetSHA: pierre.String(current.HeadRevision),
		TargetBranch:      "main",
	})
	if err != nil {
		return err
	}

	return applySavedVersion(ctx, client, current, restored.Commit.CommitSHA)
}
```

Use the same failure rule for an undo. If the apply step fails, record the restored commit as the
repository head and keep the prior active revision.

## Limit service access

Keep the organization private key on your server or in a secret manager. Give each product service a
temporary token for one managed repository. Grant only the permissions that the service needs.

Use a separate `repo:write` token when the provisioning service creates a managed repository. See
[Authentication](https://code.storage/docs/platform/authentication.md) for token details.

## Related workflow

Use [Load Agent Memory](https://code.storage/docs/guides/agent-memory.md) when an agent reads and updates selected memory files
without human approval.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.