Git for Node, at native speed.
Open, inspect, stage, commit, blame, branch and push real repositories through libgit2 — no git shell-out, with JS-native Date / number / Buffer types.
import { Repository, BranchType, RemoteCallbacks, PushOptions } from '@napi-rs/simple-git'
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
// 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~30× faster than shelling out
Reading getFileLatestModifiedDate 1,000× — the native call versus spawning the git CLI as a child process.
speedup
29×
1,000 reads drop from 1.9 s of child processes to 65 ms in-process.
Reading getFileLatestModifiedDate 1000× vs spawning the git CLI as a child process (source: README Performance · performance.mjs).
Powers ‘Last updated on’ for your docs
Doc generators compute 'last updated' per page from git history. @napi-rs/simple-git does it in-process — fast, with no child_process per file.
const repo = new Repository('/path/to/repo') // Open an existed repo
// 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([])) // {}A full Git toolbox
Native libgit2 speed
Talks to Git through compiled Rust/libgit2, not a child-process git shell-out.
"Last updated" dates for docs
getFileLatestModifiedDate / getFileLastModifiedDate / getFilesLatestModified power "last updated on" stamps in Nextra, Docusaurus, Starlight, Fumadocs, Rspress.
Full repository toolbox
Init, open, discover, clone; status, stage, commit, blame, diff, branch, checkout, tag, refs, remotes, revwalk — one cohesive Repository API.
Off-main-thread async
cloneAsync, commitAsync, fetchAsync, pushAsync, plus async status/blame/file-date variants, each accepting an AbortSignal.
First-class TypeScript
Hand-annotated index.d.ts with JS-native types: Date, number, Buffer instead of raw libgit2 primitives.
Typed, catchable errors
Git-layer failures carry a stable GitErrorCode, narrowable with the total, never-throwing isGitError() guard; an aborted async call rejects with napi's AbortError instead.
Prebuilt for 15 platforms
Windows (x64/x86/ARM64), macOS (Intel + Apple Silicon), Linux glibc & musl, Android, FreeBSD — no compiler needed.
Push/fetch with real creds & progress
SSH-agent, SSH-key, userpass via Cred; transfer + per-ref update callbacks via RemoteCallbacks.
Deterministic cleanup
dispose() / free() release native handles eagerly (frees Windows packfile fds); using supported.
Show me the code
// ---- 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// ---- 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))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
}
}Prebuilt everywhere
npm install pulls a ready binary — no compiler, no node-gyp.
Windows
3- x64
x86_64-pc-windows-msvc - x86
i686-pc-windows-msvc - ARM64
aarch64-pc-windows-msvc
macOS
2- Intel
x86_64-apple-darwin - Apple Silicon
aarch64-apple-darwin
Linux (glibc)
5- x64
x86_64-unknown-linux-gnu - ARM64
aarch64-unknown-linux-gnu - ARMv7
armv7-unknown-linux-gnueabihf - ppc64le
powerpc64le-unknown-linux-gnu - s390x
s390x-unknown-linux-gnu
Linux (musl)
2- x64
x86_64-unknown-linux-musl - ARM64
aarch64-unknown-linux-musl
Android
2- ARM64
aarch64-linux-android - ARMv7
armv7-linux-androideabi
FreeBSD
1- x64
x86_64-unknown-freebsd
15 prebuilt triples · Node >= 10