1.0 is a one-time breaking sweep that regularizes the whole API surface: JS-native
types (Date, number, Buffer), consistent names, nullable finders, OR-able
numeric bitflags, typed error codes, and an explicit dispose(). This guide lists
every breaking change, ordered by how likely it is to affect you.
Who needs to read what
| You use the library for… | Read |
|---|---|
| Doc-site "Last updated on" dates (Nextra, Docusaurus, Starlight, Fumadocs, Rspress) | §1 (and it's probably all you need) |
| Reading config | §3 |
| Anything with commit/blame/signature times | §2 |
| Diff / cred / sort flags | §5 |
| Staging, remotes, tags, push callbacks | §4, §6, §7, §8, §10 (behavior) |
The 30-second version: the widely-used getFileLatestModifiedDate() /
getFileCreatedDate() are unchanged from 0.1.x — still a millisecond number,
still throwing for a path with no history. 1.0 adds a null-safe
getFileLastModifiedDate() that returns Date | null. Everything else is
renames, type changes, and the runtime behavior changes in §10 (single-use
options, async fetch/push pre-throws, dispose semantics).
Preserved from 0.1.x — no migration needed. They still return a millisecond
number (since the Unix epoch) and still throw for a path with no matching
commit.
const ms = repo.getFileLatestModifiedDate('docs/intro.md') // number (ms); throws if no history
const iso = new Date(ms).toISOString()1.0 adds getFileLastModifiedDate, returning Date | null — a Date for the
last commit that touched the file, or null (instead of throwing) for a path with
no history. Reach for it when you'd rather branch on null than try/catch.
const date = repo.getFileLastModifiedDate('docs/intro.md') // Date | null
const label = date ? formatDate(date) : 'unpublished'Warning
null → 1970, silently. getFileLastModifiedDate returns null for a file
with no history, and new Date(null) is 1970-01-01T00:00:00Z, not
Invalid Date — so new Date(repo.getFileLastModifiedDate(f)) renders
"last updated: 1970" for untracked/new files. Branch on null explicitly:
const date = repo.getFileLastModifiedDate(f)
const label = date ? formatDate(date) : 'unpublished'The object/bulk metadata accessors dropped the mismatched …Modification stem:
- repo.getFileLatestModification(filepath) // → FileModification | null
+ repo.getFileLatestModified(filepath)
- repo.getFileLatestModificationAsync(filepath)
+ repo.getFileLatestModifiedAsync(filepath)
- repo.getFilesLatestModification(filepaths) // → Record<string, FileModification | undefined | null>
+ repo.getFilesLatestModified(filepaths)
- repo.getFilesLatestModificationAsync(filepaths)
+ repo.getFilesLatestModifiedAsync(filepaths)getFilesLatestModified return type is now Record<string, FileModification | null>
(the spurious | undefined is gone; a requested path that resolves to nothing is
present as an own key with value null). The number-returning
getFileLatestModifiedDate and getFileCreatedDate were not renamed.
FileModification also changed — see §2 (its timestamp field was removed; its
times are now Date).
Most time-bearing fields/returns unified on JS Date. The two legacy file-date
number getters — getFileLatestModifiedDate / getFileCreatedDate — are the
deliberate exception: they keep their 0.1.x millisecond number shape (see §1),
and getFileLastModifiedDate is the new Date | null twin. Mind the old numeric
unit when converting stored values: it was milliseconds since epoch for the
file/blame/FileModification times, but seconds since epoch for Signature.
| Member | 0.1.x | 1.0 |
|---|---|---|
new Signature(name, email, time) |
time: number (seconds) |
time: Date |
Signature.when() |
number (seconds) |
Date |
FileModification.authorTime / .committerTime |
number (ms) |
Date |
FileModification.timestamp |
number (ms) |
removed (use committerTime) |
BlameHunk.finalTime |
number (ms) |
Date |
getFileLastModifiedDate (§1, new) |
(n/a) | Date | null |
- new Signature('A', 'a@x', Math.floor(Date.now() / 1000)) // 0.1.x took seconds
+ new Signature('A', 'a@x', new Date())Converting a stored value: Signature times were seconds → new Date(sec * 1000);
file/blame/FileModification times were already ms → new Date(ms).
Warning
BlameHunk.finalTime sentinel flipped truthiness. When a hunk has no final
signature (e.g. an in-memory buffer blame), 0.1.x returned 0 (falsy); 1.0
returns new Date(0) = 1970-01-01T00:00:00Z (truthy). An if (hunk.finalTime)
guard that treated 0 as "missing" now passes — instead test the author fields,
which are omitted (undefined) when absent (hunk.finalAuthorName == null),
or check hunk.finalTime.getTime() === 0.
The Config accessors were renamed to JS-idiomatic names and the integer split
changed to number vs bigint.
| 0.1.x | 1.0 |
|---|---|
getStringValue(k) |
getString(k) |
getBool(k) |
getBoolean(k) |
getI32(k) |
getNumber(k) |
getI64(k) |
getNumber(k) (safe-int) or getBigInt(k) (full i64) |
setStr(k, v) |
setString(k, v) |
setBool(k, v) |
setBoolean(k, v) |
setI32(k, v) |
setNumber(k, v) |
setI64(k, v) |
setNumber(k, v) or setBigInt(k, v) |
Note
getNumber throws InvalidArg if the stored value is outside the JS safe-integer
range (±(2⁵³−1)); setNumber throws on a non-integer or out-of-range value. Reach
for getBigInt / setBigInt when you need the full 64-bit range.
Small counts/sizes stopped returning bigint. Drop the n suffixes and BigInt()
wrappers.
| Member | 0.1.x | 1.0 |
|---|---|---|
Blob.size() |
bigint |
number |
Commit.parentCount() |
bigint |
number |
Tree.len() → Tree.size() |
bigint |
number (also renamed) |
Index.count() → Index.size() |
number |
number (renamed) |
DiffFile.size() |
bigint |
number |
- if (blob.size() > 1000n) …
+ if (blob.size() > 1000) …Diff/cred/sort/open flags moved to raw OR-able numbers so you can combine them.
Inputs are typed number but still accept the enum members (their values are
numbers), so setSorting(Sort.Time) keeps working:
- setSorting(sorting: Sort): this
+ setSorting(sorting: number): this // setSorting(Sort.Time | Sort.Reverse) now valid
- static openExt(path, flags: RepositoryOpenFlags, ceilingDirs)
+ static openExt(path, flags: number, ceilingDirs)Returns are now raw numbers — test them with the *Contains helpers (or a
bitwise &) instead of ===:
- if (delta.flags() === DiffFlags.Binary) …
+ if (diffFlagsContains(delta.flags(), DiffFlags.Binary)) …Cred.credtype() → credType() and now returns number. The
CredInfo.credType field (the object passed to a credentials callback)
likewise changed CredentialType → number — compare it with the helper, not ===,
or you'll misread a value that has several bits set:
- if (credInfo.credType === CredentialType.SshKey) …
+ if (credTypeContains(credInfo.credType, CredentialType.SshKey)) …Remote.updateTips's first argument was retyped RemoteUpdateFlags → number (and
renamed updateFetchhead → updateFlags), so you can OR flags together; an existing
updateTips(RemoteUpdateFlags.UpdateFetchHead, …) call still works.
Warning
RepositoryOpenFlags values changed so they can be OR-ed:
NoSearch 0→1, CrossFS 1→2, Bare 2→4, NoDotGit 3→8, FromEnv 4→16.
Any code passing raw integers to openExt breaks (old 2 meant Bare, now
it means CrossFS). Always pass the enum members:
Repository.openExt(path, RepositoryOpenFlags.NoSearch | RepositoryOpenFlags.CrossFS, [])Warning
FromEnv + *Async re-reads env vars on the worker. A handle opened with
RepositoryOpenFlags.FromEnv re-reads GIT_INDEX_FILE / GIT_OBJECT_DIRECTORY /
GIT_ALTERNATE_OBJECT_DIRECTORIES on the worker thread when you call an *Async
method (git-dir, workdir, and namespace are pinned; those lazily-resolved index/ODB
inputs are not). Mutating those env vars between a sync call and a later *Async
call on the same handle can make async observe different index/object state — don't
mutate them mid-flight, or use the synchronous API.
findTag / findTagByPrefix return Tag | null (was: threw NotFound). A
missing tag is null; only real errors throw.
- try { const t = repo.findTag(oid) } catch { /* missing */ }
+ const t = repo.findTag(oid)
+ if (t === null) { /* missing */ }(findRemote/findTree/findCommit, and workdir()/namespace(), already
returned null.)
Methods (call-site rename required):
| 0.1.x | 1.0 |
|---|---|
repo.message() |
repo.mergeMessage() |
repo.removeMessage() |
repo.removeMergeMessage() |
repo.tagAnnotationCreate(…) |
repo.tagAnnotation(…) |
repo.remoteSetPushurl(…) |
repo.remoteSetPushUrl(…) |
remote.pushurl() |
remote.pushUrl() |
tree.iter() |
tree.entries() |
repo.treeEntryToObject(entry) |
entry.toObject(repo) (the redundant Repository method was removed; TreeEntry.toObject already existed in 0.1.x) |
Parameters (positional — no code change needed, names shown for TS/JSDoc):
| 0.1.x | 1.0 |
|---|---|
credTypeContains(credType, another) |
credTypeContains(credType, flag) |
remoteWithFetch(…, refspect) |
remoteWithFetch(…, refspec) |
(updateTips's first arg also changed type — see §5.)
Multi-arg callbacks became single-object callbacks; tagForeach's tuple became an
object.
- remote.pushTransferProgress((current, total, bytes) => …)
+ remote.pushTransferProgress(({ current, total, bytes }) => …) // PushTransferProgress
- remote.pushUpdateReference((refname, status) => …)
+ remote.pushUpdateReference(({ refname, status }) => …) // PushUpdateReference
- repo.tagForeach(([oid, nameBytes]) => true)
+ repo.tagForeach(({ id, nameBytes }) => true) // TagForeachItemBlob.content() and TreeEntry.nameBytes() return Buffer instead of
Uint8Array. Buffer is a Uint8Array subclass, so reads and
instanceof Uint8Array still pass — this only matters if you round-tripped the
exact type. You gain Buffer conveniences (.toString('utf8'), etc.).
dispose()/free()(new). Release the native repo handle eagerly (frees the Windows.packfd). After disposal, every handle derived from that repo —Index,Config,Tree,Remote,Commit, … — throwsRepository has been disposedif used (as receiver or argument) — with three exceptions. (1) TheRepository'sOption-returning lookups — exactlyworkdir(),namespace(),findRemote(),findTree(),findCommit(),findTag(),findTagByPrefix()— returnnullafter disposal, indistinguishable from ordinary not-found, so guard post-dispose use yourself. (Other nullable lookups such asfindBranch()are not in this set — they throwRepository has been disposedafter dispose, returningnullonly for a genuinely missing branch.) (2) A previously-obtainedRevWalk/TreeIter/Deltasiterator'snext()cannot throw, so after disposal it simply ends (yields nothing) — which can silently look like an empty result. (3) A new*Asynccall made after disposal throws synchronously at the call site (not a rejected promise).dispose()does not cancel already-scheduled*Asyncwork (it runs to completion); cancel with theAbortSignalparameter instead.- Single-use option objects. A
RemoteCallbacksis consumed when attached to aFetchOptions/PushOptions; aProxyOptionsis consumed when attached to either;FetchOptions/PushOptionsare consumed by the firstfetch/push(sync or async), and aFetchOptionsis also consumed byRepoBuilder.fetchOptionsbefore any fetch. Reuse throwsInvalidArg(…"can only be used once"). Construct a fresh instance per attachment/call.Remote.updateTipsis check-only: it does not consume a freshRemoteCallbacks(you may reuse one across severalupdateTipscalls), but it rejects a callbacks object already consumed by aFetchOptions/PushOptionsremoteCallback()(throwsInvalidArg, "RemoteCallbacks has already been used"). fetchAsync/pushAsyncare not drop-in forfetch/push. Their argument/state validation runs synchronously — the CALL itself throws (it does not return a rejected promise), so wrap the call, not just the awaited promise. Both throw for aFetchOptions/PushOptionsthat carries aRemoteCallbacks(.remoteCallback(...)) —InvalidArg, "…does not support RemoteCallbacks; use the synchronous …() instead" — a different rule from the "already used" one above; credential/progress callbacks are main-thread only, so keep those on the synchronousfetch()/push().pushAsyncadditionally throws synchronously if reading the remote'spushurlfails, if neitherpushurlnorurlis a valid UTF-8 URL (a normal remote with justurland nopushurlworks fine), or if configured push refspecs can't be read (only when you pass an empty refspec list). Do not use the sameRemotefrom the main thread while one of its async ops is pending — the underlying git2 handle is notSync. For a named remote,fetchAsyncre-resolves it by name on the worker (reading current on-disk config live); an anonymous remote (remoteAnonymous(url)) uses its captured URL.pushAsyncalways captures the effective push URL + refspecs as a snapshot at call time and pushes via an anonymous remote (working around a libgit2 local-transport quirk that otherwise ignores a configuredpushurl). Because that remote is anonymous,new PushOptions().proxyOptions(new ProxyOptions().auto())underpushAsyncskips per-remoteremote.<name>.proxyconfig (falling back tohttp.*.proxy/env) — a known gap vs the named-remote synchronouspush().- File-date error propagation. The file-history walkers now surface a real
object-read error (e.g. a corrupt object) as a throw; previously any error was
swallowed to
null. For "no matching commit", the scalar null-safe accessors (getFileLastModifiedDate,getFileLatestModified) returnnull, and the bulkgetFilesLatestModifiedreturns a record whose missing paths map tonull(the call itself is nevernull); the legacy number getters (getFileLatestModifiedDate,getFileCreatedDate) throw (0.1.x behavior — see §1). - Typed error codes. Every thrown error carries
.code: GitErrorCodeon both sync and async paths. Narrow with theisGitError(e)guard. Note:AbortSignalcancellation rejects with napi's ownAbortError(.code === 'Cancelled'), which is not aGitErrorCode—isGitErrorreturnsfalsefor it.
import { isGitError, GitErrorCode } from '@napi-rs/simple-git'
try { /* … */ } catch (e) {
if (isGitError(e) && e.code === GitErrorCode.NotFound) { /* handle missing */ }
}- Async, off-thread variants:
cloneAsync,commitAsync,fetchAsync,pushAsync(all take an optionalAbortSignal).fetchAsync/pushAsyncare not behavior-identical to the sync forms (RemoteCallbacks, config/URL resolution, proxy auto-config) — see §10. GitErrorCodeenum +isGitError(e)type guard (§10).diffFlagsContains(flags, flag)helper (mirrorscredTypeContains).getFileLastModifiedDate/getFileLastModifiedDateAsync— null-safeDate | nullcompanion to the unchangednumbergetFileLatestModifiedDate(§1).DiffOptionsargument ondiffTreeToWorkdir/diffTreeToWorkdirWithIndex.Repository.dispose()/free()(§10).