rtb-vcs¶
rtb-vcs ships two slices in the same crate:
- Release-provider slice (v0.1, shipped 0.2.0). The
ReleaseProviderasync trait + GitHub / GitLab / Bitbucket / Gitea / Codeberg / Direct backends. Consumed byrtb-updatefor tool self-update. - Git-operations slice (v0.2, shipped at v0.5 milestone). The
Repotype — a thin async wrapper overgix(with shell-out togitfor write paths) that downstream tools compose richer git-based functionality on top of. Designed as a foundation, not a curated facade for any one consumer.
Both slices live behind feature flags so a downstream tool that only needs releases doesn't pay the git-ops compile cost.
Overview¶
| Slice | Feature gate | Default | Consumers |
|---|---|---|---|
| Release providers | per-backend (github, gitlab, …) |
on | rtb-update, future rtb-ai release-notes |
Git ops (Repo) |
git |
on | rtb-cli-bin scaffolder, release-note tools, generic git-aware CLIs |
The framework spec §9 (docs/development/specs/rust-tool-base.md)
is the authoritative contract; the v0.5 scope addendum
(2026-05-11-v0.5-scope.md)
records the design decisions for the git-ops slice.
Design rationale (git-ops slice)¶
Repois 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.gixfor read paths, shell-out togitfor write paths. Per A8 wrap-not-leak, the backend choice is internal. v0.5 picks shell-out forcommit/fetch/checkout/ authenticatedclone/pushbecause gix 0.72 has no high-level helpers for staging + commit + auth and rebuilding them would be 50+ lines of fiddly plumbing. Anonymousclonestays on gix. Migration to pure-gix is internal — public API stable.gix-blamefor blame. Wraps the raw gix-blame engine, flattening hunk-level output into per-lineBlameLineentries that matchgit blame --porcelainsemantics.- Async surface; blocking work in
spawn_blocking. Every public method isasync fn; gix calls andgitsubprocesses run insidetokio::task::spawn_blockingso callers stay async. - Errors wrapped, not leaked.
RepoErrorcarries semantic variants (OpenFailed,CloneFailed,RevspecNotFound, etc.) with stringifiedcause. Backend errors (gix / git stderr) are never reachable through the public API — A8. - Auth via
rtb-credentials::Resolver. No parallelTokenSourcetrait per A2.CloneOptions::with_credential/FetchOptions::with_credential/PushOptions::with_credentialtake aCredentialRef; resolution walks env → keychain → literal → fallback-env. The resolvedSecretStringis passed togitvia process env (RTB_VCS_GIT_TOKEN) — never argv.
Release-provider slice¶
The trait, value types, and registry pattern. See the
2026-04-23-rtb-vcs-v0.1.md
spec for the full design.
ReleaseProvider¶
#[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_vcs::lookup(source_type) and rtb_vcs::registered_types().
Git-operations slice (Repo)¶
The async git wrapper. Gated on the git Cargo feature
(default-on per A7).
Repo¶
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¶
All three …Options structs are #[non_exhaustive] with builder
methods so future field additions stay non-breaking:
CloneOptions::default().with_credential(cred);
FetchOptions::default().with_credential(cred);
PushOptions::default().with_credential(cred);
CheckoutOptions::forced(); // skip dirty-tree guard
InitOptions::default(); // no knobs yet
CommitWalk — async commit stream¶
Repo::walk(revspec) returns a CommitWalk implementing
futures_core::Stream<Item = Result<CommitInfo, RepoError>>.
Consume via futures::StreamExt. Supports Include (HEAD),
Range (A..B), and Merge (A...B) revspecs. Each item is a
CommitInfo carrying id / summary / message / author name+email /
Unix timestamp.
The gix walk runs on a spawn_blocking task piping commits through
a 64-entry mpsc channel; dropping the stream cancels the producer
at the next send.
Diff / FileChange / ChangeKind¶
Repo::diff(a, b) returns a structured Diff { changes: Vec<FileChange> }.
Each FileChange carries path: PathBuf and kind: ChangeKind:
Added— file appeared in destination tree.Modified— file present on both sides, different content / mode.Deleted— file removed from destination.Renamed { from }— gix's rewrite tracker matched a deletion + addition pair.
Hunk-level diffing is deferred to v0.5.x; Diff is
#[non_exhaustive] so adding hunks later is non-breaking.
Blame / BlameLine¶
Repo::blame(path, revspec) returns one BlameLine per line of
the blamed file — carrying line number, content, commit id,
author name / email, and Unix timestamp. Author info is cached per
commit id internally so repeated appearances of the same commit
don't re-traverse the object store.
RepoStatus¶
Three buckets: staged: Vec<PathBuf>, unstaged: Vec<PathBuf>,
untracked: Vec<PathBuf>. Mutually exclusive — a path appears in
exactly one bucket. (staged is empty until commit 3's
head_tree wiring lands — the foundation slice ships with
unstaged + untracked tracking only.)
RepoError¶
#[non_exhaustive] enum with semantic variants. Backend errors
are stringified into cause; gix / git2 / git stderr never leak.
| Variant | When |
|---|---|
Io { path, source } |
Filesystem path failure |
OpenFailed { path, cause } |
Repo::open couldn't load the repo |
InitFailed { path, cause } |
Repo::init couldn't create the repo |
CloneFailed { url, cause } |
Repo::clone failed (transport, refs, dst non-empty) |
FetchFailed { remote, cause } |
Repo::fetch failed |
CheckoutFailed { revspec, cause } |
Repo::checkout failed (non-revspec-not-found cause) |
CommitFailed { cause } |
Repo::commit failed (empty paths, no author, etc.) |
StatusFailed { cause } |
Repo::status failed |
WalkFailed { cause } |
Repo::walk internal failure (not bad revspec) |
DiffFailed { cause } |
Repo::diff internal failure |
PushFailed { remote, refspec, cause } |
Repo::push failed |
RevspecNotFound { revspec } |
revspec didn't resolve (used by walk / diff / blame / checkout) |
DirtyWorkingTree { paths } |
checkout's dirty-tree guard tripped |
Auth(CredentialError) |
credential resolution failed |
PushUnsupported |
vestigial; no longer produced post-A4 obsolescence |
Auth model¶
Auth-bearing operations (clone / fetch / push) accept an
optional CredentialRef on their options struct. When set,
rtb-vcs 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.-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=0to 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.
API surface¶
| Item | Slice | Since |
|---|---|---|
release::{ReleaseProvider, Release, ReleaseAsset, ProviderError, RELEASE_PROVIDERS, lookup, registered_types} |
release | 0.2.0 |
config::{ReleaseSourceConfig, GithubParams, GitlabParams, …} |
release | 0.2.0 |
github, gitlab, bitbucket, gitea, codeberg, direct modules |
release | 0.2.0 |
git::{Repo, RepoError, InitOptions, CloneOptions, FetchOptions, CheckoutOptions, PushOptions, RepoStatus, Diff, FileChange, ChangeKind, CommitInfo, CommitWalk, Blame, BlameLine} |
git-ops | v0.5 |
git::auth::{resolve_for_git, resolve_default, expose, TOKEN_ENV, CREDENTIAL_HELPER} |
git-ops | v0.5 |
Cargo features¶
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 lane (release backends)
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 | ReleaseProvider + backends for self-update |
Future rtb-ai release-notes |
Repo::walk for commit-graph summarisation |
Future 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¶
- 41 unit tests across
tests/repo_unit.rs,tests/repo_read_paths_unit.rs,tests/repo_blame_unit.rs,tests/repo_write_paths_unit.rs,tests/repo_fetch_checkout_unit.rs,tests/repo_auth_unit.rs,tests/repo_push_unit.rs. - BDD scenarios in
tests/features/repo_lifecycle.feature,tests/features/repo_read_paths.feature,tests/features/repo_write_paths.feature. - Release-provider backend tests under
tests/{github,gitlab,bitbucket,gitea,direct}_backend.rs - the testcontainers Gitea integration test (opt-in via the
integrationfeature). - Fixture pattern: tests shell out to the host
gitCLI to build multi-commit / bare-repo fixtures in tempdirs. CI runners havegitinstalled across all matrix lanes.
Spec and status¶
- Status:
IMPLEMENTEDsince v0.5 (git-ops slice). Release slice has beenIMPLEMENTEDsince 0.2.0. - Specs:
- Release slice —
2026-04-23-rtb-vcs-v0.1.md - Git-ops slice —
2026-05-11-v0.5-scope.md - Source:
crates/rtb-vcs/.
Related¶
rtb-update— self-update consumer of the release-provider slice.rtb-credentials— the auth resolver feedingCloneOptions/FetchOptions/PushOptions.- Framework spec §9 — authoritative contract.