Skip to content

Brooooooklyn/simple-git

Repository files navigation

@napi-rs/simple-git

https://github.com/Brooooooklyn/simple-git/actions Install size

Repository

Usage

import { Repository, BranchType, RemoteCallbacks, PushOptions } from '@napi-rs/simple-git'

Repository.init('/path/to/repo') // init a git repository

const repo = new Repository('/path/to/repo') // Open an existed repo

// Last-modified commit time of `build.rs` in milliseconds since the Unix epoch.
// Returns a `number`; throws when no commit in history touched the path.
const lastModified = repo.getFileLatestModifiedDate('build.rs')
console.log(new Date(lastModified)) // 2022-03-13T12:47:47.920Z

// Async version of `getFileLatestModifiedDate`, also a `number` (rejects if no history).
const lastModifiedAsync = await repo.getFileLatestModifiedDateAsync('build.rs')
console.log(lastModifiedAsync) // 1647175667920

// Null-safe alternative: a `Date`, or `null` (never throws) when no commit ever
// touched the path.
const lastModifiedDate = repo.getFileLastModifiedDate('build.rs')
if (lastModifiedDate) console.log(lastModifiedDate) // 2022-03-13T12:47:47.920Z

// Enriched metadata for the last commit that touched a file.
// Returns `null` (does **not** throw) when the path has no commit history.
const mod = repo.getFileLatestModified('build.rs')
if (mod) {
  console.log(mod.authorName, mod.authorEmail) // 'LongYinan' 'github@lyn.one'
  console.log(mod.committerTime) // a `Date`, identical to getFileLastModifiedDate('build.rs')
  console.log(mod.commitId, mod.summary)
}

// Bulk: resolve many files in a single history walk (early-exits once all are found).
// Every input path is present as a key; a never-committed path maps to `null`.
const mods = repo.getFilesLatestModified(['build.rs', 'Cargo.toml'])
console.log(mods['build.rs']?.committerName)
console.log(mods['Cargo.toml']?.committerTime) // a `Date`
// Empty input returns `{}`:
console.log(repo.getFilesLatestModified([])) // {}

// ---- Working-tree status (like `git status`) ----
const changes = repo.statuses() // => FileStatus[]
for (const file of changes) {
  console.log(file.path, file.isWtModified, file.isIndexNew)
}
console.log(repo.statusFile('README.md').isWtModified) // status of a single path
const scanned = await repo.statusesAsync({ includeIgnored: true }) // off-thread scan

// ---- Config + default signature ----
const config = repo.config() // => Config (system + global + repo, prioritized)
config.setString('user.name', 'LongYinan')
console.log(config.getString('user.name')) // 'LongYinan'
console.log(config.getBoolean('core.bare')) // false
const sig = repo.signature() // built from user.name / user.email
console.log(sig.name(), sig.email()) // 'LongYinan' 'github@lyn.one'

// ---- Stage from the working tree and commit ----
const index = repo.index() // => Index (the staging area)
index.addPath('file.txt')
index.write()
const treeOid = index.writeTree() // OID of the staged tree
const tree = repo.findTree(treeOid)!
const parent = repo.head().target()! // current tip OID
const commitId = repo.commit('HEAD', sig, sig, 'commit from workdir', tree, [parent])
console.log(commitId) // 40-char hex OID
console.log(repo.blob(Buffer.from('hello'))) // write a blob straight to the ODB => OID

// ---- Branches ----
const tip = repo.findCommit(parent)!
const branch = repo.branch('feature', tip, false) // => Branch
console.log(branch.name(), branch.isHead()) // 'feature' false
for (const b of repo.branches(BranchType.Local)) console.log(b.name())
repo.findBranch('feature', BranchType.Local)?.delete()

// ---- Checkout / HEAD / refs ----
repo.checkoutTree(tip.asObject(), { force: true }) // update workdir to a tree-ish
repo.setHead('refs/heads/feature') // move HEAD (without touching the workdir)
repo.checkoutHead({ force: true }) // sync the workdir to HEAD
repo.setHeadDetached(tip.id()) // detach HEAD at a commit
repo.reference('refs/heads/tmp', tip.id(), true, 'create tmp') // a direct ref
repo.referenceSymbolic('refs/heads/alias', 'refs/heads/main', true, 'alias main')

