Skip to content

rtb-vcs

rtb-vcs ships two slices in the same crate:

  1. Release-provider slice (v0.1, shipped 0.2.0). The ReleaseProvider async trait + GitHub / GitLab / Bitbucket / Gitea / Codeberg / Direct backends. Consumed by rtb-update for tool self-update.
  2. Git-operations slice (v0.2, shipped at v0.5 milestone). The Repo type — a thin async wrapper over gix (with shell-out to git for write paths) that downstream tools compose richer git-based functionality on top of. Designed as a foundation, not a curated facade for any one consumer.

Both slices live behind feature flags so a downstream tool that only needs releases doesn't pay the git-ops compile cost.

Overview

Slice Feature gate Default Consumers
Release providers per-backend (github, gitlab, …) on rtb-update, future rtb-ai release-notes
Git ops (Repo) git on rtb-cli-bin scaffolder, release-note tools, generic git-aware CLIs

The framework spec §9 (docs/development/specs/rust-tool-base.md) is the authoritative contract; the v0.5 scope addendum (2026-05-11-v0.5-scope.md) records the design decisions for the git-ops slice.

Design rationale (git-ops slice)

  • Repo is a foundation, not a curated facade. Inherited from the go-tool-base experience: started over-engineered, got pared back, then quietly became the load-bearing piece downstream tools composed on. Designed for that pattern up front.
  • gix for read paths, shell-out to git for write paths. Per A8 wrap-not-leak, the backend choice is internal. v0.5 picks shell-out for commit / fetch / checkout / authenticated clone / push because gix 0.72 has no high-level helpers for staging + commit + auth and rebuilding them would be 50+ lines of fiddly plumbing. Anonymous clone stays on gix. Migration to pure-gix is internal — public API stable.
  • gix-blame for blame. Wraps the raw gix-blame engine, flattening hunk-level output into per-line BlameLine entries that match git blame --porcelain semantics.
  • Async surface; blocking work in spawn_blocking. Every public method is async fn; gix calls and git subprocesses run inside tokio::task::spawn_blocking so callers stay async.
  • Errors wrapped, not leaked. RepoError carries semantic variants (OpenFailed, CloneFailed, RevspecNotFound, etc.) with stringified cause. Backend errors (gix / git stderr) are never reachable through the public API — A8.
  • Auth via rtb-credentials::Resolver. No parallel TokenSource trait per A2. CloneOptions::with_credential / FetchOptions::with_credential / PushOptions::with_credential take a CredentialRef; resolution walks env → keychain → literal → fallback-env. The resolved SecretString is passed to git via process env (RTB_VCS_GIT_TOKEN) — never argv.

Release-provider slice

The trait, value types, and registry pattern. See the 2026-04-23-rtb-vcs-v0.1.md spec for the full design.

ReleaseProvider

#[async_trait::async_trait]
pub trait ReleaseProvider: Send + Sync {
    async fn latest_release(&self) -> Result<Release, ProviderError>;
    async fn release_by_tag(&self, tag: &str) -> Result<Release, ProviderError>;
    async fn list_releases(&self, limit: usize) -> Result<Vec<Release>, ProviderError>;
    async fn download_asset(&self, asset: &ReleaseAsset)
        -> Result<(Box<dyn AsyncRead + Send + Unpin>, u64), ProviderError>;
}

Built-in backends (github, gitlab, bitbucket, gitea, codeberg, direct) register at link time via [linkme::distributed_slice] on RELEASE_PROVIDERS. Discover via rtb_vcs::lookup(source_type) and rtb_vcs::registered_types().

Git-operations slice (Repo)

The async git wrapper. Gated on the git Cargo feature (default-on per A7).

Repo

pub struct Repo { /* gix::ThreadSafeRepository + path */ }

impl Repo {
    pub async fn init(path, InitOptions) -> Result<Self, RepoError>;
    pub async fn open(path) -> Result<Self, RepoError>;
    pub async fn clone(url, dst, CloneOptions) -> Result<Self, RepoError>;
    pub fn walk(&self, revspec) -> Result<CommitWalk, RepoError>;
    pub async fn diff(&self, a, b) -> Result<Diff, RepoError>;
    pub async fn blame(&self, path, revspec) -> Result<Blame, RepoError>;
    pub async fn status(&self) -> Result<RepoStatus, RepoError>;
    pub async fn commit(&self, paths, message) -> Result<String /* OID */, RepoError>;
    pub async fn fetch(&self, remote, FetchOptions) -> Result<(), RepoError>;
    pub async fn checkout(&self, revspec, CheckoutOptions) -> Result<(), RepoError>;
    pub async fn push(&self, remote, refspec, PushOptions) -> Result<(), RepoError>;
    pub fn path(&self) -> &Path;
}

Repo is Send + Sync + Clone — every field is Arc-wrapped via gix::ThreadSafeRepository, so handles fan out across tokio::spawn boundaries cheaply.

Options types

All three …Options structs are #[non_exhaustive] with builder methods so future field additions stay non-breaking:

CloneOptions::default().with_credential(cred);
FetchOptions::default().with_credential(cred);
PushOptions::default().with_credential(cred);
CheckoutOptions::forced();              // skip dirty-tree guard
InitOptions::default();                 // no knobs yet

CommitWalk — async commit stream

Repo::walk(revspec) returns a CommitWalk implementing futures_core::Stream<Item = Result<CommitInfo, RepoError>>. Consume via futures::StreamExt. Supports Include (HEAD), Range (A..B), and Merge (A...B) revspecs. Each item is a CommitInfo carrying id / summary / message / author name+email / Unix timestamp.

