Docs Packages

@procedures/git

packages/client-git/README.md on GitHub · Package map · Procedures

Git operations as RPC procedures - git.add, git.commit, git.push, git.status, and 22 more git commands exposed as type-safe procedures.

Overview#

@procedures/git exposes comprehensive Git functionality as RPC procedures through the client system. It provides 26 procedures covering all major Git operations including repository state management, staging, commits, branching, remote operations, stashing, and repository predicates.

This package wraps Git commands directly (not through client-shell), providing structured input/output with full type safety via Zod schemas.

Architecture#

Diagram source (Mermaid). GitHub draws it on the page of the file.
graph TB
    A[client-git] -->|depends on| B[@procedures/client]
    A -->|validates with| C[zod]

    D[Application] -->|calls| E[git.* procedures]
    E -->|executes| F[Git CLI]
    E -->|parses| G[Structured Output]

    H[Repository State<br/>status, log, diff, branch] -.-> E
    I[Staging & Commits<br/>add, commit, checkout] -.-> E
    J[Remote Sync<br/>push, pull, fetch, clone] -.-> E
    K[Stash Operations<br/>stash.*, export, import] -.-> E
    L[Predicates<br/>hasChanges, isClean, etc] -.-> E

    style A fill:#e1f5ff
    style E fill:#e1ffe1

Procedure Categories#

Diagram source (Mermaid). GitHub draws it on the page of the file.
graph LR
    A[client-git<br/>26 Procedures] --> B[Repository State<br/>5 procedures]
    A --> C[Staging & Commits<br/>3 procedures]
    A --> D[Remote Operations<br/>5 procedures]
    A --> E[Predicates<br/>6 procedures]
    A --> F[Stash Operations<br/>7 procedures]

    B -.-> B1[status, log, diff,<br/>branch, init]
    C -.-> C1[add, commit,<br/>checkout]
    D -.-> D1[push, pull, fetch,<br/>clone, remote]
    E -.-> E1[hasChanges, isClean,<br/>hasStagedChanges,<br/>hasUnstagedChanges,<br/>hasUntrackedFiles,<br/>hasLocalCommits]
    F -.-> F1[stash.list, stash.push,<br/>stash.pop, stash.apply,<br/>stash.drop, stash.export,<br/>stash.import]

    style A fill:#e1f5ff
    style B fill:#ffe1e1
    style C fill:#ffe1e1
    style D fill:#ffe1e1
    style E fill:#fff4e1
    style F fill:#e1ffe1

Installation#

# Using pnpm (recommended)
pnpm add @procedures/git

# Using npm
npm install @procedures/git

# Using yarn
yarn add @procedures/git

Quick Start#

