Expand description
vcs-cli-support — the processkit-coupled plumbing the CLI wrappers reuse.
vcs-git / vcs-jj / vcs-github all drive a CLI through processkit, so
they share three concerns that touch processkit::Error: an argv injection
guard, a fetch-retry policy, and a set of Error classifiers. Extracting them
here keeps the std-only vcs-diff clean of the processkit dependency, and —
more to the point — keeps the marker lists and classifier logic from drifting
between backends. The wrapper crates re-export these items (so you reach them
as vcs_git::is_merge_conflict, not via this crate’s name) and rarely name
vcs-cli-support directly.
§The surface
reject_flag_like— the injection guard for bare positional argv slots. A caller value that is empty/whitespace, or starts with-, is refused before spawning (the CLI would parse it as a flag); flag-value slots (-m <msg>) are consumed verbatim and skip the check. Wrappers call it with their own binary name so the surfacedError::Spawnnames the rightprogram.FETCH_ATTEMPTS/FETCH_BACKOFF— the shared transient-retry policy forfetch(one try plus two retries, fixed backoff between them).is_merge_conflict/is_nothing_to_commit/is_transient_fetch_error/is_lock_contention— classify a returnedErrorso callers branch on intent (“conflict, resolve it”; “nothing to commit, no-op”; “transient, retry”; “another process holds the lock, retry”) instead of matching on error internals. They inspect capturedError::Exitoutput against fixed marker lists; aprocesskitError::Timeoutis not treated as a transient fetch error (it already spent the full deadline — seeis_transient_fetch_error); any unfamiliar#[non_exhaustive]variant falls through to “no”.RetryPolicy/retry_async/ManagedClient— an opt-in retry strategy (attempts + exponential, jittered backoff) for lock-contention failures.ManagedClientwraps aprocesskitCliClientand applies the policy to every command, so thevcs-git/vcs-jjclients gain retry viawith_retry(...)without changing a call site. Lock-acquisition failures are pre-execution, so retrying is safe even for mutating commands.CredentialProvider/Credential/Secret— an opt-in seam for supplying a secret per operation (a CI token, a vault lookup) instead of relying on ambient CLI auth.ManagedClientinjects the resolved token into each command (the forgeGH_TOKEN/GITLAB_TOKENenv); git usesgit_credential_helperto keep the secret out ofargv. Default is no provider → ambient auth, unchanged. See thecredentialsmodule for the full picture.
§Recipes
Classify a failed fetch to drive a retry decision — branch on intent, not on
the error’s internals:
use vcs_cli_support::{is_transient_fetch_error, FETCH_ATTEMPTS, FETCH_BACKOFF};
for attempt in 1..=FETCH_ATTEMPTS {
match run() {
Ok(()) => break,
Err(e) if is_transient_fetch_error(&e) && attempt < FETCH_ATTEMPTS => {
std::thread::sleep(FETCH_BACKOFF); // DNS / dropped connection — worth a retry
}
Err(e) => return Err(e), // anything else: give up
}
}Re-exports§
pub use credentials::Credential;pub use credentials::CredentialProvider;pub use credentials::CredentialRequest;pub use credentials::CredentialService;pub use credentials::EnvToken;pub use credentials::FnProvider;pub use credentials::GitCredentialHelper;pub use credentials::Secret;pub use credentials::StaticCredential;pub use credentials::git_credential_helper;pub use credentials::https_host;pub use credentials::provider_fn;
Modules§
- credentials
- Credential provisioning for the CLI wrappers.
- json
serde - JSON helpers shared by the forge wrappers, behind the
serdefeature — so the three forge parsers share onenull -> ""and parse-error convention.
Macros§
- at_
forwarders - Generate the cwd-bound forwarders for a CLI wrapper’s
…Atview. - managed_
client - Emit the common client scaffold every CLI wrapper hand-writes around a
ManagedClient.
Structs§
- Managed
Client - A
CliClientwrapper that adds two opt-in concerns the CLI wrappers (vcs-git,vcs-jj,vcs-github,vcs-gitlab) all share, without touching a single call site: - Retry
Policy - A bounded retry strategy: how many attempts, the (exponential) backoff between
them, and whether to add full jitter. Used by
ManagedClientto retryis_lock_contentionfailures. TheDefaultisnone(no retry) — retry is opt-in.
Constants§
- FETCH_
ATTEMPTS - Total attempts for a transient-retried
fetch(1 try + 2 retries). - FETCH_
BACKOFF - Fixed backoff between fetch retries.
- FETCH_
TIMEOUT_ GRACE - Grace period for a timed-out fetch: on the deadline processkit signals the
process tree (terminate), waits this long for it to exit cleanly — flush, close
the connection, drop any lock — then hard-kills. Only takes effect when a
per-client timeout is set (
Git::default_timeout/Jj::default_timeout); a fetch with no deadline is unaffected.
Functions§
- is_
invalid_ input - Whether
erris an input rejection — a bad caller argument, encoded as anError::Spawnwhose source isio::ErrorKind::InvalidInput. This is the pattern the toolkit’s own argument guards raise (reject_flag_likeand the validating newtypesRefName/RevSpec/RevsetExpr) for a value that would be misparsed as a flag, is empty, or contains a NUL — and it also covers the spawn-timeInvalidInputthe OS raises for an un-spawnable argument (an interior NUL in a flag-value, or Windows’ batch-arg-escaping refusal). All are genuine bad input, distinct from a real spawn failure (missing binary →NotFound, no perms →PermissionDenied) or a non-zero exit. A binding maps this to aValueError; the facades re-expose it asError::is_invalid_input(). - is_
lock_ contention - Whether
erris a whole-repository lock-contention failure — another process held git’sindex.lockor jj’s working-copy / op-heads lock, so the command couldn’t even start. Such a failure is pre-execution and therefore safe to retry even on a mutating operation (the repo was never modified). Per-ref lock failures (cannot lock ref,<ref>.lock) are deliberately not classified here — they can occur mid-way through a multi-refpush/fetch, where a retry would not be idempotent. Conflict, “nothing to commit”, a real non-zero exit, a timeout, a signal, or a missing binary are also not lock contention and must not be retried this way. - is_
merge_ conflict - Whether a failed
merge/merge_commitstopped on a merge conflict. (jj surfaces conflicts as state rather than as errors, so this only fires on git output — seevcs_core::Error::is_merge_conflict.) - is_
nothing_ to_ commit - Whether a failed
commit/commit_pathsreported nothing to commit (a clean tree), as opposed to a real error. - is_
transient_ fetch_ error - Whether a failed
fetch/fetch_branch/remote_branch_existslooks transient (DNS, a dropped connection, a fast network blip) and is worth retrying. - reject_
flag_ like - Injection guard for bare positional argv slots: a caller-supplied value with a
leading
-would be parsed by the CLI as a flag (verified:git checkout -evil→ “unknown switch”; jj likewise), and an empty (or whitespace-only) value silently changes most commands’ meaning. Refuse both before anything spawns, surfacing anError::Spawnnamingprogram. An interior NUL is refused too (it can’t be passed in argv and otherwise surfaces as an opaque OS spawn error). Flag-VALUE positions (-m <msg>,--branch <b>) don’t need this — the CLI consumes the next token verbatim there. - retry_
async - Run
op, retrying its result whileshould_retrysays so andpolicyhas attempts left, sleeping the (jittered, exponential) backoff between tries. The op is re-invoked from scratch each attempt, so it must be idempotent for the errorsshould_retryselects (lock-contention failures are — the command never ran). Returns the firstOk, or the lastErr.