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