Skip to main content

Installation & Setup

pnpm i @pierre/storage
Initialize the client:
import { GitStorage } from '@pierre/storage';

const store = new GitStorage({
  name: 'your-org', // Your organization identifier
  key: env.privateKey,
});
Need to generate JWTs yourself? See Authentication & security → Manual JWT generation.

Creating Repositories

// Basic repository creation (defaults to 'main' branch)
const repo = await store.createRepo();

// With custom ID
const repo = await store.createRepo({
  id: 'my-custom-repo',
});

// With custom default branch
const repo = await store.createRepo({
  id: 'my-custom-repo',
  defaultBranch: 'develop',
});

// With namespacing
const repo = await store.createRepo({
  id: 'production/api-service',
  defaultBranch: 'production',
});
Options
ParameterTypeDescription
idOptionalRepository ID. If not provided, a UUID will be auto-generated. Supports namespacing with / (e.g., team/project-alpha).
defaultBranch (TypeScript)
default_branch (Python)
OptionalDefault branch name for the repository. Defaults to main.
baseRepo (TypeScript)
base_repo (Python)
OptionalGitHub repository configuration for sync. Contains owner, name, and optionally default_branch. The provider is automatically set to github.
ttlOptionalToken TTL in seconds for this invocation. Defaults to 3600 (1 hour).
BaseRepo Structure
PropertyTypeDescription
ownerRequiredGitHub repository owner (username or organization).
nameRequiredGitHub repository name.
defaultBranch (TypeScript)
default_branch (Python)
OptionalGitHub repository’s default branch.
When baseRepo is provided, Code Storage links the repository to GitHub for automatic syncing. See the GitHub Sync guide for details.

Finding Repositories

const repo = await store.findOne({ id: 'team/project-alpha' });
if (repo) {
  console.log(`Found repository: ${repo.id}`);
}

Generating Remote URLs

The SDK automatically creates secure Git URLs with embedded JWT authentication:
// Default: read/write access, 1-year TTL
const url = await repo.getRemoteURL();

// Read-only access with 1-hour TTL
const readOnlyUrl = await repo.getRemoteURL({
  permissions: ['git:read'],
  ttl: 3600,
});

// CI/CD pipeline with extended permissions
const ciUrl = await repo.getRemoteURL({
  permissions: ['git:read', 'git:write'],
  ttl: 86400, // 24 hours
});

Error Handling (SDK)

The SDK surfaces API failures as ApiError and ref update failures as RefUpdateError.
import { ApiError, RefUpdateError, GitStorage } from '@pierre/storage';

const store = new GitStorage({ name: 'your-name', key: env.privateKey });

try {
  const repo = await store.createRepo({ id: 'existing' });
  console.log(repo.id);
} catch (err) {
  if (err instanceof ApiError) {
    console.error('API error:', err.message);
    console.error('Status code:', err.statusCode);
  } else {
    throw err;
  }
}

const repo = await store.findOne({ id: 'repo-id' });
const builder = repo?.createCommit({
  targetBranch: 'main',
  commitMessage: 'Update docs',
  author: { name: 'Docs Bot', email: '[email protected]' },
});

try {
  const result = await builder
    ?.addFileFromString('docs/changelog.md', '# v2.0.1\n- add streaming SDK\n')
    .send();
  console.log(result?.commitSha);
} catch (err) {
  if (err instanceof RefUpdateError) {
    console.error('Ref update failed:', err.message);
    console.error('Status:', err.status);
    console.error('Reason:', err.reason);
    console.error('Ref update:', err.refUpdate);
  } else {
    throw err;
  }
}

Repository Methods

Once you have a repository instance from createRepo() or findOne(), you can use these methods:

listFiles()

List all files in the repository at a specific ref:
// List files from default branch
const files = await repo.listFiles();
console.log(files.paths); // Array of file paths

// List files from specific branch or commit
const branchFiles = await repo.listFiles({
  ref: 'feature-branch', // branch name or commit SHA
});
console.log(`Files in ${branchFiles.ref}:`, branchFiles.paths);

// List files from the ephemeral namespace
const ephemeralFiles = await repo.listFiles({
  ref: 'feature/demo-work',
  ephemeral: true,
});
console.log(`Ephemeral files:`, ephemeralFiles.paths);