The gix walk runs on a spawn_blocking task piping commits through a 64-entry mpsc channel; dropping the stream cancels the producer at the next send.

Diff / FileChange / ChangeKind

Repo::diff(a, b) returns a structured Diff { changes: Vec<FileChange> }. Each FileChange carries path: PathBuf and kind: ChangeKind:

  • Added — file appeared in destination tree.
  • Modified — file present on both sides, different content / mode.
  • Deleted — file removed from destination.
  • Renamed { from } — gix's rewrite tracker matched a deletion + addition pair.

Hunk-level diffing is deferred to v0.5.x; Diff is #[non_exhaustive] so adding hunks later is non-breaking.

Blame / BlameLine

Repo::blame(path, revspec) returns one BlameLine per line of the blamed file — carrying line number, content, commit id, author name / email, and Unix timestamp. Author info is cached per commit id internally so repeated appearances of the same commit don't re-traverse the object store.

RepoStatus

Three buckets: staged: Vec<PathBuf>, unstaged: Vec<PathBuf>, untracked: Vec<PathBuf>. Mutually exclusive — a path appears in exactly one bucket. (staged is empty until commit 3's head_tree wiring lands — the foundation slice ships with unstaged + untracked tracking only.)

RepoError

#[non_exhaustive] enum with semantic variants. Backend errors are stringified into cause; gix / git2 / git stderr never leak.

Variant When
Io { path, source } Filesystem path failure
OpenFailed { path, cause } Repo::open couldn't load the repo
InitFailed { path, cause } Repo::init couldn't create the repo
CloneFailed { url, cause } Repo::clone failed (transport, refs, dst non-empty)
FetchFailed { remote, cause } Repo::fetch failed
CheckoutFailed { revspec, cause } Repo::checkout failed (non-revspec-not-found cause)
CommitFailed { cause } Repo::commit failed (empty paths, no author, etc.)
StatusFailed { cause } Repo::status failed
WalkFailed { cause } Repo::walk internal failure (not bad revspec)
DiffFailed { cause } Repo::diff internal failure
PushFailed { remote, refspec, cause } Repo::push failed
RevspecNotFound { revspec } revspec didn't resolve (used by walk / diff / blame / checkout)
DirtyWorkingTree { paths } checkout's dirty-tree guard tripped
Auth(CredentialError) credential resolution failed
PushUnsupported vestigial; no longer produced post-A4 obsolescence

Auth model

Auth-bearing operations (clone / fetch / push) accept an optional CredentialRef on their options struct. When set, rtb-vcs resolves the credential via rtb_credentials::Resolver::with_platform_default() and passes the resulting SecretString to the git subprocess via:

  • RTB_VCS_GIT_TOKEN=<secret> in the subprocess env.
  • -c credential.helper='!f() { echo username=x-access-token; echo password=$RTB_VCS_GIT_TOKEN; }; f' in argv (the snippet contains no secret).
  • GIT_TERMINAL_PROMPT=0 to fail fast on auth errors.

Username is hardcoded x-access-token (GitHub PAT convention, accepted by GitLab and Gitea). Tools needing other usernames per provider can wrap Repo::clone / fetch / push themselves.

API surface

Item Slice Since
release::{ReleaseProvider, Release, ReleaseAsset, ProviderError, RELEASE_PROVIDERS, lookup, registered_types} release 0.2.0
config::{ReleaseSourceConfig, GithubParams, GitlabParams, …} release 0.2.0
github, gitlab, bitbucket, gitea, codeberg, direct modules release 0.2.0
git::{Repo, RepoError, InitOptions, CloneOptions, FetchOptions, CheckoutOptions, PushOptions, RepoStatus, Diff, FileChange, ChangeKind, CommitInfo, CommitWalk, Blame, BlameLine} git-ops v0.5
git::auth::{resolve_for_git, resolve_default, expose, TOKEN_ENV, CREDENTIAL_HELPER} git-ops v0.5

Cargo features

default = ["github", "gitlab", "gitea", "codeberg", "direct", "bitbucket", "git"]
github / gitlab / gitea / codeberg / direct / bitbucket # release backends
git = ["dep:gix", "dep:rtb-credentials", "dep:futures-core"]
integration = []  # testcontainers-backed lane (release backends)

The git2-fallback feature originally reserved in spec A4 was obsoleted by the consistent shell-out architecture (all v0.5 write paths use git, not libgit2). RepoError::PushUnsupported stays in the enum for backwards compat but is no longer produced.

Consumers

Crate Uses
rtb-update ReleaseProvider + backends for self-update
Future rtb-ai release-notes Repo::walk for commit-graph summarisation
Future rtb-cli-bin scaffolder Repo::init + Repo::commit for rtb new; Repo::diff for rtb regenerate
Downstream operator-flow tools Repo as the git foundation

Testing

  • 41 unit tests across tests/repo_unit.rs, tests/repo_read_paths_unit.rs, tests/repo_blame_unit.rs, tests/repo_write_paths_unit.rs, tests/repo_fetch_checkout_unit.rs, tests/repo_auth_unit.rs, tests/repo_push_unit.rs.
  • BDD scenarios in tests/features/repo_lifecycle.feature, tests/features/repo_read_paths.feature, tests/features/repo_write_paths.feature.
  • Release-provider backend tests under tests/{github,gitlab,bitbucket,gitea,direct}_backend.rs
  • the testcontainers Gitea integration test (opt-in via the integration feature).
  • Fixture pattern: tests shell out to the host git CLI to build multi-commit / bare-repo fixtures in tempdirs. CI runners have git installed across all matrix lanes.

Spec and status