rtb-forge 0.8.1

Release-provider abstractions and backends (GitHub, GitLab, Bitbucket, Gitea, Codeberg, Direct) plus an async git-operations Repo built on gix. Part of the phpboyscout Rust toolkit.
Documentation
---
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?

| If you want to | Go to |
|---|---|
| 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

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

| Crate | Uses |
|---|---|
| [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"

    ![phpbotscout](https://phpboyscout.uk/images/projects/logo-phpbotscout.png){ 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 }