listBranches()

List all branches in the repository with pagination support:
// List all branches
const branches = await repo.listBranches();
console.log(branches.branches); // Array of branch info

// With pagination
const page = await repo.listBranches({
  limit: 10,
  cursor: branches.nextCursor,
});

listCommits()

List commit history with optional branch filtering:
// List commits from default branch
const commits = await repo.listCommits();

// List commits from specific branch
const branchCommits = await repo.listCommits({
  branch: 'feature/new-auth',
  limit: 20,
});

getBranchDiff()

Get the diff between a branch and its base:
// Compare feature branch to main
const diff = await repo.getBranchDiff({
  branch: 'feature/new-auth',
  base: 'main', // optional, defaults to default branch
});

console.log(`Changed files: ${diff.stats.files}`);
console.log(`+${diff.stats.additions} -${diff.stats.deletions}`);

getCommitDiff()

Get the diff for a specific commit:
// Get commit diff (shows changes from parent)
const commitDiff = await repo.getCommitDiff({
  sha: 'abc123def456...',
});

// Get commit diff compared to a specific base
const customDiff = await repo.getCommitDiff({
  sha: 'abc123def456...',
  baseSha: 'def789abc123...', // optional base commit to compare against
});

// Process file changes
commitDiff.files.forEach((file) => {
  console.log(`${file.state}: ${file.path}`);
  console.log(`Raw status: ${file.rawState}`);
});
Options
ParameterTypeDescription
shaRequiredThe commit SHA to get the diff for
baseSha (TypeScript)
base_sha (Python)
OptionalBase commit SHA to compare against. If not specified, compares against the commit’s parent(s)

createCommit()

Build and push a commit to a branch using the fluent commit builder.
const fs = await import('node:fs/promises');

const result = await repo
  .createCommit({
    targetBranch: 'main',
    commitMessage: 'Update dashboard docs',
    expectedHeadSha: currentHeadSha, // optional safety check
    author: { name: 'Docs Bot', email: '[email protected]' },
  })
  .addFileFromString('docs/changelog.md', '# v2.1.0\n- refresh docs\n')
  .addFile('public/logo.svg', await fs.readFile('assets/logo.svg'))
  .deletePath('docs/legacy.txt')
  .send();

console.log(result.commitSha);
console.log(result.refUpdate.newSha);
console.log(result.refUpdate.oldSha);
If the backend rejects the update (for example, the branch moved past expectedHeadSha), repo.createCommit().send() throws a RefUpdateError containing the status, reason, and ref details. Builder Methods
MethodDescription
addFile(path, source, options)Attach bytes, async iterables, readable streams, or buffers.
addFileFromString(path, contents, options)Add UTF-8 text files.
deletePath(path)Remove files or folders.
send()Finalize the commit and receive metadata about the new commit.
Options
ParameterTypeDescription
targetBranchRequiredBranch name that will receive the commit (for example main).
commitMessageRequiredThe commit message.
authorRequiredProvide name and email for the commit author.
expectedHeadShaOptionalCommit SHA that must match the remote tip; omit to fast-forward unconditionally.
baseBranchOptionalMirrors the base_branch metadata field. Point to an existing branch whose tip should seed targetBranch if it does not exist. When bootstrapping a new branch, omit expectedHeadSha so the service copies from baseBranch; if both fields are provided and the branch already exists, the expectedHeadSha guard still applies.
ephemeralOptionalStore the branch under the refs/namespaces/ephemeral/... namespace. When enabled, the commit is kept out of the primary Git remotes (for example, GitHub) but remains available through storage APIs.
ephemeralBaseOptionalUse alongside baseBranch when the seed branch also lives in the ephemeral namespace. Requires baseBranch to be set.
committerOptionalProvide name and email. If omitted, the author identity is reused.
signalOptionalAbort an in-flight upload with AbortController.
targetRefDeprecated, OptionalFully qualified ref (for example refs/heads/main). Prefer targetBranch.
Ephemeral commits are ideal for short-lived review environments or intermediate build artifacts. They stay isolated from upstream mirrors while still benefiting from the same authentication and storage guarantees.
Files are chunked to 4 MiB segments under the hood, so you can stream large assets without buffering them entirely in memory. File paths are normalized relative to the repository root. The targetBranch must already exist on the remote repository unless you provide baseBranch (or the repository has no refs). To initialize an empty repository, point to its default branch and omit expectedHeadSha. To seed a missing branch inside an existing repo, set baseBranch to the branch you want to copy and omit expectedHeadSha so the service clones that tip before applying your changes.

