The machine contract: what rgit exits with, and what it writes when asked for
machine-readable output. This file is authoritative for both — every other
document and every Go doc comment points here rather than restating it.
For the flags that produce these, see USAGE.md.
| Exit | Condition |
|---|---|
| 0 | Success (possibly with stderr warnings for unchanged targets) |
| 1 | diff only, and only under --exit-code/--quiet: something is committable |
| 3 | Anchor unresolvable — missing in both worktree and HEAD; candidates listed |
| 4 | Ambiguous anchor — candidates listed |
| 5 | Contradictory anchors — one path named both as a path and as a symbol anchor |
| 6 | Normalized LSP ↔ tree-sitter extent mismatch (commit only) |
| 7 | Refused path — gitignored and untracked |
| 8 | Commit succeeded; --push failed |
| 9 | Unsupported / deferred language for a symbol anchor |
| 10 | Symbol anchor refused on a special path (commit only: symlink, gitlink, binary, unmerged, skip-worktree, assume-unchanged) |
| 11 | All named targets resolve but have no uncommitted changes |
| 12 | Symbol anchor refused on a structured-data file (JSON, YAML, TOML) (commit only) |
| 128 | Fatal git / system failure (includes hook rejection, GPG failure, a -C directory that cannot be entered) |
| 129 | Invalid usage (bad flags, missing message, no targets, path escape, malformed -C) |
128 and 129 follow git's own conventions. 1 is git's --exit-code convention
and is deliberately absent from the named constants: unlike every other status
here, its meaning is conditional on a flag rather than fixed.
Those two are the only codes every command can produce, languages,
doctor and completion included, because the global -C <path>
(USAGE.md) is validated before dispatch: a
missing directory argument is 129 and an unenterable directory is 128 even
for a command that would never have opened a repository. Git chdirs before
it dispatches too, so a broken -C cannot be silent on one command and
fatal on the next.
The numeric bindings live in internal/exitcode, which spells no meaning of
its own — the constant names carry it, and this table defines it.
rgit diff reports the identical disagreement as a [warning] on stderr and
still exits 0 (or 1 under --exit-code). A diff is a read-only report, and the
point of surfacing it there is that the caller learns of it while reading the
diff rather than mid-commit.
rgit diff --sym, rgit blame, and rgit log all resolve a FILE:SYMBOL
anchor into JSON, YAML, or TOML exactly like any other anchor — none of them
writes a blob, so there is nothing for the guard to protect. Only rgit commit refuses it, because only rgit commit would splice a synthesized
extent into a blob and stage it. Name the path instead: rgit commit config.yaml stages the whole file, unaffected.
When no language server is reached, any command that runs the diff
cross-check — diff, commit, and context, which composes diff's own
default scope — prints [ts-only] on stderr and proceeds — degraded
resolution is normal, not an error
(AGENTS.md). rgit diff --quiet still
prints it, since --quiet suppresses the report on stdout, not
diagnostics.
Both sides of the exit-6 comparison reduce to a 0-based start/end line pair
before comparing (internal/resolve/crosscheck.go's lineOf); a language
server and tree-sitter agreeing on every line but disagreeing on a column
within one passes silently. Deliberate, not a gap: the anchors this
resolver stages are whole declarations, never sub-line ranges, so a
same-line disagreement has nothing narrower for either side to report
against.
rgit blame FILE:SYMBOL resolves its one anchor exactly like commit and
diff --sym do, so 3 (unresolvable), 4 (ambiguous), and 9 (unsupported
language) mean the same thing there. It never stages or commits anything, so
1, 5, 6, 7, 8, 10, 11, and 12 do not apply — a failure past resolution is git blame's own exit, folded into 128 the same way any other unexpected git
failure is.
rgit log FILE:SYMBOL resolves its one anchor the same way blame does —
3 (unresolvable), 4 (ambiguous), and 9 (unsupported language) mean the same
thing — except against HEAD's own blob rather than the worktree, since
history is a question about what has already been committed. It never
stages or commits anything either, so the same 1, 5, 6, 7, 8, 10, 11, and 12
exclusions apply, and a failure past resolution is git log's own exit,
folded into 128.
rgit log --since/--until with no FILE:SYMBOL positional is the second,
unanchored shape (see USAGE.md); it resolves
no anchor, so none of 3, 4, or 9 apply to it. When a FILE:SYMBOL positional
is present, the anchor shape remains selected and the date bounds are passed
to git log -L. In either shape, 129 covers bad flags, missing values, and a
path that escapes the repository root; 128 covers an unwalkable path or bad
date, folded from git log's own exit.
rgit context names no symbol, so 3, 4, and 9 never apply either. It never
stages or commits anything, so the same 1, 5, 6, 7, 8, 10, 11, and 12
exclusions as blame and log hold. A failure reaching git log or git diff
underneath it is folded into 128, same as everywhere else.
The record formats defined here are for --porcelain output only; a
command's own default is git's or rgit's aligned human-readable layout
(diff, commit, blame, log, languages). --porcelain replaces
that with stable tab-separated records, no header.
This is not a hypothetical contract: shell completion scripts call rgit symbols and rgit symbols --for-commit (internal/app/completion.go), while
rgit context's F records reuse the same field layout as rgit diff --porcelain (internal/app/context.go). Reordering or adding a column here is
a breaking change for the context F records and rgit diff --porcelain, not
just for external scripts.
FILE<TAB>SYMBOL<TAB>STATUS<TAB>ADDED<TAB>DELETED
auth.go<TAB>ValidateToken<TAB>MOD<TAB>12<TAB>3
auth.go<TAB>oldHelper<TAB>DELETED<TAB>0<TAB>14
auth.go<TAB><TAB>UNANCHORABLE<TAB>2<TAB>0
newfile.go<TAB>@header<TAB>MOD<TAB>2<TAB>0
newfile.go<TAB>NewFunc<TAB>MOD<TAB>13<TAB>0
config.ini<TAB><TAB>UNTRACKED<TAB>4<TAB>0
script.sh<TAB><TAB>MODE<TAB>0<TAB>0
logo.png<TAB><TAB>BINARY<TAB>-<TAB>-
STATUS is one of:
| Token | Means |
|---|---|
MOD |
A changed symbol, or a changed file whose language has no grammar |
DELETED |
A symbol present in HEAD and gone from the worktree |
UNANCHORABLE |
Hunks in a supported file that no symbol owns |
UNTRACKED |
A file git does not track and cannot attribute by symbol — binary, or a language with no grammar |
MODE |
A permission change with no content edit |
BINARY |
A binary file; both counts are - |
An untracked file with a supported grammar (newfile.go above) attributes
per symbol exactly like a brand-new tracked file — there is no HEAD blob
to diff against, so every declared symbol is wholly new and rows read MOD,
not a distinct "new" token. --sym filters it the same way it filters any
other file. UNTRACKED survives only as the collapsed fallback for a binary
file or one whose language has no grammar to attribute by at all
(config.ini above), where HintSymbol, when resolvable, still points a
caller at --sym/--file.
H<TAB>SHA
FILE<TAB>SYMBOL<TAB>ADDED<TAB>DELETED
H<TAB>a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
auth.go<TAB>ValidateToken<TAB>12<TAB>3
package.json<TAB><TAB>4<TAB>1
The leading H record is emitted after every successful real commit and
contains the full commit object id. It precedes all target rows, so a caller
can learn the committed revision without a follow-up git rev-parse HEAD.
--dry-run --porcelain emits target rows only and never invents an H record.
An --allow-empty commit still emits H even when no target rows follow it.
A pathspec produces one record per file it stages, not one for the pathspec.
- Naming a directory stages everything under it, so
rgit commit apps/authlistsapps/auth/a.go,apps/auth/b.goand so on, each with its own counts — the same breakdownrgit diffgives, rather than a single total that says something moved without saying what. - Untracked files under that pathspec are listed too, since
git addstages them as well, and a renamed file is listed at its new path. - A binary file is listed with zero counts: it is being staged, and git reports no line counts for it.
- A pathspec that matches nothing keeps one record naming the pathspec itself,
so
git add's own "did not match any files" is still what answers for it.
There is no STATUS column: an unchanged target is omitted from the listing
entirely (it gets its own stderr warning instead), so every target record would
carry the same value.
Target records are identical for --dry-run and for the
commit it previews; a successful real commit additionally has the leading H
record above.
--porcelainreplacesgit commit's own summary rather than adding to it — exactly asgit commit --porcelaindoes.- Because unchanged targets are omitted,
--porcelain --allow-emptywrites only the leadingHrecord while still creating a commit and exiting 0. - The full object id is the committed revision, so no before-and-after
git rev-parse HEADcomparison is needed.
NAME<TAB>EXTENSIONS<TAB>GATED<TAB>CROSS-CHECK
css<TAB>.css<TAB>0<TAB>wired
go<TAB>.go<TAB>0<TAB>wired
html<TAB>.html .htm<TAB>0<TAB>wired
json<TAB>.json<TAB>0<TAB>wired
markdown<TAB>.md .markdown<TAB>0<TAB>wired
python<TAB>.py .pyi<TAB>0<TAB>wired
shell<TAB>.sh .bash<TAB>0<TAB>wired
sql<TAB>.sql<TAB>1<TAB>ts-only
toml<TAB>.toml<TAB>0<TAB>ts-only
tsx<TAB>.tsx .jsx .js .mjs .cjs<TAB>0<TAB>wired
typescript<TAB>.ts .mts .cts<TAB>0<TAB>wired
yaml<TAB>.yaml .yml<TAB>0<TAB>wired
Sampled from a -tags rgit_sql build; a plain build has no sql row (see
INSTALL.md).
One record per grammar compiled into
this binary, sorted alphabetically by NAME.
EXTENSIONS is every extension the grammar claims, leading dot
included on each, joined with a single space — unambiguous, since a real
extension is always .something and never itself contains whitespace.
GATED is 1 when the grammar exists in this binary only because a build
tag selected it (SQL alone, -tags rgit_sql; see
INSTALL.md) and 0 otherwise, present on every
row rather than only the gated ones, so a reader always gets a definite
answer instead of inferring "not gated" from an absent column.
CROSS-CHECK is wired when the language has a compile-time entry in the
language-server catalog and ts-only when it does not (currently TOML and
SQL).
This is design-time wiring, not reachability: a wired language still
degrades to [ts-only] when its server is missing, cold, or unreachable; see
rgit doctor for environment and server status.
There is no row at all for a grammar this build was not compiled with: a
plain build's records have no sql line, matching rgit languages's own
human output and rgit --version's second line.
--in-repo narrows the same rows to grammars with a matching tracked file in
the current repository (requires a git repo); the record shape is
unchanged, only which rows appear.
See USAGE.md.
env<TAB>git<TAB>ok<TAB>/usr/bin/git
env<TAB>git version<TAB>ok<TAB>2.43.0
env<TAB>tree-sitter CLI<TAB>MISSING<TAB>optional -- only needed to rebuild with SQL support, see docs/INSTALL.md § SQL support
server<TAB>gopls (go)<TAB>ok<TAB>/home/user/go/bin/gopls (reachable)
server<TAB>vtsls (typescript, tsx)<TAB>MISSING<TAB>not on PATH -- see docs/INSTALL.md § Language servers
One record per check, tab-separated, no header:
| Field | Meaning |
|---|---|
KIND |
env for git presence, git's own version, and the optional tree-sitter CLI; server for a language server |
NAME |
The check's own name — a server's includes the languages it covers, matching the human report |
STATUS |
ok or MISSING, the same two spellings the human [ok]/MISSING report uses -- unaffected by --deep, which only ever adds detail, never changes this column |
DETAIL |
The resolved path when ok, or a caller-facing note when MISSING. Under --deep, an ok server's path gains a trailing (reachable) or (degraded -- handshake timed out or unanswered) from a real dial (internal/lsp.Dial) |
Grammars are not repeated here: rgit languages --porcelain above already
owns that listing, and rgit doctor reusing a second copy would be one more
place for the two to drift. Exit code is unaffected by --porcelain — a
missing git is still fatal (exit 128), a missing language server or the
tree-sitter CLI is still exit 0 with MISSING in its own record.
Not a new record shape: --porcelain passes straight through to git's own
git blame --porcelain output, unmodified. See git help blame for that
format — rewrapping it in a second, rgit-specific shape would be exactly the
kind of duplication this file exists to avoid, for a fact git already
establishes on its own.
Unlike every other command in this file, rgit context has no aligned
human default to alternate with: its one output shape is always this
tab-separated record stream, no header, no --porcelain flag to ask for it
— see specs/design.md for why.
B<TAB>main<TAB>origin/main<TAB>0<TAB>2
S<TAB>merge
W<TAB>ts-only
W<TAB>warning<TAB>extent disagreement reported on stderr
F<TAB>auth.go<TAB>ValidateToken<TAB>MOD<TAB>12<TAB>3
F<TAB>config.ini<TAB><TAB>UNTRACKED<TAB>4<TAB>0
C<TAB>a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2<TAB>fix(auth): reject expired tokens
C<TAB>9e8f7d6c5b4a9e8f7d6c5b4a9e8f7d6c5b4a9e8f<TAB>feat(auth): add ValidateToken
X<TAB>TRUNCATED<TAB>3
Seven record types, distinguished by the first field:
| Type | Fields after the type tag | Means |
|---|---|---|
B |
BRANCH, UPSTREAM, AHEAD, BEHIND |
At most one, first when present: the current branch. UPSTREAM is empty and AHEAD/BEHIND are both 0 when no upstream is configured — a definite answer, not inferred from an absent column. Absent on an unborn branch and replaced by H on detached HEAD |
H |
SHA |
One detached-HEAD record with the full commit object id; absent on an unborn branch |
S |
OP |
One active sequencer operation: merge, cherry-pick, revert, rebase, or bisect |
W |
ts-only |
One when at least one file had symbols to cross-check but no live language server was reached. The identical [ts-only] notice remains on stderr |
W |
stash |
One when the repository has a refs/stash ref |
W |
sparse |
One when core.sparseCheckout is true |
W |
warning, TEXT |
One per non-fatal diff warning. TEXT is the warning body without the human [warning] prefix; the identical [warning] TEXT line remains on stderr |
F |
FILE, SYMBOL, STATUS, ADDED, DELETED |
One rgit diff --porcelain row, identical fields — STATUS is the same six tokens § Output records defines above |
C |
HASH, SUBJECT |
One recent commit, newest first, bounded to the last 20 |
X |
TRUNCATED, COUNT |
At most one, always last: COUNT records were withheld to hold the 16 KiB byte budget |
A B or H record, when present, always sorts first, S follows it, W
diagnostics follow that, and F records always precede C records — a breaking change from the
original commits-first order — while an X record, when present, is always
the last line.
Bis a single record and costs the budget almost nothing.- Diagnostics are emitted before the actionable, unbounded diff rows; commit
history is already bounded to 20 and cheap to drop, and is one
git logcall away if the caller needs it back. - See
USAGE.mdfor the byte budget and../specs/design.mdfor why it is 16 KiB and what happens at the boundary.
HASH<TAB>SUBJECT
a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2<TAB>fix(auth): reject expired tokens
9e8f7d6c5b4a9e8f7d6c5b4a9e8f7d6c5b4a9e8f<TAB>feat(auth): add ValidateToken
For the anchor form, one record per commit whose own diff touched the named
symbol's current extent, newest first — the same ordering git log's own
default gives.
For the unanchored form, one record per matching commit in the date/path scope, with the same ordering.
HASH is the full commit object id, never abbreviated (unlike the aligned
default's <abbrev-hash> <subject>, which is for a human to read, not to
paste elsewhere).
Patch-free: this is the one record shape in this file that
is never emitted alongside -p/--patch, since asking for both would mean
asking for a record format and a patch dump at once.
rgit log -p instead
prints git's own git log -L output unmodified, the same "pass through
git's own format rather than inventing a second one" choice rgit blame --porcelain already makes.
START,END<TAB>SYMBOL
1,3 @imports
12,27 ValidateToken
One record per declared symbol, in source order — the same symbols and the
same order the bare rgit symbols form prints, which is unchanged by this
flag. START,END is git's own -L range grammar: 1-based and inclusive on
both ends, over the source the symbols were resolved from (the worktree file,
or its HEAD blob when the worktree copy is gone).
The range is the one rgit blame FILE:SYMBOL and rgit log FILE:SYMBOL bound
themselves to, not a second measurement of the same thing — all three convert
the identical resolved extent. A symbol whose range disagreed with what blame
blames would be a resolver bug, not a formatting difference.
SYMBOL is last because it is the unbounded field: an anchor may carry
container qualification, an ordinal (init#2), or a gopls-spelled receiver,
while the range never contains a tab. Splitting on the first tab is therefore
always correct.
These rules are specific to SYMBOL-bearing records — rgit languages --porcelain has no symbol, count, or ordering concept to share with them,
and its own rules are stated in full above.
SYMBOL is empty for every row or record that owns no anchor. A non-empty
SYMBOL is always exactly the string a symbol anchor accepts back, so output
copy-pastes into the next invocation.
Binary entries use - for both counts, matching git diff --numstat.
Ordering is alphabetical by path, then ascending by position within each
file — source order, not alphabetical by symbol, so a file's own structure is
preserved. rgit commit lists its targets the same way regardless of the order
they were named. Output is therefore stable between runs on an unchanged tree,
and greppable. A file's (unanchorable) row sorts last, since it covers hunks
spread across the file rather than any one position.
A symbol that is wholly added or removed carries one blank separator line with it, because that is what staging it actually moves — a top-level declaration is spliced in with one blank line before it and excised with that gap collapsed again. A container member carries none: members sit flush against their siblings, separated by a single newline their own extent already covers.
Where a formatter writes more than one blank line, the surplus belongs to
nobody and shows up as (unanchorable). PEP 8 writes two between top-level
definitions, so adding a Python function reports the symbol plus one
unanchorable line — and staging the anchor alone really does leave exactly
that line behind. In Go and TypeScript, where one blank line is the whole
separator, there is no remainder and no such row.
Every per-symbol row therefore agrees with rgit commit --dry-run, in every
language. An (unanchorable) row is the one thing --dry-run has no
counterpart for, by definition: it is the part of the change no anchor will
stage. It appears only when that part is real.