rusty_commit/utils/
hooks.rs1use anyhow::{Context, Result};
2use std::fs;
3use std::io::Write;
4use std::path::PathBuf;
5use std::process::{Command, Stdio};
6use std::time::Duration;
7
8pub struct HookOptions<'a> {
10 pub name: &'a str,
12 pub commands: Vec<String>,
14 pub strict: bool,
16 pub timeout: Duration,
18 pub envs: Vec<(&'a str, String)>,
20}
21
22fn parse_command(cmd: &str) -> Option<(String, Vec<String>)> {
25 let cmd = cmd.trim();
26 if cmd.is_empty() {
27 return None;
28 }
29
30 let mut parts = cmd.split_whitespace();
32 let executable = parts.next()?.to_string();
33 let args: Vec<String> = parts.map(|s| s.to_string()).collect();
34
35 Some((executable, args))
36}
37
38pub fn run_hooks(opts: HookOptions) -> Result<()> {
66 for (idx, cmd) in opts.commands.iter().enumerate() {
67 let (executable, args) = match parse_command(cmd) {
69 Some(parts) => parts,
70 None => {
71 eprintln!("Warning: Empty command in {} hook {}", opts.name, idx + 1);
72 continue;
73 }
74 };
75
76 let executable_lower = executable.to_lowercase();
78 if executable_lower == "sh"
79 || executable_lower == "bash"
80 || executable_lower == "cmd"
81 || executable_lower == "powershell"
82 {
83 eprintln!(
84 "Warning: Shell execution in {} hook {} is deprecated for security reasons. \
85 Consider using direct command execution instead: {}",
86 opts.name,
87 idx + 1,
88 cmd
89 );
90 }
91
92 let mut command = Command::new(&executable);
93 command.args(&args);
94
95 for (k, v) in &opts.envs {
96 command.env(k, v);
97 }
98
99 command.stdout(Stdio::inherit()).stderr(Stdio::inherit());
100
101 let mut child = command
102 .spawn()
103 .with_context(|| format!("Failed to start {} hook {}: {}", opts.name, idx + 1, cmd))?;
104
105 let start = std::time::Instant::now();
107 let status = loop {
108 if let Some(status) = child.try_wait()? {
109 break status;
110 }
111 if start.elapsed() > opts.timeout {
112 let _ = child.kill();
114 return Err(anyhow::anyhow!(
115 "{} hook timed out after {:?} while running: {}",
116 opts.name,
117 opts.timeout,
118 cmd
119 ));
120 }
121 std::thread::sleep(Duration::from_millis(100));
122 };
123
124 if !status.success() {
125 let msg = format!(
126 "{} hook failed (exit status {:?}) for command: {}",
127 opts.name,
128 status.code(),
129 cmd
130 );
131 if opts.strict {
132 return Err(anyhow::anyhow!(msg));
133 } else {
134 eprintln!("Warning: {}", msg);
135 }
136 }
137 }
138
139 Ok(())
140}
141
142pub fn write_temp_commit_file(initial: &str) -> Result<PathBuf> {
145 let mut temp_file = tempfile::NamedTempFile::new()?;
146 temp_file.write_all(initial.as_bytes())?;
147 let path = temp_file.into_temp_path();
149 let final_path = std::env::temp_dir().join(format!("rco-commit-{:}.txt", std::process::id()));
150 path.persist(&final_path)?;
151 Ok(final_path)
152}
153
154#[allow(dead_code)]
156pub fn cleanup_temp_commit_file(path: &PathBuf) {
157 let _ = fs::remove_file(path);
158}