commit_wizard/adapters/git/
cmd.rs

1use anyhow::{Context, Result, anyhow};
2use std::io::Write;
3use std::process::Command;
4use tempfile::NamedTempFile;
5
6use crate::ports::git::{CommitOptions, GitPort};
7
8/// Shells out to the `git` CLI to perform commits.
9#[derive(Default)]
10pub struct CmdGit;
11
12impl CmdGit {
13    fn run_status(args: &[&str]) -> Result<std::process::ExitStatus> {
14        Command::new("git")
15            .args(args)
16            .status()
17            .with_context(|| format!("failed to launch `git {}`", args.join(" ")))
18    }
19
20    fn run_capture(args: &[&str]) -> Result<std::process::Output> {
21        Command::new("git")
22            .args(args)
23            .output()
24            .with_context(|| format!("failed to launch `git {}`", args.join(" ")))
25    }
26}
27
28impl GitPort for CmdGit {
29    fn commit(&self, message: &str, opts: &CommitOptions) -> Result<()> {
30        // Are we in a repo?
31        if !Self::run_status(&["rev-parse", "--git-dir"])?.success() {
32            return Err(anyhow!(
33                "Not inside a git repository (run `git init` first)."
34            ));
35        }
36
37        // Skip staged check only if allow_empty is true
38        if !opts.allow_empty {
39            // exit=0 => no staged changes; exit=1 => there are staged changes
40            if Self::run_status(&["diff", "--cached", "--quiet"])?.success() {
41                return Err(anyhow!(
42                    "No staged changes. Stage files with `git add -A` (or pass --allow-empty)."
43                ));
44            }
45        }
46
47        // Write message to a temp file
48        let mut tf = NamedTempFile::new().context("failed to create temp file")?;
49        tf.write_all(message.as_bytes())
50            .context("write commit message")?;
51        let msg_path = tf.path().to_string_lossy().to_string();
52
53        // Build git args
54        let mut args = vec!["commit", "-F", &msg_path];
55        if opts.allow_empty {
56            args.push("--allow-empty");
57        }
58
59        let out = Self::run_capture(&args)?;
60        if !out.status.success() {
61            let stderr = String::from_utf8_lossy(&out.stderr);
62            return Err(anyhow!("git commit failed: {}", stderr.trim()));
63        }
64        let stdout = String::from_utf8_lossy(&out.stdout);
65        if !stdout.trim().is_empty() {
66            println!("{stdout}");
67        }
68        Ok(())
69    }
70}