gitwatch_rs/
commit_message.rs1use 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}