mars_agents/source/
git_cli.rs1use std::path::{Path, PathBuf};
4
5use crate::error::MarsError;
6use crate::platform::cache::git_cache_component;
7use crate::platform::process::{display_command, run_git, run_git_with_ref};
8use crate::source::{AvailableVersion, GlobalCache};
9
10use super::git::parse_semver_tag;
11
12pub(crate) fn ls_remote_ref(url: &str, reference: &str) -> Result<String, MarsError> {
13 let command_display = display_command(&["ls-remote", url, reference]);
14 let output = run_git_with_ref(
15 &["ls-remote", url],
16 reference,
17 Path::new("."),
18 "resolve remote git reference",
19 )?;
20
21 for line in output.lines() {
22 if let Some((sha, _)) = line.split_once('\t')
23 && !sha.trim().is_empty()
24 {
25 return Ok(sha.trim().to_string());
26 }
27 }
28
29 Err(MarsError::GitCli {
30 command: command_display,
31 message: format!("reference `{reference}` not found"),
32 })
33}
34
35pub fn ls_remote_tags(url: &str) -> Result<Vec<AvailableVersion>, MarsError> {
37 let output = run_git(
38 &["ls-remote", "--tags", url],
39 Path::new("."),
40 "list remote git tags",
41 )?;
42 let mut versions = Vec::new();
43
44 for line in output.lines() {
45 let Some((sha, reference)) = line.split_once('\t') else {
46 continue;
47 };
48 let Some(tag) = reference.strip_prefix("refs/tags/") else {
49 continue;
50 };
51
52 if tag.ends_with("^{}") {
55 continue;
56 }
57
58 let Some(version) = parse_semver_tag(tag) else {
59 continue;
60 };
61
62 versions.push(AvailableVersion {
63 tag: tag.to_string(),
64 version,
65 commit_id: sha.trim().to_string(),
66 });
67 }
68
69 versions.sort_by(|a, b| a.version.cmp(&b.version));
70 Ok(versions)
71}
72
73pub fn ls_remote_head(url: &str) -> Result<String, MarsError> {
75 ls_remote_ref(url, "HEAD")
76}
77
78pub(crate) fn fetch_git_clone(
79 url: &str,
80 tag: Option<&str>,
81 sha: Option<&str>,
82 cache: &GlobalCache,
83) -> Result<PathBuf, MarsError> {
84 let cache_name = git_cache_component(url)?;
85 let cache_path = cache.git_dir().join(cache_name);
86
87 let lock_path = cache_path.with_extension("lock");
90 let _lock = crate::fs::FileLock::acquire(&lock_path)?;
91
92 let cache_path_display = cache_path.to_string_lossy().to_string();
93 let was_cached = cache_path.exists();
94
95 if !was_cached {
96 let mut args = vec!["clone"];
97 if sha.is_none() {
98 args.push("--depth");
99 args.push("1");
100 }
101 if let Some(tag_name) = tag {
102 args.push("--branch");
103 args.push(tag_name);
104 }
105 args.push(url);
106 args.push(&cache_path_display);
107
108 run_git(&args, &cache.git_dir(), "clone git source into cache")?;
109 } else {
110 run_git(
111 &["fetch", "--depth", "1", "--tags", "--prune-tags", "origin"],
112 &cache_path,
113 "fetch cached git source",
114 )?;
115 }
116
117 if was_cached {
118 if let Some(sha) = sha {
119 match run_git(
120 &["fetch", "--depth", "1", "origin", sha],
121 &cache_path,
122 "fetch cached git commit",
123 ) {
124 Ok(_) => {}
125 Err(_) => {
126 run_git(
127 &["fetch", "--unshallow", "origin"],
128 &cache_path,
129 "unshallow cached git source for locked commit",
130 )
131 .or_else(|_| {
132 run_git(
133 &["fetch", "origin"],
134 &cache_path,
135 "fetch cached git source for locked commit",
136 )
137 })?;
138 }
139 }
140 }
141
142 if let Some(tag_name) = tag {
143 run_git(
144 &["checkout", tag_name],
145 &cache_path,
146 "checkout cached git tag",
147 )?;
148 }
149
150 if let Some(sha) = sha {
151 run_git(
152 &["checkout", sha],
153 &cache_path,
154 "checkout cached git commit",
155 )?;
156 } else if tag.is_none() {
157 run_git(
158 &["checkout", "origin/HEAD"],
159 &cache_path,
160 "checkout cached git default head",
161 )?;
162 }
163 } else if let Some(sha) = sha {
164 run_git(
165 &["checkout", sha],
166 &cache_path,
167 "checkout cloned git commit",
168 )?;
169 }
170
171 Ok(cache_path)
172}