gitwatch_rs/
commit_message.rs

1use anyhow::{bail, Context, Result};
2use log::debug;
3use std::path::Path;
4use std::process::Command;
5
6pub fn generate_commit_message(script_path: &Path, repo_path: &Path) -> Result<String> {
7    debug!(
8        "Executing commit message script '{}'",
9        script_path.display()
10    );
11    let output = Command::new(script_path)
12        .current_dir(repo_path)
13        .output()
14        .with_context(|| {
15            format!(
16                "Failed to execute commit message script '{}'",
17                script_path.display()
18            )
19        })?;
20
21    if !output.status.success() {
22        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
23        bail!(
24            "Commit message script '{}' failed with exit code {}.\nError: {}",
25            script_path.display(),
26            output.status,
27            stderr
28        );
29    }
30
31    let commit_message = String::from_utf8(output.stdout)
32        .context("Commit message script output is not valid UTF-8")?;
33
34    let trimmed_message = commit_message.trim();
35    if trimmed_message.is_empty() {
36        bail!("Commit message script output is empty");
37    }
38
39    if let Some(first_line) = trimmed_message.lines().next() {
40        debug!("Generated commit message: '{first_line}'");
41    }
42
43    Ok(commit_message)
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use std::fs::OpenOptions;
50    use std::io::Write;
51    use std::os::unix::fs::OpenOptionsExt;
52    use std::thread;
53    use std::time::Duration;
54    use tempfile::TempDir;
55    use testresult::TestResult;
56
57    #[test]
58    #[cfg(unix)]
59    fn test_successful_script_execution() -> TestResult {
60        let temp_dir = TempDir::new()?;
61        let script_content = "echo 'Test commit message'";
62        let script_path = create_test_script(&temp_dir, script_content)?;
63
64        let result = generate_commit_message(&script_path, temp_dir.path())?;
65        assert_eq!(result.trim(), "Test commit message");
66        Ok(())
67    }
68
69    #[test]
70    #[cfg(unix)]
71    fn test_working_directory() -> TestResult {
72        let temp_dir = TempDir::new()?;
73        let script_content = "echo $PWD";
74        let script_path = create_test_script(&temp_dir, script_content)?;
75
76        let result = generate_commit_message(&script_path, temp_dir.path())?;
77        assert_eq!(
78            result.trim(),
79            temp_dir.path().canonicalize()?.display().to_string()
80        );
81        Ok(())
82    }
83
84    #[test]
85    #[cfg(unix)]
86    fn test_script_execution_failure() -> TestResult {
87        let temp_dir = TempDir::new()?;
88        let script_content = "exit 1";
89        let script_path = create_test_script(&temp_dir, script_content)?;
90
91        let result = generate_commit_message(&script_path, temp_dir.path());
92        assert!(result
93            .unwrap_err()
94            .to_string()
95            .contains("failed with exit code"));
96        Ok(())
97    }
98
99    #[test]
100    fn test_nonexistent_script() {
101        let result = generate_commit_message(Path::new("/nonexistent/script/path"), Path::new(""));
102        assert!(result
103            .unwrap_err()
104            .to_string()
105            .contains("Failed to execute commit message script"));
106    }
107
108    #[test]
109    #[cfg(unix)]
110    fn test_empty_output() -> TestResult {
111        let temp_dir = TempDir::new()?;
112        let script_content = "echo ''";
113        let script_path = create_test_script(&temp_dir, script_content)?;
114
115        let result = generate_commit_message(&script_path, temp_dir.path());
116        assert!(result
117            .unwrap_err()
118            .to_string()
119            .contains("Commit message script output is empty"));
120        Ok(())
121    }
122
123    #[test]
124    #[cfg(unix)]
125    fn test_whitespace_only_output() -> TestResult {
126        let temp_dir = TempDir::new()?;
127        let script_content = "echo '   \n  \t  '";
128        let script_path = create_test_script(&temp_dir, script_content)?;
129
130        let result = generate_commit_message(&script_path, temp_dir.path());
131        assert!(result
132            .unwrap_err()
133            .to_string()
134            .contains("Commit message script output is empty"));
135        Ok(())
136    }
137
138    fn create_test_script(dir: &TempDir, content: &str) -> Result<std::path::PathBuf> {
139        let script_path = dir.path().join("test_script.sh");
140        OpenOptions::new()
141            .write(true)
142            .create(true)
143            .truncate(true)
144            .mode(0o755)
145            .open(&script_path)?
146            .write_all(format!("#!/bin/sh\n{content}").as_bytes())?;
147        thread::sleep(Duration::from_millis(50));
148        Ok(script_path)
149    }
150}