Streaming Large Files

Use streams or async generators to upload large files without loading them into memory:
import { createReadStream } from 'node:fs';

// Simplest approach: Node.js ReadableStream
await repo
  .createCommit({
    targetBranch: 'assets',
    expectedHeadSha: 'abc123...',
    commitMessage: 'Upload latest design bundle',
    author: { name: 'Assets Uploader', email: '[email protected]' },
  })
  .addFile('assets/design-kit.zip', createReadStream('/tmp/large-file.zip'))
  .send();

// Alternative: Async generator for custom chunking
async function* fileChunks() {
  const fs = await import('node:fs/promises');
  const file = await fs.open('/tmp/large-file.zip', 'r');
  const chunkSize = 1024 * 1024; // 1MB chunks

  try {
    while (true) {
      const buffer = Buffer.alloc(chunkSize);
      const { bytesRead } = await file.read(buffer, 0, chunkSize);
      if (bytesRead === 0) break;
      yield buffer.subarray(0, bytesRead);
    }
  } finally {
    await file.close();
  }
}

await repo
  .createCommit({
    targetBranch: 'assets',
    commitMessage: 'Upload with custom chunking',
    author: { name: 'Assets Uploader', email: '[email protected]' },
  })
  .addFile('assets/design-kit.zip', fileChunks())
  .send();
The SDK automatically chunks files to 4 MiB segments, so you can stream large assets (videos, archives, datasets) without buffering them entirely in memory.

createCommitFromDiff()

Apply a pre-generated Git patch without constructing a builder. This helper wraps the /api/v1/repos/diff-commit endpoint and is designed for workflows that already have git diff --binary output available.
const diff = await fs.readFile('dist/generated.patch', 'utf8');

const result = await repo.createCommitFromDiff({
  targetBranch: 'main',
  expectedHeadSha: currentHeadSha,
  commitMessage: 'Apply generated SDK patch',
  author: { name: 'Diff Bot', email: '[email protected]' },
  committer: { name: 'Diff Bot', email: '[email protected]' },
  diff,
});

console.log(result.commitSha);
console.log(result.refUpdate.newSha);
console.log(result.refUpdate.oldSha);
createCommitFromDiff shares the same branch metadata (expectedHeadSha, baseBranch, ephemeral, ephemeralBase) as createCommit. Instead of calling .addFile(), pass the patch contents through the diff field. The SDK accepts strings, Uint8Array, ArrayBuffer, Blob/File objects, or any iterable/async iterable of byte chunks and handles chunking + base64 encoding. The gateway applies the patch with git apply --cached --binary, so make sure your diff is compatible with that command. It must include file headers (diff --git), mode lines, and hunk headers. Empty patches and patches that do not apply cleanly result in a RefUpdateError with the mapped status ( conflict, precondition_failed, etc.) and partial ref update information.

restoreCommit()

Create a commit rolling a branch back to a certain commit.
const reset = await repo.restoreCommit({
  targetBranch: 'main',
  expectedHeadSha: currentHeadSha, // optional safety check
  targetCommitSha: safeCommitSha,
  commitMessage: `Reset to "${safeCommitMessage}"`,
  author: { name: 'Docs Bot', email: '[email protected]' },
  committer: { name: 'Docs Bot', email: '[email protected]' },
});

console.log('New tip:', reset.refUpdate.newSha);
console.log('Previous tip:', reset.refUpdate.oldSha);
Options
ParameterTypeDescription
targetBranchRequiredBranch name (without refs/heads/) that receives the reset commit.
targetCommitShaRequiredCommit SHA to reset the branch back to. Commits after this point are undone.
authorRequiredProvide name and email for the commit author.
expectedHeadShaOptionalCommit SHA that must match the current tip. Use to avoid races.
commitMessageOptionalCustom message. Defaults to Reset <branch> to "<target subject>".
committerOptionalProvide name and email. If omitted, the author identity is reused.
The method throws a RefUpdateError when the backend rejects the reset (for example, the branch tip changed). On success it returns the commit metadata and the ref update; refUpdate.oldSha is the previous branch tip (000000... when the ref did not exist) and refUpdate.newSha is the reset commit.

