---
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`.
Part of the [phpboyscout Rust toolkit](https://rust.phpboyscout.uk); extracted
from rust-tool-base, which still depends on it.
```rust
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?
| Get something working from nothing | [Tutorial: read a repository's history](tutorials/01-read-a-repositorys-history.md) |
| Solve one specific task | [How-to guides](how-to/index.md) |
| Look up a key, flag, method or error | [Reference](reference/index.md) |
| Understand why it is built this way | [Explanation](explanation/index.md) |
| Know what it will *not* do | [What rtb-forge does not do](explanation/04-what-this-does-not-do.md) |
| Read the generated API docs | [docs.rs/rtb-forge](https://docs.rs/rtb-forge) |
## What the two slices are
| 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](explanation/01-two-slices-one-crate.md).
### 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:
```rust
#[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).
```rust
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](reference/05-repo-operations.md), 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](explanation/03-why-write-paths-shell-out.md)
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
says `VCS` because the crate was renamed from `rtb-vcs`; it is part of the
runtime contract and was deliberately left alone.
- an inline `-c credential.helper` snippet 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](how-to/02-authenticate-git-operations.md).
## Cargo features
```toml
default = ["github", "gitlab", "gitea", "codeberg", "direct", "bitbucket", "git"]
```
Trim to what you use:
```toml
# 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"] }
```
The full table is in [Cargo features](reference/01-cargo-features.md). It
includes the `integration` test feature, and explains why there is no
`git2-fallback` feature.
## Formerly `rtb-vcs`
Renamed at 0.7.0, 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` environment variable.
Migrating is a rename: change the dependency name and the `rtb_vcs::` import
prefix. Nothing else moves.
## Consumers
| [rtb-update](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/explanation/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 |
## 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 host `git`
CLI to build multi-commit and bare-repo fixtures in temporary directories.
- BDD scenarios (`cucumber`) in `tests/features/`.
- Release-provider backend tests (`wiremock`) under `tests/*_backend.rs`, plus
testcontainers-backed Gitea integration tests behind the `integration`
feature. The Gitea containers are serialised via `.config/nextest.toml`
because 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](https://credentials.rust.phpboyscout.uk), the auth
resolver feeding `CloneOptions` / `FetchOptions` / `PushOptions`.
- [rtb-update](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/explanation/components/rtb-update.md),
the self-update consumer of the release-provider slice.
- [phpboyscout Rust toolkit](https://rust.phpboyscout.uk), for the other modules.
## Further reading
The blog carries a curated route through this subject: **[Rust, and what survived the port](https://phpboyscout.uk/topics/rust/)** collects
everything written about it, ordered so you can start at the beginning rather
than newest-first.
!!! tip "Ask phpbotscout"
{ width="84" align=left style="border-radius:10px;margin-right:1rem" }
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.
[Join the Discord](https://discord.gg/mQzGbmGyzZ){ .md-button .md-button--primary }