git-span

Command reference

Global options

Every subcommand accepts:

  • --perf — emit performance timings for major git-span operation groups to stderr. Can also be enabled with GIT_SPAN_PERF=1.
  • -h, --help — print help (a summary with -h, full help with --help).
  • -V, --version — print the version (top-level only).

Bare git span with no arguments prints help and exits 0 — it does not list every span. Run git span list with no arguments for that. Any clap usage error (bad flag, missing required argument) exits 2.

Declare and edit

add

git span add [OPTIONS] <NAME> <ANCHORS>...

Add anchors to a span, writing the span file under the span root.

Each anchor is either a bare <path> (whole-file) or <path>#L<start>-L<end> (line range, 1-indexed, inclusive); for example, git span add api-contract src/api.ts#L1-L3 tests/api.test.ts. With --at <commit-ish>, every anchor in this invocation is hashed against the file content at that commit instead of the working tree — hashing against a commit-ish rather than the working tree can make a new anchor itself drifted when the working-tree content differs; the span-wide line reports it honestly.

Re-adding an address the span already tracks does not duplicate the anchor — it is resolved in place, re-hashed against the current content (Added 0 anchors and resolved 1 in place). That is the idiom for confirming an intentional content change drift flagged on a range that did not move — after checking that the span's other anchors still agree with the new content; a coupled doc that now lags is updated first.

Every add ends with a scoped post-write check over the touched span. After the requested-address lines it prints superseded old anchors and old anchors that remain drifted — each with a runnable git span remove next action — then one span-wide line that alone asserts span-wide state: 0 drift across ..., N anchors drifted — ..., state indeterminate (index changed during check), or state unverified. The requested-address lines never assert span health; "clean" appears only as the check's span-wide fact. add shares drift's exit contract: 0 = clean, 1 = drift remains or the check errored, 2 = indeterminate (index changed during check) — retryable. Exits 1 on an invalid name or an out-of-range anchor.

Stage and commit the change with git add .span && git commit.

Key flags:

FlagDescription
--at <COMMIT-ISH>Hash every anchor in this invocation against the file content at <commit-ish> (an ordinary git commit-ish). When omitted, anchors are hashed against the working tree
--format <human|json>Output format for the write-mode result (human or json).
Applies to the write mode: json emits the mutation document (schema_version 1) instead of the human summary.

remove

git span remove <NAME> <ANCHORS>...

Remove anchors from a span, editing the span file under the span root. Each anchor must match an existing one on the span exactly. Stage and commit the change with git add .span && git commit

replace

git span replace [OPTIONS] <NAME> <OLD_ANCHOR> <NEW_ANCHOR>

Replace one anchor on a span with another address, in a single atomic transaction: either the old identity is retired and the new identity installed together, or nothing changes — there is never an intermediate state with both or neither. The new anchor passes the same validation as add (path safety, existence, gitignored/rewritten-target rejection, line-count extent), so a poisoned declaration is never written.

The old address must match an existing anchor exactly — replace never falls back to additive behavior. To refresh an anchor's content hash at an unchanged address, use git span add instead. Refusals (all leave the declaration byte-for-byte unchanged, exit 1): - old-anchor matches no anchor on the span, or matches more than one record (a hand-edited or legacy declaration can hold two same-identity records with different hashes — no canonical choice exists). - old-anchor and new-anchor are the same identity — in-place hash refresh is add's job; replace changes identity. - new-anchor is already tracked by the span — the swap would leave two same-identity records behind; retire the old anchor with remove instead.

On success the output names the retired and installed addresses and reports whether the span is drift-free; --format json carries span, retired, installed, drift_free, and the drifted addresses.

Stage and commit the change with git add .span && git commit.

Key flags:

FlagDescription
--format <human|json>Output format (human or json)

why

git span why [OPTIONS] <NAME> [WHY_TEXT]

Read or stage the span's why — one or two complete present-tense clauses carrying decision-relevant nonlocal context.

State the relationship plus any decisive authority, invariant, permitted difference, lifecycle state, evidence gate, or focused conditional verification. Use role words rather than file names and give every clause a subject and verb. Labels such as Authority: or Removal gate: are optional, but must introduce complete clauses. Omit generic work orders and CLI procedure.