import { Client, LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
import "@procedures/git"; // registers its procedures

const client = new Client(new LocalTransport({ registry: PROCEDURE_REGISTRY }));

// Get repository status
const status = await client.exec(["git", "status"], {
  cwd: "/path/to/repo",
});
console.log(status.branch, status.files);

// Stage and commit
await client.exec(["git", "add"], { all: true });
await client.exec(["git", "commit"], { message: "feat: add new feature" });

// Push to remote
await client.exec(["git", "push"], { remote: "origin" });

API Reference#

Repository State Procedures#

git.status#

Get current repository status including branch, tracking info, and file changes.

Path: ["git", "status"]

interface GitStatusInput {
  /** Working directory (default: process.cwd()) */
  cwd?: string;
  /** Also give `lines` in the format of `git status --short` (default: false) */
  short?: boolean;
}

interface GitStatusFile {
  /** The path (for a rename or a copy, the new path). Paths are exact: no quotes. */
  path: string;
  /** For a rename or a copy, the old path */
  from?: string;
  status: "modified" | "added" | "deleted" | "renamed" | "copied" | "untracked" | "ignored";
  staged: boolean;
}

interface GitStatusOutput {
  branch: string;
  ahead: number;
  behind: number;
  files: GitStatusFile[];
  clean: boolean;
  /** With `short: true`: one line per file, for example "R  old.ts -> new.ts" */
  lines?: string[];
}

Example:

const status = await client.exec(["git", "status"], { cwd: "./my-repo" });
console.log(`On branch ${status.branch}`);
console.log(`${status.ahead} commits ahead, ${status.behind} behind`);
console.log(`${status.files.length} changed files`);

git.log#

Show commit history.

Path: ["git", "log"]

interface GitLogInput {
  /** Number of commits to show (default: 10) */
  count?: number;
  /** Also give `lines`: one line per commit, the short hash and the subject (default: false) */
  oneline?: boolean;
  /** Branch/tag/commit to start from (default: HEAD) */
  ref?: string;
  /** Working directory */
  cwd?: string;
}

interface GitLogCommit {
  hash: string;
  shortHash: string;
  author: string;
  email: string;
  date: string;
  message: string;
}

interface GitLogOutput {
  commits: GitLogCommit[];
  /** With `oneline: true` */
  lines?: string[];
}

Example:

const log = await client.exec(["git", "log"], { count: 5 });
for (const commit of log.commits) {
  console.log(`${commit.shortHash} - ${commit.message}`);
}

git.diff#

Show changes between commits, branches, or working directory.

Path: ["git", "diff"]

interface GitDiffInput {
  /** Show staged changes (default: false, shows unstaged) */
  staged?: boolean;
  /** Compare against ref (branch/commit) */
  ref?: string;
  /** Specific paths to diff */
  paths?: string[];
  /** Show stat summary only (default: false) */
  stat?: boolean;
  /** Working directory */
  cwd?: string;
}

interface GitDiffFile {
  /** The path (for a rename, the new path) */
  path: string;
  /** For a rename, the old path */
  from?: string;
  additions: number;
  deletions: number;
}

interface GitDiffOutput {
  files: GitDiffFile[];
  additions: number;
  deletions: number;
  diff?: string;
}

Example:

// Show unstaged changes
const diff = await client.exec(["git", "diff"], { stat: true });
console.log(`${diff.additions} additions, ${diff.deletions} deletions`);

// Show staged changes
const staged = await client.exec(["git", "diff"], { staged: true });

git.branch#

Branch operations - list, create, delete branches.

Path: ["git", "branch"]

interface GitBranchInput {
  /** Branch name (for create/delete) */
  name?: string;
  /** Delete the branch (default: false) */
  delete?: boolean;
  /** List branches (default: true if no name provided) */
  list?: boolean;
  /** Include remote branches in list (default: false) */
  remote?: boolean;
  /** Working directory */
  cwd?: string;
}

interface GitBranchInfo {
  name: string;
  current: boolean;
  remote: boolean;
  tracking?: string;
}

interface GitBranchOutput {
  branches?: GitBranchInfo[];
  created?: string;
  deleted?: string;
  current: string;
}

Example:

// List all branches
const branches = await client.exec(["git", "branch"], { list: true });
console.log("Branches:", branches.branches.map(b => b.name));

// Create new branch
await client.exec(["git", "branch"], { name: "feature/new-feature" });

// Delete branch
await client.exec(["git", "branch"], { name: "old-feature", delete: true });

git.init#

Initialize a new git repository.

Path: ["git", "init"]

interface GitInitInput {
  /** Directory to initialize (default: process.cwd()) */
  cwd?: string;
  /** Create a bare repository (default: false) */
  bare?: boolean;
  /** Name for the initial branch */
  initialBranch?: string;
}

interface GitInitOutput {
  /** The initialized repository path */
  path: string;
  /** Whether the repo was newly created */
  created: boolean;
}

Staging & Commit Procedures#

git.add#

Stage files for commit.

Path: ["git", "add"]

interface GitAddInput {
  /** Paths to stage (default: []) */
  paths?: string[];
  /** Stage all changes (default: false) */
  all?: boolean;
  /** Working directory */
  cwd?: string;
}

interface GitAddOutput {
  staged: string[];
}

Example:

// Stage specific files
await client.exec(["git", "add"], { paths: ["file1.ts", "file2.ts"] });

// Stage all changes
await client.exec(["git", "add"], { all: true });

git.commit#

Create a commit with staged changes.

Path: ["git", "commit"]

interface GitCommitInput {
  /** Commit message */
  message: string;
  /** Automatically stage modified files (default: false) */
  all?: boolean;
  /** Amend the last commit (default: false) */
  amend?: boolean;
  /** Working directory */
  cwd?: string;
}

interface GitCommitOutput {
  hash: string;
  message: string;
  author: string;
  date: string;
  /** True if commit was skipped (nothing to commit) */
  skipped?: boolean;
}

Example:

const commit = await client.exec(["git", "commit"], {
  message: "feat: add new feature\n\nDetailed description here",
  all: true, // Auto-stage modified files
});
console.log(`Created commit ${commit.hash}`);

git.checkout#

Checkout branch, tag, or restore files.

Path: ["git", "checkout"]

interface GitCheckoutInput {
  /** Branch, tag, or commit to checkout */
  ref: string;
  /** Create branch if it doesn't exist (default: false) */
  create?: boolean;
  /** Specific paths to checkout (default: all) */
  paths?: string[];
  /** Working directory */
  cwd?: string;
}

interface GitCheckoutOutput {
  ref: string;
  created: boolean;
}

Example:

// Checkout existing branch
await client.exec(["git", "checkout"], { ref: "main" });

// Create and checkout new branch
await client.exec(["git", "checkout"], { ref: "feature/new", create: true });

// Restore specific file
await client.exec(["git", "checkout"], { ref: "HEAD", paths: ["file.ts"] });

Remote Operations#

git.push#

Push commits to remote repository.

Path: ["git", "push"]

interface GitPushInput {
  /** Remote name (default: origin) */
  remote?: string;
  /** Branch to push (default: current branch) */
  branch?: string;
  /** Force push (default: false) */
  force?: boolean;
  /** Set upstream tracking (default: false) */
  setUpstream?: boolean;
  /** Working directory */
  cwd?: string;
}

interface GitPushOutput {
  remote: string;
  branch: string;
  commits: number;
}

Example:

// Push to origin
await client.exec(["git", "push"], {});

// Push and set upstream
await client.exec(["git", "push"], {
  remote: "origin",
  branch: "feature/new",
  setUpstream: true,
});

// Force push (use with caution!)
await client.exec(["git", "push"], { force: true });

git.pull#

Pull changes from remote repository.

Path: ["git", "pull"]

interface GitPullInput {
  /** Remote name (default: origin) */
  remote?: string;
  /** Branch to pull (default: current branch) */
  branch?: string;
  /** Rebase instead of merge (default: false) */
  rebase?: boolean;
  /** Working directory */
  cwd?: string;
}

interface GitPullOutput {
  remote: string;
  branch: string;
  commits: number;
  fastForward: boolean;
}

git.fetch#

Fetch from remote without merging.

Path: ["git", "fetch"]

interface GitFetchInput {
  /** Remote name (default: origin) */
  remote?: string;
  /** Specific branch to fetch (default: all) */
  branch?: string;
  /** Fetch all remotes (default: false) */
  all?: boolean;
  /** Prune deleted branches (default: false) */
  prune?: boolean;
  /** Working directory */
  cwd?: string;
}

interface GitFetchOutput {
  remote: string;
  fetched: boolean;
}

git.clone#

Clone a repository.

Path: ["git", "clone"]

interface GitCloneInput {
  /** Repository URL */
  url: string;
  /** Destination directory (default: derived from URL) */
  dest?: string;
  /** Branch to clone (default: default branch) */
  branch?: string;
  /** Create shallow clone with depth (default: full clone) */
  depth?: number;
  /** Working directory */
  cwd?: string;
}

interface GitCloneOutput {
  path: string;
  branch: string;
}

Example:

const clone = await client.exec(["git", "clone"], {
  url: "https://github.com/user/repo.git",
  dest: "./my-repo",
  depth: 1, // Shallow clone
});
console.log(`Cloned to ${clone.path}`);

git.remote#

Get or set remote URLs.

Path: ["git", "remote"]

interface GitRemoteInput {
  /** Remote name (default: origin) */
  name?: string;
  /** New URL to set (if provided, sets the URL; otherwise gets it) */
  url?: string;
  /** Working directory */
  cwd?: string;
}

interface GitRemoteOutput {
  /** Remote name */
  name: string;
  /** Remote URL */
  url: string;
}

Predicate Procedures#

These procedures return boolean predicates useful for conditional logic.

git.hasChanges#

Check if there are any changes (unstaged, staged, or untracked).

Path: ["git", "hasChanges"]

interface GitPredicateInput {
  cwd?: string;
}

interface GitPredicateOutput {
  value: boolean;
}

git.hasStagedChanges#

Check if there are staged changes ready to commit.

Path: ["git", "hasStagedChanges"]

git.hasUnstagedChanges#

Check if there are unstaged changes.

Path: ["git", "hasUnstagedChanges"]

git.hasUntrackedFiles#

Check if there are untracked files.

Path: ["git", "hasUntrackedFiles"]

git.hasLocalCommits#

Check if there are local commits that haven't been pushed.

Path: ["git", "hasLocalCommits"]

git.isClean#

Check if the working directory is clean (no changes).

Path: ["git", "isClean"]

Example:

const { value: hasChanges } = await client.exec(["git", "hasChanges"], {});
if (hasChanges) {
  console.log("Repository has uncommitted changes");
}

const { value: isClean } = await client.exec(["git", "isClean"], {});
if (isClean) {
  console.log("Working directory is clean");
}

Stash Procedures#

git.stash.list#

List all stashes.

Path: ["git", "stash", "list"]

interface GitStashEntry {
  /** Stash index (0 = most recent) */
  index: number;
  /** Stash reference (e.g., stash@{0}) */
  ref: string;
  /** Commit hash of the stash */
  hash: string;
  /** Stash message */
  message: string;
}

interface GitStashListOutput {
  /** List of stashes */
  stashes: GitStashEntry[];
  /** Total count */
  count: number;
}

git.stash.push#

Save changes to stash.

Path: ["git", "stash", "push"]

interface GitStashPushInput {
  /** Stash message */
  message?: string;
  /** Include untracked files (default: false) */
  includeUntracked?: boolean;
  /** Keep staged changes in index (default: false) */
  keepIndex?: boolean;
  /** Specific paths to stash */
  paths?: string[];
  /** Working directory */
  cwd?: string;
}

interface GitStashPushOutput {
  /** Whether changes were stashed */
  stashed: boolean;
  /** Stash reference if created */
  ref?: string;
  /** Stash message */
  message?: string;
}

git.stash.pop#

Apply and remove most recent stash.

Path: ["git", "stash", "pop"]

interface GitStashPopInput {
  /** Stash index to pop (default: 0 = most recent) */
  index?: number;
  cwd?: string;
}

interface GitStashPopOutput {
  /** Whether stash was applied */
  applied: boolean;
  /** Stash reference that was popped */
  ref: string;
  /** Whether stash was dropped (false if conflict) */
  dropped: boolean;
  /** Whether there was a merge conflict */
  conflict?: boolean;
}

git.stash.apply#

Apply stash without removing it.

Path: ["git", "stash", "apply"]

interface GitStashApplyInput {
  /** Stash index to apply (default: 0 = most recent) */
  index?: number;
  cwd?: string;
}

interface GitStashApplyOutput {
  /** Whether stash was applied */
  applied: boolean;
  /** Stash reference that was applied */
  ref: string;
  /** Whether there was a merge conflict */
  conflict?: boolean;
}

git.stash.drop#

Delete a stash.

Path: ["git", "stash", "drop"]

interface GitStashDropInput {
  /** Stash index to drop (default: 0 = most recent) */
  index?: number;
  cwd?: string;
}

interface GitStashDropOutput {
  /** Whether stash was dropped */
  dropped: boolean;
  /** Stash reference that was dropped */
  ref: string;
}

git.stash.export#

Export stash as patch for snapshot storage.

Path: ["git", "stash", "export"]

interface GitStashExportInput {
  /** Stash index to export (default: 0 = most recent) */
  index?: number;
  cwd?: string;
}

interface GitStashExportOutput {
  /** Stash reference */
  ref: string;
  /** Commit hash */
  hash: string;
  /** Stash message */
  message: string;
  /** Patch content (diff) */
  patch: string;
  /** Whether stash includes untracked files */
  hasUntracked: boolean;
  /** Untracked files patch if present */
  untrackedPatch?: string;
}

git.stash.import#

Import stash from patch.

Path: ["git", "stash", "import"]

interface GitStashImportInput {
  /** Patch content to apply and stash */
  patch: string;
  /** Message for the new stash */
  message?: string;
  /** Include untracked files (default: false) */
  includeUntracked?: boolean;
  cwd?: string;
}

interface GitStashImportOutput {
  /** Whether import succeeded */
  imported: boolean;
  /** New stash reference if created */
  ref?: string;
  /** Error message if failed */
  error?: string;
}

Stash Example:

// Save current work
await client.exec(["git", "stash", "push"], {
  message: "WIP: feature in progress",
  includeUntracked: true,
});

// List stashes
const stashes = await client.exec(["git", "stash", "list"], {});
console.log(`${stashes.count} stashes`);

// Apply most recent stash
await client.exec(["git", "stash", "apply"], { index: 0 });

// Export stash for backup
const exported = await client.exec(["git", "stash", "export"], { index: 0 });
fs.writeFileSync("backup.patch", exported.patch);

Complete Procedure List#

CategoryProcedureDescription
Repository Stategit.statusGet repository status
git.logShow commit history
git.diffShow changes
git.branchBranch operations
git.initInitialize repository
Staging & Commitsgit.addStage files
git.commitCreate commit
git.checkoutCheckout branch/files
Remote Operationsgit.pushPush to remote
git.pullPull from remote
git.fetchFetch from remote
git.cloneClone repository
git.remoteManage remotes
Predicatesgit.hasChangesCheck for any changes
git.hasStagedChangesCheck for staged changes
git.hasUnstagedChangesCheck for unstaged changes
git.hasUntrackedFilesCheck for untracked files
git.hasLocalCommitsCheck for unpushed commits
git.isCleanCheck if working dir is clean
Stashgit.stash.listList stashes
git.stash.pushSave to stash
git.stash.popApply and remove stash
git.stash.applyApply stash
git.stash.dropDelete stash
git.stash.exportExport stash as patch
git.stash.importImport stash from patch

Usage Examples#

Complete Git Workflow#

// 1. Check status
const status = await client.exec(["git", "status"], {});
console.log(`On branch ${status.branch}`);

// 2. Stage changes
await client.exec(["git", "add"], { all: true });

// 3. Create commit
const commit = await client.exec(["git", "commit"], {
  message: "feat: implement new feature",
});

// 4. Push to remote
await client.exec(["git", "push"], {
  remote: "origin",
  setUpstream: true,
});

Conditional Operations with Predicates#

const { value: hasChanges } = await client.exec(["git", "hasChanges"], {});
if (hasChanges) {
  await client.exec(["git", "stash", "push"], {
    message: "Auto-stash before pull",
  });
}

await client.exec(["git", "pull"], { rebase: true });

const { value: hasStashes } = await client.exec(["git", "stash", "list"], {});
if (hasStashes.count > 0) {
  await client.exec(["git", "stash", "pop"], {});
}

Branch Management#

// Create feature branch
await client.exec(["git", "checkout"], {
  ref: "feature/new-feature",
  create: true,
});

// Work on feature...
await client.exec(["git", "add"], { all: true });
await client.exec(["git", "commit"], { message: "feat: complete feature" });

// Merge to main
await client.exec(["git", "checkout"], { ref: "main" });
await client.exec(["git", "pull"], {});

// Delete feature branch
await client.exec(["git", "branch"], {
  name: "feature/new-feature",
  delete: true,
});

Working with Remotes#

// Clone repository
const clone = await client.exec(["git", "clone"], {
  url: "https://github.com/user/repo.git",
  dest: "./my-repo",
});

// Set remote
await client.exec(["git", "remote"], {
  name: "upstream",
  url: "https://github.com/original/repo.git",
});

// Fetch from upstream
await client.exec(["git", "fetch"], {
  remote: "upstream",
  prune: true,
});

Dependencies#

Diagram source (Mermaid). GitHub draws it on the page of the file.
graph LR
    A[@procedures/git] --> B[@procedures/client]
    A --> C[zod]

    B -.->|provides| D[Client & Procedure System]
    C -.->|provides| E[Schema Validation]

    style A fill:#e1f5ff
    style B fill:#ffe1e1
    style C fill:#fff4e1
  • @procedures/client - Core client library providing the procedure system
  • zod - Schema validation library

Integration with Ecosystem#

Used By#

// bundle-dev/src/register.ts
import "@procedures/git"; // registers its procedures

// client-lib uses git procedures internally
import { gitStatus, gitAdd, gitCommit } from "@procedures/git";

Usage in client-lib#

The client-lib package uses git procedures extensively for package management:

// From client-lib/src/git/operations.ts
export async function stageAll(cwd: string, ctx: ProcedureContext): Promise<void> {
  await ctx.client.call(["git", "add"], { all: true, cwd });
}

export async function commit(
  message: string,
  cwd: string,
  ctx: ProcedureContext
): Promise<string> {
  const result = await ctx.client.call(["git", "commit"], { message, cwd });
  return result.hash;
}

Configuration#

Auto-Registration#

Procedures are automatically registered when the package is loaded:

{
  "client": {
    "procedures": "./dist/register.js"
  },
  "scripts": {
    "postinstall": "client-registry announce"
  }
}

Build Scripts#

# Build the package
pnpm build

# Type check
pnpm typecheck

# Clean build artifacts
pnpm clean

Advanced Usage#

Direct Function Imports#

The handlers start git themselves. Their context is optional: it gives only the abort signal.

import { gitStatus, gitCommit, gitPush } from "@procedures/git";

const status = await gitStatus({ cwd: "./repo" });
await gitCommit({ message: "feat: new feature", cwd: "./repo" });
await gitPush({ remote: "origin", cwd: "./repo" }, { signal: AbortSignal.timeout(60_000) });

Custom Registration#

import { createProcedure, registerProcedures } from "@procedures/client";
import { gitStatus, GitStatusInputSchema } from "@procedures/git";

// Register under custom namespace
const customStatusProcedure = createProcedure()
  .path(["custom", "git", "status"])
  .input(GitStatusInputSchema)
  .handler(gitStatus)
  .build();

registerProcedures([customStatusProcedure]);

Error Handling#

All git procedures may throw errors for invalid operations:

try {
  await client.exec(["git", "commit"], {
    message: "feat: new feature",
  });
} catch (error) {
  if (error.message.includes("nothing to commit")) {
    console.log("No changes to commit");
  } else {
    throw error;
  }
}

Performance Considerations#

  • Command Execution: Each procedure spawns a git process
  • Large Repositories: Operations may be slower on repos with extensive history
  • Batch Operations: Use predicate checks to avoid unnecessary operations
  • Stash Export/Import: Useful for snapshotting state without git history overhead

Troubleshooting#

Git Not Found#

Ensure git is installed and in PATH:

git --version

Permission Errors#

Check repository permissions:

ls -la .git

Detached HEAD State#

Use git.checkout to return to a branch:

await client.exec(["git", "checkout"], { ref: "main" });

License#

MIT