rtb-forge 0.7.0

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`.

**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

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

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