Bare git span why <name> prints the current why; a positional argument writes a new why into the span file, and bare non-terminal invocations fail instead of reading stdin. A write ends with the same post-write span-wide check as add and shares its exit contract (0 clean / 1 drift remains or check errored / 2 index changed, retryable) — the output prints Set why on span <name>. (idempotent), then any remains lines and the span-wide line; superseded lines are absent because why touches no addresses. --format json applies to the write mode only: it emits the mutation document (command: "why", why_written: true) instead of prose, and read mode rejects it fail-closed (exit 1, no stdout) rather than printing prose.

The why is inherited across routine re-anchors only while its meaning remains true. Revise or retire it when the relationship or lifecycle state changes. Commit with git add .span && git commit.

Key flags:

FlagDescription
--format <human|json>Output format for the write-mode result (human or json).
Applies to the write mode; read mode always prints prose.

config

git span config <NAME> [KEY] [VALUE]

Read or set a span's resolver configuration — the span file's trailing [config] block.

Bare git span config <name> prints the span's effective configuration: all three keys with their current values, defaults included, so the output answers what the span is actually doing rather than what the file happens to say.

git span config <name> <key> <value> sets one key and reports the transition it made. The accepted vocabulary is the parser's vocabulary: keys are copy_detection, ignore_whitespace, and follow_moves; values are the documented wire names (off, same-commit, any-file-in-commit, any-file-in-repo) for copy_detection and true/false for the two flags. An unknown key or an out-of-vocabulary value is rejected before any file I/O, and the span file is left byte-identical.

Writes go through the same serializer as every other mutating command, under the exclusive repository lock: a span whose configuration returns to the documented defaults (same-commit / false / false) loses its [config] block entirely, and a no-op write says so while rewriting nothing. A span whose file does not parse — including a Git-conflict-markered one — is still refused; recovery for such spans is git span resolve's job.

Stage and commit the change with git add .span && git commit.

delete

git span delete <NAME>

Delete a span — removes its file under the span root. Destructive. Run git span list afterward to confirm it is gone, then commit the change

Inspect

show

git span show <NAME>

Show the named span — its anchors, why, and config. The bare git span <name> positional form is equivalent to git span show <name>.

A span whose file is in a Git conflict state is refused rather than rendered from conflict-marker text; git span resolve settles it.

list

git span list [OPTIONS] [TARGETS]...

List files and anchors currently tracked by a span

Key flags:

FlagDescription
--porcelainEmit one tab-separated row per anchor instead of human blocks
--offset <N>Skip the first N spans (after filtering, before --limit)
--limit <N>Cap output at N spans (after filtering and --offset)
--onelineOne line per anchor: <span-name> <canonical-address>

tree

git span tree [OPTIONS] <GLOBS>...

Trace blast radius: render a clique-grouped impact tree rooted at the files matched by the given paths/globs.

Starting from the matched anchor paths, the tree expands outward through span co-occurrence to the files each could affect. Files that all anchor the same span — and are therefore mutually connected — collapse onto a single comma-separated line and expand once as a unit, so a cluster that moves together reads as one line.

Roots are file paths and globs only, resolved repo-relative with the same globset matching and exact-path lookup as list/drift (no CWD-relative joining). Unlike list/drift, tree does NOT accept #L<start>-L<end> line-range addresses or bare span names. At least one argument is required, and a pattern matching no anchored file is an error. -d/--depth bounds the expansion (default 3; --depth 0 prints roots only). --format human (the default) prints the nested markdown list; --format json emits the same structure as nested { "members": [...], "children": [...] } nodes for tooling.

Multiple roots are supported and encouraged: pass every file whose blast radius you're tracing in one call — e.g. git span tree fileA fileB fileC --depth 2 — rather than invoking tree once per file and merging the outputs by hand. All roots are unioned before clique grouping, so files that share a span collapse onto one clique line exactly as they would from a single-root call, while roots with no span in common still surface as separate top-level trees in the same listing.

Key flags:

FlagDescription
-d, --depth <N>Maximum expansion depth (0 = roots only)
--format <human|json>Output format

history

git span history [OPTIONS] <SPAN>

