Mountain/IPC/WindServiceHandlers/
Git.rs1#![allow(non_snake_case, unused_variables, dead_code)]
2use std::{
17 collections::HashMap,
18 path::PathBuf,
19 sync::{Mutex, OnceLock},
20};
21
22use serde_json::{Value, json};
23use tokio::process::Command;
24
25use crate::dev_log;
26
27fn RunningProcesses() -> &'static Mutex<HashMap<String, u32>> {
28 static SLOT:OnceLock<Mutex<HashMap<String, u32>>> = OnceLock::new();
29 SLOT.get_or_init(|| Mutex::new(HashMap::new()))
30}
31
32fn RegisterPid(OperationId:&str, Pid:u32) {
33 if OperationId.is_empty() {
34 return;
35 }
36 if let Ok(mut Map) = RunningProcesses().lock() {
37 Map.insert(OperationId.to_string(), Pid);
38 }
39}
40
41fn ClearPid(OperationId:&str) {
42 if OperationId.is_empty() {
43 return;
44 }
45 if let Ok(mut Map) = RunningProcesses().lock() {
46 Map.remove(OperationId);
47 }
48}
49
50fn TakePid(OperationId:&str) -> Option<u32> {
51 if OperationId.is_empty() {
52 return None;
53 }
54 RunningProcesses().lock().ok().and_then(|mut M| M.remove(OperationId))
55}
56
57fn ResolveCwd(Raw:&str) -> PathBuf {
58 if Raw.is_empty() {
59 std::env::current_dir().unwrap_or_default()
60 } else {
61 PathBuf::from(Raw)
62 }
63}
64
65async fn RunGit(OperationId:&str, Args:&[String], Cwd:Option<&str>) -> Result<(i32, String, String), String> {
66 dev_log!(
67 "git",
68 "[Git] exec-begin op={} cwd={} Arguments=[{}]",
69 OperationId,
70 Cwd.unwrap_or("<inherit>"),
71 Args.join(" ")
72 );
73
74 let WorkingDir = Cwd
75 .map(ResolveCwd)
76 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
77
78 let mut Spawn = Command::new("git");
79 Spawn.args(Args).current_dir(&WorkingDir).kill_on_drop(true);
80
81 let Child = Spawn.spawn().map_err(|Error| {
82 dev_log!(
83 "git",
84 "[Git] exec-spawn-fail op={} Arguments=[{}] error={}",
85 OperationId,
86 Args.join(" "),
87 Error
88 );
89 format!("git spawn failed: {}", Error)
90 })?;
91
92 if let Some(Pid) = Child.id() {
93 RegisterPid(OperationId, Pid);
94 }
95
96 let Output = Child.wait_with_output().await.map_err(|Error| {
97 ClearPid(OperationId);
98 format!("git wait failed: {}", Error)
99 })?;
100
101 ClearPid(OperationId);
102
103 let ExitCode = Output.status.code().unwrap_or(-1);
104 let Stdout = String::from_utf8_lossy(&Output.stdout).into_owned();
105 let Stderr = String::from_utf8_lossy(&Output.stderr).into_owned();
106
107 dev_log!(
108 "git",
109 "[Git] exec-done op={} Arguments=[{}] exit={} stdout={}B stderr={}B",
110 OperationId,
111 Args.join(" "),
112 ExitCode,
113 Stdout.len(),
114 Stderr.len()
115 );
116
117 Ok((ExitCode, Stdout, Stderr))
118}
119
120fn AsStringArray(Value:&Value) -> Vec<String> {
121 Value
122 .as_array()
123 .map(|Arr| Arr.iter().filter_map(|V| V.as_str().map(str::to_string)).collect())
124 .unwrap_or_default()
125}
126
127fn Generated() -> String { uuid::Uuid::new_v4().to_string() }
128
129pub async fn HandleExec(Arguments:Vec<Value>) -> Result<Value, String> {
133 let (Argv, Cwd, OperationId) = match Arguments.first() {
134 Some(First) if First.is_object() => {
135 let Obj = First.as_object().unwrap();
136 let Argv = Obj.get("Arguments").map(AsStringArray).unwrap_or_default();
137 let Cwd = Obj.get("cwd").and_then(Value::as_str).unwrap_or("").to_string();
138 let OperationId = Obj.get("operationId").and_then(Value::as_str).unwrap_or("").to_string();
139 (Argv, Cwd, OperationId)
140 },
141 Some(First) if First.is_array() => {
142 let Argv = AsStringArray(First);
143 let Cwd = Arguments.get(1).and_then(Value::as_str).unwrap_or("").to_string();
144 (Argv, Cwd, String::new())
145 },
146 _ => (Vec::new(), String::new(), String::new()),
147 };
148
149 if Argv.is_empty() {
150 return Err("git:exec requires non-empty Arguments".to_string());
151 }
152
153 let OperationIdRef = if OperationId.is_empty() { Generated() } else { OperationId };
154 let CwdOpt = if Cwd.is_empty() { None } else { Some(Cwd.as_str()) };
155 let (ExitCode, Stdout, Stderr) = RunGit(&OperationIdRef, &Argv, CwdOpt).await?;
156
157 Ok(json!({
158 "stdout": Stdout,
159 "stderr": Stderr,
160 "exitCode": ExitCode,
161 }))
162}
163
164pub async fn HandleClone(Arguments:Vec<Value>) -> Result<Value, String> {
166 let OperationId = Arguments.first().and_then(Value::as_str).unwrap_or("").to_string();
167 let CloneURL = Arguments.get(1).and_then(Value::as_str).unwrap_or("").to_string();
168 let TargetPath = Arguments.get(2).and_then(Value::as_str).unwrap_or("").to_string();
169 let Reference = Arguments.get(3).and_then(Value::as_str).map(str::to_string);
170
171 if CloneURL.is_empty() || TargetPath.is_empty() {
172 return Err("git:clone requires cloneUrl and targetPath".to_string());
173 }
174
175 let mut Argv:Vec<String> = vec!["clone".to_string()];
176 if let Some(Ref) = Reference {
177 Argv.push("--branch".to_string());
178 Argv.push(Ref);
179 }
180 Argv.push("--".to_string());
181 Argv.push(CloneURL);
182 Argv.push(TargetPath);
183
184 let (ExitCode, _, Stderr) = RunGit(&OperationId, &Argv, None).await?;
185 if ExitCode != 0 {
186 return Err(format!("git clone failed: {}", Stderr));
187 }
188 Ok(Value::Null)
189}
190
191pub async fn HandlePull(Arguments:Vec<Value>) -> Result<Value, String> {
193 let OperationId = Arguments.first().and_then(Value::as_str).unwrap_or("").to_string();
194 let RepoPath = Arguments.get(1).and_then(Value::as_str).unwrap_or("").to_string();
195 if RepoPath.is_empty() {
196 return Err("git:pull requires repoPath".to_string());
197 }
198
199 let (BeforeExit, Before, _) =
200 RunGit(&OperationId, &["rev-parse".to_string(), "HEAD".to_string()], Some(&RepoPath)).await?;
201 if BeforeExit != 0 {
202 return Err("git:pull: failed to read HEAD before pull".to_string());
203 }
204
205 let (PullExit, _, PullStderr) =
206 RunGit(&OperationId, &["pull".to_string(), "--ff-only".to_string()], Some(&RepoPath)).await?;
207 if PullExit != 0 {
208 return Err(format!("git pull failed: {}", PullStderr));
209 }
210
211 let (AfterExit, After, _) =
212 RunGit(&OperationId, &["rev-parse".to_string(), "HEAD".to_string()], Some(&RepoPath)).await?;
213 if AfterExit != 0 {
214 return Err("git:pull: failed to read HEAD after pull".to_string());
215 }
216
217 Ok(json!(Before.trim() != After.trim()))
218}
219
220pub async fn HandleCheckout(Arguments:Vec<Value>) -> Result<Value, String> {
222 let OperationId = Arguments.first().and_then(Value::as_str).unwrap_or("").to_string();
223 let RepoPath = Arguments.get(1).and_then(Value::as_str).unwrap_or("").to_string();
224 let Treeish = Arguments.get(2).and_then(Value::as_str).unwrap_or("").to_string();
225 let Detached = Arguments.get(3).and_then(Value::as_bool).unwrap_or(false);
226
227 if RepoPath.is_empty() || Treeish.is_empty() {
228 return Err("git:checkout requires repoPath and treeish".to_string());
229 }
230
231 let Argv:Vec<String> = if Detached {
232 vec!["checkout".to_string(), "--detach".to_string(), Treeish]
233 } else {
234 vec!["checkout".to_string(), Treeish]
235 };
236
237 let (ExitCode, _, Stderr) = RunGit(&OperationId, &Argv, Some(&RepoPath)).await?;
238 if ExitCode != 0 {
239 return Err(format!("git checkout failed: {}", Stderr));
240 }
241 Ok(Value::Null)
242}
243
244pub async fn HandleRevParse(Arguments:Vec<Value>) -> Result<Value, String> {
246 let RepoPath = Arguments.first().and_then(Value::as_str).unwrap_or("").to_string();
247 let Reference = Arguments.get(1).and_then(Value::as_str).unwrap_or("HEAD").to_string();
248 if RepoPath.is_empty() {
249 return Err("git:revParse requires repoPath".to_string());
250 }
251 let (ExitCode, Stdout, Stderr) =
252 RunGit(&Generated(), &["rev-parse".to_string(), Reference], Some(&RepoPath)).await?;
253 if ExitCode != 0 {
254 return Err(format!("git rev-parse failed: {}", Stderr));
255 }
256 Ok(json!(Stdout.trim()))
257}
258
259pub async fn HandleFetch(Arguments:Vec<Value>) -> Result<Value, String> {
261 let OperationId = Arguments.first().and_then(Value::as_str).unwrap_or("").to_string();
262 let RepoPath = Arguments.get(1).and_then(Value::as_str).unwrap_or("").to_string();
263 if RepoPath.is_empty() {
264 return Err("git:fetch requires repoPath".to_string());
265 }
266 let (ExitCode, _, Stderr) = RunGit(&OperationId, &["fetch".to_string()], Some(&RepoPath)).await?;
267 if ExitCode != 0 {
268 return Err(format!("git fetch failed: {}", Stderr));
269 }
270 Ok(Value::Null)
271}
272
273pub async fn HandleRevListCount(Arguments:Vec<Value>) -> Result<Value, String> {
275 let RepoPath = Arguments.first().and_then(Value::as_str).unwrap_or("").to_string();
276 let FromRef = Arguments.get(1).and_then(Value::as_str).unwrap_or("").to_string();
277 let ToRef = Arguments.get(2).and_then(Value::as_str).unwrap_or("").to_string();
278 if RepoPath.is_empty() || FromRef.is_empty() || ToRef.is_empty() {
279 return Err("git:revListCount requires repoPath, fromRef, toRef".to_string());
280 }
281 let Range = format!("{}..{}", FromRef, ToRef);
282 let (ExitCode, Stdout, Stderr) = RunGit(
283 &Generated(),
284 &["rev-list".to_string(), "--count".to_string(), Range],
285 Some(&RepoPath),
286 )
287 .await?;
288 if ExitCode != 0 {
289 return Err(format!("git rev-list failed: {}", Stderr));
290 }
291 Ok(json!(Stdout.trim().parse::<u64>().unwrap_or(0)))
292}
293
294pub async fn HandleCancel(Arguments:Vec<Value>) -> Result<Value, String> {
297 let OperationId = Arguments.first().and_then(Value::as_str).unwrap_or("").to_string();
298 if let Some(Pid) = TakePid(&OperationId) {
299 dev_log!("git", "[Git] cancel op={} pid={}", OperationId, Pid);
300 #[cfg(unix)]
301 {
302 let _ = std::process::Command::new("kill").args(["-TERM", &Pid.to_string()]).output();
303 }
304 #[cfg(windows)]
305 {
306 let _ = std::process::Command::new("taskkill")
307 .args(["/PID", &Pid.to_string(), "/T", "/F"])
308 .output();
309 }
310 } else {
311 dev_log!("git", "[Git] cancel op={} pid=<unknown>", OperationId);
312 }
313 Ok(Value::Null)
314}
315
316pub async fn HandleIsAvailable(_Arguments:Vec<Value>) -> Result<Value, String> {
319 static CACHE:OnceLock<bool> = OnceLock::new();
320 if let Some(Cached) = CACHE.get() {
321 return Ok(json!(*Cached));
322 }
323 let Available = Command::new("git")
324 .arg("--version")
325 .output()
326 .await
327 .map(|O| O.status.success())
328 .unwrap_or(false);
329 let _ = CACHE.set(Available);
330 dev_log!("git", "[Git] isAvailable={}", Available);
331 Ok(json!(Available))
332}