Skip to content

What rtb-forge does not do

Stated as limits, so you can rule things out without reading the source.

The release slice cannot write anything

ReleaseProvider has four methods and all four are reads. There is no create-release, no upload-asset, no edit-tag, no delete. This is a scope decision, not an omission: the slice exists so a tool can discover and download its own newer build, and write access to a forge is a different security posture entirely.

If you need to publish releases, use the forge's own CLI or API client. Nothing in this crate will grow that capability.

Nothing is cached

Every call hits the network. Two consecutive latest_release() calls make two requests; there is no ETag handling, no conditional request, no in-memory memoisation, and no on-disk cache.

For a self-updating CLI that checks once per invocation this is correct. For anything polling on a timer it is not, and the intended fix is to add HTTP caching middleware to the reqwest client — which this crate does not expose. You cannot supply your own reqwest::Client; each factory builds its own.

Release listings do not paginate

list_releases(limit) fetches exactly one page. The limit is clamped to the backend's page cap — 100 on GitHub and GitLab, 50 on Gitea and Codeberg — and no Link header or next cursor is followed. Asking for 500 releases returns at most 100.

There is no way to reach older releases beyond the first page. If you need full history, use the forge's API directly.

Bitbucket cannot list releases at all

list_releases on the Bitbucket backend returns ProviderError::Unsupported unconditionally. Bitbucket Cloud has no releases concept; the backend synthesises single releases by pairing a git tag with repository Downloads whose filenames contain the tag string. Doing that for a listing would mean a downloads lookup per tag, and the filename matching is a heuristic, not a guarantee.

Bitbucket Data Center / Server is not supported at all. Its URL shape (/rest/api/1.0) and response JSON both differ from Cloud, and only Cloud is implemented. A Data Center host will produce parse failures, not a clear error.

Bitbucket OAuth 2.0 is also unsupported; App Passwords over HTTP Basic are the only authentication path.

You cannot mix a per-backend host with Codeberg

source_type: codeberg has no host key — the host is a compile-time constant pinned to codeberg.org. There is no configuration that points the Codeberg backend at another instance. Use source_type: gitea with an explicit host instead; it is the same code path.

Likewise, enabling the codeberg Cargo feature always enables gitea. There is no build that has one without the other.

The private configuration key does nothing

Every forge parameter struct has a private: bool, documented as "true when auth is required even for read operations". No backend reads it. Requests are authenticated when a token is supplied to the factory and anonymous when it is not, regardless of this key.

Setting private: true will not cause an early failure when no credential is available, and setting it to false will not suppress an Authorization header. Treat it as reserved.

Bitbucket authentication fails open, not closed

The Bitbucket backend only sends credentials when username and a token are both present. Supply a token but no username and the request is sent anonymously with no warning — the failure arrives later as a 404 from Bitbucket, indistinguishable from a genuinely missing repository.

HTTP is not configurable, and neither is HTTPS enforcement

Every backend builds its own reqwest::Client with https_only on. You cannot inject a client, add middleware, set a proxy programmatically, disable certificate verification, or add a custom root CA. The allow_insecure_base_url field on every params struct exists for the crate's own tests, is #[serde(skip)], and cannot be set from any config file.

timeout_seconds is the only HTTP knob, and setting it to 0 removes the timeout rather than making it short. There is no separate connect timeout and no retry policy — a RateLimited error tells you what the host asked for, and backing off is your job.

RepoStatus::staged is always empty

Repo::status() returns three buckets and populates two of them. The tree-to-index comparison is never configured, so staged is constructed empty on every call regardless of what is in the index. A newly added file that has been git add-ed appears in none of the three buckets: not in staged, because that bucket is never filled, and not in untracked, because the index knows about it.

The knock-on effect is in Repo::checkout: its dirty-tree guard inspects staged and unstaged, so staged-but-uncommitted changes do not block a non-forced checkout. Do not use status() to decide whether there is anything to commit.

The git surface is a foundation, not a full client

Repo covers eleven operations and stops. There is no branch creation, no merge, no rebase, no cherry-pick, no stash, no tag creation, no remote management, no submodule handling, no reflog, no config access, and no worktree management.

