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.
Part of the phpboyscout Rust toolkit; extracted from — and battle-tested by — rust-tool-base.
use rtb_forge::git::Repo;
let repo = Repo::open(".").await?;
let status = repo.status().await?;
println!("{} untracked files", status.untracked.len());
Where do I start?¶
| If you want to | Go to |
|---|---|
| Get something working from nothing | Tutorial: read a repository's history |
| Solve one specific task | How-to guides |
| Look up a key, flag, method or error | Reference |
| Understand why it is built this way | Explanation |
| Know what it will not do | What rtb-forge does not do |
| Read the generated API docs | docs.rs/rtb-forge |
What the two slices are¶
| 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 |
They share no types, and each can be compiled without the other. See why they share one crate.
Release providers¶
A read-only abstraction over "where do this tool's binaries live". Six backends — GitHub, GitLab, Gitea, Codeberg, Bitbucket, and a plain-HTTPS Direct source — implement four methods:
#[async_trait::async_trait]
pub trait ReleaseProvider: Send + Sync + 'static {
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>;
}
Backends register at link time via linkme::distributed_slice on
RELEASE_PROVIDERS — there is no init(). Resolve one with
rtb_forge::lookup(source_type), and list what this binary supports with
rtb_forge::registered_types().
Nothing here writes: creating releases, uploading assets and editing tags are deliberately out of scope. Nothing here caches, either — every call hits the wire.
Git operations (Repo)¶
The async git wrapper, gated on the git Cargo feature (default-on).
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 and cheap to clone — the underlying gix handle
is a gix::ThreadSafeRepository — so handles fan out across tokio::spawn
boundaries. Every blocking call runs inside tokio::task::spawn_blocking.
Each method is covered in
Repo operations, including its options
struct, its failure variants and which backend performs it.
Does this need the git binary at runtime?¶
For read operations, no. For write operations, yes.
open, init, walk, diff, blame, status and anonymous clone run
on gix in pure Rust. commit, checkout, fetch, push and
authenticated clone shell out to the git binary, which therefore has to
be on PATH wherever your binary runs. libgit2 is not used anywhere, under
any feature combination.
This is the most common surprise in the crate, and why the write paths shell out to git explains the reasoning per operation.
How does authentication work?¶
clone, fetch and push take an optional CredentialRef on their options
struct. When set, the credential is resolved through
rtb_credentials::Resolver::with_platform_default() — environment variable,
then OS keychain, then a config literal, then a fallback environment variable —
and the resulting secret reaches git through:
RTB_VCS_GIT_TOKEN=<secret>in the subprocess environment. The name still saysVCSbecause the crate was renamed fromrtb-vcs; it is part of the runtime contract and was deliberately left alone.- an inline
-c credential.helpersnippet that reads that variable. The snippet placed in argv contains no secret. GIT_TERMINAL_PROMPT=0, so an auth failure exits instead of prompting.
The username is hard-coded to x-access-token. Recipes and failure modes in
Authenticate clone, fetch and push.
Cargo features¶
Trim to what you use:
# A self-updater that only talks to GitHub.
rtb-forge = { version = "0.7", default-features = false, features = ["github"] }
# A scaffolder that only needs git.
rtb-forge = { version = "0.7", default-features = false, features = ["git"] }
Full table — including the integration test feature, and the git2-fallback
feature that does not exist despite being referenced in the crate's API
docs — in Cargo features.
Formerly rtb-vcs¶
Renamed at 0.7.0, 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 environment variable.
Migrating is a rename: change the dependency name and the rtb_vcs:: import
prefix. Nothing else moves.
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 |
How this crate is tested¶
- 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 and bare-repo fixtures in temporary directories. - BDD scenarios (
cucumber) intests/features/. - Release-provider backend tests (
wiremock) undertests/*_backend.rs, plus testcontainers-backed Gitea integration tests behind theintegrationfeature. The Gitea containers are serialised via.config/nextest.tomlbecause racing them exhausts testcontainers' startup timeout on shared runners.
just ci runs the full local gate: fmt-check, lint, docs, test,
audit.
Related¶
- rtb-credentials — the auth
resolver feeding
CloneOptions/FetchOptions/PushOptions. - rtb-update — self-update consumer of the release-provider slice.
- phpboyscout Rust toolkit — the other modules.
Further reading¶
The blog carries a curated route through this subject: Rust, and what survived the port collects everything written about it, ordered so you can start at the beginning rather than newest-first.
Ask phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.