Skip to content

Latest commit

 

History

History
369 lines (294 loc) · 16.7 KB

File metadata and controls

369 lines (294 loc) · 16.7 KB

Migrating to @napi-rs/simple-git 1.0

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).


§1 — File history / "Last updated" dates ★ most consumers

getFileLatestModifiedDate / getFileCreatedDate (+ …Async) — unchanged

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()

getFileLastModifiedDate (+ …Async) — new, null-safe Date

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'

getFileLatestModification* / getFilesLatestModification* — renamed

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).


§2 — Times are 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.


§3 — Config get/set renamed

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.


§4 — Counts & sizes are number (not bigint)

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) …

§5 — Bitflags are plain numbers now

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.


§6 — Nullable finders

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.)


§7 — Renamed methods & params (quick reference)

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.)


§8 — Callback shapes

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)                      // TagForeachItem

§9 — Binary returns are Buffer

Blob.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.).


§10 — Behavior changes (no signature change)

  • dispose() / free() (new). Release the native repo handle eagerly (frees the Windows .pack fd). After disposal, every handle derived from that repo — Index, Config, Tree, Remote, Commit, … — throws Repository has been disposed if used (as receiver or argument) — with three exceptions. (1) The Repository's Option-returning lookups — exactly workdir(), namespace(), findRemote(), findTree(), findCommit(), findTag(), findTagByPrefix() — return null after disposal, indistinguishable from ordinary not-found, so guard post-dispose use yourself. (Other nullable lookups such as findBranch() are not in this set — they throw Repository has been disposed after dispose, returning null only for a genuinely missing branch.) (2) A previously-obtained RevWalk / TreeIter / Deltas iterator's next() cannot throw, so after disposal it simply ends (yields nothing) — which can silently look like an empty result. (3) A new *Async call made after disposal throws synchronously at the call site (not a rejected promise). dispose() does not cancel already-scheduled *Async work (it runs to completion); cancel with the AbortSignal parameter instead.
  • Single-use option objects. A RemoteCallbacks is consumed when attached to a FetchOptions/PushOptions; a ProxyOptions is consumed when attached to either; FetchOptions/PushOptions are consumed by the first fetch/push (sync or async), and a FetchOptions is also consumed by RepoBuilder.fetchOptions before any fetch. Reuse throws InvalidArg (…"can only be used once"). Construct a fresh instance per attachment/call. Remote.updateTips is check-only: it does not consume a fresh RemoteCallbacks (you may reuse one across several updateTips calls), but it rejects a callbacks object already consumed by a FetchOptions/PushOptions remoteCallback() (throws InvalidArg, "RemoteCallbacks has already been used").
  • fetchAsync / pushAsync are not drop-in for fetch / 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 a FetchOptions/PushOptions that carries a RemoteCallbacks (.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 synchronous fetch()/push(). pushAsync additionally throws synchronously if reading the remote's pushurl fails, if neither pushurl nor url is a valid UTF-8 URL (a normal remote with just url and no pushurl works fine), or if configured push refspecs can't be read (only when you pass an empty refspec list). Do not use the same Remote from the main thread while one of its async ops is pending — the underlying git2 handle is not Sync. For a named remote, fetchAsync re-resolves it by name on the worker (reading current on-disk config live); an anonymous remote (remoteAnonymous(url)) uses its captured URL. pushAsync always 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 configured pushurl). Because that remote is anonymous, new PushOptions().proxyOptions(new ProxyOptions().auto()) under pushAsync skips per-remote remote.<name>.proxy config (falling back to http.*.proxy/env) — a known gap vs the named-remote synchronous push().
  • 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) return null, and the bulk getFilesLatestModified returns a record whose missing paths map to null (the call itself is never null); the legacy number getters (getFileLatestModifiedDate, getFileCreatedDate) throw (0.1.x behavior — see §1).
  • Typed error codes. Every thrown error carries .code: GitErrorCode on both sync and async paths. Narrow with the isGitError(e) guard. Note: AbortSignal cancellation rejects with napi's own AbortError (.code === 'Cancelled'), which is not a GitErrorCodeisGitError returns false for it.
import { isGitError, GitErrorCode } from '@napi-rs/simple-git'
try { /* … */ } catch (e) {
  if (isGitError(e) && e.code === GitErrorCode.NotFound) { /* handle missing */ }
}

New in 1.0 (additive — nothing to migrate)

  • Async, off-thread variants: cloneAsync, commitAsync, fetchAsync, pushAsync (all take an optional AbortSignal). fetchAsync/pushAsync are not behavior-identical to the sync forms (RemoteCallbacks, config/URL resolution, proxy auto-config) — see §10.
  • GitErrorCode enum + isGitError(e) type guard (§10).
  • diffFlagsContains(flags, flag) helper (mirrors credTypeContains).
  • getFileLastModifiedDate / getFileLastModifiedDateAsync — null-safe Date | null companion to the unchanged number getFileLatestModifiedDate (§1).
  • DiffOptions argument on diffTreeToWorkdir / diffTreeToWorkdirWithIndex.
  • Repository.dispose() / free() (§10).