PushStack Docs
git-fs-s3

Trees, history & diff

Tree primitives, the result-cache-keyed-by-head-sha commit log pattern, file history, and diffs.

Tree primitives

functionupsertTree
upsertTree(repo: Repo, treeOid: string | undefined, entries: Map<string, string>): Promise<string>
ParameterTypeDescription
treeOidstring | undefinedThe existing root tree, or undefined to start empty.
entriesMap<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.

deleteFromTree(repo: Repo, treeOid: string, filePath: string): Promise<string>

Returns: Promise<string>

Removes one path from a tree, returning the new root tree oid.

findTreeEntry(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.

listTreeEntries(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.

getTreeFromRef(repo: Repo, options: { ref, treePath? }, hooks?: OpsHooks): Promise<TreeEntry[]>
ParameterTypeDescription
options.refstringBranch name or commit sha. A ref that doesn't resolve throws GitRefNotFoundError.
options.treePath?stringDefaults 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

functiongetCommitLog
getCommitLog(repo: Repo, options: CommitLogOptions, hooks?: OpsHooks): Promise<CommitInfo[]>
ParameterTypeDescription
options.refstringBranch name or commit sha. An unborn branch yields [].
options.depth?numberDefaults to 50.
options.knownHeadSha?stringPass 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

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

getFileHistory(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.

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

getCommitDiff(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.

getDiffBetweenRefs(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.

On this page