Files
bun.sh/docs/git-api-reference.md
Claude Bot 3422bd6411 feat(bun:git): Initial implementation of bun:git module with libgit2
- Add libgit2 as a dependency in CMake build system
- Register bun:git module in HardcodedModule.zig and isBuiltinModule.cpp
- Create JSGit.h with class declarations for Repository, Commit, Branch, etc.
- Implement JSGitRepository with constructor, static methods (find, init, clone)
- Implement JSGitCommit with sha, message, author, parents, diff, getFile
- Add iso subspaces for all git classes
- Add structure caching in ZigGlobalObject

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 06:49:25 +00:00

12 KiB

bun:git Module API Design (Class-Based)

Core Classes

Repository

class Repository { constructor(path?: string) // Finds git root from path, throws if not a repo

static find(startPath?: string): Repository | null  // Non-throwing factory
static init(path: string, options?: { bare?: boolean; initialBranch?: string }):

Repository static clone(url: string, targetPath: string, options?: CloneOptions): Repository

readonly path: string      // Repo root (worktree root)
readonly gitDir: string    // .git directory path
readonly isBare: boolean

// State
get head(): Commit
get branch(): Branch | null  // null if detached HEAD
get isClean(): boolean
get isTransient(): boolean  // merge/rebase/cherry-pick in progress

// References
getCommit(ref: string): Commit | null
getBranch(name: string): Branch | null
getRemote(name?: string): Remote | null  // default: "origin"
getDefaultBranch(): Branch | null

// Collections
get branches(): BranchCollection
get remotes(): RemoteCollection
get worktrees(): WorktreeCollection
get stash(): StashCollection
get config(): Config

// Working tree
status(options?: StatusOptions): StatusEntry[]
diff(options?: DiffOptions): Diff

// Index operations
add(paths: string | string[]): void
reset(paths?: string | string[]): void  // Unstage

// Commit
commit(message: string, options?: CommitOptions): Commit

// Checkout
checkout(ref: string | Branch | Commit, options?: CheckoutOptions): void

// Reset working tree
resetHard(ref?: string | Commit): void
clean(options?: CleanOptions): void

// Abort transient states
abortMerge(): void
abortRebase(): void
abortCherryPick(): void
abortRevert(): void

}


Commit

class Commit { readonly sha: string // Full 40-char SHA readonly shortSha: string // First 7 chars readonly message: string // Full message readonly summary: string // First line readonly author: Signature readonly committer: Signature readonly parents: Commit[] readonly tree: string // Tree SHA

// Navigation
parent(n?: number): Commit | null  // Default: first parent

// Diff
diff(other?: Commit | string): Diff  // Default: diff against parent

// File access
getFile(path: string): Blob | null  // git show <sha>:<path>
listFiles(): string[]  // git diff-tree --name-only

// Ancestry
isAncestorOf(other: Commit | string): boolean
distanceTo(other: Commit | string): number  // rev-list --count

}


Branch

class Branch { readonly name: string // e.g., "main" or "feature/foo" readonly fullName: string // e.g., "refs/heads/main" readonly isRemote: boolean readonly isHead: boolean // Currently checked out

get commit(): Commit
get upstream(): Branch | null  // Tracking branch

// Comparison with upstream
get ahead(): number
get behind(): number

// Operations
setUpstream(upstream: Branch | string | null): void
delete(force?: boolean): void
rename(newName: string): void

// Static
static create(repo: Repository, name: string, target?: Commit | string): Branch

}


Remote

class Remote { readonly name: string // e.g., "origin" readonly url: string // Fetch URL readonly pushUrl: string // Push URL (may differ)

// Normalized for comparison (handles SSH vs HTTPS)
readonly normalizedUrl: string
readonly urlHash: string       // SHA256 hash for privacy-safe logging

// Branches
get defaultBranch(): Branch | null  // origin/HEAD target
getBranch(name: string): Branch | null
listBranches(): Branch[]

// Operations
fetch(options?: FetchOptions): void
fetchBranch(branch: string): void

}


Worktree

class Worktree { readonly path: string readonly gitDir: string readonly isMain: boolean // Is this the main worktree?

get head(): Commit
get branch(): Branch | null
get isClean(): boolean

// Get a Repository instance for this worktree
asRepository(): Repository

// Operations
remove(force?: boolean): void

// Static
static add(
  repo: Repository,
  path: string,
  options?: { branch?: string; detach?: boolean; commit?: string }
): Worktree

}

class WorktreeCollection { list(): Worktree[] get(path: string): Worktree | null add(path: string, options?: WorktreeAddOptions): Worktree prune(): void readonly count: number }


Diff

class Diff { readonly stats: DiffStats readonly files: DiffFile[]

// Raw output
toString(): string  // Unified diff format
toNumstat(): string

// Iteration
[Symbol.iterator](): Iterator<DiffFile>

}

class DiffFile { readonly path: string readonly oldPath: string | null // For renames readonly status: 'A' | 'M' | 'D' | 'R' | 'C' | 'T' | 'U' readonly isBinary: boolean readonly additions: number readonly deletions: number readonly hunks: DiffHunk[]

// Content
readonly patch: string

}

class DiffHunk { readonly oldStart: number readonly oldLines: number readonly newStart: number readonly newLines: number readonly header: string readonly lines: DiffLine[] }

type DiffLine = { type: '+' | '-' | ' ' content: string oldLineNo?: number newLineNo?: number }

type DiffStats = { filesChanged: number insertions: number deletions: number }


StatusEntry