// ---- Blame ----
for (const hunk of repo.blameFile('build.rs')) {
  console.log(hunk.finalStartLine, hunk.linesInHunk, hunk.finalCommitId, hunk.finalAuthorName)
}
console.log(repo.blameLine('build.rs', 10)?.finalAuthorName) // hunk for line 10, or null

// ---- Push ----
const remote = repo.findRemote('origin')!
const callbacks = new RemoteCallbacks()
  // Per-ref result: one object per updated reference.
  .pushUpdateReference(({ refname, status }) => {
    console.log(refname, status) // 'refs/heads/main' null   (null === accepted)
  })
  // Pack-transfer progress: a single PushTransferProgress object.
  .pushTransferProgress(({ current, total, bytes }) => {
    console.log(`${current}/${total} objects, ${bytes} bytes`)
  })
remote.push(['refs/heads/main'], new PushOptions().remoteCallback(callbacks))

// ---- Async (off-main-thread) network + commit ----
// Clone, commit, fetch and push also have async variants that run their git
// work on a worker thread and return a Promise. `commitAsync` mirrors `commit`.
const cloned = await Repository.cloneAsync('https://example.com/repo.git', '/tmp/clone')
const asyncOid = await repo.commitAsync('HEAD', sig, sig, 'async commit', tree, [parent])
// fetchAsync / pushAsync accept data-only Fetch/Push options. They do NOT accept
// RemoteCallbacks (JS callbacks can't run off the main thread) — use the sync
// fetch()/push() when you need credential or progress callbacks.
await remote.fetchAsync(['refs/heads/main:refs/remotes/origin/main'])
await remote.pushAsync(['refs/heads/main'], new PushOptions().packbuilderParallelism(1))

// ---- Tags & diff ----
// tagForeach hands the callback a single { id, nameBytes } object per tag.
repo.tagForeach(({ id, nameBytes }) => {
  console.log(id, nameBytes.toString('utf8')) // '<40-hex>' 'refs/tags/v1.0.0'
  return true // return false to stop iterating
})
// DiffOptions.showUnmodified pulls unmodified files into the diff so they show
// up in `deltas()` (with an Unmodified status) instead of being skipped.
const headTree = repo.head().peelToTree()
repo.diffTreeToWorkdir(headTree, { showUnmodified: true })

API

