mars_agents/source/
git_cli.rs1use std::path::PathBuf;
4use std::process::Command;
5
6use crate::error::MarsError;
7use crate::source::{AvailableVersion, GlobalCache};
8
9use super::git::{parse_semver_tag, url_to_dirname};
10
11pub(crate) fn run_command(command: &mut Command, display: String) -> Result<String, MarsError> {
12 let output = command.output().map_err(|err| MarsError::GitCli {
13 command: display.clone(),
14 message: err.to_string(),
15 })?;
16
17 if !output.status.success() {
18 let stderr = String::from_utf8_lossy(&output.stderr);
19 let stdout = String::from_utf8_lossy(&output.stdout);
20 let message = if !stderr.trim().is_empty() {
21 stderr.trim().to_string()
22 } else if !stdout.trim().is_empty() {
23 stdout.trim().to_string()
24 } else {
25 format!("command exited with status {}", output.status)
26 };
27
28 return Err(MarsError::GitCli {
29 command: display,
30 message,
31 });
32 }
33
34 Ok(String::from_utf8_lossy(&output.stdout).to_string())
35}
36
37pub(crate) fn ls_remote_ref(url: &str, reference: &str) -> Result<String, MarsError> {
38 let mut command = Command::new("git");
39 command.arg("ls-remote").arg(url).arg(reference);
40
41 let command_display = format!("git ls-remote {url} {reference}");
42 let output = run_command(&mut command, command_display.clone())?;
43
44 for line in output.lines() {
45 if let Some((sha, _)) = line.split_once('\t')
46 && !sha.trim().is_empty()
47 {
48 return Ok(sha.trim().to_string());
49 }
50 }
51
52 Err(MarsError::GitCli {
53 command: command_display,
54 message: format!("reference `{reference}` not found"),
55 })
56}
57
58pub fn ls_remote_tags(url: &str) -> Result<Vec<AvailableVersion>, MarsError> {
60 let mut command = Command::new("git");
61 command.arg("ls-remote").arg("--tags").arg(url);
62
63 let output = run_command(&mut command, format!("git ls-remote --tags {url}"))?;
64 let mut versions = Vec::new();
65
66 for line in output.lines() {
67 let Some((sha, reference)) = line.split_once('\t') else {
68 continue;
69 };
70 let Some(tag) = reference.strip_prefix("refs/tags/") else {
71 continue;
72 };
73
74 if tag.ends_with("^{}") {
77 continue;
78 }
79
80 let Some(version) = parse_semver_tag(tag) else {
81 continue;
82 };
83
84 versions.push(AvailableVersion {
85 tag: tag.to_string(),
86 version,
87 commit_id: sha.trim().to_string(),
88 });
89 }
90
91 versions.sort_by(|a, b| a.version.cmp(&b.version));
92 Ok(versions)
93}
94
95pub fn ls_remote_head(url: &str) -> Result<String, MarsError> {
97 ls_remote_ref(url, "HEAD")
98}
99
100pub(crate) fn fetch_git_clone(
101 url: &str,
102 tag: Option<&str>,
103 sha: Option<&str>,
104 cache: &GlobalCache,
105) -> Result<PathBuf, MarsError> {
106 let cache_path = cache.git_dir().join(url_to_dirname(url));
107
108 let lock_path = cache_path.with_extension("lock");
111 let _lock = crate::fs::FileLock::acquire(&lock_path)?;
112
113 let cache_path_display = cache_path.to_string_lossy().to_string();
114 let was_cached = cache_path.exists();
115
116 if !was_cached {
117 let mut command = Command::new("git");
118 command.arg("clone").arg("--depth").arg("1");
119 if let Some(tag) = tag {
120 command.arg("--branch").arg(tag);
121 }
122 command.arg(url).arg(&cache_path);
123
124 let mut display = String::from("git clone --depth 1");
125 if let Some(tag) = tag {
126 display.push_str(&format!(" --branch {tag}"));
127 }
128 display.push_str(&format!(" {url} {cache_path_display}"));
129
130 run_command(&mut command, display)?;
131 } else {
132 let mut fetch_cmd = Command::new("git");
133 fetch_cmd
134 .arg("-C")
135 .arg(&cache_path)
136 .arg("fetch")
137 .arg("--depth")
138 .arg("1")
139 .arg("origin");
140 run_command(
141 &mut fetch_cmd,
142 format!("git -C {cache_path_display} fetch --depth 1 origin"),
143 )?;
144 }
145
146 if was_cached {
147 if let Some(tag) = tag {
148 let mut checkout_tag = Command::new("git");
149 checkout_tag
150 .arg("-C")
151 .arg(&cache_path)
152 .arg("checkout")
153 .arg(tag);
154 run_command(
155 &mut checkout_tag,
156 format!("git -C {cache_path_display} checkout {tag}"),
157 )?;
158 }
159
160 if let Some(sha) = sha {
161 let mut checkout_sha = Command::new("git");
162 checkout_sha
163 .arg("-C")
164 .arg(&cache_path)
165 .arg("checkout")
166 .arg(sha);
167 run_command(
168 &mut checkout_sha,
169 format!("git -C {cache_path_display} checkout {sha}"),
170 )?;
171 } else if tag.is_none() {
172 let mut checkout_head = Command::new("git");
173 checkout_head
174 .arg("-C")
175 .arg(&cache_path)
176 .arg("checkout")
177 .arg("origin/HEAD");
178 run_command(
179 &mut checkout_head,
180 format!("git -C {cache_path_display} checkout origin/HEAD"),
181 )?;
182 }
183 } else if let Some(sha) = sha {
184 let mut checkout_sha = Command::new("git");
185 checkout_sha
186 .arg("-C")
187 .arg(&cache_path)
188 .arg("checkout")
189 .arg(sha);
190 run_command(
191 &mut checkout_sha,
192 format!("git -C {cache_path_display} checkout {sha}"),
193 )?;
194 }
195
196 Ok(cache_path)
197}