- Go 1.26+ with cgo enabled — the tree-sitter grammars are C.
- git 2.32+ on
PATH.rgitshells out to it for everything git already does; 2.32 is the newest behaviour any code path relies on (git commit --trailer,internal/gitx.go).rgit doctorchecks the resolved version, not just presence, and reports it as informational rather than refusing to run below the floor. - Optionally, a language server per language you want cross-checked (see Language servers).
- Optionally, the tree-sitter CLI for
.sqlanchors (see SQL support) — everything else builds and works without it.
The Go floor is not chosen — it tracks whatever the dependencies declare, since
rgit keeps them at their latest releases, and rises whenever one of them
raises its own.
Three ways to get a binary, in order of how much they do for you:
make install builds and installs in one step, wrapping
cmd/rgit-install — it checks prerequisites, generates
the SQL parser when it can (see SQL support), builds,
installs, and reports each step:
make install # to $GOBIN, or $(go env GOPATH)/bin
make install PREFIX=~/.local/bin # anywhere elseThe default target is $GOBIN, falling back to $(go env GOPATH)/bin —
usually ~/go/bin. That is where the binary lands unless you pass PREFIX
(or -prefix), and it is the path the uninstall step below assumes.
Run the installer directly for its own flags, including a preview that changes nothing:
go run ./cmd/rgit-install -dry-run
go run ./cmd/rgit-install -prefix ~/.local/binIt can also install or update the language servers rgit's LSP cross-check
uses, opt-in via -with-servers — see
Installing and updating servers automatically.
make build builds ./rgit for the host only, no install step.
Plain go build needs no make:
go build -ldflags="-s -w" -o rgit ./cmd/rgitThe binary is ~13.5 MB stripped, or ~16 MB built with -tags rgit_sql;
the grammars account for nearly all of it, measured per grammar in
specs/design.md.
Install any of the above onto PATH yourself if you didn't use make install — anywhere on PATH works; this matches where make install would
have put it:
dest="$(go env GOBIN)"; [ -n "$dest" ] || dest="$(go env GOPATH)/bin"
install -m 0755 rgit "$dest/rgit"go env GOBIN prints an empty line and exits 0 when it is unset, so the
fallback has to test the value rather than the exit status.
make help — the default target — lists them all; the ones worth knowing:
| Target | Does |
|---|---|
build |
go build the host binary to ./rgit |
install |
Build and install via cmd/rgit-install (PREFIX= to override) |
test, test-short, test-race |
Three go test invocations: full suite, unit lane alone (-short), full suite raced. test/test-short are the two lanes CONTRIBUTING.md documents; test-race is a separate concern, not a third lane |
cover, cover-short |
Coverage with -coverpkg=./..., as CONTRIBUTING.md requires |
fix-diff, fix |
go fix preview and apply |
cross |
Cross-compile linux/amd64, linux/arm64, windows/amd64 into dist/, with SQL when the tree-sitter CLI is present |
cross-darwin |
Native-compile darwin/amd64, darwin/arm64 into dist/ — run this on a macOS host, not cross-compiled by cross above |
clean |
Remove build outputs |
rgit links tree-sitter through cgo, so CGO_ENABLED=0 is not an option —
every cross target needs a matching C toolchain. Measured from a Linux host
with zig as the single cross-compilation tool:
| Target | Works | CC |
|---|---|---|
| linux/amd64 | yes | zig cc -target x86_64-linux-gnu |
| linux/arm64 | yes | zig cc -target aarch64-linux-gnu |
| windows/amd64 | yes | zig cc -target x86_64-windows-gnu |
| darwin/amd64, darwin/arm64 | no, not via zig | needs a macOS SDK — see cross-darwin below |
make cross # all three zig-cross-compiled targets, into dist/
make cross-linux-arm64 # a single targetdarwin fails at link time through zig with unable to find dynamic system library 'resolv': net is a real dependency (go.lsp.dev/jsonrpc2 uses it
for the gopls socket), and linking it needs -lresolv and -framework CoreFoundation from an actual macOS SDK — building with -tags netgo,osusergo does not clear it, and it is why darwin is deliberately not
in make cross's own zig-based matrix.
make cross-darwin builds darwin/amd64 and darwin/arm64 natively — run
it on an actual macOS host, not this one. A real Mac's own clang and SDK
build both darwin arches without a cross-compilation toolchain at all
(Xcode ships a universal SDK, the same reason a Mac already builds for
either Apple Silicon or Intel without a second one), so this target carries
no zig/CC override the way cross's three targets do:
make cross-darwin # both darwin arches, into dist/ -- macOS host onlyThe release workflow (.github/workflows/release.yml) runs this on a
macos-latest GitHub-hosted runner as its own job, so tagged releases ship
rgit-vX.Y.Z-darwin-amd64 and rgit-vX.Y.Z-darwin-arm64 alongside the
three zig-built artifacts, all covered by the same SHA256SUMS and its
cosign signature.
Cross binaries carry SQL when the build host can generate the parser.
make cross runs generation once through cmd/rgit-install -generate-only,
then passes -tags rgit_sql to every target. Generation is
host-independent — it turns grammar.js into C a single time — so the only
per-target cost is compiling that C, measured at ~6–7s each with zig cc
(a full three-target make cross from a clean tree, generation included,
measures ~28s).
Without the tree-sitter CLI on the build host, nothing is generated and
every target builds SQL-less instead of failing — the same fallback
make install makes. .sql anchors then resolve as any other unsupported
language does (exit 9, docs/ANCHORS.md); every other language is
unaffected either way.
-generate-only combines with -with-servers rather than skipping it.
cmd/rgit-install's main() runs the -with-servers step before
-generate-only's own early return, so cmd/rgit-install -generate-only -with-servers — what make cross would need if it ever grew a
server-installing mode — still installs or updates every managed language
server (§ Installing and updating servers
automatically) even though
it exits before touching rgit's own build or install.
make cross also regenerates dist/SHA256SUMS from that run's own
artifacts — each invocation overwrites the file rather than appending to
it, so the checksums on disk always match the binaries currently in
dist/.
SQL is a second grammar behind the rgit_sql build tag: a plain go build ./... or go install ./cmd/rgit builds and works identically without it.
Its parser has no pre-built Go bindings — the grammar module gitignores its
own parser.c at every tag — so rgit generates that file at build time
instead of vendoring it: tree-sitter generate turns the module's
grammar.js into a working parser.c, which is copied into the SQL
adapter package's csrc/ subdirectory. That directory is gitignored and
never committed; it regenerates on demand.
cmd/rgit-install does this automatically once the SQL adapter package
exists and the tree-sitter CLI
is on PATH — no separate Node.js install is needed even though grammar.js
is JavaScript: the CLI evaluates it with its own embedded JS engine, measured
directly by running tree-sitter generate with every node/nodejs binary
removed from PATH. Without the CLI, the installer installs rgit without
SQL support and says so plainly rather than failing; .sql anchors then
resolve as any other unsupported language does (exit 9, docs/ANCHORS.md),
and every other language is unaffected. What .sql addresses once built is
in ANCHORS.md.
rgit works without any language server — it falls back to tree-sitter alone
and prints [ts-only] on stderr. Installing one enables the extent
cross-check, which catches build-tag, macro, and type-level mismatches.
| Language | Server | Install | How rgit runs it |
|---|---|---|---|
| Go | gopls |
go install golang.org/x/tools/gopls@v0.23.0 (pinned; -with-servers installs this exact tag) |
Background daemon, reused |
| TypeScript/JavaScript | vtsls |
npm i -g @vtsls/language-server |
One-shot subprocess per query |
| Python | pyright-langserver |
npm i -g pyright |
One-shot subprocess per query |
| Shell | bash-language-server |
npm i -g bash-language-server |
One-shot subprocess per query |
| YAML | yaml-language-server |
npm i -g yaml-language-server |
One-shot subprocess per query |
| JSON | vscode-json-language-server |
npm i -g vscode-langservers-extracted |
One-shot subprocess per query |
| CSS | vscode-css-language-server |
npm i -g vscode-langservers-extracted |
One-shot subprocess per query |
| Markdown | marksman |
GitHub release binary — no package manager publishes it | One-shot subprocess per query |
| HTML | vscode-html-language-server |
npm i -g vscode-langservers-extracted |
One-shot subprocess per query |
JSON, CSS, and HTML share one npm package, vscode-langservers-extracted — a
single install produces all three binaries. marksman is the one server in
this table
-with-servers does not
manage, for the same reason: nothing to shell out to.
Only gopls has a listen mode, so Go is the only language with a reusable
daemon: rgit probes for one and starts it in the background if none answers.
That first invocation finishes in [ts-only] mode rather than blocking on a
cold index; later ones get the full cross-check. Every other language's
server has no listen mode, so rgit spawns one over stdio per query and
kills it on close — nothing persists, and the cross-check is live on the
first invocation. The transport survey behind this split is in
specs/design.md.
TOML and SQL stay [ts-only] permanently — see
LIMITATIONS.md for why. taplo
still completes the LSP handshake once built with -with-servers'
--features lsp, and other tooling can use it; it just never drives
rgit's own cross-check. HTML is wired; void elements still need a
declOnlyEndTrimmer seam (same section), not a class-suffix mismatch.
go run ./cmd/rgit-install -with-servers # alongside a normal install
go run ./cmd/rgit-install -dry-run -with-servers # preview only -- nothing runs-with-servers is opt-in and off by default: a plain rgit-install never
touches anything beyond this repo's own build. Passed, it additionally
installs or updates every server it knows how to manage, by
shelling out to that ecosystem's own package manager rather than fetching
release binaries itself:
| Server | Manager | Command |
|---|---|---|
gopls |
go | go install golang.org/x/tools/gopls@v0.23.0 (pinned; see cmd/rgit-install/servers.go) |
vtsls |
npm/bun | npm install -g @vtsls/language-server (bun add -g when bun is on PATH) |
pyright-langserver |
npm/bun | npm install -g pyright |
bash-language-server |
npm/bun | npm install -g bash-language-server |
yaml-language-server |
npm/bun | npm install -g yaml-language-server |
vscode-json-language-server, vscode-css-language-server, vscode-html-language-server |
npm/bun | npm install -g vscode-langservers-extracted (one package, all three binaries) |
taplo |
cargo | cargo install taplo-cli --locked --features lsp |
Every row above except taplo is a server rgit dials for its own
cross-check, matching the table in Language servers.
taplo is the exception: TOML stays [ts-only]
permanently, on measured range disagreement rather than availability (see
Language servers above) — -with-servers still manages
taplo since other tooling can use it, but installing it will not turn on a
TOML cross-check.
taplo needs the non-default --locked --features lsp explicitly. A
bare cargo install taplo-cli and npm's @taplo/cli package both build a
taplo that answers on PATH while speaking no LSP at all — presence
without capability. -with-servers checks for taplo's own lsp subcommand,
not just that the binary exists, and reports which is missing when it isn't
there.
The same invocation both installs a missing server and updates a present
one: every npm/bun/cargo entry above already resolves to the latest
available version and reinstalls only when something actually changed
(cargo additionally reinstalls when the requested --features differ from
what's already built). gopls is the one exception — it is pinned (see the
table above), so re-running installs or reinstalls exactly v0.23.0
regardless of what gopls has tagged since; bumping it needs an edit to
cmd/rgit-install/servers.go, not another -with-servers run. Either way,
running -with-servers again is always safe.
marksman is deliberately out of scope. No package manager publishes
it — only GitHub release binaries, platform-named per target. Teaching this
installer HTTP fetching and checksum verification for one server was judged
not worth it; -with-servers reports marksman as unmanaged and prints the
release URL instead.
A server installed to a directory that isn't on PATH is still
invisible. cargo install in particular writes to $CARGO_HOME/bin
(~/.cargo/bin by default), which is not on every system's PATH.
-with-servers checks every manager's bin directory against PATH after
each install and warns loudly, by name and directory, rather than reporting
success and leaving the binary unreachable.
A failed install is detectable, not just printed. Each job is bounded
by a generous timeout so a hung registry or a stalled build cannot block
-with-servers indefinitely, and a FAILED line for any job makes the
whole invocation exit non-zero — rgit itself still gets built and
installed normally (a language server failing is not a reason to withhold
it), but automation driving -with-servers can tell a partial run from a
clean one instead of reading exit 0 either way.
| Variable | Effect |
|---|---|
RGIT_LSP_SOCKET |
Path to an existing gopls socket. Checked before the default location. No effect on the stdio servers. |
RGIT_LSP_DIAL_TIMEOUT |
Overrides how long rgit waits to reach a live language-server daemon before falling back to [ts-only] (default 150ms). A Go duration string (e.g. 500ms, 1s); unset, malformed, zero, or negative values keep the default. |
RGIT_LSP_QUERY_TIMEOUT |
Overrides the deadline for a single cross-check round trip once connected (default 2s). Same duration-string and fail-closed rules as above. |
XDG_RUNTIME_DIR |
Where rgit creates its private rgit-<uid>/ subdirectory, holding rgit-gopls.sock and its spawn lock. Falls back to the system temp dir. |
TMPDIR |
Consulted by the system-temp-dir fallback above when XDG_RUNTIME_DIR is unset (Go's own os.TempDir(), POSIX only), before it falls back further to /tmp. See § Uninstall for the exact lookup order. |
GIT_TERMINAL_PROMPT |
Set to 0 automatically when stdin is not a terminal. Set it yourself to override. |
Everything else is git's own configuration, honoured because git commit does
the committing. Which settings that covers, and the one exception, is in
USAGE.md.
What rgit completion bash|zsh|fish|pwsh completes, and how the dynamic part
works, is documented in USAGE.md.
Load once per session:
source <(rgit completion bash) # bash
source <(rgit completion zsh) # zsh, after compinit has run
rgit completion fish | source # fish
rgit completion pwsh | Invoke-Expression # PowerShell 7Persist across sessions:
# bash
rgit completion bash > /etc/bash_completion.d/rgit # system-wide
rgit completion bash > ~/.local/share/bash-completion/completions/rgit # per-user
# zsh -- write it anywhere already on $fpath, then start a new shell
rgit completion zsh > "$fpath[1]/_rgit"
# fish -- picked up automatically by every new fish session, no reload step
rgit completion fish > ~/.config/fish/completions/rgit.fish
# PowerShell 7 -- add this line to $PROFILE
Add-Content -Path $PROFILE -Value 'Invoke-Expression (rgit completion pwsh | Out-String)'zsh's compdef needs compinit to already have run, so autoload -Uz compinit && compinit must come first in .zshrc — the standard
precondition for any zsh completion, not one of rgit's own.
For a machine with only git — no Go toolchain, no zig — scripts/install.sh
downloads a release binary and verifies it against that release's own
SHA256SUMS before installing it:
curl -fsSL https://raw.githubusercontent.com/Rethunk-Tech/rethunk-git-cli/main/scripts/install.sh | shOn Windows/amd64, scripts/install.ps1 provides the equivalent
release-download and checksum-verification path:
$env:VERSION = 'v1.2.0' # optional on a real install; defaults to latest
.\scripts\install.ps1It installs rgit.exe to $HOME\.local\bin by default; set $env:PREFIX to
override the destination. -DryRun prints the download URL, checksum source,
and install path without touching the network, plus the planned signature
source when cosign is available. It requires an explicit VERSION tag —
there is no latest-tag lookup on that path:
$env:VERSION = 'v1.2.0'
.\scripts\install.ps1 -DryRunWhen cosign is on PATH, install.ps1 also downloads
SHA256SUMS.sigstore.json and verifies SHA256SUMS with the identity and OIDC
issuer shown below before checking the binary's SHA256. Without cosign, it
keeps the SHA256-only path. -DryRun stays network-free; when cosign is
available, it reports the planned signature verification without downloading
the bundle.
Every release also publishes SHA256SUMS.sigstore.json, a keyless
cosign signature over
SHA256SUMS itself -- verifying it proves the checksums came from this
repo's own release workflow, not just that a downloaded file matches some
checksum file:
cosign verify-blob \
--bundle SHA256SUMS.sigstore.json \
--certificate-identity-regexp 'https://github.com/Rethunk-Tech/rethunk-git-cli/.github/workflows/release.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMSWhen cosign is on PATH, scripts/install.sh and scripts/install.ps1
perform this verification automatically: they download
SHA256SUMS.sigstore.json beside SHA256SUMS and verify the checksum file
before checking the binary's SHA256. A failed verification stops the install.
Without cosign, both installers retain their SHA256-only paths. --dry-run
and -DryRun exit before any download, so neither fetches the signature bundle.
PREFIX (default $HOME/.local/bin) and VERSION (default latest) are
environment variables, not flags — VERSION=v1.2.0 PREFIX=/usr/local/bin sh install.sh installs that exact tag system-wide. --dry-run prints the plan
(download URL, checksum source, install path) without touching the network
at all, which is what CI runs to lint the script's own control flow on every
push.
Scope is deliberately narrow. scripts/install.sh supports
linux/amd64, linux/arm64, darwin/amd64, and darwin/arm64; install.ps1
supports Windows/amd64. Linux verifies the release line with sha256sum;
macOS uses its native shasum -a 256. Language servers stay opt-in exactly
as they are everywhere else — neither installer installs one. The shell
script prints the -with-servers pointer as its last line, while the
PowerShell script points at this documentation. There is no Homebrew formula:
rgit links tree-sitter
through cgo, so a formula would need to build from source per-platform (a
bottle per target) rather than fetch one, which is a materially bigger
undertaking than this script covers, not a small addition to it.
rgit --version
cd /some/git/repo && rgit diffA repo with no uncommitted changes prints nothing and exits 0. rgit diff --quiet exits 1 when anything is committable, 0 when clean — the scriptable
form of the same check.
To confirm the cross-check is active rather than degraded, look for the absence
of [ts-only] on stderr:
rgit diff 2>&1 >/dev/null | grep -q 'ts-only' && echo "degraded" || echo "cross-check active"rm "$(command -v rgit)"
rm -rf "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/rgit-$(id -u)"command -v rgit resolves whichever copy your shell actually runs, which is
the one to remove — hardcoding a path guesses wrong for anyone who installed
to the default $GOBIN/$(go env GOPATH)/bin rather than passing PREFIX.
That subdirectory exists only for gopls; the stdio servers leave nothing
behind. A gopls daemon rgit started exits on its own idle timeout. The
lookup order above mirrors rgit's own (internal/lsp/dial.go's
runtimeDir): $XDG_RUNTIME_DIR first, then Go's os.TempDir(), which on
POSIX honours $TMPDIR before falling back to /tmp — a host with $TMPDIR
set and no $XDG_RUNTIME_DIR puts the socket there, not in /tmp. rgit
only ever creates or trusts the rgit-<uid>/ subdirectory itself, never the
base directory directly — see AGENTS.md § State.