Skip to content

rtb-forge

Release-source APIs and git operations for Rust CLI tools, in two feature-gated slices: the ReleaseProvider trait with six built-in backends, and the Repo async git wrapper built on gix.

Formerly rtb-vcs — renamed at extraction from the rust-tool-base monorepo for name convergence with the GTB family (the go counterpart lives at forge.go.phpboyscout.uk). Only the crate identity changed: every type, module, and Cargo feature is exactly as it was in rtb-vcs 0.7.0, including the RTB_VCS_GIT_TOKEN auth environment variable.

Part of the phpboyscout Rust toolkit; extracted from — and battle-tested by — rust-tool-base.

Overview

Slice Feature gate Default Typical consumers
Release providers per-backend (github, gitlab, …) on rtb-update self-update, release-notes tools
Git ops (Repo) git on scaffolders, release tools, generic git-aware CLIs

The framework spec §9 (rust-tool-base spec) 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 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.
  • Auth via rtb-credentials::Resolver. No parallel TokenSource trait. 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

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_forge::lookup(source_type) and rtb_forge::registered_types(). The full design is recorded in the v0.1 spec.

Git-operations slice (Repo)

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

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. CloneOptions / FetchOptions / PushOptions are #[non_exhaustive] with builder methods (with_credential); CheckoutOptions::forced() skips the dirty-tree guard.
  • CommitWalk. Repo::walk(revspec) returns a stream implementing futures_core::Stream<Item = Result<CommitInfo, RepoError>>; supports Include (HEAD), Range (A..B), and Merge (A...B) revspecs. The gix walk runs on a spawn_blocking task piping commits through a bounded channel; dropping the stream cancels the producer.
  • Diff / FileChange / ChangeKind. Structured file-level diff (Added / Modified / Deleted / Renamed { from }); hunk-level diffing is deferred and Diff is #[non_exhaustive].
  • Blame / BlameLine. One entry per line — line number, content, commit id, author, timestamp — with per-commit author caching.
  • RepoStatus. Three mutually exclusive buckets: staged, unstaged, untracked.
  • RepoError. #[non_exhaustive] enum with semantic variants (OpenFailed, CloneFailed, RevspecNotFound, DirtyWorkingTree, Auth(CredentialError), …); backend errors are stringified into cause and never leak.

Full API reference: docs.rs/rtb-forge.

Auth model

Auth-bearing operations (clone / fetch / push) accept an optional CredentialRef on their options struct. When set, rtb-forge 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 (the name is unchanged from rtb-vcs — it is part of the runtime contract).
  • -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.

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 Gitea lane (needs Docker)

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
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

  • Unit suites under tests/ covering lifecycle, read paths, blame, write paths, fetch/checkout, auth, and push. Fixtures shell out to the host git CLI to build multi-commit / bare-repo fixtures in tempdirs.
  • BDD scenarios (cucumber) in tests/features/.
  • Release-provider backend tests (wiremock) under tests/*_backend.rs, plus testcontainers-backed Gitea integration tests (opt-in via the integration feature; the Gitea containers are serialised via .config/nextest.toml).