export class Repository {
  static init(p: string): Repository
  constructor(gitDir: string)
  /**
   * Asynchronous variant of `clone`, performed off the main thread. Resolves
   * with a ready-to-use `Repository` once the clone completes.
   */
  static cloneAsync(url: string, path: string, signal?: AbortSignal | undefined | null): Promise<Repository>
  /** Retrieve and resolve the reference pointed at by HEAD. */
  head(): Reference
  /**
   * Last-modified commit time of `filepath` in milliseconds since the Unix
   * epoch. Throws when no commit in history touched the path. For a
   * `null`-on-missing `Date`, use `getFileLastModifiedDate`.
   */
  getFileLatestModifiedDate(filepath: string): number
  getFileLatestModifiedDateAsync(filepath: string, signal?: AbortSignal | undefined | null): Promise<number>
  /**
   * Last-modified commit time of `filepath` as a `Date`, or `null` when no
   * commit in history touched the path (never throws for the missing case).
   */
  getFileLastModifiedDate(filepath: string): Date | null
  getFileLastModifiedDateAsync(filepath: string, signal?: AbortSignal | undefined | null): Promise<Date | null>
  /**
   * Last commit that modified `filepath`, with author/committer identity.
   * Returns `null` when no commit in history touched the path.
   */
  getFileLatestModified(filepath: string): FileModification | null
  getFileLatestModifiedAsync(filepath: string, signal?: AbortSignal | undefined | null): Promise<FileModification | null>
  /**
   * Resolve the last commit that modified each of `filepaths` in a single
   * history walk. Every input path is a key; never-committed paths map to `null`.
   */
  getFilesLatestModified(filepaths: Array<string>): Record<string, FileModification | null>
  getFilesLatestModifiedAsync(filepaths: Array<string>, signal?: AbortSignal | undefined | null): Promise<Record<string, FileModification | null>>
  /** Repository config view (system + global + repo, prioritized). */
  config(): Config
  /** Default signature built from `user.name` / `user.email`. */
  signature(): Signature
  /** List the branches in the repository, optionally filtered by type. */
  branches(filter?: BranchType | undefined | null): Array<Branch>
  /** Lookup a branch by name and type; `null` when it does not exist. */
  findBranch(name: string, branchType: BranchType): Branch | null
  /** Create a new branch pointing at a target commit. */
  branch(branchName: string, target: Commit, force: boolean): Branch
  /** Check out a tree-ish into the working directory (does not move HEAD). */
  checkoutTree(treeish: GitObject, options?: CheckoutOptions | undefined | null): void
  /** Update the index and the working tree to match HEAD. */
  checkoutHead(options?: CheckoutOptions | undefined | null): void
  /** Update the working tree to match the index. */
  checkoutIndex(options?: CheckoutOptions | undefined | null): void
  /** Make HEAD point to the reference named `refname`. */
  setHead(refname: string): void
  /** Detach HEAD directly at the commit with the given OID. */
  setHeadDetached(oid: string): void
  /** Create a new direct reference named `name` pointing at `oid`. */
  reference(name: string, oid: string, force: boolean, logMessage: string): Reference
  /** Create a new symbolic reference named `name` pointing at `target`. */
  referenceSymbolic(name: string, target: string, force: boolean, logMessage: string): Reference
  /** Create a new commit; `parents` is an optional list of parent OID hex strings. */
  commit(updateRef: string | undefined | null, author: Signature, committer: Signature, message: string, tree: Tree, parents?: Array<string> | undefined | null): string
  /** Asynchronous variant of `commit`, performed off the main thread. Resolves with the new commit's OID hex. */
  commitAsync(updateRef: string | undefined | null, author: Signature, committer: Signature, message: string, tree: Tree, parents?: Array<string> | undefined | null, signal?: AbortSignal | undefined | null): Promise<string>
  /** Get the index (staging area) for this repository. */
  index(): Index
  /** Write an in-memory buffer to the object database as a blob; returns its OID hex. */
  blob(data: Uint8Array): string
  /** Read a file and write its content to the object database as a blob; returns its OID hex. */
  blobPath(path: string): string
  /** List the working-tree and index status of files (like `git status`). */
  statuses(options?: StatusOptions | undefined | null): Array<FileStatus>
  /** Status of a single file by its workdir-relative path. */
  statusFile(path: string): FileStatus
  /** Asynchronous variant of `statuses`, computed off the main thread. */
  statusesAsync(options?: StatusOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise<Array<FileStatus>>
  /** Blame `path`: who last changed each line, as ordered hunks. */
  blameFile(path: string, options?: BlameOptions | undefined | null): Array<BlameHunk>
  /** Blame `path` and return only the hunk covering `lineNo` (1-based), or `null`. */
  blameLine(path: string, lineNo: number, options?: BlameOptions | undefined | null): BlameHunk | null
  /** Asynchronous variant of `blameFile`, computed off the main thread. */
  blameFileAsync(path: string, options?: BlameOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise<Array<BlameHunk>>
  /** Create an annotated tag (and its ref); returns the new tag object's OID. */
  tag(name: string, target: GitObject, tagger: Signature, message: string, force: boolean): string
  /** Create an annotated tag object WITHOUT a ref; returns its OID. */
  tagAnnotation(name: string, target: GitObject, tagger: Signature, message: string): string
  /** Lookup a tag object by OID; `null` when it does not exist. */
  findTag(oid: string): Tag | null
  /** Lookup a tag object by hash prefix; `null` when no tag matches. */
  findTagByPrefix(prefixHash: string): Tag | null
  /** Read libgit2's merge message (`.git/MERGE_MSG`). */
  mergeMessage(): string
  /** Remove the merge message (`.git/MERGE_MSG`). */
  removeMergeMessage(): void
  /** Add a remote with the provided fetch refspec to the configuration. */
  remoteWithFetch(name: string, url: string, refspec: string): Remote
}

/**
 * Last commit that modified a file, with author/committer identity.
 * All times are `Date`s (UTC; timezone offset ignored).
 */
export interface FileModification {
  /** 40-char lowercase hex OID of the last commit that modified the file. */
  commitId: string
  /** Commit summary (first line). Undefined if absent or not valid UTF-8. */
  summary?: string
  /** Author name. Undefined if not valid UTF-8. */
  authorName?: string
  /** Author email. Undefined if not valid UTF-8. */
  authorEmail?: string
  /** Author time, as a `Date`. */
  authorTime: Date
  /** Committer name. Undefined if not valid UTF-8. */
  committerName?: string
  /** Committer email. Undefined if not valid UTF-8. */
  committerEmail?: string
  /** Committer time, as a `Date`. Identical to `getFileLastModifiedDate`. */
  committerTime: Date
  /**
   * The commit that FIRST added this file (its creation), or `undefined` for a
   * glob, or a directory/submodule path at HEAD. A delete-then-re-add returns
   * the ORIGINAL add; merge commits are included; no rename-follow.
   */
  created?: CommitInfo
}

/**
 * A single commit's identity/time metadata, reused for the commit that CREATED
 * a file (`FileModification.created`). Same 8 fields as `FileModification`'s
 * commit metadata. All times are `Date`s (UTC; timezone offset ignored).
 */
export interface CommitInfo {
  /** 40-char lowercase hex OID of the commit that first added the file. */
  commitId: string
  /** Commit summary (first line). Undefined if absent or not valid UTF-8. */
  summary?: string
  /** Author name. Undefined if not valid UTF-8. */
  authorName?: string
  /** Author email. Undefined if not valid UTF-8. */
  authorEmail?: string
  /** Author time, as a `Date`. */
  authorTime: Date
  /** Committer name. Undefined if not valid UTF-8. */
  committerName?: string
  /** Committer email. Undefined if not valid UTF-8. */
  committerEmail?: string
  /** Committer time, as a `Date`. */
  committerTime: Date
}

/**
 * A git configuration store. Obtain one with `repo.config()` (system + global +
 * repository, prioritized) or `Config.openDefault()` (system/global/XDG only).
 */
export class Config {
  /** Open global, XDG and system config into one prioritized object. */
  static openDefault(): Config
  /** Get a string config value (highest-priority occurrence wins). */
  getString(name: string): string
  getBoolean(name: string): boolean
  getNumber(name: string): number
  /** i64 value as a `bigint` (no >2^53 truncation). */
  getBigInt(name: string): bigint
  /** Set a value in the highest-level config file (usually the local one). */
  setString(name: string, value: string): void
  setBoolean(name: string, value: boolean): void
  setNumber(name: string, value: number): void
  /** Set an i64 value from a `bigint`; throws if it doesn't fit in i64. */
  setBigInt(name: string, value: bigint): void
  /** Delete a variable from the highest-level config file. */
  removeEntry(name: string): void
  /** Create a read-only point-in-time snapshot of this configuration. */
  snapshot(): Config
  /** List entries, optionally filtered by a glob pattern. */
  entries(glob?: string | undefined | null): Array<ConfigEntry>
}

/**
 * A single configuration entry: its fully-qualified name, value, and the
 * level (file) it was read from.
 */
export interface ConfigEntry {
  name: string
  value: string
  level: ConfigLevel
}

/**
 * The priority level a configuration entry or file applies to. Higher levels
 * take precedence; `Local` (the repository's own `.git/config`) is where
 * `set_*`/`remove_entry` write by default.
 */
export declare const enum ConfigLevel {
  /** System-wide on Windows, for compatibility with portable git */
  ProgramData = 0,
  /** System-wide configuration file, e.g. /etc/gitconfig */
  System = 1,
  /** XDG-compatible configuration file, e.g. ~/.config/git/config */
  Xdg = 2,
  /** User-specific configuration, e.g. ~/.gitconfig */
  Global = 3,
  /** Repository specific config, e.g. $PWD/.git/config */
  Local = 4,
  /** Worktree specific configuration file, e.g. $GIT_DIR/config.worktree */
  Worktree = 5,
  /** Application specific configuration file */
  App = 6,
  /** Highest level available */
  Highest = 7
}

/**
 * A git index (the staging area). Obtain one with `repo.index()`. Mutating
 * methods change memory only; call `write()` to persist it or `writeTree()` to
 * write its current state to the object database as a tree.
 */
export class Index {
  /** Add or update an index entry from a file on disk. */
  addPath(path: string): void
  /** Add or update entries matching files in the working directory. */
  addAll(pathspecs?: Array<string> | undefined | null, force?: boolean | undefined | null): void
  /** Update all index entries to match the working directory. */
  updateAll(pathspecs?: Array<string> | undefined | null): void
  /** Remove an index entry corresponding to a file on disk. */
  removePath(path: string): void
  /** Get the count of entries currently in the index. */
  size(): number
  /** Write the in-memory index back to disk using an atomic file lock. */
  write(): void
  /** Write the index as a tree to the object database and return its OID. */
  writeTree(): string
}

/**
 * A git branch — a thin wrapper around an underlying reference; the full
 * reference name is available via `referenceName`.
 */
export class Branch {
  /** Name of the local or remote branch (`null` if not valid utf-8). */
  name(): string | null
  /** Whether the current local branch is pointed at by HEAD. */
  isHead(): boolean
  /** Full reference name backing this branch (e.g. `refs/heads/main`). */
  referenceName(): string | null
  /** Delete this branch reference. */
  delete(): void
  /** The configured upstream tracking branch, or `null`. */
  upstream(): Branch | null
  /** The reference backing this branch as a live `Reference`. */
  get(): Reference
}

/** An enumeration for the possible types of branches. */
export declare const enum BranchType {
  /** A local branch not on a remote. */
  Local = 0,
  /** A branch for a remote. */
  Remote = 1
}

export class PushOptions {
  constructor()
  /** Set the callbacks to use for the push operation. */
  remoteCallback(callback: RemoteCallbacks): this
  /** Set the proxy options to use for the push operation. */
  proxyOptions(options: ProxyOptions): this
  /** Number of packbuilder worker threads (0 = auto-detect, default 1). */
  packbuilderParallelism(parallel: number): this
  /** Set remote redirection settings. */
  followRedirects(opt: RemoteRedirect): this
  /** Set extra headers for this push operation. */
  customHeaders(headers: Array<string>): this
  /** Set "push options" to deliver to the remote. */
  remotePushOptions(options: Array<string>): this
}

/**
 * Status of a single file in the working tree and/or index.
 *
 * The boolean flags mirror the `git2::Status` bits; `bits` carries the raw
 * value as a forward-compatible escape hatch for flags not surfaced here.
 */
export interface FileStatus {
  /** Workdir-relative path. Undefined if the path is not valid UTF-8. */
  path?: string
  /** Raw `git2::Status` bits — forward-compat escape hatch. */
  bits: number
  /** Staged: a new file was added to the index. */
  isIndexNew: boolean
  /** Staged: a tracked file was modified in the index. */
  isIndexModified: boolean
  /** Staged: a tracked file was deleted from the index. */
  isIndexDeleted: boolean
  /** Staged: a tracked file was renamed in the index. */
  isIndexRenamed: boolean
  /** Staged: a tracked file changed type in the index. */
  isIndexTypechange: boolean
  /** Unstaged: an untracked file (new in the working directory). */
  isWtNew: boolean
  /** Unstaged: a tracked file was modified in the working directory. */
  isWtModified: boolean
  /** Unstaged: a tracked file was deleted from the working directory. */
  isWtDeleted: boolean
  /** Unstaged: a tracked file changed type in the working directory. */
  isWtTypechange: boolean
  /** Unstaged: a tracked file was renamed in the working directory. */
  isWtRenamed: boolean
  /** The file is ignored. */
  isIgnored: boolean
  /** The file has merge conflicts. */
  isConflicted: boolean
}

/**
 * Options controlling how a working-tree status scan is performed.
 *
 * Every field is optional; omitted fields fall back to the git CLI defaults
 * (`include_untracked` is `true`, everything else `false`).
 */
export interface StatusOptions {
  /** Include untracked files in the status. Defaults to `true`. */
  includeUntracked?: boolean
  /** Include ignored files in the status. Defaults to `false`. */
  includeIgnored?: boolean
  /** Include unmodified files in the status. Defaults to `false`. */
  includeUnmodified?: boolean
  /** Skip submodules. Defaults to `false`. */
  excludeSubmodules?: boolean
  /**
   * Recurse into untracked directories instead of reporting the directory
   * itself. Defaults to `false`.
   */
  recurseUntrackedDirs?: boolean
  /** Detect renames between the HEAD tree and the index. Defaults to `false`. */
  renamesHeadToIndex?: boolean
  /** Detect renames between the index and the working directory. Defaults to `false`. */
  renamesIndexToWorkdir?: boolean
  /** Restrict the scan to the given pathspecs. */
  pathspec?: Array<string>
}

/**
 * A single blame hunk: a contiguous run of lines attributed to one commit.
 *
 * All identity fields are copied out of the borrowed `git2::BlameHunk` so the
 * value can safely outlive the underlying `git2::Blame`.
 */
export interface BlameHunk {
  /** Number of lines covered by this hunk. */
  linesInHunk: number
  /** 40-char lowercase hex OID of the commit where these lines were last changed. */
  finalCommitId: string
  /** Line number where this hunk begins in the final file (1-based). */
  finalStartLine: number
  /** Author name of the final commit. Undefined if absent or not valid UTF-8. */
  finalAuthorName?: string
  /** Author email of the final commit. Undefined if absent or not valid UTF-8. */
  finalAuthorEmail?: string
  /** Author time of the final commit, as a `Date`. The Unix epoch if no signature. */
  finalTime: Date
  /** 40-char lowercase hex OID of the commit where this hunk was found. */
  origCommitId: string
  /** Line number where this hunk begins in the original file (1-based). */
  origStartLine: number
  /** Path to the file where this hunk originated. Undefined if not valid UTF-8. */
  origPath?: string
  /** Whether this hunk was tracked to a boundary commit (root or `oldest_commit`). */
  isBoundary: boolean
}

/**
 * Options controlling how a blame is computed.
 *
 * Every field is optional; omitted fields fall back to libgit2's defaults
 * (no copy tracking, the whole file, starting from the current HEAD).
 */
export interface BlameOptions {
  /**
   * Track lines that have moved within a file. Defaults to `false`.
   *
   * Note: libgit2 1.9.4 does not implement blame copy/move tracking, so setting
   * this flag has no effect today (accepted for forward-compat; effectively a no-op).
   */
  trackCopiesSameFile?: boolean
  /**
   * Track lines that have moved across files in the same commit. Defaults to `false`.
   *
   * Note: libgit2 1.9.4 does not implement blame copy/move tracking, so setting
   * this flag has no effect today (accepted for forward-compat; effectively a no-op).
   */
  trackCopiesSameCommitMoves?: boolean
  /** 40-char hex OID of the newest commit to consider (the blame starts here). */
  newestCommit?: string
  /** 40-char hex OID of the oldest commit to consider (a boundary). */
  oldestCommit?: string
  /** Restrict the search to first-parent history only. Defaults to `false`. */
  firstParent?: boolean
  /** Map names/emails through the repository's mailmap. Defaults to `false`. */
  useMailmap?: boolean
  /** Ignore whitespace differences. Defaults to `false`. */
  ignoreWhitespace?: boolean
  /** The first line in the file to blame (1-based). */
  minLine?: number
  /** The last line in the file to blame (1-based). */
  maxLine?: number
}

/**
 * Options controlling how a checkout writes files into the working directory.
 *
 * The default is a **safe** checkout (matching `git checkout`): files with
 * local modifications are left untouched. Set `force` to overwrite them, which
 * can discard uncommitted changes — use it deliberately.
 */
export interface CheckoutOptions {
  /**
   * Force the checkout, overwriting any local changes in the working tree.
   * Defaults to a safe checkout when omitted or `false`.
   */
  force?: boolean
  /**
   * Recreate files that are missing from the working tree even in a safe
   * checkout.
   */
  recreateMissing?: boolean
  /** Allow the checkout to write files that conflict with the working tree. */
  allowConflicts?: boolean
  /**
   * Restrict the checkout to these pathspecs. When omitted, all paths are
   * checked out.
   */
  paths?: Array<string>
  /**
   * Write the checked-out files into this directory instead of the
   * repository's working directory.
   */
  targetDir?: string
}

Disposal

dispose() — and its alias free() — eagerly releases the native git2 handle without waiting for garbage collection; both are idempotent. After disposal every throwing method throws "Repository has been disposed" (the Option-returning lookups return null instead), and every handle derived from the repo (Remote, Tree, Commit, Reference, RevWalk, …) throws the same error on use — whether it is the receiver or passed as an argument to another method (machine-enforced, not merely documented). Disposal does NOT cancel already-scheduled *Async operations: they reopen the repo on their own worker thread and run to completion. To cancel a pending async op, pass an AbortSignal to the *Async method rather than relying on dispose().

Reference

Usage

import { Repository } from '@napi-rs/simple-git'

const repo = new Repository('/path/to/repo') // Open an existed repo

const headReference = repo.head()

headReference.shorthand() // 'main'
headReference.name() // 'refs/heads/main'
headReference.target() // 7a1256e2f847f395219980bc06c6dadf0148f18d

API

/** An enumeration of all possible kinds of references. */
export const enum ReferenceType {
  /** A reference which points at an object id. */
  Direct = 0,
  /** A reference which points at another reference. */
  Symbolic = 1,
  Unknown = 2
}
export class Reference {
  /**
   * Ensure the reference name is well-formed.
   *
   * Validation is performed as if [`ReferenceFormat::ALLOW_ONELEVEL`]
   * was given to [`Reference.normalize_name`]. No normalization is
   * performed, however.
   *
   * ```ts
   * import { Reference } from '@napi-rs/simple-git'
   *
   * console.assert(Reference.isValidName("HEAD"));
   * console.assert(Reference.isValidName("refs/heads/main"));
   *
   * // But:
   * console.assert(!Reference.isValidName("main"));
   * console.assert(!Reference.isValidName("refs/heads/*"));
   * console.assert(!Reference.isValidName("foo//bar"));
   * ```
   */
  static isValidName(name: string): boolean
  /** Check if a reference is a local branch. */
  isBranch(): boolean
  /** Check if a reference is a note. */
  isNote(): boolean
  /** Check if a reference is a remote tracking branch */
  isRemote(): boolean
  /** Check if a reference is a tag */
  isTag(): boolean
  kind(): ReferenceType
  /**
   * Get the full name of a reference.
   *
   * Returns `None` if the name is not valid utf-8.
   */
  name(): string | null
  /**
   * Get the full shorthand of a reference.
   *
   * This will transform the reference name into a name "human-readable"
   * version. If no shortname is appropriate, it will return the full name.
   *
   * Returns `None` if the shorthand is not valid utf-8.
   */
  shorthand(): string | null
  /**
   * Get the OID pointed to by a direct reference.
   *
   * Only available if the reference is direct (i.e. an object id reference,
   * not a symbolic one).
   */
  target(): string | null
  /**
   * Return the peeled OID target of this reference.
   *
   * This peeled OID only applies to direct references that point to a hard
   * Tag object: it is the result of peeling such Tag.
   */
  targetPeel(): string | null
  /**
   * Get full name to the reference pointed to by a symbolic reference.
   *
   * May return `None` if the reference is either not symbolic or not a
   * valid utf-8 string.
   */
  symbolicTarget(): string | null
}

Error handling

Every error thrown by this library — from a synchronous method or from a rejected *Async promise — is a standard Error carrying a code string property drawn from a fixed union of tokens. The tokens are exported as the GitErrorCode const enum, and any caught value can be narrowed to a coded error with the isGitError(e) type guard:

import { isGitError, GitErrorCode } from '@napi-rs/simple-git'

try {
  // …some git operation…
} catch (e) {
  if (isGitError(e) && e.code === GitErrorCode.NotFound) {
    // handle the missing object/reference/config entry
  }
}

isGitError(e) returns true only when e is a genuine Error object whose code is a real member of the GitErrorCode enum; it narrows e to Error & { code: GitErrorCode } in TypeScript. The Error check is the native Node-API napi_is_error test (not a JS-level instanceof), so it recognizes cross-realm and subclassed errors while rejecting look-alike proxies and plain objects. Membership is validated against the enum (the single source of truth), so a non-git Error that merely exposes some other string .code (e.g. Node's ENOENT) returns false. The guard is total — it never throws for any input, even a hostile value with a throwing proxy trap or Symbol.hasInstance, or an Error whose code is a throwing getter (all return false) — so it is always safe to call inside a catch.

The 29 members of GitErrorCode are the 28 libgit2 error classes plus one napi-level token:

Token Meaning
GenericError Unclassified catch-all. Any unmapped or future libgit2 code collapses here — treat it as "something went wrong" rather than a specific class.
NotFound A requested object/reference/config entry does not exist.
Exists The object already exists and cannot be overwritten.
Ambiguous A short OID or name matched more than one object.
BufSize An output buffer was too small.
User A user-supplied callback returned an error.
BareRepo The operation is not allowed on a bare repository.
UnbornBranch HEAD points at a branch with no commits yet.
Unmerged The index contains unmerged entries.
NotFastForward A reference update was rejected because it was not fast-forward.
InvalidSpec A refspec/name is not well-formed.
Conflict A checkout/merge conflict prevents the operation.
Locked The resource is locked by another operation.
Modified The reference/object was modified concurrently.
Auth Authentication failed.
Certificate The server TLS certificate was rejected.
Applied The patch/commit was already applied.
Peel An object could not be peeled to the requested type.
Eof Unexpected end of file/stream.
Invalid An invalid operation or input for the current state.
Uncommitted Uncommitted changes prevent the operation.
Directory The operation is not valid on a directory.
MergeConflict A merge produced conflicts.
HashsumMismatch A hash verification failed.
IndexDirty The index has unsaved changes.
ApplyFail A patch failed to apply.
Owner The repository is owned by an unexpected user.
Timeout The operation timed out.
InvalidArg napi-level argument validation / API misuse (e.g. an option object reused across two *Async calls, or RemoteCallbacks passed to fetchAsync/pushAsync). Not a libgit2 class.

Synchronous methods and *Async promise rejections expose the same GitErrorCode token set, so you can branch on it either way:

import { isGitError, GitErrorCode } from '@napi-rs/simple-git'

// Synchronous
try {
  repo.findRemote('does-not-exist')
} catch (e) {
  if (isGitError(e) && e.code === GitErrorCode.NotFound) {
    // handle the missing remote
  }
}

// Asynchronous
await repo.getFileLatestModifiedDateAsync('build.rs').catch((e) => {
  if (isGitError(e) && e.code === GitErrorCode.GenericError) {
    // unclassified failure
  }
})

Cancellation is different. *Async methods that accept an AbortSignal reject an aborted call with napi's own AbortError, whose .code === "Cancelled". That is napi's runtime cancellation code — it is not a GitErrorCode member (it comes from napi, not the git layer), because cancellation bypasses the task's own reject hook. Because "Cancelled" is not a GitErrorCode member, isGitError returns false for it. Detect an aborted call separately — via the AbortSignal's aborted/abort event or by checking e.code === "Cancelled" — rather than through isGitError.

Performance

Compared with the exec function, which gets the file's latest modified date by spawning a child process. Getting the latest modified date from the file 1000 times:

Child process took 1.9s
@napi-rs/simple-git took 65ms

About

Simple and fast git helper functions.

Topics

Resources

License

Stars

190 stars

Watchers

1 watching

Forks

Sponsor this project

 

Packages

 
 
 

Contributors