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)¶
Repois 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.gixfor read paths, shell-out togitfor write paths. Per A8 wrap-not-leak, the backend choice is internal. v0.5 picks shell-out forcommit/fetch/checkout/ authenticatedclone/pushbecause gix has no high-level helpers for staging + commit + auth and rebuilding them would be 50+ lines of fiddly plumbing. Anonymousclonestays on gix. Migration to pure-gix is internal — public API stable.gix-blamefor blame. Wraps the raw gix-blame engine, flattening hunk-level output into per-lineBlameLineentries that matchgit blame --porcelainsemantics.- Async surface; blocking work in
spawn_blocking. Every public method isasync fn; gix calls andgitsubprocesses run insidetokio::task::spawn_blockingso callers stay async. - Errors wrapped, not leaked.
RepoErrorcarries semantic variants (OpenFailed,CloneFailed,RevspecNotFound, etc.) with stringifiedcause. Backend errors (gix / git stderr) are never reachable through the public API. - Auth via
rtb-credentials::Resolver. No parallelTokenSourcetrait.CloneOptions::with_credential/FetchOptions::with_credential/PushOptions::with_credentialtake aCredentialRef; resolution walks env → keychain → literal → fallback-env. The resolvedSecretStringis passed togitvia 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/PushOptionsare#[non_exhaustive]with builder methods (with_credential);CheckoutOptions::forced()skips the dirty-tree guard. CommitWalk.Repo::walk(revspec)returns a stream implementingfutures_core::Stream<Item = Result<CommitInfo, RepoError>>; supportsInclude(HEAD),Range(A..B), andMerge(A...B) revspecs. The gix walk runs on aspawn_blockingtask 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 andDiffis#[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 intocauseand 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 fromrtb-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=0to 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 hostgitCLI to build multi-commit / bare-repo fixtures in tempdirs. - BDD scenarios (
cucumber) intests/features/. - Release-provider backend tests (
wiremock) undertests/*_backend.rs, plus testcontainers-backed Gitea integration tests (opt-in via theintegrationfeature; the Gitea containers are serialised via.config/nextest.toml).
Related¶
- rtb-credentials — the auth
resolver feeding
CloneOptions/FetchOptions/PushOptions. - rtb-update — self-update consumer of the release-provider slice.
- Framework spec §9 — authoritative contract.