Skip to content

Proposal: replace shell-outs to git with a Rust-native git library (gix) #153

@bezhermoso

Description

@bezhermoso

Summary

I'd like to propose — and get the maintainer group's input on — replacing tinty's shell-outs to the git CLI with a pure-Rust git implementation (gix from the gitoxide project). The intent is to land this incrementally: the gix path would ship alongside the existing CLI path, gated behind an opt-in env var (TINTY_USE_GIX=1), validated by a parity test suite, and only then promoted to default in a later release.

I'd like to start this discussion before I open a PR I'd like input on the trade-offs and the open questions at the bottom.

Motivation

tinty currently shells out to git for clone/update/status during tinty install and tinty update (src/utils.rs, called from src/operations/{install,update}.rs). Concretely we issue ~12 distinct git commands across git_clone, git_update, git_resolve_revision, git_to_revision, and git_is_working_dir_clean.

The pain points with the status quo:

  • Runtime dependency on the user's git binary. Tinty fails opaquely if git isn't installed (very unlikely) or is too old (more likely). We have no version checks.
  • Output coupling. We parse git ls-remote and git branch --format=... stdout and depend on stable text formats. git status --porcelain is at least documented as stable; the others are conventions.
  • Atomicity hacks. git_update creates a randomly-named temporary remote so a failed update doesn't corrupt .git/config. This works but leaks a tinty-remote-<random> entry if the process is killed mid-operation.
  • Cross-platform fragility. Argument quoting on Windows, locale-dependent error messages, and which git differences across distros.
  • Test surface. Duplicates git logic almost verbatim because the integration tests need to set up fixtures the same way.

A native Rust implementation removes the runtime dependency, lets us hold structured types instead of parsing stdout, and lets the update flow be genuinely atomic (no on-disk temp remote at all).

Proposed approach

  1. Library: gix (gitoxide), not git2. Rationale below.
  2. Coexistence, not replacement. Introduce a GitBackend trait with two implementations: CliBackend (current logic, moved as-is) and GixBackend (new). Both compiled in.
  3. Runtime opt-in. Dispatch on TINTY_USE_GIX (truthy: 1, true, yes). Default stays CLI for at least one release cycle.
  4. Parity test suite. A separate tests/git_backend_parity_tests.rs runs each operation against both backends in temp dirs and asserts identical observable on-disk state and equivalent error categories.
  5. Phased rollout across multiple small PRs (refactor → add dep → status → clone → update), each independently revertible.

Why gix and not git2?

This is the single most consequential choice and I want maintainer input.

deny.toml hard-bans openssl, openssl-sys, libssh2-sys, cmake, and windows. git2's https feature pulls openssl-sys on non-macOS Unix targets (Linux, BSDs); macOS and Windows take other paths inside libgit2. vendored-openssl would still bring openssl-sys into the dep graph. The currently-published git2 (0.20.4) also enables ssh by default, which transitively pulls libssh2-sys via libgit2-sys; master has dropped ssh from defaults, so a future release may change this. But the https/openssl-sys problem is independent. Under the current deny.toml, git2 is effectively non-viable.

gix with the blocking-http-transport-reqwest-rust-tls feature is pure Rust, uses rustls, has no openssl-sys/libssh2-sys in its dep tree, and is the explicit successor that the cargo team is migrating to (away from git2). The gitoxide maintainer is funded by the Rust Foundation for that integration.

If the group wants to revisit the deny.toml bans instead — i.e. allow openssl-sys for git2 — that's a separate conversation worth having on its own merits, but I'd argue the existing bans are well-justified (static builds, GPL-3 license clarity) and worth keeping.

What this buys us

  • No runtime git binary requirement.
  • Atomic update with no on-disk temp-remote leakage.
  • Structured ref/object access — no stdout parsing.
  • Eliminates the tests/utils.rs duplicate of src/utils.rs git logic (follow-up work).
  • Removes ~200 lines of Command/Stdio/shell_words::split plumbing from lib code.

What it costs us

  • Binary size. rustls + reqwest + gix likely add ~5–10 MB to the release binary. Order-of-magnitude estimate; I'll measure in the spike.
  • Dependency surface. Adds gix and its tree (~50+ crates). multiple-versions = "deny" in deny.toml will flag duplicates that today's tree doesn't have; we'll need a handful of skip = [...] entries.
  • gix is pre-1.0. Expect minor breakages on version bumps. Most crates are tagged "Initial Development" in their crate-status.md.
  • gix-status may diverge from git status --porcelain on edge cases (submodules, ignored files, conflicted state). We'll catch divergences with the parity suite, but some may need explicit handling or a hybrid (CLI status, gix everything else) as a fallback.
  • MSRV. gix tracks recent stable Rust. tinty's Cargo.toml doesn't currently pin a rust-version; we'd inherit gix's effective MSRV.
  • One more thing to monitor. Each gix bump means re-running cargo deny check and the parity suite.

Alternatives considered

  • git2 — blocked by deny.toml bans on openssl-sys/libssh2-sys. Would require relaxing those.
  • Stay on shell-outs, fix the temp-remote leak only — addresses one pain point, leaves the others. Cheapest option.
  • gix with ureq transport instead of reqwest — smaller binary, but less battle-tested in the gix ecosystem. Worth measuring if binary size becomes a sticking point.

Phased plan (sketch)

  1. Refactor. Extract GitBackend trait, move existing logic into CliBackend. Zero behavior change.
  2. Add gix dep + deny.toml updates + stub GixBackend. Stub returns errors. CI green.
  3. Parity test harness + is_working_dir_clean (gix). Smallest operation, no network.
  4. Clone (gix), with and without revision.
  5. Update (gix).

After all phases land and sit through one release on opt-in:

  1. Flip default to gix; CLI path moves behind an opt-out for one release.
  2. Remove CLI path; deduplicate tests/utils.rs.

Open questions

  1. Are the deny.toml bans on openssl-sys/libssh2-sys a hard policy, or open to revisit? This determines whether git2 is actually off the table.
  2. Tolerance for binary size growth? I expect +5–10 MB on release. Acceptable?
  3. MSRV policy? Are we OK inheriting gix's effective MSRV (current stable, no LTS commitment)?
  4. Does anyone have prior gix experience in another project that would inform the spike or surface gotchas I haven't considered?

I'd like to hear concerns or counter-proposals before doing the spike. If there's no fundamental objection, my plan is to start with the pure refactor — (GitBackend trait + move existing logic) since it's reviewable in isolation and locks in nothing.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions