Trees, history & diff
Tree primitives, the result-cache-keyed-by-head-sha commit log pattern, file history, and diffs.
Tree primitives
upsertTreeupsertTree(repo: Repo, treeOid: string | undefined, entries: Map<string, string>): Promise<string>| Parameter | Type | Description |
|---|---|---|
treeOid | string | undefined | The existing root tree, or undefined to start empty. |
entries | Map<string, string> | relativePath → blobOid. |
Returns: Promise<string>
Builds/updates a tree by overlaying blobs onto an existing one, returning the new root tree oid. Handles nested paths — intermediate subtrees are created or updated as needed. Sibling subdirectories are built in parallel.
deleteFromTreedeleteFromTree(repo: Repo, treeOid: string, filePath: string): Promise<string>Returns: Promise<string>
Removes one path from a tree, returning the new root tree oid.
findTreeEntryfindTreeEntry(repo: Repo, rootTreeOid: string, treePath: string): Promise<TreeEntry | null>Returns: Promise<TreeEntry | null>
Resolves a path inside a tree to its entry ({ path, mode, type, oid }),
or null if it doesn't exist. An empty treePath returns the root tree
itself.
listTreeEntrieslistTreeEntries(repo: Repo, treeOid: string, prefix?: string): Promise<TreeEntry[]>Returns: Promise<TreeEntry[]>
A tree's direct children, with path prefixed by prefix if given —
used to build result entries whose keys are already the full path a
caller expects.
getTreeFromRefgetTreeFromRef(repo: Repo, options: { ref, treePath? }, hooks?: OpsHooks): Promise<TreeEntry[]>| Parameter | Type | Description |
|---|---|---|
options.ref | string | Branch name or commit sha. A ref that doesn't resolve throws GitRefNotFoundError. |
options.treePath? | string | Defaults to the repo root. |
Returns: Promise<TreeEntry[]>
A directory listing at a ref's tip — resolves the ref to a commit, then
treePath inside that commit's tree. Results are memoized per resolved
head sha, same pattern as getCommitLog below.
On every cache miss, getTreeFromRef unconditionally calls
hooks.prefetch if provided — unlike getCommitLog's depth-gated
prefetch (below), a tree read has no shallow case to skip it for: it
always needs at least the head commit's tree object, and one more per
path segment on top of that. Wire pack prefetching (GitFs.prefetchPacks)
through hooks.prefetch so a cache-miss tree read doesn't fall through to
isomorphic-git probing indexed packs one at a time.
History: getCommitLog
getCommitLoggetCommitLog(repo: Repo, options: CommitLogOptions, hooks?: OpsHooks): Promise<CommitInfo[]>| Parameter | Type | Description |
|---|---|---|
options.ref | string | Branch name or commit sha. An unborn branch yields []. |
options.depth? | number | Defaults to 50. |
options.knownHeadSha? | string | Pass this when the caller already resolved ref — skips a redundant resolveRef. |
Returns: Promise<CommitInfo[]>
The commit chain from a ref, newest first. ref not resolving (empty
repo, unborn branch) returns []; a commit the walk needs but can't read
after ref already resolved throws instead — see
empty repo vs. missing data.
The one pattern worth internalizing from this whole package
Walking a commit chain is inherently sequential — each commit's oid is
only discoverable by reading its child first — and it's network-round
-trip-bound against object storage. getCommitLog memoizes the
deepest walk seen per resolved head sha in hooks.resultCache and
slices/reuses it for shallower or repeated requests, rather than
re-walking from scratch every time a page needs "the last 10 commits" and
then, moments later, "the last 50."
This is very easy to reimplement badly (or not at all) if you're calling
git.log directly instead of going through this function — and it's
the single biggest performance lever in this whole layer for any page
that shows commit history more than once per request. Don't bypass it.
getCommitHistory: pagination on top of the log cache
getCommitHistorygetCommitHistory(repo: Repo, options: { ref, limit?, skip? }, hooks?: OpsHooks): Promise<CommitInfo[]>Returns: Promise<CommitInfo[]>
A page of commit history (limit/skip): a slice of getCommitLog's
memoized walk, so paging never re-walks what an earlier page already
walked. [] when the ref doesn't exist.
File history & last-commits-per-directory
getFileHistorygetFileHistory(repo: Repo, options: { ref, filePath, limit?, maxDepth? }, hooks?: OpsHooks): Promise<FileHistoryResult>Returns: Promise<FileHistoryResult>
All commits (newest first) that changed one file's blob oid, walking the
first-parent chain. { entries, truncated } — truncated: true means
the walk hit its depth budget (or the requested limit) before
exhausting the branch's full commit chain; there may be older commits
touching this file a deeper walk would surface.
getLastCommitsForTreegetLastCommitsForTree(repo: Repo, options: { ref, treePath?, depth? }, hooks?: OpsHooks): Promise<Record<string, LastCommitInfo>>Returns: Promise<Record<string, CommitSummary>>
For each direct child of treePath, the most recent commit that changed
it — the tree-view "last commit" column GitHub-style UIs show next to
every file and folder.
Both walk history in fixed-size windows, prefetching each window's tree reads in parallel before processing that window sequentially — the walk itself must advance commit-by-commit for correctness (each step needs to know what's still unresolved from the step before), but the tree-object reads backing it don't depend on each other, since every commit's tree oid is already known upfront from the commit log. Preserve this two-phase structure if you ever need to touch this algorithm — collapsing it back to one-commit-at-a-time turns a batched-round-trip walk back into a round-trip-per-commit one, which is the single largest contributor to slow directory listings on large repos.
Diff
getCommitDiffgetCommitDiff(repo: Repo, commitSha: string): Promise<DiffResult>Returns: Promise<DiffResult>
The diff a single commit introduced, against its first parent. For a root
commit (no parent), every file in the tree is reported as added — the
whole tree is walked recursively, not just its top level.
getDiffBetweenRefsgetDiffBetweenRefs(repo: Repo, baseRef: string, compareRef: string): Promise<DiffResult>Returns: Promise<DiffResult>
What compareRef changed since it diverged from baseRef — a pull
request's diff, git's three-dot base...compare: compared against their
merge base, so commits that landed on baseRef after the branch point
don't show up as reverts. Branch names or commit shas are accepted. DiffResult is { files: DiffFile[], totalAdditions, totalDeletions, totalFiles }; each DiffFile carries a git-style
unified patch (diff --git a/... b/... header, no leaking diff
package internals like an Index:/=== block) when kind is "text".
Binary files (kind: "binary") and text over MAX_DIFF_TEXT_BYTES
(kind: "too-large") carry no patch and no content — only
oldSize/newSize; fetch either side by path at its ref if it needs
displaying.
The too-large decision is made before anything is read: each side's size
comes from its pack entry (getObjectSize), so a file over
MAX_DIFF_TEXT_BYTES is reported as too-large without ever being inflated.
Blob reads and size probes share one semaphore (MAX_CONCURRENT_READS), so a
wide commit can't fan out into one object-store request per entry. When a
side's size isn't available — a loose object, or a store without getRange —
the blob is read and the same limit is applied afterwards.