feat(cli): add helpers::git_run with stderr capture + context bail
This commit is contained in:
@@ -55,6 +55,37 @@ pub fn git_command(repo: &Path, args: &[&str]) -> Command {
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Run `git <args>` in `repo` with the same hardening as `git_command`,
|
||||
/// capturing stdout/stderr and reproducing them on failure so the caller
|
||||
/// sees git's exact diagnostic instead of just a verb.
|
||||
///
|
||||
/// `context` should be a short caller-supplied label like `"commit add: <id>"`
|
||||
/// or `"sync: git push"`; it prefixes the bail message so the failing call is
|
||||
/// identifiable from the error alone.
|
||||
///
|
||||
/// Trade-off vs. `git_command(...).status()`: this captures the child's stderr
|
||||
/// (so live progress disappears during long-running fetches/pushes) but the
|
||||
/// captured chunk is replayed verbatim on failure. The win is that
|
||||
/// non-interactive callers (tests, hooks, CI, redirected stdout) finally see
|
||||
/// pre-receive rejections, signing-key prompts, and dirty-tree complaints
|
||||
/// instead of one-line "git X failed" bails. Use `git_command` directly when
|
||||
/// live streaming is required.
|
||||
pub fn git_run(repo: &Path, args: &[&str], context: &str) -> Result<()> {
|
||||
let output = git_command(repo, args)
|
||||
.output()
|
||||
.with_context(|| format!("{context}: failed to spawn git"))?;
|
||||
if !output.status.success() {
|
||||
if !output.stdout.is_empty() {
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
bail!("{context}: git failed ({})", output.status);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Format a Unix-seconds timestamp as an ISO-8601 UTC string.
|
||||
/// Audit M11: replaces the old `now_iso8601` helper that actually returned
|
||||
/// a numeric string.
|
||||
@@ -220,6 +251,24 @@ mod tests {
|
||||
assert_eq!(sanitize_for_commit("emoji \u{1F4AA}"), "emoji \u{1F4AA}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_run_bails_with_context_on_failure() {
|
||||
// Empty tempdir — `git status` will fail with "not a git repository".
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let err = git_run(tmp.path(), &["status"], "test_ctx").unwrap_err();
|
||||
let msg = format!("{err}");
|
||||
assert!(msg.contains("test_ctx"), "context not in error: {msg}");
|
||||
assert!(msg.contains("git failed"), "missing failure marker: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_run_succeeds_for_a_zero_exit_command() {
|
||||
// `git --version` always succeeds and is independent of cwd.
|
||||
let tmp = TempDir::new().unwrap();
|
||||
git_run(tmp.path(), &["--version"], "version probe")
|
||||
.expect("git --version should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn humanize_age_buckets() {
|
||||
assert_eq!(humanize_age(0), "just now");
|
||||
|
||||
Reference in New Issue
Block a user