> ## 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 id on your
product record, so a later request finds the right repository. The examples use `products` and
`productFiles` for services that you own.

<CodeGroup>
  ```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  import { GitStorage } from '@pierre/storage';

  const storage = new GitStorage({
    name: 'your-org',
    key: process.env.PIERRE_PRIVATE_KEY!,
  });

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

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

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

  from pierre_storage import GitStorage

  storage_client = GitStorage({
      "name": "your-org",
      "key": os.environ["PIERRE_PRIVATE_KEY"],
  })

  product = await products.create(name="Support agent")
  repo = await storage_client.create_repo(id=f"products/{product.id}")

  await products.update(product.id, {"repository_id": repo.id})
  ```

  ```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  ctx := context.Background()

  client, err := storage.NewClient(storage.Options{
  	Name: "your-org",
  	Key:  os.Getenv("PIERRE_PRIVATE_KEY"),
  })
  if err != nil {
  	return err
  }

  product, err := products.Create(ctx, "Support agent")
  if err != nil {
  	return err
  }
  repo, err := client.CreateRepo(ctx, storage.CreateRepoOptions{
  	ID: fmt.Sprintf("products/%s", product.ID),
  })
  if err != nil {
  	return err
  }

  err = products.Update(ctx, product.ID, ProductUpdate{RepositoryID: repo.ID})
  ```
</CodeGroup>

## 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.

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

  let change = repo.createCommit({
    targetBranch: 'main',
    commitMessage: 'Create product definition',
    author: { name: 'Product Service', email: 'product-service@example.com' },
  });

  for (const [path, contents] of Object.entries(initialFiles)) {
    change = change.addFileFromString(path, contents);
  }

  const committed = await change.send();
  const archive = await repo.getArchiveStream({ ref: committed.commitSha });

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

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

  change = repo.create_commit(
      target_branch="main",
      commit_message="Create product definition",
      author={"name": "Product Service", "email": "product-service@example.com"},
  )

  for path, contents in initial_files.items():
      change = change.add_file_from_string(path, contents)

  committed = await change.send()
  archive = await repo.get_archive_stream(ref=committed["commit_sha"])
  archive_bytes = await archive.aread()

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

  ```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  initialFiles, err := productFiles.BuildInitial(ctx, product.ID)
  if err != nil {
  	return err
  }

  change, err := repo.CreateCommit(storage.CommitOptions{
  	TargetBranch:  "main",
  	CommitMessage: "Create product definition",
  	Author:        storage.CommitSignature{Name: "Product Service", Email: "product-service@example.com"},
  })
  if err != nil {
  	return err
  }

  for path, contents := range initialFiles {
  	change = change.AddFileFromString(path, contents, nil)
  }

  committed, err := change.Send(ctx)
  if err != nil {
  	return err
  }
  archive, err := repo.ArchiveStream(ctx, storage.ArchiveOptions{Ref: committed.CommitSHA})
  if err != nil {
  	return err
  }
  defer archive.Body.Close()

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

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.

<CodeGroup>
  ```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  import { RefUpdateError } from '@pierre/storage';

  const current = await products.find(product.id);
  const candidateFiles = await productFiles.buildCandidate(current, submittedEdit);

  let change = repo.createCommit({
    targetBranch: 'main',
    expectedHeadSha: current.headRevision,
    commitMessage: `Update ${current.name}`,
    author: { name: editor.name, email: editor.email },
  });

  for (const [path, contents] of Object.entries(candidateFiles)) {
    change = change.addFileFromString(path, contents);
  }

  for (const path of productFiles.deletedPaths(current.files, candidateFiles)) {
    change = change.deletePath(path);
  }

  let committed;

  try {
    committed = await change.send();
  } catch (error) {
    if (error instanceof RefUpdateError) {
      if (error.reason === 'conflict') {
        return { ok: false, reason: 'conflict' };
      }
      if (error.reason === 'precondition_failed') {
        return { ok: true, reason: 'no_changes' };
      }
    }

    throw error;
  }
  ```

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

  current = await products.find(product.id)
  candidate_files = await product_files.build_candidate(current, submitted_edit)

  change = repo.create_commit(
      target_branch="main",
      expected_head_sha=current.head_revision,
      commit_message=f"Update {current.name}",
      author={"name": editor.name, "email": editor.email},
  )

  for path, contents in candidate_files.items():
      change = change.add_file_from_string(path, contents)

  for path in product_files.deleted_paths(current.files, candidate_files):
      change = change.delete_path(path)

  try:
      committed = await change.send()
  except RefUpdateError as error:
      if error.reason == "conflict":
          return {"ok": False, "reason": "conflict"}
      if error.reason == "precondition_failed":
          return {"ok": True, "reason": "no_changes"}
      raise
  ```

  ```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  current, err := products.Find(ctx, product.ID)
  if err != nil {
  	return EditResult{}, err
  }
  candidateFiles, err := productFiles.BuildCandidate(ctx, current, submittedEdit)
  if err != nil {
  	return EditResult{}, err
  }

  change, err := repo.CreateCommit(storage.CommitOptions{
  	TargetBranch:    "main",
  	ExpectedHeadSHA: current.HeadRevision,
  	CommitMessage:   fmt.Sprintf("Update %s", current.Name),
  	Author:          storage.CommitSignature{Name: editor.Name, Email: editor.Email},
  })
  if err != nil {
  	return EditResult{}, err
  }

  for path, contents := range candidateFiles {
  	change = change.AddFileFromString(path, contents, nil)
  }
  for _, path := range productFiles.DeletedPaths(current.Files, candidateFiles) {
  	change = change.DeletePath(path)
  }

  committed, err := change.Send(ctx)
  if err != nil {
  	var refErr *storage.RefUpdateError
  	if errors.As(err, &refErr) {
  		switch refErr.Reason {
  		case storage.RefUpdateReasonConflict:
  			return EditResult{OK: false, Reason: "conflict"}, nil
  		case storage.RefUpdateReasonPreconditionFailed:
  			return EditResult{OK: true, Reason: "no_changes"}, nil
  		}
  	}
  	return EditResult{}, err
  }
  ```
