git-super
v0.5.0
Published
Git commands that treat superprojects and submodule interiors as one product
Readme
git-super
Git commands that treat a superproject and its submodule interiors as one product.
Ordinary Git plumbing stops at a gitlink. git diff --name-only A..B reports vendor/tool; it does not report vendor/tool/src/index.ts. git merge-base --is-ancestor <sha> <ref> returns a false negative when the SHA belongs to a submodule and the ref is a superproject commit. git-super asks each question in the repository that owns the answer, prefixes inner paths, and names every repository it consulted.
Install from npm; a git install resolves the TypeScript source and runs only under Bun.
Why it exists
The dangerous failure is not an error — it is a check that passes because it never looked.
A guard that lists changed files and decides whether to run tests, require a review, or block a release will happily report "nothing changed here" for a commit that rewrote a submodule entirely. It sees one path, vendor/tool, and no rule matches it. Nothing errors. The build is green. This is the whole class: a gitlink is a boundary that plumbing silently treats as a leaf.
The same boundary produces false negatives elsewhere. Ancestry questions answered against the wrong repository report already-merged commits as unmerged. Automation that retries a rebase against a moving target cannot tell which repository moved. Recursive checkouts fetch objects they already have, or hang fetching objects nobody recorded.
Git has display flags for submodule diffs, but no native flag that turns gitlinks into a composable file set. The answer belongs at the Git invocation layer: callers change git diff to git super diff, whether they are TypeScript programs, shell scripts, CI jobs, or humans.
Commands
git super --repo /work/product diff --name-only <range>
git super --repo /work/product diff --stat <range>
git super --repo /work/product diff --patch <range>
git super --repo /work/product status --porcelain
git super --repo /work/product --json status --index-file /absolute/path/to/commit-index
git super --repo /work/product merge-base --is-ancestor <sha> <superproject-ref>
git super --repo /work/product merge <commit> [-m <message>] [--no-verify]
git super --repo /work/product gitlink write <path> <commit>
git super --repo /work/product submodule prepare <exact-root-commit> --remote <root-remote-name-or-url> --json
git super --repo /work/product pull --ff-only [<repository> [<refspec>...]]
git super --repo /work/product push [--recurse-submodules=check|on-demand|only|no] [<remote> [<refspec>...]]
git super --repo /work/product worktree add <path> <commit> [--reference <path>]
git super --repo /work/product --json worktree remove <path> --retain <directory>Use --repo to name the superproject explicitly for enriched operations. diff accepts --diff-filter, --cached, and -z. status includes tracked and untracked changes in checked-out submodules. In hook context, status --index-file <absolute path> reads the index Git will commit for the root repository only; nested submodules use their own indexes. The JSON root consultedRepositories row names the selected index file. A relative or missing index file is refused. merge-base --is-ancestor discovers which repository owns the first commit and compares it with that repository's pin in the selected superproject ref.
status --json always includes submoduleProblems, an empty array when none are found. Each entry names the submodule path and reason, plus gitDir when resolved. Status can exit 0 while reporting these problems: the root read succeeded, but the affected child remains unknown. Ordinary tracked, untracked, and staged gitlink changes remain in records. A nonempty submoduleProblems array bars worktree removal even when records is empty. Non-JSON output names the problems on stderr.
Normal path or porcelain output stays on stdout. A rendered report of the repositories consulted goes to stderr, so existing pipelines stay composable. --json puts the result and the consulted repositories together on stdout.
diff reports removed components in notCompared with reason: "removed" and continues comparing readable repositories. It does not open a removed component. Missing component objects are collected as reason: "unreadable", with the repository, object IDs and fetch remedy; the CLI prints every observation before exiting 2. Readable paths remain available in the partial result. Consumers must inspect notCompared before treating a comparison as complete.
diff, status, merge-base, pull and worktree remove accept repeated --exclude-submodule <path> options after the command. Each path is literal and relative to the superproject root; its descendants are excluded too. JSON results name excluded paths in notCompared, and human output explains each skip on stderr. Comparison exclusions skip component content without declaring it clean. Pull admits empty uninitialized excluded checkouts and absent additions; unsafe checkouts refuse. Removal also requires parent identity and surviving store custody: stores within either deletion path refuse, and external stores remain untouched.
{
"consultedRepositories": [
{ "path": ".", "root": "/work/product" },
{
"from": "0123456789abcdef0123456789abcdef01234567",
"path": "vendor/tool",
"root": "/work/product/vendor/tool",
"to": "89abcdef0123456789abcdef0123456789abcdef"
}
],
"deletedPaths": [],
"notCompared": [],
"paths": ["vendor/tool/src/index.ts"]
}Status reports an existing empty submodule directory as path: not checked out on stderr and in the JSON uninitializedSubmodules list (always present, empty when all checkouts exist). It does not consult the parent repository as that child or invent a dirty record. A staged gitlink change remains a native dirty record, and clean worktree removal can proceed when submodules have never been checked out. A nonempty uninitialized directory refuses with its path, so ignored files cannot be discarded.
Missing or unreadable checkout directories, missing commit objects and ambiguous commit ownership fail loudly. Removed gitlinks are named skips rather than fabricated empty comparisons. Pull, push and merge refuse included uninitialized child repositories already recorded before the operation. Newly added included gitlinks are initialized by pull and merge at their exact selected pins.
Merge and settle gitlinks
merge <commit> computes the prospective merge tree before applying it. Submodules with the same logical remote host and namespace as the root participate in branch forwarding; other hosted submodules remain as-written and receive no publication. Logical identities are read before Git's transport URL rewrites. A local path or file URL cannot establish this ownership relation.
Resolve ordinary conflicts on the original branch. merge <target> --preserve-conflicts leaves ordinary file conflicts in Git's native index and working files. It returns state: "failed", partial: true, exit 2, and a pending block containing branch, head, target, and unmergedPaths. Its diagnostic prints the exact continuation command and git merge --abort.
$ git super --repo . merge feature --preserve-conflicts
$ git add path/to/resolved-fileAfter resolving and staging the ordinary files, run the printed merge <target> --continue --expected-head <oid> --expected-branch <full-ref> command. Branch, HEAD and target are required caller leases; continuation validates the native state it finds. The two modes are mutually exclusive. Mixed ordinary/gitlink conflicts are refused unchanged, naming the gitlink paths. The default merge refusal remains unchanged.
Continuation requires one matching MERGE_HEAD, no unmerged stages, and clean submodule checkouts at the pins recorded by pre-merge HEAD. It recomputes the prospective merge solely to validate required gitlinks while preserving the staged ordinary resolution, including a resolved .gitmodules. A lowered pin from git add -A, an extra gitlink, moved or dirty child work, or a mismatched lease produces exit 1, partial: false, the observed pending block, and a diagnostic confirming that the pending merge was left as found. A pin refusal names the staged and required pins and the restaging command. Native rebase or cherry-pick state is refused before starting a merge.
The submodule branch comes from submodule.<name>.branch in local Git config, then the frozen .gitmodules, then the remote's symbolic HEAD. A value of . uses the current superproject branch and refuses when that HEAD is detached. Each participating pin is compared with the fetched branch:
--no-fetch reuses only untouched Equal pins whose persistent component store records a successful refresh of the current origin URL and tracking OID within the last ten minutes. Moved pins and cached Behind/Ahead/Diverged pins fetch fresh before classification. Borrowed checkouts use the validated reference/alternate store, never the timestamp of copied refs. Successful fetches append an explicit same-OID reflog observation, including unchanged refs with logging disabled. These observations are a cache: losing the store or its observation expires reuse and requires a fetch; correctness never depends on retaining them. A configured origin that differs from the declared repository is named and refreshed from the declared URL, without reusing that read for another publication destination. Unreadable observations or failed recording produce a named diagnostic and no recording retry. Root and component publication retain their independent remote observations and leases.
| Authored pin relative to the submodule branch | Result |
| --------------------------------------------- | ------------------------------------------------------------------------------- |
| Equal | Keep the pin. |
| Behind | Raise the root gitlink to the fetched branch tip. |
| Ahead | Keep the authored pin and freeze its branch publication. |
| Diverged | Refuse an incoming change; preserve an untouched divergence as left-off-main. |
Fresh merges apply a no-ff merge without committing it, then write the proved raises. Continuation uses the resolved native index and writes only its proved raises. Existing affected submodule checkouts settle at their staged pins before the concluding commit and hooks, including active nested submodules at the exact pins recorded by their parent commits. Native checkout refuses changes it would overwrite and missing nested objects; it does not fetch or select newer nested branch tips. Newly introduced submodules use persistent stores for object and branch inspection, then the shared recursive materializer initializes them at stage-zero index pins after any raises and before existing checkouts settle. An uninitialized new nested gitlink in an existing changed parent is refused before the root merge, with its full path and required pin in an initializations row. Raises and retained anomalies appear in Settled: trailers; the merge also freezes recursive publication inputs for ordered pushing.
After either a fresh merge or continuation, Git Super verifies the committed parents, gitlinks and .gitmodules against the frozen inputs. Hooks may format ordinary files. A mismatch after HEAD moves returns a partial failure with the observed commit; it writes no success receipt and preserves the commit without rollback.
When Git Super raises root gitlinks, it writes a temporary receipt at refs/git-super/receipts/<merge>. The receipt's sole parent is that exact merge, and its receipt.json contains only the automatic root-entry changes made by that invocation. Raises already staged by an earlier failed invocation are excluded. Callers can copy the exact payload into a durable record before deleting the temporary ref under its exact old-value lease.
Human output puts the resulting merge commit on stdout and settlement evidence on stderr. --json emits one byte-clean SuperMergeResult with the same commit and gitlink rows. Its additive checkouts rows record each affected direct submodule's pin in root HEAD (recorded), staged gitlink (index), exact pre-operation checkout (preCheckout), observed checkout, and whether it is settled, settle-failed, restored, restore-failed, or not-run. Nested checkout settlement and restoration follow the pins recorded by those parent commits.
Its additive initializations rows name newly added paths, staged index, observed checkout when available, and initialized, initialization-failed, not-run, or initialization-required. These paths have no invented prior recorded pin. An initialization failure leaves the root merge staged and existing checkouts unmoved; it names incoming, staged, and observed pins and preserves every directory and store.
Its additive steps rows time the merge's phases as { "name", "ms" }, in the order they ran. The names form a closed list, and the phases run one after another without overlapping, covering the whole call, so their ms add up to its wall time:
| name | covers |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| preflight | finding the root, taking the worktree lock (including any wait for it), the clean-worktree check, and resolving HEAD and the target |
| merge-tree | the prospective merge tree, plus composing any diverged gitlinks it conflicts on |
| plan | classifying every gitlink against its submodule main: child-main fetches and the nested descent |
| capture | the Settled: trailers and the frozen push intent |
| checkouts | preparing affected submodule checkouts and proving the worktree clean |
| merge | the native no-ff merge and the gitlink raises |
| initialize | materializing added submodules at staged index pins before existing checkouts move |
| settle | checking affected submodules out at their staged pins |
| commit | the concluding commit, its hooks, and the root receipt |
A merge that stops early ends its steps on the phase that stopped it. A new phase is added to this list, never left outside every step. Consumers should record an unfamiliar name as given rather than drop it.
A failure before the root merge exits 1 and leaves root HEAD, index and working files unchanged; object fetching and submodule-store preparation may already have occurred. A failure after Git applies the uncommitted merge exits 2 with partial: true, completed and not-run gitlink rows, and checkout recovery evidence. If the concluding commit is rejected, Git Super keeps the root merge and staged index intact while restoring each submodule to the pin recorded by pre-merge root HEAD. If any restoration cannot be proved, it leaves the partial state untouched, marks the affected row restore-failed, and prints full recorded, staged-index, checkout, and pre-checkout object IDs; do not retry until those rows are restored and re-observed. A repository with no submodules or nothing to raise still returns the real merge commit plus an empty gitlink-row set. --no-verify is an explicit emergency bypass, not the normal settlement path.
Exact gitlink write
gitlink write <path> <commit> updates one existing mode-160000 index entry to an exact commit without moving the submodule checkout. It is mechanics only: the caller decides which pin should be written. The command serializes through the shared mutation lock and observes the resulting stage-zero entry before reporting success; an unreadable or mismatched post-write observation reports unknown and exits nonzero. A lock-release failure after an accepted write reports failed and partial; if observation also failed, the repository remains unknown instead of being overclaimed as updated.
The path must already be a gitlink, and the exact commit object must exist in either its initialized checkout or its configured repository under the superproject's common Git directory. A missing path, repository, or commit fails with a diagnostic naming the repository, path, object ID, and remedy. The operation never adds a path, fetches a commit, checks out a submodule, or chooses whether a pin should advance. --json emits the same GitSuperResult returned by the writeGitlink library export.
Prepare persistent submodule stores
submodule prepare <exact-root-commit> --remote <root-remote-name-or-url> --json reads direct gitlinks and .gitmodules only from the named root commit. Both inputs are required: it never chooses checkout HEAD or treats a stored submodule origin as authority. The selected root remote resolves relative frozen URLs; JSON returns the normal GitSuperResult envelope plus submodules, each with name, path, gitlink, resolved url, and absolute gitdir.
First use needs a readable root and exact commit, a configured remote name or explicit URL, and a writable root common Git directory. Under the shared mutation lock it creates one checkout-free repository at that common directory's existing modules/<name> location, configures it non-bare with an initial frozen URL origin, and validates it again. It performs no clone, fetch, checkout, root ref, or index write. Warm calls preserve existing store configuration and origin while returning the frozen descriptor URL. A valid root with no direct gitlinks succeeds with submodules: [].
An unresolved root, malformed frozen descriptor, unsafe store location, or partial/invalid existing store returns a nonzero structured detail; it is never an empty result or implicit reinitialization. Cold local stores are reported as updated, warm stores as unchanged, and a later failure keeps already prepared rows as partial evidence rather than deleting them. The returned stores are compatible with a later ordinary git submodule update; observation code remains responsible for any network read and exact-object fetch.
Preparation covers one parent commit's direct gitlinks. To prepare another level, use the returned parent gitdir as --repo, its exact selected gitlink as the commit, and its frozen resolved url as --remote. The parent commit must already be readable in that store. Preparation itself does not fetch the child commits; those exact commits must be fetched into the returned child stores before a checkout can borrow them.
When merge planning finds absent nested stores beneath a prepared parent, it refuses before moving the root and reports nested-store-missing. The diagnostic names every absent store at the first failing depth, full paths, exact parent and child commits, and the parent store. Run its printed submodule prepare and exact-pin fetch commands, then retry the original operation. A deeper missing level appears in the next repair round. These commands work without a parent checkout and use the selected declarations' URLs, even when an existing origin or local submodule URL differs. If the declared parent remote cannot be proved, the refusal names that prerequisite rather than guessing commands. Invalid or unreadable stores retain their causes and are not treated as absent stores to recreate.
Observe submodule tips
git-super super observe --protocol=1 (or git super observe --protocol=1) reads one UTF-8 JSON document from stdin in the owning root repository:
{
"version": 1,
"root": {
"remote": "https://example.org/team/product.git",
"targetRef": "refs/heads/main",
"targetOid": "<full root OID>"
},
"checked": [
{
"mergeOid": "<actual checked merge OID>",
"recordRef": "refs/changes/main/example",
"recordOid": "<captured record OID>"
}
],
"fence": {
"prefixes": ["refs/changes/main/"],
"refs": [{ "ref": "refs/changes/main/example", "oid": "<captured record OID>" }]
}
}The caller supplies every advertised ref under its literal prefixes, including refs it does not otherwise recognize. Each checked record must match that same reading. Empty checked/history lists still examine current direct-submodule tips. GitSuper reads frozen descriptors and merge intents, uses the existing native branch resolver, and excludes children outside the root's hosted namespace. Local paths cannot establish ownership. A current tip is explained only by the captured root pin or the exact published source of a current checked merge; its expected old value is not authority.
Each read gets one attempt. After all child reads, a complete root advertisement must still match the captured target and selected refs. Only then does stdout receive {version:1,outcome,message,notices:[{id,text}]}. IDs are stable opaque strings; text is complete human wording. Exit 0 means observed, including an explicitly described empty observation; 3 means changed-during-read; 4 means unavailable-transport; 2 means invalid. Every non-observed result has no notices. These exits belong to this protocol only.
The operation may prepare and fetch into the existing isolated submodule object stores. It writes no caller refs, FETCH_HEAD, worktrees, queue records or remote refs. Missing objects, invalid descriptors and malformed witnesses remain explicit failures. The caller owns observation cadence, process deadline, raw evidence retention and notice delivery; this command adds no service or verdict cache.
Safe fast-forward pull
pull --ff-only fetches and freezes one exact root target. It then works out the full graph of initialized submodules without checking anything out, fetches only the recorded child commits it is missing, and tests every working-tree change before the first write. Applying the change rechecks the remote ref and every repository HEAD under a shared lock, fast-forwards the root, then checks out changed submodules at their exact recorded commits. Additions use the same recursive materializer as merge, reading .gitmodules and pins from the updated parent HEAD and borrowing durable local stores where available.
If the root already contains the target, pull keeps the current root tree and its submodule pins and reports why it is already up to date.
With no repository or refspec, pull uses the current branch's configured upstream. With no refspec, a named repository supplies that same upstream branch. A branch with no upstream fails and says so, rather than guessing origin/main.
Unrelated staged, tracked, untracked, and ignored files survive. A path the incoming graph would overwrite fails before the root moves. Divergence, an unpublished detached child commit, a remote target that changes mid-operation, lock contention, and unavailable objects all fail without merging, rebasing, stashing, forcing, or resolving conflicts. If native Git fails after an earlier repository already changed, the result says partial and every later repository is not-run.
--dry-run fetches, freezes, and checks without changing a checkout, index, or local branch. It is evidence about the current plan, not a promise that hooks, credentials, remote refs, or filesystems will hold still afterwards.
--json emits one stable GitSuperResult on success and on operational failure alike. Success exits 0; failed, partial, or unknown results exit nonzero.
Recursive push
push resolves every nonempty selected source to an exact object ID, freezes each destination's advertised old value, and rechecks it under the shared lock. With no refspecs it asks Git for the configured default push selection using a non-writing dry run, then applies exactly those rows. General force refspecs and implicit fetch-racy leases are refused; --force-with-lease=<full-ref>:<expected> is explicit, and an empty expected value means create-only.
checkrequires every recorded child commit to be reachable from at least one configured remote, then pushes root refs.on-demandpushes missing nested commits leaf-first and root-last.onlypublishes the nested commits and leaves root refs untouched.nopushes only the selected root refs.
An explicit :<destination> refspec deletes that ref only with an exact --force-with-lease=<destination>:<expected-old-oid> (or a library expectedDestination). A missing lease refuses. An already absent destination is an unchanged retry. Deletions can share one atomic root group with ordinary updates; every ref keeps its lease.
--atomic is passed separately to each single-repository push. It never makes several repositories atomic. A child may stay published when a later root hook or remote rejects; the result then reports partial: true. Hooks run unless --no-verify is explicit. Signed-push mode and push options pass through unchanged.
Hooks, credential helpers, and remote helpers stay native Git behavior. A timeout, a rejected hook, an unreachable remote, an unreadable response, or a post-write check that disagrees is never turned into an empty or successful result. Selecting no refs at all is an input error with an explanation, not a silent success. Push is covered by tests/push.test.ts.
Landing across repositories
Gerrit's cross-repository topics and Aviator's ChangeSets each group several repositories' changes into one submission gesture, but neither documents an atomic guarantee once repositories start merging independently. Gerrit: a same-repository topic submits atomically, while a multi-repository topic can fail into a partial submission (cross-repository-changes); Gerrit documents compensating revert commits, reviewed and submitted normally, but it does not guarantee automatic rollback of a partial multi-repository submission. Aviator: a ChangeSet is validated as a whole and fails before merging if any check fails, but partial-merge behavior once some repositories in a set have already merged isn't documented (ChangeSets). git-super does not claim an automatic cross-repository rollback guarantee either; see Yrd's own README for submission policy, which this section does not repeat.
The merge stores resolved child remotes, destination branches, source commits and expected old values in its Git-Super-Push: trailer. An ordinary recursive push of that exact merge uses these frozen values even if local branch or remote configuration has changed. It validates destinations before publication, advances children before root refs, and accepts an identical completed update on retry. A third destination value refuses further writes.
A record can retain the merge through commit ancestry while keeping its own tree empty. Before publishing such a record, Git Super finds newly reachable frozen merges, retains owned child sources at refs/git-super/pins/<oid>, and verifies that each source can be fetched through its retained ref. This does not advance child branches. Later record pushes do not replay already published historical intents.
A fresh clone can fetch the record and retry publication of its exact merge using the retained child sources, without the author's checkout, a replacement merge, or a materialized child worktree. Retention refs are not automatically reclaimed. External submodules receive no retention writes; indirect record publication refuses when it cannot establish durable external sources without writing external refs.
On first publication of an existing project, a cold owned child store may lack both the pinned commit and its retention ref. Git Super reads that exact ref first. If absent, it fetches advertised branch and tag tips from the frozen child remote and proves one contains the commit before creating the immutable retention ref with an absent lease. An identical concurrent winner succeeds; a conflicting ref, an unreachable commit or a failed read refuses. Adoption retention finishes before child branches or root refs move, and successful retention remains visible if a later publication fails. Warm unchanged children still perform no remote work.
These mechanisms provide ordered publication and retry, not cross-repository rollback. A queue must publish its checked record durably before beginning the landing and retain its root leases for recovery. Yrd owns queue activation and restart orchestration; the Git Super mechanisms alone do not enable that integration.
Worktree with submodules
worktree add <path> <commit> creates a detached worktree and materializes every gitlink at the pins that commit records. It is one program for the whole operation, because git worktree add alone leaves every submodule an empty directory and the recursive checkout that fills them is where callers reimplement borrowing, fallback limits, and rollback slightly differently each time.
Gitlinks borrow their objects from --reference when it is given and from the repository the command stands in otherwise. A pin the reference's stores lack is fetched from the submodule's own remote rather than refused: this is the one caller for which an unbounded fallback is correct, since a commit whose submodules the reference has never seen is exactly what it exists to check out.
Either the worktree stands complete or it does not stand. Any failure after git worktree add already succeeded removes the worktree again and exits nonzero with the reason, so a half-materialized tree is never left behind. When the removal itself fails the result is unknown rather than failed, names the surviving path, and gives the exact command that clears it — that is a different situation from a clean rollback and must not read like one.
git super --json worktree remove <path> --retain <directory> removes one registered, clean, unlocked linked worktree. It checks the root and every populated submodule through the existing recursive status operation. Before Git removes anything, it copies the complete owned per-worktree module metadata (including ordinary objects, refs, and reflogs) outside both deletion paths and compares typed manifests: SHA256 file bytes and objects-link identity are separate evidence. The proof is printed on stderr before one native git worktree remove --force; the force only bypasses Git's blanket refusal of populated submodules after the stronger checks have passed. Dirty, locked, unreadable, or unretained work refuses. The JSON result names the proof. Choose a durable retention directory; copies are never deleted automatically and must be kept at least until the proof's retainUntil date and any longer retention your repository requires.
An objects directory may be a symlink only when its existing canonical directory target is inside the selected Git common directory and outside both the checkout and linked gitdir being removed. Other metadata links, missing targets, targets outside this custody, and targets inside either deletion path refuse with their paths. Retention never copies, mutates, or deletes the external objects. It retains an absolute objects link and records the original declaration and surviving target in manifest.json under externalObjectStores; the archive therefore retains complete owned metadata with a named external dependency, not a self-contained object archive.
Each retained dependency is registered atomically under <git-common-dir>/git-super-retained-borrowers before native removal. Later worktree removal checks the retained copy's actual objects links and refuses if any still target its deletion paths. Repointing a retained link to another linked store keeps its registry entry and protects that new owner. A copy that is gone, has no objects links, or depends only on primary stores under <git-common-dir>/modules is dropped from the registry with its name reported. Other target custody remains unknown and refuses. Unreadable or unresolved retained dependencies refuse; changing a manifest is not proof that the actual copy is independent. Live borrower alternates are still dissociated before removal. A live submodule whose objects directory links into the owner gitdir blocks owner removal, naming its checkout, module and target; an unresolved live objects link also refuses. Git Super does not copy or rewrite those shared objects links.
A commit that records no .gitmodules is not an error. The command is then exactly git worktree add, and the report line says so.
The report line goes to stderr and names the path, the resolved commit, the ref that was asked for when it differs, and the split:
worktree add /work/candidate at 0123456789abcdef0123456789abcdef01234567 (main): 3 gitlinks (2 borrowed, 1 fetched, 0 absent)borrowed + fetched + absent always equals the number of gitlinks considered. Borrowed were already present in the reference's stores; fetched had to come over the network, whether into the reference or straight from the submodule's remote; absent had no reference store offered for them at all. Counting a pin that only became borrowable after a fetch as borrowed would report 0 fetched for a run that went to the network for every single pin, so it does not.
--json emits one stable GitSuperResult carrying the path, the requested and resolved commits, and those counts. Success exits 0 and every failure exits nonzero; git super worktree with an unknown subcommand exits 2 with usage.
Try it
The CLI requires Bun 1.3.14 or newer. Native Git delegation uses process.execve to preserve the process ID, streams, and signals. Invoking the executable with Node prints the Bun requirement before loading CLI dependencies.
The library supports Node 24 or newer and Bun 1.3.14 or newer.
The CLI runs under Bun; the library imports from Node 24 and Bun. verifyPublishable.bunOnlyBins declares this, so release verification runs the CLI under Bun and records its Node row as not asked.
Node consumers need the built npm package and a built Node-compatible @bearly/flock dependency. Installing TypeScript source beneath node_modules does not provide that distribution.
Node library operations have been exercised on Linux. Node transport and graph operations on macOS remain unmeasured.
The built flock artifact passes Node 24 acquire, contention, release and reacquire checks on Linux and macOS; see the CI run.
The package name is reserved; this first source release is not yet on npm. Clone it, install its public dependencies, and put its executable on PATH for one command:
git clone https://github.com/beorn/git-super.git
cd git-super
bun install
PATH="$PWD/bin:$PATH" git super -hNo global installation is required, and a clean clone stands alone:
bun install --frozen-lockfile
bun run test
bun run typecheckArchitecture
src/diff.ts,src/status.ts, andsrc/merge-base.tsare pure read-plumbing services over an explicit Git process adapter.src/worktree.tsis the injected write-plumbing service: add, lock, unlock, inspect, exact removal, recovery, and hook quarantine. Its lock lives at<common-dir>/yrd-worktree-mutations/writer.lock. That path is a compatibility name kept deliberately: an earlier tool used it, and sharing the name is what makes old and new callers exclude one another instead of writing at the same time.src/submodules.tsis the single recursive materializer. It proves exact gitlinks before borrowing local objects, reports remote fallbacks, supports a top-level path allowlist, and recurses through nested gitlinks.src/submodule-origin.tsresolves absolute, URL, scp-like, and relative.gitmodulesorigins without imposing any product policy.src/commit-graph.tsis the strict, read-only parser for gitlinks recorded in an exact commit. Pull and push share it rather than reading.gitmodulesindependently.src/objects.tsis the exact-commit presence and fetch primitive shared by graph consumers.src/gitlink.tsis the update-only index-pin writer. It validates the existing gitlink and target commit, writes under the shared mutation lock, and never checks out or chooses a target.src/merge.tspreflights one no-ff merge, fetches submodule main refs, refuses incoming off-main pins, and settles proven-behind pins while preserving partial-write evidence.src/process.tsis the public injected Git process capability. Its local process retries stalledfetchandls-remotereads up to three attempts. An exact SSHPermission denied (publickey).refusal on one of those reads gets one announced retry after three seconds. The retry keeps Git's effective SSH command (GIT_SSH_COMMAND,core.sshCommand,GIT_SSH, thenssh) and appends-vso its stderr records the offered key. Other nonzero exits and writes are never retried.src/result.tsowns the shared repository/ref result vocabulary and how results aggregate.src/pull.tsowns the fetch, freeze, check, recheck, and apply fast-forward operation.src/worktree-add.tscomposes the two write services: one detachedgit worktree addplus one recursive materialization, joined by the rollback that keeps them a single outcome.src/push.tsplans exact ref updates, proves recursive commit availability, and applies explicit per-ref leases child-first and root-last. It exposes transport mechanics and no submission, promotion, or retry policy.src/commands.tsexposes a platform-neutral command tree from the published@silvery/commandpackage; every CLI request passes throughresolveInvocation().src/report.tsxrenders the fail-loud repository witness.src/cli.tsadapts Commander parsing, stdout-compatible data, stable JSON, and the report around the command tree.
The package depends only on published packages: @bearly/flock, @silvery/command, @silvery/commander, react, and silvery. It contains no scheduler, no delivery daemon, no task tracker, and no imports from any host repository.
Library consumers may import the root git-super surface, or git-super/gitlink for exact index-pin writes, git-super/commit-graph for frozen submodule descriptors, git-super/objects for exact-object loading, git-super/submodule-origin for remote resolution, git-super/worktree for injected worktree mechanics, git-super/status for recursive repository inventory and status, and git-super/submodules for recursive materialization.
GitWorktreeStore.inspectRemoval(path, { excludedSubmodules? }) checks excluded checkout and store custody without writer leases, retention or rehoming, and returns notCompared, consultedRepositories and uninitializedSubmodules from the existing status population. This read-only inspection is a snapshot, not permission: remove rechecks admission under its mutation lock. Uninitialized included components remain observations; a caller such as Bearly may require initialization before it can classify their ignored content. Exclusions select skipped components and do not change the store's default removal policy.
The recursive materializer's source option defaults to "head", reading top-level .gitmodules and pins from HEAD. Merge passes "index" to read stage-0 declarations and pins from its staged merge; named paths without a declared stage-0 gitlink refuse. Nested levels always read their checked-out parent HEAD. Host adapters expose HEAD materialization only and omit source from their options.
Materialization classifies excludedSubmodules from frozen parent evidence before probing child repositories, including when no submodules remain. Declared absent or empty checkouts and actual absence produce explicit skipped observations. Conflicting declarations, file content, symlinks and unsafe ancestors refuse. Stage-0 observations retain the captured manifest object and index entries separately from the actual parent HEAD; unmerged entries refuse before classification.
materializeSubmodulesWithProcess(process, options, processOptions) accepts resolveReferenceWorktree: true to discover the primary worktree through the same resolver as the host adapters. Merge and pull enable it: linked worktrees borrow validated durable prepared stores, including prepared nested stores, with --reference and --no-fetch. A reference that deliberately removed a path refuses; an unreadable removal history refuses before admission or fetching. When the caller is the primary worktree, discovery supplies no separate reference, so an absent pin uses ordinary submodule initialization and its configured remote. This includes a primary submodule checkout whose registered primary path spells its Git directory: discovery binds the actual target checkout and compares absolute Git directories to recognize self. Failed identity reads or a probe ascending into an enclosing repository refuse by name. The result counts initialized paths without a separate reference as unreferenced, and merge and pull report them.
The third argument also accepts timeoutMs, bounding each Git command, and detached, putting each command in its own process group. Merge passes its command timeout while holding the writer lock; pull preserves detached apply commands. Materialization failures retain the process's timeout and failure details.
What this package deliberately does not decide: worktree naming, leases, branch shapes, queue admission, queue-round retry policy, and lifecycle. Those belong to the caller. Yrd's separate one-time retry of a remote-class could-not-judge result re-judges the change in a later round; it does not replace or suppress the bounded Git read retries above. Both layers announce their retry, and a second refusal at the command layer remains a failure.
