Skip to main content

Crate vcs_cli_support

Crate vcs_cli_support 

Source
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 surfaced Error::Spawn names the right program.
  • FETCH_ATTEMPTS / FETCH_BACKOFF — the shared transient-retry policy for fetch (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 returned Error so 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 captured Error::Exit output against fixed marker lists; a processkit Error::Timeout is not treated as a transient fetch error (it already spent the full deadline — see is_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. ManagedClient wraps a processkit CliClient and applies the policy to every command, so the vcs-git/vcs-jj clients gain retry via with_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. ManagedClient injects the resolved token into each command (the forge GH_TOKEN/GITLAB_TOKEN env); git uses git_credential_helper to keep the secret out of argv. Default is no provider → ambient auth, unchanged. See the credentials module 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.
jsonserde
JSON helpers shared by the forge wrappers, behind the serde feature — so the three forge parsers share one null -> "" and parse-error convention.

Macros§

at_forwarders
Generate the cwd-bound forwarders for a CLI wrapper’s …At view.
managed_client
Emit the common client scaffold every CLI wrapper hand-writes around a ManagedClient.

Structs§

ManagedClient
A CliClient wrapper 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:
RetryPolicy
A bounded retry strategy: how many attempts, the (exponential) backoff between them, and whether to add full jitter. Used by ManagedClient to retry is_lock_contention failures. The Default is none (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 err is an input rejection — a bad caller argument, encoded as an Error::Spawn whose source is io::ErrorKind::InvalidInput. This is the pattern the toolkit’s own argument guards raise (reject_flag_like and the validating newtypes RefName/RevSpec/RevsetExpr) for a value that would be misparsed as a flag, is empty, or contains a NUL — and it also covers the spawn-time InvalidInput the 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 a ValueError; the facades re-expose it as Error::is_invalid_input().
is_lock_contention
Whether err is a whole-repository lock-contention failure — another process held git’s index.lock or 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-ref push/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_commit stopped on a merge conflict. (jj surfaces conflicts as state rather than as errors, so this only fires on git output — see vcs_core::Error::is_merge_conflict.)
is_nothing_to_commit
Whether a failed commit/commit_paths reported nothing to commit (a clean tree), as opposed to a real error.
is_transient_fetch_error
Whether a failed fetch/fetch_branch/remote_branch_exists looks 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 an Error::Spawn naming program. 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 while should_retry says so and policy has 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 errors should_retry selects (lock-contention failures are — the command never ran). Returns the first Ok, or the last Err.