</CodeGroup>

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 `expectedHeadSha`.

## 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.

<CodeGroup>
  ```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  const archive = await repo.getArchiveStream({ ref: committed.commitSha });

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

  ```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  archive = await repo.get_archive_stream(ref=committed["commit_sha"])

  try:
      archive_bytes = await archive.aread()
      await product_files.apply(archive_bytes, committed["commit_sha"])
      await products.update(product.id, {
          "active_revision": committed["commit_sha"],
          "head_revision": committed["commit_sha"],
      })
  except Exception:
      await products.update(product.id, {"head_revision": committed["commit_sha"]})
      raise
  ```

  ```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  archive, err := repo.ArchiveStream(ctx, storage.ArchiveOptions{Ref: committed.CommitSHA})
  if err != nil {
  	return err
  }

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

  err = products.Update(ctx, product.ID, ProductUpdate{
  	ActiveRevision: committed.CommitSHA,
  	HeadRevision:   committed.CommitSHA,
  })
  ```
</CodeGroup>

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 `listCommits` and `getCommitDiff` to show
who changed the product and what changed.

<CodeGroup>
  ```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  const history = await repo.listCommits({ branch: 'main', limit: 20 });
  const selected = history.commits[0];
  const diff = await repo.getCommitDiff({ sha: selected.sha });
  ```

  ```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  history = await repo.list_commits(branch="main", limit=20)
  selected = history["commits"][0]
  diff = await repo.get_commit_diff(sha=selected["sha"])
  ```

  ```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  history, err := repo.ListCommits(ctx, storage.ListCommitsOptions{
  	Branch: "main",
  	Limit:  20,
  })
  if err != nil {
  	return err
  }
  selected := history.Commits[0]
  diff, err := repo.GetCommitDiff(ctx, storage.GetCommitDiffOptions{SHA: selected.SHA})
  ```
</CodeGroup>

Use `restoreCommit` to create a new commit from an earlier version. Apply and record the new commit,
not the old commit that supplied its files.

<CodeGroup>
  ```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  const restored = await repo.restoreCommit({
    targetBranch: 'main',
    expectedHeadSha: current.headRevision,
    targetCommitSha: selectedSafeRevision,
    commitMessage: `Restore ${selectedSafeRevision.slice(0, 7)}`,
    author: { name: editor.name, email: editor.email },
  });

  const archive = await repo.getArchiveStream({ ref: restored.commitSha });

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

  ```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  restored = await repo.restore_commit(
      target_branch="main",
      expected_head_sha=current.head_revision,
      target_commit_sha=selected_safe_revision,
      commit_message=f"Restore {selected_safe_revision[:7]}",
      author={"name": editor.name, "email": editor.email},
  )

  archive = await repo.get_archive_stream(ref=restored["commit_sha"])

  try:
      archive_bytes = await archive.aread()
      await product_files.apply(archive_bytes, restored["commit_sha"])
      await products.update(product.id, {
          "active_revision": restored["commit_sha"],
          "head_revision": restored["commit_sha"],
      })
  except Exception:
      await products.update(product.id, {"head_revision": restored["commit_sha"]})
      raise
  ```

  ```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
  restored, err := repo.RestoreCommit(ctx, storage.RestoreCommitOptions{
  	TargetBranch:    "main",
  	ExpectedHeadSHA: current.HeadRevision,
  	TargetCommitSHA: selectedSafeRevision,
  	CommitMessage:   fmt.Sprintf("Restore %s", selectedSafeRevision[:7]),
  	Author:          storage.CommitSignature{Name: editor.Name, Email: editor.Email},
  })
  if err != nil {
  	return err
  }

  archive, err := repo.ArchiveStream(ctx, storage.ArchiveOptions{Ref: restored.CommitSHA})
  if err != nil {
  	return err
  }
  defer archive.Body.Close()
  if err := productFiles.Apply(ctx, archive.Body, restored.CommitSHA); err != nil {
  	if updateErr := products.Update(ctx, product.ID, ProductUpdate{
  		HeadRevision: restored.CommitSHA,
  	}); updateErr != nil {
  		return updateErr
  	}
  	return err
  }
  err = products.Update(ctx, product.ID, ProductUpdate{
  	ActiveRevision: restored.CommitSHA,
  	HeadRevision:   restored.CommitSHA,
  })
  ```
</CodeGroup>

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 & Security](/docs/getting-started/authentication) for token details.

## Related workflow

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