type FileStatus = | 'unmodified' // ' ' | 'modified' // 'M' | 'added' // 'A' | 'deleted' // 'D' | 'renamed' // 'R' | 'copied' // 'C' | 'untracked' // '?' | 'ignored' // '!' | 'unmerged' // 'U'

class StatusEntry { readonly path: string readonly indexStatus: FileStatus // Staged status readonly workTreeStatus: FileStatus // Unstaged status readonly origPath: string | null // For renames/copies

get isStaged(): boolean
get isUnstaged(): boolean
get isUntracked(): boolean
get isConflicted(): boolean

}

type StatusOptions = { includeUntracked?: boolean // Default: true includeIgnored?: boolean // Default: false noOptionalLocks?: boolean // --no-optional-locks }


Index (Staging Area)

class Index { readonly entries: IndexEntry[]

// Stage files
add(paths: string | string[]): void
addAll(): void

// Unstage files
reset(paths?: string | string[]): void
resetAll(): void

// Query
has(path: string): boolean
get(path: string): IndexEntry | null

// Diff
diff(): Diff  // Staged changes (--cached)

}

class IndexEntry { readonly path: string readonly sha: string readonly mode: number }


Config

class Config { // Get values get(key: string): string | null getAll(key: string): string[] getBool(key: string): boolean | null getInt(key: string): number | null

// Set values
set(key: string, value: string): void
unset(key: string): void

// Common shortcuts
get userEmail(): string | null
get userName(): string | null
get hooksPath(): string | null

set userEmail(value: string | null)
set userName(value: string | null)
set hooksPath(value: string | null)

}


Stash

class StashEntry { readonly index: number readonly message: string readonly commit: Commit

apply(options?: { index?: boolean }): void
pop(options?: { index?: boolean }): void
drop(): void

}

class StashCollection { list(): StashEntry[] get(index: number): StashEntry | null

push(message?: string, options?: { includeUntracked?: boolean }): StashEntry
pop(): boolean
apply(index?: number): boolean
drop(index?: number): boolean
clear(): void

readonly count: number
readonly isEmpty: boolean

}


Blob (File Content)

class Blob { readonly sha: string readonly size: number readonly isBinary: boolean

// Content access
content(): Buffer
text(): string  // Throws if binary

// Streaming for large files
stream(): ReadableStream<Uint8Array>

}


Signature

class Signature { readonly name: string readonly email: string readonly date: Date readonly timezone: string

toString(): string  // "Name <email>"

}


Supporting Types

type CloneOptions = { depth?: number branch?: string recurseSubmodules?: boolean shallowSubmodules?: boolean bare?: boolean }

type CommitOptions = { amend?: boolean allowEmpty?: boolean author?: Signature | string noVerify?: boolean // Skip hooks }

type CheckoutOptions = { create?: boolean // -b force?: boolean // -f track?: boolean // --track }

type CleanOptions = { directories?: boolean // -d force?: boolean // -f dryRun?: boolean // -n }

type FetchOptions = { prune?: boolean tags?: boolean depth?: number }

type DiffOptions = { cached?: boolean // Staged changes only ref?: string | Commit // Compare against (default: HEAD) paths?: string[] // Limit to paths contextLines?: number // -U nameOnly?: boolean nameStatus?: boolean stat?: boolean }


Error Classes

class GitError extends Error { readonly command?: string readonly exitCode?: number readonly stderr?: string }

class NotARepositoryError extends GitError {} class RefNotFoundError extends GitError { readonly ref: string } class MergeConflictError extends GitError { readonly conflictedFiles: string[] } class CheckoutConflictError extends GitError { readonly conflictedFiles: string[] } class DetachedHeadError extends GitError {}


Usage Examples

import { Repository } from 'bun:git'

// Open repository const repo = Repository.find('/path/to/project') if (!repo) throw new Error('Not a git repository')

// Basic info console.log(repo.head.sha) console.log(repo.branch?.name) // null if detached console.log(repo.isClean)

// Status for (const entry of repo.status()) { if (entry.isUntracked) { console.log(New file: ${entry.path}) } }

// Diff const diff = repo.diff() console.log(${diff.stats.insertions}+ ${diff.stats.deletions}-) for (const file of diff.files) { console.log(${file.status} ${file.path}) }

// Commit repo.add(['src/file.ts']) const commit = repo.commit('Fix bug') console.log(commit.sha)

// Branch operations const feature = Branch.create(repo, 'feature/new-thing') repo.checkout(feature)

// Remote operations const origin = repo.getRemote('origin') origin?.fetch()

// Worktrees const worktree = Worktree.add(repo, '/tmp/worktree', { branch: 'experiment' }) const wtRepo = worktree.asRepository() // ... work in worktree ... worktree.remove()

// File content from history const oldFile = repo.head.parent()?.getFile('README.md') console.log(oldFile?.text())

// Config repo.config.set('core.hooksPath', '/path/to/hooks') console.log(repo.config.userEmail)


Sync vs Async Considerations

Most operations should be synchronous since git operations on local repos are fast:

// Sync (preferred for most operations) const repo = Repository.find(path) const status = repo.status() const head = repo.head.sha

// Async only for network operations await repo.getRemote('origin')?.fetch() await Repository.clone(url, path)

If Bun wants to keep the API async-friendly, consider: // Sync accessors for commonly-used properties repo.head // Sync getter repo.headAsync // Async getter (if needed for consistency)


Priority Implementation Order

  1. Repository - Core class, find(), basic properties
  2. Commit - sha, message, getFile()
  3. Branch - name, commit, basic operations
  4. Status/Diff - Critical for UI display
  5. Index - add(), reset()
  6. Remote - url, fetch()
  7. Worktree - Full worktree support
  8. Stash - Stash operations
  9. Config - Config get/set