1use std::borrow::Cow;
2use std::path::Path;
3use std::process::Command;
4
5use cause::Cause;
6use cause::cause;
7use git2::Repository;
8use regex::Regex;
9use temp_dir::TempDir;
10
11use super::ErrorType;
12use super::ErrorType::{
13 GitCheckoutChangeDirectory, GitCheckoutCommand, GitCheckoutCommandExitStatus, GitCloneCommand,
14 GitFetchCommand, GitFetchCommandExitStatus, GitLsRemoteCommand, GitLsRemoteCommandExitStatus,
15 GitLsRemoteCommandStdoutDecode, GitLsRemoteCommandStdoutRegex, TempDirCreation,
16};
17use super::Method;
18use super::Parsed;
19
20pub fn fetch_target_to_tempdir(prefix: &str, parsed: &Parsed) -> Result<TempDir, Cause<ErrorType>> {
21 let tempdir = TempDir::with_prefix(prefix).map_err(|e| cause!(TempDirCreation).src(e))?;
22
23 std::env::set_current_dir(tempdir.path())
24 .map_err(|e| cause!(GitCheckoutChangeDirectory).src(e))?;
25
26 git_clone(prefix, tempdir.path(), parsed)?;
27
28 let method = match parsed.mtd.as_ref() {
29 Some(Method::Partial) => git_checkout_partial,
30 Some(Method::ShallowNoSparse) => git_checkout_shallow_no_sparse,
31 Some(Method::Shallow) | None => git_checkout_shallow_with_sparse,
32 };
33
34 method(prefix, tempdir.path(), parsed)?;
35
36 Ok(tempdir)
37}
38
39fn git_clone(prefix: &str, path: &Path, parsed: &Parsed) -> Result<(), Cause<ErrorType>> {
40 println!(" - {prefix}clone --no-checkout: {}", parsed.url);
41
42 std::env::set_current_dir(path).map_err(|e| cause!(GitCloneCommand).src(e))?;
43
44 Repository::clone(&parsed.url, ".").map_err(|e| cause!(GitCloneCommand).src(e))?;
45
46 Ok(())
47}
48
49fn git_checkout_partial(
50 prefix: &str,
51 path: &Path,
52 parsed: &Parsed,
53) -> Result<(), Cause<ErrorType>> {
54 let rev = identify_commit_hash(path, parsed)?;
55 let rev = if let Some(r) = rev {
56 println!(" - {prefix}checkout partial: {} ({})", r, parsed.rev);
57 r
58 } else {
59 println!(" - {prefix}checkout partial: {}", parsed.rev);
60 parsed.rev.clone()
61 };
62
63 let out = Command::new("git")
64 .args([
65 "-C",
66 path.to_str().expect("Failed to convert path to string for git checkout; path contains invalid Unicode characters"),
67 "checkout",
68 "--progress",
69 rev.as_ref(),
70 "--",
71 parsed.src.as_ref(),
72 ])
73 .output()
74 .map_err(|e| cause!(GitCheckoutCommand).src(e))?;
75
76 handle_git_output(out, "git checkout", GitCheckoutCommandExitStatus)
77}
78
79fn git_checkout_shallow_no_sparse(
80 prefix: &str,
81 path: &Path,
82 parsed: &Parsed,
83) -> Result<(), Cause<ErrorType>> {
84 git_checkout_shallow_core(prefix, path, parsed, false)
85}
86
87fn git_checkout_shallow_with_sparse(
88 prefix: &str,
89 path: &Path,
90 parsed: &Parsed,
91) -> Result<(), Cause<ErrorType>> {
92 git_checkout_shallow_core(prefix, path, parsed, true)
93}
94
95fn git_checkout_shallow_core(
96 prefix: &str,
97 path: &Path,
98 parsed: &Parsed,
99 use_sparse: bool,
100) -> Result<(), Cause<ErrorType>> {
101 let rev = identify_commit_hash(path, parsed)?;
102 let no_sparse = if use_sparse { "" } else { " (no sparse)" };
103 let rev = if let Some(r) = rev {
104 println!(
105 " - {prefix}checkout shallow{no_sparse}: {r} ({})",
106 parsed.rev
107 );
108 r
109 } else {
110 println!(" - {prefix}checkout shallow{no_sparse}: {}", parsed.rev);
111 parsed.rev.clone()
112 };
113
114 if use_sparse {
115 let sparse_path: Cow<'_, str> = if parsed.src.starts_with('/') {
117 parsed.src.as_str().into()
118 } else {
119 format!("/{}", &parsed.src).into()
120 };
121
122 let out = Command::new("git")
123 .args([
124 "-C",
125 path.to_str()
126 .expect("Failed to convert path to string for sparse checkout; path contains invalid Unicode characters"),
127 "sparse-checkout",
128 "set",
129 "--no-cone",
130 &sparse_path,
131 ])
132 .output();
133
134 let output = out.expect("Failed to execute git sparse-checkout command. Ensure 'git' is installed and in your PATH, and you have necessary permissions.");
135
136 if !output.status.success() {
137 println!(" - {prefix}Could not activate sparse-checkout feature.");
140 println!(" - {prefix}Your git client might not support this feature.");
141
142 let stderr = String::from_utf8_lossy(&output.stderr);
144 if !stderr.trim().is_empty() {
145 println!(" - {prefix} stderr: {}", stderr.trim());
146 }
147 }
148 }
149
150 let out = Command::new("git")
151 .args([
152 "-C",
153 path.to_str().expect("Failed to convert path to string for git fetch; path contains invalid Unicode characters"),
154 "fetch",
155 "--depth",
156 "1",
157 "--progress",
158 "origin",
159 rev.as_ref(),
160 ])
161 .output()
162 .map_err(|e| cause!(GitFetchCommand).src(e))?;
163
164 if !out.status.success() {
165 let error = String::from_utf8(out.stderr)
166 .unwrap_or("Could not get even a error output of git fetch command".to_string());
167 return Err(cause!(GitFetchCommandExitStatus, error));
168 }
169
170 let out = Command::new("git")
171 .args([
172 "-C",
173 path.to_str().expect("Failed to convert path to string for git checkout; path contains invalid Unicode characters"),
174 "checkout",
175 "--progress",
176 "FETCH_HEAD",
177 ])
178 .output()
179 .map_err(|e| cause!(GitCheckoutCommand).src(e))?;
180
181 handle_git_output(out, "git checkout", GitCheckoutCommandExitStatus)
182}
183
184fn handle_git_output(
185 out: std::process::Output,
186 command_name: &str,
187 error_variant: ErrorType,
188) -> Result<(), Cause<ErrorType>> {
189 if out.status.success() {
190 Ok(())
191 } else {
192 let error = String::from_utf8(out.stderr).unwrap_or(format!(
193 "Could not get even a error output of {command_name} command"
194 ));
195 Err(cause!(error_variant, error))
196 }
197}
198
199fn identify_commit_hash(path: &Path, parsed: &Parsed) -> Result<Option<String>, Cause<ErrorType>> {
200 let out = Command::new("git")
201 .args([
202 "-C",
203 path.to_str().expect("Failed to convert path to string for git ls-remote; path contains invalid Unicode characters"),
204 "ls-remote",
205 "--heads",
206 "--tags",
207 parsed.url.as_ref(),
208 ])
209 .output()
210 .map_err(|e| cause!(GitLsRemoteCommand).src(e))?;
211
212 if !out.status.success() {
213 let error = String::from_utf8(out.stderr)
214 .unwrap_or("Could not get even a error output of git ls-remote command".to_string());
215 return Err(cause!(GitLsRemoteCommandExitStatus).msg(error));
216 }
217
218 let stdout =
219 String::from_utf8(out.stdout).map_err(|e| cause!(GitLsRemoteCommandStdoutDecode).src(e))?;
220 let lines = stdout.lines();
221
222 let re_in_line = Regex::new(&format!(
223 "^((?:[0-9a-fA-F]){{40}})\\s+(.*{})(\\^\\{{\\}})?$",
224 regex::escape(parsed.rev.as_ref())
225 ))
226 .map_err(|e| cause!(GitLsRemoteCommandStdoutRegex).src(e))?;
227
228 let matched = lines.filter_map(|l| {
229 let cap = re_in_line.captures(l)?;
230 let hash = cap.get(1)?.as_str().to_owned();
231 let name = cap.get(2)?.as_str().to_owned();
232
233 if !name.contains(&parsed.rev) {
237 return None;
238 }
239
240 let wrongness = usize::from(cap.get(3).is_some());
241
242 Some((hash, name, wrongness))
243 });
244 let identified = matched.min_by(|l, r| l.2.cmp(&r.2));
245
246 if let Some((rev, _, _)) = identified {
247 Ok(Some(rev))
248 } else {
249 Ok(None)
252 }
253}