Show a span's git history as a git-log-style timeline: newest-first commit entries whose patches are real unified diffs of the span declaration and of each anchor's content at its declared address, rename-aware by content similarity — a re-anchor whose old and new content pair at ≥ 50% similarity (git's -M default) renders as a rename, a sub-threshold re-anchor renders as a deleted anchor plus a new anchor, and a re-anchor whose recorded side cannot be read (binary or unrecoverable content) renders rename from/rename to lines with no similarity claim, the move asserted by the declaration itself. The span's live drift is rendered before the first commit as a headerless diff. That drift is not working-tree-only: it covers every resolver layer git span drift reports, and each block names its observational layers on a drift source line. HEAD identifies the comparison layer; it does not by itself prove that the declaration or content change was committed. Source order is the resolver's: line ranges use Worktree → Index → Head, while whole-file anchors use Index → Worktree → Head.

Outputs human-readable text by default; use --format json for schema_version: 2 JSON carrying the same patches as raw text. When --limit drops older qualifying commits, the requested window is still printed but a warning goes to stderr, and JSON output sets scoped: true (omitted, not false, when the timeline is the complete record). Treat scoped JSON output as a partial record — never read it as evidence that a span has no history or no drift.

Key flags:

FlagDescription
--format <human|json>Output format (human or json)
-n, --limit <LIMIT>Show only the newest N entries.
N counts rendered timeline entries, not walked commits: the walk is always complete, so a narrow anchor in a busy file can never fill the window with commits that changed nothing. Older entries are dropped, a warning goes to stderr, and JSON output carries scoped: true — such a document is a partial record, so never read it as evidence that a span has no history or no drift. -n 0 yields an empty, scoped document.

context

git span context [OPTIONS] [ADDRESSES]...

Return exact dependency context for repository-relative paths or inclusive line ranges as one versioned JSON document.

With --fix, position-only or whitespace-equivalent drift is repaired before reporting, and --operation-id keys a repair retry so the same attempt is idempotent. Output is always the versioned context document.

Key flags:

FlagDescription
--format <json>Output format (json — the versioned context document is the only shape)
--fixRepair position-only or whitespace-equivalent drift before reporting
--operation-id <UUID>Stable idempotency key for a repair retry. Valid only with --fix

Audit and automate

drift

git span drift [OPTIONS] [PATHS]...

Report anchors whose content has drifted from their anchored state. paths may be file paths, globs, or span names; omit them to scan all spans.

With --fix, also re-anchors Moved anchors unconditionally and whitespace-equivalent Changed anchors in place, and resolves .span/ merge conflicts structurally. A Changed anchor whose content differs beyond whitespace is left drifting so the coupling resurfaces for human confirmation — the operator reviews the rewritten spans with git diff and stages only what they agree with. Conflict resolution splits Git textual conflict markers into ours/theirs, enforces a clean-source precondition (all referenced source files must be conflict-free), and calls the structural merge kernel.

Fail-closed cases: - A referenced source file itself contains conflict markers. - The --why text diverged between ours and theirs with no merge base (textual marker split — no base available).

Fully resolved conflicts are written clean and re-anchored in the worktree; spans with residual divergence (same anchor, different range/hash with no clean source) or a divergent --why write the resolved anchors cleanly with minimal residue markers and are not re-staged.

--format human (the default) prints a summary line on a clean scan (0 drift across M spans (K anchors checked)) and per-span listings (each anchor, then the span's why) when drift is found. --format json always emits a document: a clean scan carries "clean": true (schema_version 3) — never empty stdout; informational RESOLVED_PENDING_COMMIT findings may still be listed, so clean does not imply findings: [] — and a drifted scan carries "clean": false plus the findings. --format porcelain prints nothing on a clean scan (no header row).

--fix re-anchors Moved anchors unconditionally — pure line shifts and file renames alike — and whitespace-equivalent Changed anchors (indentation-only edits), tagging them resolved, pending commit — auto-updated on the run that fixes them. Meaning-altering Changed drift — content differs beyond whitespace — is always left for you to resolve with git span add; re-running --fix will not touch it. After --fix, re-run git span drift and re-anchor anything still drifting yourself. --fix never commits; it only rewrites .span/ worktree files — stage and commit with git add .span && git commit.

Exit codes: 0 (no drift, or --no-exit-code with drift present), 1 (drift found, or an interior-anchor integrity violation — the latter is never maskable by --no-exit-code).

A span --fix cannot finish is not analyzable by re-running drift. See git span resolve (--dry-run first), which settles a conflict-markered span file under one explicitly chosen side.

Key flags:

FlagDescription
--format <human|porcelain|json>Output format (human, porcelain, or json; default human). See the command description for what each format prints on clean and drifted scans
--no-exit-codeExit 0 even when drift is found (report-only mode). Does not mask the interior-anchor integrity case
--perf-trace <PATH>Write a CSV of per-anchor wall-clock traces to PATH. Requires a full scan (no positional paths). Columns: span,anchor_id,anchor_sha,path,wall_us,fast_path,status. See the wiki page "Profiling git span drift" (wiki/guides/) for schema and examples
--fixRe-anchor Moved anchors unconditionally and whitespace-equivalent Changed anchors in place by rewriting the span worktree files. A Changed anchor whose content differs beyond whitespace is left drifting — the stored hash gates on content-equivalence so that meaning-altering edits keep surfacing until the operator confirms them. Each surfacing anchor is re-hashed against the deepest drifting layer (Worktree > Index > HEAD). Also resolves .span/ merge conflicts structurally: splits conflict markers into ours/theirs, enforces a clean-source precondition (all referenced source files must be conflict-free), and calls the structural merge kernel. Fully resolved spans are written clean; spans with residual unresolvable anchors or divergent --why text (no merge base) write resolved anchors cleanly with minimal residue markers and are not re-staged. No commit is produced. Only supported with --format human
--clusterGroup this run's drifted spans into connected-component clusters by shared anchored file, so each cluster can be dispatched independently. Rendered as an additional section/field in every --format

doctor

git span doctor

Audit the local span setup — reports span count and findings, plus a store-size summary (bytes used). Exits 0

merge-driver

git span merge-driver <BASE> <OURS> <THEIRS> <MARKER_LEN>

Resolve .span/ merge conflicts structurally when invoked by git as a merge driver. Receives three clean blob temp files and the marker length from git. Resolves only what is structurally derivable without trusting the worktree, which may be mid-merge. Same-anchor range/hash divergence is deferred: minimal conflict markers are written and the command exits non-zero so git span drift --fix can finish authoritatively. Exits 0 when fully resolved, 1 when partial (residue markers written).

Register in .gitattributes:

.span/** merge=span

Register in .git/config:

[merge "span"]
name = git-span structural span merge
driver = git span merge-driver %O %A %B %L

resolve

git span resolve [OPTIONS] <NAME>

Settle every residue entry in a conflict-markered span file under one explicitly chosen side, and write a clean span.

This is the one command whose supported input is a span file carrying Git conflict markers — the file git span merge-driver leaves behind when it cannot resolve an anchor without trusting the worktree. On a file without markers it is a no-op that says so.

--rehash (the default) re-reads each conflicted anchor's source from the worktree and hashes it. --ours/--theirs choose between conflicting values for the same anchor key; anchors declared by only one side remain in the union. They also choose that side's prose for a divergent --why, which is the path for a source that cannot be re-read at all.

Resolution is all-or-nothing per span: any entry that cannot be settled under the chosen side leaves the file byte-identical and names what stopped it, so a failed run is always safe to retry with another side. The result is written to the worktree and never staged — review it with git diff and stage what you agree with.

Key flags:

FlagDescription
--rehashRe-read each conflicted anchor's source file from the worktree and compute its hash. Default when no side flag is given
--oursFor a conflicting value on the same anchor key, keep this side's record; anchors declared only by theirs remain in the union. Keep this side's prose/settings for a divergent --why or [config]
--theirsFor a conflicting value on the same anchor key, keep the other side's record; anchors declared only by ours remain in the union. Keep its prose/settings for a divergent --why or [config]
--dry-runReport what EACH of --rehash/--ours/--theirs would produce, without writing. Side flags are ignored when this is set — all three are always evaluated together so the outcomes can be compared before committing to one
--format <human|json>Output format (human or json)