Skip to main content

gi/command/
commit.rs

1use crate::command::Command;
2use crate::error::{GiError, Result};
3use crate::repo::GitRepo;
4use crate::ui;
5use std::process;
6
7/// Commit staged changes.
8pub struct Commit;
9
10impl Commit {
11    fn normalize_message(message: String) -> Option<String> {
12        (!message.trim().is_empty()).then_some(message)
13    }
14
15    /// Runs a git hook if it exists. Returns Ok if hook doesn't exist or succeeds.
16    fn run_hook(repo: &GitRepo, hook_name: &str) -> Result<()> {
17        let workdir = repo.workdir().unwrap_or(repo.path());
18
19        let output = process::Command::new("git")
20            .args(["hook", "run", hook_name])
21            .current_dir(workdir)
22            .output()
23            .map_err(|e| GiError::Other(e.into()))?;
24
25        // Check if hook doesn't exist (exit code is not 0 and stderr contains "cannot find")
26        let stderr = String::from_utf8_lossy(&output.stderr);
27        if stderr.contains("cannot find") || stderr.contains("not found") {
28            // Hook doesn't exist, that's fine
29            return Ok(());
30        }
31
32        if !output.status.success() {
33            return Err(GiError::Other(anyhow::anyhow!(
34                "{} hook failed:\n{}",
35                hook_name,
36                stderr.trim()
37            )));
38        }
39
40        Ok(())
41    }
42
43    /// Executes commit, optionally using a provided commit message.
44    pub fn execute_with_message(repo: &GitRepo, message: Option<String>) -> Result<()> {
45        if !repo.has_staged_changes()? {
46            println!("No staged changes to commit");
47            return Ok(());
48        }
49
50        let message = match message {
51            Some(message) => Self::normalize_message(message),
52            None => ui::required_text(
53                "Commit message:",
54                Some("Enter a descriptive commit message"),
55            )?,
56        };
57
58        let Some(message) = message else {
59            println!("Aborting commit due to empty commit message");
60            return Ok(());
61        };
62
63        // Run pre-commit hook (skips if not found)
64        Self::run_hook(repo, "pre-commit")?;
65
66        // Create the commit using git2
67        repo.commit(&message)?;
68
69        // Run post-commit hook (ignore errors as commit is already done)
70        let _ = Self::run_hook(repo, "post-commit");
71
72        println!("Committed: {}", message);
73        Ok(())
74    }
75}
76
77impl Command for Commit {
78    fn execute(repo: &GitRepo) -> Result<()> {
79        Self::execute_with_message(repo, None)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::Commit;
86
87    #[test]
88    fn normalize_message_keeps_non_empty_message() {
89        assert_eq!(
90            Commit::normalize_message("commit message".to_string()),
91            Some("commit message".to_string())
92        );
93    }
94
95    #[test]
96    fn normalize_message_rejects_blank_message() {
97        assert_eq!(Commit::normalize_message("   ".to_string()), None);
98    }
99}