Within the operations that do exist, the option sets are minimal by design:

  • InitOptions is empty. No bare repositories, no initial-branch override, no templates.
  • CloneOptions carries only a credential. No depth, no branch, no single-branch, no progress reporting.
  • FetchOptions carries only a credential. No refspec, no prune, no tags.
  • PushOptions carries only a credential. No force flag, no lease, no atomic — encode what you need in the refspec.
  • CheckoutOptions carries only force.
  • Repo::commit has no author override, no amend, no signing, no --allow-empty.
  • Repo::diff is file-granularity only. No hunks, no line counts, no patch text.
  • Repo::blame has no line range, no ignore-revisions, no rename-following control.

Each is absent because no consumer has needed it, and each options struct is #[non_exhaustive] so adding one later is not a breaking change.

Repo::walk supports three revspec forms

HEAD-style include, A..B range, and A...B merge. Every other gix revision kind — HEAD^!, ^HEAD, and the rest — is rejected with RepoError::WalkFailed { cause: "unsupported revspec kind: …" }.

The direct backend cannot verify anything

With no pinned_version configured, release_by_tag makes no request at all and synthesises a release for whatever string you pass. Ask for v99.0.0 and you get a Release tagged v99.0.0 whose asset URL 404s at download time. There is no listing endpoint to check against, and none is synthesised.

Its created_at is always the Unix epoch, its body is always empty, and its asset size is always 0. Chronological ordering across direct sources is not meaningful.

{target} silently vanishes on unusual platforms

The direct backend's {target} placeholder is resolved from a table of six Rust triples: x86_64 and aarch64 on Linux, macOS and Windows. Anything else — 32-bit ARM, musl targets addressed as such, FreeBSD — substitutes the empty string rather than raising an error or leaving the placeholder in place. The rendered URL looks plausible and 404s.

Build the URL from {os} and {arch} if you ship outside that table.

GitLab metadata is partly inferred

GitLab's release API has no draft flag and no prerelease flag, so the backend fills them by other means: draft is always false, and prerelease is set when the tag string contains a hyphen. That is right for 1.2.3-rc.1 and wrong for a date-shaped tag like 2026-04-23, which will be reported as a prerelease.

GitLab release assets are links rather than uploaded blobs, so their size is always 0 and content_type carries GitLab's link_type (package, image, runbook, other) rather than a MIME type.

GitLab's latest_release can report "not found" on a repository with releases

It requests a single release and filters out upcoming (scheduled) ones. If the newest release is upcoming, the filtered list is empty and the call returns ProviderError::NotFound { what: "latest release" } even though published releases exist. Use list_releases and filter yourself if you schedule releases ahead.

A plain 403 is reported as a transport error

The shared status mapping recognises 401, 404, 429, and GitHub's rate-limit 403. Everything else — a plain 403 from an under-scoped token, a 500, a 502 from a proxy — becomes ProviderError::Transport("unexpected status … from …"). There is no server-error variant and no permission-denied variant distinct from 401.

Known documentation-versus-code discrepancies

These are defects in the crate's own API documentation, listed here so that finding one does not send you looking for behaviour that is not there:

Claim Where Reality
"Not Git. Repository operations … land as rtb-forge v0.2" crate-level rustdoc The git module has shipped and is default-on
"git2 is an opt-in fallback … Gated on the git2-fallback Cargo feature" git module rustdoc No such feature is declared; cargo rejects it
"PushFailed … only reachable when the git2-fallback Cargo feature is enabled" RepoError::PushFailed rustdoc It is the ordinary failure variant for Repo::push
"push is not supported without the git2-fallback Cargo feature" RepoError::PushUnsupported message Nothing constructs this variant; push works
"Pass CheckoutOptions::force(true) to override" Repo::checkout rustdoc The constructor is CheckoutOptions::forced()
"CheckoutOptions { force: true }" checkout module rustdoc The struct is #[non_exhaustive]; that literal does not compile downstream
"staged — paths that are staged for the next commit (index vs HEAD tree diff)" RepoStatus rustdoc Always empty; the diff is never computed
"unknown combinations produce an empty target string, which surfaces as {target} staying literal" direct::host_substitutions rustdoc {target} is replaced by the empty string, not left in place
"true when auth is required even for read operations" every private field Never read by any backend

The code is authoritative in every row.