Skip to main content

Mountain/IPC/WindServiceHandlers/Git/Shared/
RunGit.rs

1//! Spawns `git`, registers the PID, awaits output, returns
2//! `(exit_code, stdout, stderr)`.
3
4use std::time::Duration;
5
6use tokio::process::Command;
7
8use crate::dev_log;
9
10/// Upper-bound wall time for any single `git` invocation. Generous enough that
11/// legitimately slow operations (large monorepo clones with credential
12/// prompts, file-share-backed working copies, full-repo `log` walks) finish
13/// well within budget, but tight enough that a hung subprocess - stalled on
14/// a credential prompt with no TTY, a stuck index lock, or a network mount
15/// that has gone unresponsive - releases the Mountain effect slot before
16/// the extension host's own watchdog fires.
17const GIT_EXEC_TIMEOUT:Duration = Duration::from_secs(30);
18
19pub async fn Fn(OperationId:&str, Args:&[String], Cwd:Option<&str>) -> Result<(i32, String, String), String> {
20	dev_log!(
21		"git",
22		"[Git] exec-begin op={} cwd={} Arguments=[{}]",
23		OperationId,
24		Cwd.unwrap_or("<inherit>"),
25		Args.join(" ")
26	);
27
28	let WorkingDir = Cwd
29		.map(super::ResolveCwd::Fn)
30		.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
31
32	let mut Spawn = Command::new("git");
33
34	Spawn.args(Args).current_dir(&WorkingDir).kill_on_drop(true);
35
36	let Child = Spawn.spawn().map_err(|Error| {
37		dev_log!(
38			"git",
39			"[Git] exec-spawn-fail op={} Arguments=[{}] error={}",
40			OperationId,
41			Args.join(" "),
42			Error
43		);
44
45		format!("git spawn failed: {}", Error)
46	})?;
47
48	if let Some(Pid) = Child.id() {
49		super::RegisterPid::Fn(OperationId, Pid);
50	}
51
52	let WaitFuture = Child.wait_with_output();
53
54	let Output = match tokio::time::timeout(GIT_EXEC_TIMEOUT, WaitFuture).await {
55		Ok(WaitResult) => {
56			WaitResult.map_err(|Error| {
57				super::ClearPid::Fn(OperationId);
58
59				format!("git wait failed: {}", Error)
60			})?
61		},
62
63		Err(_) => {
64			// Timeout expired. SIGTERM the PID via the registry-aware helper
65			// so the running-process accounting stays consistent; the
66			// kill_on_drop(true) on the Command also covers the race where
67			// the subprocess hasn't been observed yet.
68			let _ = super::TakePid::Fn(OperationId);
69
70			dev_log!(
71				"git",
72				"warn: [Git] exec-timeout op={} Arguments=[{}] after {}s - subprocess killed",
73				OperationId,
74				Args.join(" "),
75				GIT_EXEC_TIMEOUT.as_secs()
76			);
77
78			return Err(format!(
79				"git exec timed out after {}s: git {}",
80				GIT_EXEC_TIMEOUT.as_secs(),
81				Args.join(" ")
82			));
83		},
84	};
85
86	super::ClearPid::Fn(OperationId);
87
88	let ExitCode = Output.status.code().unwrap_or(-1);
89
90	let Stdout = String::from_utf8_lossy(&Output.stdout).into_owned();
91
92	let Stderr = String::from_utf8_lossy(&Output.stderr).into_owned();
93
94	dev_log!(
95		"git",
96		"[Git] exec-done op={} Arguments=[{}] exit={} stdout={}B stderr={}B",
97		OperationId,
98		Args.join(" "),
99		ExitCode,
100		Stdout.len(),
101		Stderr.len()
102	);
103
104	Ok((ExitCode, Stdout, Stderr))
105}