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
|
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.
|
/// Format a Unix-seconds timestamp as an ISO-8601 UTC string.
|
||||||
/// Audit M11: replaces the old `now_iso8601` helper that actually returned
|
/// Audit M11: replaces the old `now_iso8601` helper that actually returned
|
||||||
/// a numeric string.
|
/// a numeric string.
|
||||||
@@ -220,6 +251,24 @@ mod tests {
|
|||||||
assert_eq!(sanitize_for_commit("emoji \u{1F4AA}"), "emoji \u{1F4AA}");
|
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]
|
#[test]
|
||||||
fn humanize_age_buckets() {
|
fn humanize_age_buckets() {
|
||||||
assert_eq!(humanize_age(0), "just now");
|
assert_eq!(humanize_age(0), "just now");
|
||||||
|
|||||||
Reference in New Issue
Block a user