---
title: rtb-forge
description: Release-provider abstractions and an async git-operations Repo for Rust CLI tools.
---
# `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](https://gitlab.com/phpboyscout/rust-tool-base) monorepo
for name convergence with the GTB family (the go counterpart lives at
[forge.go.phpboyscout.uk](https://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](https://rust.phpboyscout.uk);
extracted from — and battle-tested by — rust-tool-base.
## Overview
| 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](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/rust-tool-base.md#9-vcs--release-providers-rtb-vcs))
is the authoritative contract; the v0.5 scope addendum
([2026-05-11-v0.5-scope.md](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/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`
```rust
#[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](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/2026-04-23-rtb-vcs-v0.1.md).
## Git-operations slice (`Repo`)
The async git wrapper. Gated on the `git` Cargo feature (default-on).
```rust
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](https://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
```toml
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
| [rtb-update](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/components/rtb-update.md) | `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`).
## Related
- [rtb-credentials](https://credentials.rust.phpboyscout.uk) — the auth
resolver feeding `CloneOptions` / `FetchOptions` / `PushOptions`.
- [rtb-update](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/components/rtb-update.md)
— self-update consumer of the release-provider slice.
- [Framework spec §9](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/rust-tool-base.md#9-vcs--release-providers-rtb-vcs)
— authoritative contract.