Authenticate clone, fetch and push¶
Three Repo operations can authenticate: clone, fetch and push. Each
takes an options struct with an optional CredentialRef.
Attach a credential¶
use rtb_credentials::CredentialRef;
use rtb_forge::git::{CloneOptions, Repo};
let cref = CredentialRef {
env: Some("GITLAB_TOKEN".into()),
..Default::default()
};
let repo = Repo::clone(
"https://gitlab.com/phpboyscout/rust/forge.git",
"/tmp/forge",
CloneOptions::default().with_credential(cref),
).await?;
FetchOptions and PushOptions work identically:
use rtb_forge::git::{FetchOptions, PushOptions};
repo.fetch("origin", FetchOptions::default().with_credential(cref.clone())).await?;
repo.push("origin", "HEAD:refs/heads/topic", PushOptions::default().with_credential(cref)).await?;
Omit with_credential and the operation runs anonymously.
Choose where the secret comes from¶
CredentialRef is rtb-credentials' declarative reference. It is resolved
through a fixed precedence chain — environment variable, then OS keychain, then
a literal in config, then a fallback environment variable — and the first one
that produces a value wins.
use rtb_credentials::{CredentialRef, KeychainRef};
// From an environment variable.
CredentialRef { env: Some("GITLAB_TOKEN".into()), ..Default::default() }
// From the OS keychain.
CredentialRef {
keychain: Some(KeychainRef { service: "mytool".into(), account: "gitlab".into() }),
..Default::default()
}
// Environment variable, falling back to an ecosystem default.
CredentialRef {
env: Some("MYTOOL_GITLAB_TOKEN".into()),
fallback_env: Some("GITLAB_TOKEN".into()),
..Default::default()
}
CredentialRef derives Deserialize, so the usual arrangement is to carry it
in your tool's own config and let the user decide:
A literal secret in config is refused when the process runs with CI=true.
That is rtb-credentials' rule, not this crate's.
Understand where the secret ends up¶
rtb-forge resolves the credential to a SecretString, then runs git with:
- the secret in the child process's environment as
RTB_VCS_GIT_TOKEN; -c credential.helper='!f() { echo username=x-access-token; echo password=$RTB_VCS_GIT_TOKEN; }; f'in argv — a script that contains no secret, only the variable name;GIT_TERMINAL_PROMPT=0, so an auth failure exits rather than prompting.
The secret is never in argv, never in a temporary file, and never written to
~/.git-credentials or any repository config. It is not logged: error cause
strings carry git's stderr, which does not include the token.
The variable name still says VCS because the crate was renamed from
rtb-vcs; the name is part of the runtime contract and was deliberately not
changed. There is no RTB_FORGE_GIT_TOKEN.
Know the constraints before you debug¶
HTTPS only. The credential helper mechanism applies to HTTPS remotes. An
ssh:// or git@host: remote authenticates through the SSH agent as usual and
ignores the CredentialRef entirely.
The username is fixed. It is always x-access-token — GitHub's PAT
convention, which GitLab and Gitea also accept. There is no way to change it.
A forge requiring a real username alongside a token cannot be authenticated
through these methods; run git yourself for that case.
Authenticated clone needs the git binary. An anonymous clone runs on
gix and needs nothing external. Adding a credential switches it to a git
subprocess. On a machine without git on PATH, the anonymous clone succeeds
and the authenticated one fails — see
Why the write paths shell out to git.
Tell the two failure modes apart¶
They are different variants and they mean different things:
use rtb_forge::git::RepoError;
match repo.fetch("origin", opts).await {
Err(RepoError::Auth(source)) => {
// The credential could not be RESOLVED locally.
// Env var unset, keychain locked, literal refused under CI=true.
eprintln!("credential problem: {source}");
}
Err(RepoError::FetchFailed { remote, cause }) => {
// The credential was resolved and the REMOTE rejected it,
// or the network failed. `cause` is git's stderr.
eprintln!("fetch from {remote} failed: {cause}");
}
Err(other) => return Err(other.into()),
Ok(()) => {}
}
RepoError::Auth never means "the server said no". If you see it, check the
environment variable name and the keychain entry, not the token's scopes.
Reuse one credential across operations¶
CredentialRef is Clone, and resolution happens per call — the secret is not
cached between operations. Clone the reference, not the secret:
let cref = CredentialRef { env: Some("GITLAB_TOKEN".into()), ..Default::default() };
let repo = Repo::clone(url, dst, CloneOptions::default().with_credential(cref.clone())).await?;
repo.fetch("origin", FetchOptions::default().with_credential(cref.clone())).await?;
repo.push("origin", "main", PushOptions::default().with_credential(cref)).await?;
Each call re-walks the precedence chain, so a token rotated in the keychain between calls takes effect immediately.