1use crate::error::{Error, Result};
55use async_trait::async_trait;
56use std::borrow::Cow;
57use std::collections::HashMap;
58use std::ffi::{OsStr, OsString};
59use std::path::PathBuf;
60use std::process::Stdio;
61use std::time::Duration;
62use tokio::process::Command as TokioCommand;
63use tracing::{debug, error, instrument, trace, warn};
64
65pub mod add;
66pub mod bisect;
67pub mod branch;
68pub mod cat_file;
69pub mod checkout;
70pub mod cherry_pick;
71pub mod clone;
72pub mod commit;
73pub mod config;
74pub mod describe;
75pub mod diff;
76pub mod fetch;
77pub mod for_each_ref;
78pub mod grep;
79pub mod hash_object;
80pub mod init;
81pub mod log;
82pub mod ls_files;
83pub mod ls_tree;
84pub mod merge;
85pub mod mv;
86pub mod pull;
87pub mod push;
88pub mod rebase;
89pub mod reflog;
90pub mod remote;
91pub mod reset;
92pub mod restore;
93pub mod rev_parse;
94pub mod rm;
95pub mod show;
96pub mod show_ref;
97pub mod stash;
98pub mod status;
99pub mod submodule;
100pub mod switch;
101pub mod symbolic_ref;
102pub mod tag;
103pub mod update_ref;
104pub mod worktree;
105
106pub const DEFAULT_COMMAND_TIMEOUT: Option<Duration> = None;
110
111#[async_trait]
113pub trait GitCommand {
114 type Output;
116
117 fn get_executor(&self) -> &CommandExecutor;
119
120 fn get_executor_mut(&mut self) -> &mut CommandExecutor;
122
123 fn build_command_args(&self) -> Vec<String>;
126
127 async fn execute(&self) -> Result<Self::Output>;
129
130 async fn execute_raw(&self) -> Result<CommandOutput> {
135 let args = self.build_command_args();
136 self.get_executor().execute_command(args).await
137 }
138
139 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
141 self.get_executor_mut().add_arg(arg);
142 self
143 }
144
145 fn args<I, S>(&mut self, args: I) -> &mut Self
147 where
148 I: IntoIterator<Item = S>,
149 S: AsRef<OsStr>,
150 {
151 self.get_executor_mut().add_args(args);
152 self
153 }
154
155 fn flag(&mut self, flag: &str) -> &mut Self {
157 self.get_executor_mut().add_flag(flag);
158 self
159 }
160
161 fn option(&mut self, key: &str, value: &str) -> &mut Self {
163 self.get_executor_mut().add_option(key, value);
164 self
165 }
166
167 fn current_dir<P: Into<PathBuf>>(&mut self, dir: P) -> &mut Self {
169 self.get_executor_mut().cwd = Some(dir.into());
170 self
171 }
172
173 fn env<K: Into<OsString>, V: Into<OsString>>(&mut self, key: K, value: V) -> &mut Self {
175 self.get_executor_mut().env.insert(key.into(), value.into());
176 self
177 }
178
179 fn with_timeout(&mut self, timeout: Duration) -> &mut Self {
182 self.get_executor_mut().timeout = Some(timeout);
183 self
184 }
185
186 fn with_timeout_secs(&mut self, seconds: u64) -> &mut Self {
188 self.get_executor_mut().timeout = Some(Duration::from_secs(seconds));
189 self
190 }
191}
192
193#[derive(Debug, Clone, Default)]
195pub struct CommandExecutor {
196 pub raw_args: Vec<String>,
198 pub cwd: Option<PathBuf>,
200 pub env: HashMap<OsString, OsString>,
202 pub timeout: Option<Duration>,
204}
205
206impl CommandExecutor {
207 #[must_use]
209 pub fn new() -> Self {
210 Self::default()
211 }
212
213 #[must_use]
215 pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
216 self.cwd = Some(path.into());
217 self
218 }
219
220 #[must_use]
222 pub fn with_env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
223 self.env.insert(key.into(), value.into());
224 self
225 }
226
227 #[must_use]
229 pub fn timeout(mut self, timeout: Duration) -> Self {
230 self.timeout = Some(timeout);
231 self
232 }
233
234 pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) {
236 self.raw_args
237 .push(arg.as_ref().to_string_lossy().into_owned());
238 }
239
240 pub fn add_args<I, S>(&mut self, args: I)
242 where
243 I: IntoIterator<Item = S>,
244 S: AsRef<OsStr>,
245 {
246 for a in args {
247 self.add_arg(a);
248 }
249 }
250
251 pub fn add_flag(&mut self, flag: &str) {
253 let normalized = if flag.starts_with('-') {
254 flag.to_string()
255 } else if flag.len() == 1 {
256 format!("-{flag}")
257 } else {
258 format!("--{flag}")
259 };
260 self.raw_args.push(normalized);
261 }
262
263 pub fn add_option(&mut self, key: &str, value: &str) {
265 let normalized = if key.starts_with('-') {
266 key.to_string()
267 } else if key.len() == 1 {
268 format!("-{key}")
269 } else {
270 format!("--{key}")
271 };
272 self.raw_args.push(normalized);
273 self.raw_args.push(value.to_string());
274 }
275
276 #[instrument(
280 name = "git.command",
281 skip(self, args),
282 fields(
283 cwd = self.cwd.as_ref().map(|p| p.display().to_string()),
284 timeout_secs = self.timeout.map(|t| t.as_secs()),
285 )
286 )]
287 pub async fn execute_command(&self, args: Vec<String>) -> Result<CommandOutput> {
288 let mut all_args = args;
289 all_args.extend(self.raw_args.iter().cloned());
290
291 trace!(args = ?all_args, "executing git command");
292
293 let result = if let Some(t) = self.timeout {
294 self.execute_with_timeout(&all_args, t).await
295 } else {
296 self.execute_internal(&all_args).await
297 };
298
299 match &result {
300 Ok(output) => debug!(
301 exit_code = output.exit_code,
302 stdout_len = output.stdout.len(),
303 stderr_len = output.stderr.len(),
304 "command completed"
305 ),
306 Err(e) => error!(error = %e, "command failed"),
307 }
308
309 result
310 }
311
312 async fn execute_internal(&self, all_args: &[String]) -> Result<CommandOutput> {
313 let mut cmd = TokioCommand::new("git");
314 cmd.args(all_args)
315 .stdout(Stdio::piped())
316 .stderr(Stdio::piped());
317
318 if let Some(dir) = &self.cwd {
319 cmd.current_dir(dir);
320 }
321 for (k, v) in &self.env {
322 cmd.env(k, v);
323 }
324
325 let output = cmd.output().await.map_err(|e| {
326 if e.kind() == std::io::ErrorKind::NotFound {
327 Error::GitNotFound
328 } else {
329 Error::Io {
330 message: format!("failed to spawn git: {e}"),
331 source: e,
332 }
333 }
334 })?;
335
336 let stdout = output.stdout;
337 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
338 let exit_code = output.status.code().unwrap_or(-1);
339 let success = output.status.success();
340
341 if !success {
342 return Err(Error::command_failed(
343 format!("git {}", all_args.join(" ")),
344 exit_code,
345 String::from_utf8_lossy(&stdout).into_owned(),
346 stderr,
347 ));
348 }
349
350 Ok(CommandOutput {
351 stdout,
352 stderr,
353 exit_code,
354 success,
355 })
356 }
357
358 async fn execute_with_timeout(
359 &self,
360 all_args: &[String],
361 timeout_duration: Duration,
362 ) -> Result<CommandOutput> {
363 match tokio::time::timeout(timeout_duration, self.execute_internal(all_args)).await {
364 Ok(r) => r,
365 Err(_) => {
366 warn!(
367 timeout_secs = timeout_duration.as_secs(),
368 "command timed out"
369 );
370 Err(Error::timeout(timeout_duration.as_secs()))
371 }
372 }
373 }
374}
375
376#[derive(Debug, Clone)]
378pub struct CommandOutput {
379 pub stdout: Vec<u8>,
387 pub stderr: String,
389 pub exit_code: i32,
391 pub success: bool,
393}
394
395impl CommandOutput {
396 #[must_use]
398 pub fn stdout_bytes(&self) -> &[u8] {
399 &self.stdout
400 }
401
402 #[must_use]
404 pub fn stdout_str(&self) -> Cow<'_, str> {
405 String::from_utf8_lossy(&self.stdout)
406 }
407
408 #[must_use]
410 pub fn stdout_lines(&self) -> Vec<String> {
411 self.stdout_str().lines().map(ToOwned::to_owned).collect()
412 }
413
414 #[must_use]
416 pub fn stderr_lines(&self) -> Vec<&str> {
417 self.stderr.lines().collect()
418 }
419
420 #[must_use]
422 pub fn stdout_trimmed(&self) -> String {
423 self.stdout_str().trim_end().to_owned()
424 }
425}
426
427pub fn find_git() -> Result<PathBuf> {
433 which::which("git").map_err(|_| Error::GitNotFound)
434}
435
436pub async fn git_version() -> Result<String> {
438 let output = CommandExecutor::new()
439 .execute_command(vec!["--version".into()])
440 .await?;
441 Ok(output.stdout_trimmed())
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn executor_args() {
450 let mut e = CommandExecutor::new();
451 e.add_arg("foo");
452 e.add_args(["a", "b"]);
453 e.add_flag("verbose");
454 e.add_flag("v");
455 e.add_option("name", "bar");
456 assert_eq!(
457 e.raw_args,
458 vec!["foo", "a", "b", "--verbose", "-v", "--name", "bar"]
459 );
460 }
461
462 #[test]
463 fn executor_timeout_builder() {
464 let e = CommandExecutor::new().timeout(Duration::from_secs(5));
465 assert_eq!(e.timeout, Some(Duration::from_secs(5)));
466 }
467
468 #[test]
469 fn command_output_helpers() {
470 let o = CommandOutput {
471 stdout: b"a\nb\n".to_vec(),
472 stderr: String::new(),
473 exit_code: 0,
474 success: true,
475 };
476 assert_eq!(o.stdout_lines(), vec!["a", "b"]);
477 assert_eq!(o.stdout_trimmed(), "a\nb");
478 assert_eq!(o.stdout_bytes(), b"a\nb\n");
479 }
480}