auths_cli/subprocess.rs
1//! Subprocess helpers — standardized command builders for external tools.
2//!
3//! All git subprocess calls should use [`git_command`] to ensure locale
4//! normalization (`LC_ALL=C`), which prevents non-English error messages
5//! from breaking stderr pattern matching.
6
7use std::path::Path;
8use std::process::{Command, Output};
9
10use anyhow::{Context, Result, anyhow};
11
12/// Build a `git` command with `LC_ALL=C` pre-set.
13///
14/// Callers can chain additional configuration (`.current_dir()`, `.stdin()`, etc.)
15/// before calling `.output()` or `.status()`.
16///
17/// Args:
18/// * `args`: Arguments to pass to git.
19///
20/// Usage:
21/// ```ignore
22/// let output = git_command(&["rev-parse", "HEAD"]).output()?;
23/// let output = git_command(&["log", "--oneline"]).current_dir(&repo).output()?;
24/// ```
25pub fn git_command(args: &[&str]) -> Command {
26 let mut cmd = Command::new("git");
27 cmd.args(args).env("LC_ALL", "C");
28 cmd
29}
30
31/// Run a git command and return its stdout as a trimmed string.
32///
33/// Returns an error with stderr context if the command fails.
34///
35/// Args:
36/// * `args`: Arguments to pass to git.
37///
38/// Usage:
39/// ```ignore
40/// let sha = git_stdout(&["rev-parse", "HEAD"])?;
41/// ```
42pub fn git_stdout(args: &[&str]) -> Result<String> {
43 let output = git_command(args)
44 .output()
45 .with_context(|| format!("Failed to run: git {}", args.join(" ")))?;
46
47 if !output.status.success() {
48 let stderr = String::from_utf8_lossy(&output.stderr);
49 return Err(anyhow!("git {} failed: {}", args.join(" "), stderr.trim()));
50 }
51
52 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
53}
54
55/// Run a git command in a specific directory and return stdout as a trimmed string.
56///
57/// Args:
58/// * `args`: Arguments to pass to git.
59/// * `dir`: Working directory for the git command.
60///
61/// Usage:
62/// ```ignore
63/// let log = git_stdout_in(&["log", "--oneline", "-1"], &repo_path)?;
64/// ```
65pub fn git_stdout_in(args: &[&str], dir: &Path) -> Result<String> {
66 let output = git_command(args)
67 .current_dir(dir)
68 .output()
69 .with_context(|| format!("Failed to run: git {}", args.join(" ")))?;
70
71 if !output.status.success() {
72 let stderr = String::from_utf8_lossy(&output.stderr);
73 return Err(anyhow!("git {} failed: {}", args.join(" "), stderr.trim()));
74 }
75
76 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
77}
78
79/// Run a git command, returning stdout on success or `None` on failure.
80///
81/// Use for optional checks where failure is not an error condition.
82///
83/// Args:
84/// * `args`: Arguments to pass to git.
85///
86/// Usage:
87/// ```ignore
88/// if let Some(sha) = git_silent(&["rev-parse", "--verify", "HEAD"]) { ... }
89/// ```
90pub fn git_silent(args: &[&str]) -> Option<String> {
91 git_command(args)
92 .output()
93 .ok()
94 .filter(|o| o.status.success())
95 .and_then(|o| String::from_utf8(o.stdout).ok())
96 .map(|s| s.trim().to_string())
97 .filter(|s| !s.is_empty())
98}
99
100/// Run a git command and return the raw [`Output`].
101///
102/// Always sets `LC_ALL=C`. Use when you need to inspect both stdout and stderr,
103/// or when the caller has custom status-checking logic.
104///
105/// Args:
106/// * `args`: Arguments to pass to git.
107///
108/// Usage:
109/// ```ignore
110/// let output = git_output(&["rev-list", "HEAD~5..HEAD"])?;
111/// if !output.status.success() { /* custom handling */ }
112/// ```
113pub fn git_output(args: &[&str]) -> Result<Output> {
114 git_command(args)
115 .output()
116 .with_context(|| format!("Failed to run: git {}", args.join(" ")))
117}