createBranch()

Create a new branch from an existing branch, optionally using the ephemeral namespace:
const branch = await repo.createBranch({
  baseBranch: 'main', // source branch
  targetBranch: 'feature/new-onboarding',
  // baseIsEphemeral: true,
  // targetIsEphemeral: true,
});

console.log(branch.targetBranch); // 'feature/new-onboarding'
console.log(branch.commitSha); // tip SHA when available
Toggle baseIsEphemeral/targetIsEphemeral to work with ephemeral refs. The method returns the API message plus the resolved branch metadata so you can confirm creation before pushing commits. Options
ParameterTypeDescription
baseBranchRequiredSource branch to copy from. Combine with baseIsEphemeral when promoting from the ephemeral namespace.
targetBranchRequiredDestination branch name. May match baseBranch when moving between namespaces.
baseIsEphemeralOptionaltrue when the source branch lives in the ephemeral namespace (defaults to false).
targetIsEphemeralOptionaltrue to create/update an ephemeral branch instead of the default namespace.
forceOptionaltrue to overwrite non–fast-forward updates. Use sparingly—most workflows should rely on expectedHeadSha.
See the Ephemeral Branches guide for concrete promotion examples using these flags.

getFileStream()

Retrieve file content as a streaming response (standard Fetch Response):
// Get file from default branch
const resp = await repo.getFileStream({ path: 'README.md' });
const text = await resp.text();
console.log(text);

// Get file from specific branch, tag, or commit
const historicalResp = await repo.getFileStream({
  path: 'package.json',
  ref: 'v1.0.0', // branch name, tag, or commit SHA
});
const historicalText = await historicalResp.text();

// Fetch from the ephemeral namespace
const ephemeralResp = await repo.getFileStream({
  path: 'notes.md',
  ref: 'feature/demo-work',
  ephemeral: true,
});

pullUpstream()

For repos that have been synced with GitHub, you can force a pull with this command. It will throw an error in any situation that doesn’t indicate a successful pull.
console.log(repo.listCommits());

// Re-pull the repo to get the freshest commits
await repo.pullUpstream();

console.log(repo.listCommits());
Note this method is necessary when opting to manage your own GitHub webhook.

Error Handling (SDK)

The SDK surfaces API failures as ApiError and ref update failures as RefUpdateError.
import { ApiError, RefUpdateError, GitStorage } from '@pierre/storage';

const store = new GitStorage({ name: 'your-name', key: env.privateKey });

try {
  const repo = await store.createRepo({ id: 'existing' });
  console.log(repo.id);
} catch (err) {
  if (err instanceof ApiError) {
    console.error('API error:', err.message);
    console.error('Status code:', err.statusCode);
  } else {
    throw err;
  }
}

const repo = await store.findOne({ id: 'repo-id' });
const builder = repo?.createCommit({
  targetBranch: 'main',
  commitMessage: 'Update docs',
  author: { name: 'Docs Bot', email: '[email protected]' },
});

try {
  const result = await builder
    ?.addFileFromString('docs/changelog.md', '# v2.0.1\n- add streaming SDK\n')
    .send();
  console.log(result?.commitSha);
} catch (err) {
  if (err instanceof RefUpdateError) {
    console.error('Ref update failed:', err.message);
    console.error('Status:', err.status);
    console.error('Reason:', err.reason);
    console.error('Ref update:', err.refUpdate);
  } else {
    throw err;
  }
}

Complete Example

import { GitStorage } from '@pierre/storage';

const store = new GitStorage({
  name,
  key: process.env.PIERRE_PRIVATE_KEY,
});

async function setupRepository() {
  // Create repository
  const repo = await store.createRepo({
    id: 'backend/api-service',
  });

  // Generate secure URL
  const url = await repo.getRemoteURL({
    permissions: ['git:read', 'git:write'],
    ttl: 31536000, // 1 year
  });

  // Use with Git
  console.log(`git clone ${url}`);

  return repo;
}