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		format!("git spawn failed: {}", Error)
45	})?;
46
47	if let Some(Pid) = Child.id() {
48		super::RegisterPid::Fn(OperationId, Pid);
49	}
50
51	let WaitFuture = Child.wait_with_output();
52
53	let Output = match tokio::time::timeout(GIT_EXEC_TIMEOUT, WaitFuture).await {
54		Ok(WaitResult) => {
55			WaitResult.map_err(|Error| {
56				super::ClearPid::Fn(OperationId);
57				format!("git wait failed: {}", Error)
58			})?
59		},
60		Err(_) => {
61			// Timeout expired. SIGTERM the PID via the registry-aware helper
62			// so the running-process accounting stays consistent; the
63			// kill_on_drop(true) on the Command also covers the race where
64			// the subprocess hasn't been observed yet.
65			let _ = super::TakePid::Fn(OperationId);
66
67			dev_log!(
68				"git",
69				"warn: [Git] exec-timeout op={} Arguments=[{}] after {}s - subprocess killed",
70				OperationId,
71				Args.join(" "),
72				GIT_EXEC_TIMEOUT.as_secs()
73			);
74
75			return Err(format!(
76				"git exec timed out after {}s: git {}",
77				GIT_EXEC_TIMEOUT.as_secs(),
78				Args.join(" ")
79			));
80		},
81	};
82
83	super::ClearPid::Fn(OperationId);
84
85	let ExitCode = Output.status.code().unwrap_or(-1);
86
87	let Stdout = String::from_utf8_lossy(&Output.stdout).into_owned();
88
89	let Stderr = String::from_utf8_lossy(&Output.stderr).into_owned();
90
91	dev_log!(
92		"git",
93		"[Git] exec-done op={} Arguments=[{}] exit={} stdout={}B stderr={}B",
94		OperationId,
95		Args.join(" "),
96		ExitCode,
97		Stdout.len(),
98		Stderr.len()
99	);
100
101	Ok((ExitCode, Stdout, Stderr))
102}