1use std::path::Path;
2use std::process::Command;
3
4use anyhow::Context as _;
5
6pub fn resolve_git_ref(url: &str, ref_name: &str) -> anyhow::Result<String> {
7 if is_hex_sha(ref_name) {
8 return Ok(ref_name.to_string());
9 }
10
11 let patterns = [
12 format!("refs/heads/{ref_name}"),
13 format!("refs/tags/{ref_name}"),
14 format!("refs/tags/{ref_name}^{{}}"),
15 ];
16
17 let mut cmd = Command::new("git");
18 cmd.arg("ls-remote").arg(url);
19 for p in &patterns {
20 cmd.arg(p);
21 }
22
23 let out = cmd.output().context("git ls-remote")?;
24 if !out.status.success() {
25 anyhow::bail!(
26 "git ls-remote failed: {}",
27 String::from_utf8_lossy(&out.stderr)
28 );
29 }
30
31 let stdout = String::from_utf8(out.stdout).context("decode git ls-remote output")?;
32 let mut peeled: Option<String> = None;
33 let mut direct: Option<String> = None;
34
35 for line in stdout.lines() {
36 let mut parts = line.split_whitespace();
37 let sha = parts.next().unwrap_or_default();
38 let r = parts.next().unwrap_or_default();
39 if sha.is_empty() || r.is_empty() {
40 continue;
41 }
42 if r.ends_with("^{}") {
43 peeled = Some(sha.to_string());
44 } else {
45 direct.get_or_insert_with(|| sha.to_string());
46 }
47 }
48
49 peeled
50 .or(direct)
51 .with_context(|| format!("ref not found: {ref_name}"))
52}
53
54pub fn clone_checkout_git(
55 url: &str,
56 ref_name: &str,
57 commit: &str,
58 dest_dir: &Path,
59 shallow: bool,
60) -> anyhow::Result<()> {
61 if dest_dir.exists() {
62 return Ok(());
63 }
64
65 let tmp_dir = dest_dir.with_extension("tmp");
66
67 let try_clone_checkout = |use_shallow: bool| -> anyhow::Result<()> {
68 if tmp_dir.exists() {
69 std::fs::remove_dir_all(&tmp_dir).ok();
70 }
71
72 let mut clone = Command::new("git");
73 clone.arg("clone");
74 if use_shallow && !is_hex_sha(ref_name) {
75 clone.arg("--depth").arg("1").arg("--branch").arg(ref_name);
76 }
77 clone.arg(url).arg(&tmp_dir);
78 let out = clone.output().context("git clone")?;
79 if !out.status.success() {
80 anyhow::bail!("git clone failed: {}", String::from_utf8_lossy(&out.stderr));
81 }
82
83 let mut checkout = Command::new("git");
84 checkout.current_dir(&tmp_dir).arg("checkout").arg(commit);
85 let out = checkout.output().context("git checkout")?;
86 if !out.status.success() {
87 anyhow::bail!(
88 "git checkout failed: {}",
89 String::from_utf8_lossy(&out.stderr)
90 );
91 }
92
93 Ok(())
94 };
95
96 let shallow_attempt = shallow && !is_hex_sha(ref_name);
97 match try_clone_checkout(shallow_attempt) {
98 Ok(()) => {}
99 Err(err) if shallow_attempt => {
100 let first_err = err.to_string();
101 try_clone_checkout(false).with_context(|| {
102 format!(
103 "shallow clone/checkout failed (retrying non-shallow); if this persists, set shallow=false in the module source: {first_err}"
104 )
105 })?;
106 }
107 Err(err) => return Err(err),
108 }
109
110 std::fs::rename(&tmp_dir, dest_dir).context("finalize git checkout")?;
111 Ok(())
112}
113
114pub fn git_in(cwd: &Path, args: &[&str]) -> anyhow::Result<String> {
115 let out = Command::new("git").current_dir(cwd).args(args).output();
116 let out = match out {
117 Ok(out) => out,
118 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
119 return Err(crate::user_error::UserError::git_not_found(cwd, args));
120 }
121 Err(err) => return Err(err).with_context(|| format!("git {args:?}")),
122 };
123 if !out.status.success() {
124 anyhow::bail!(
125 "git {:?} failed: {}",
126 args,
127 String::from_utf8_lossy(&out.stderr)
128 );
129 }
130 String::from_utf8(out.stdout).context("decode git output")
131}
132
133fn is_hex_sha(s: &str) -> bool {
134 if s.len() != 40 {
135 return false;
136 }
137 s.chars().all(|c| c.is_ascii_hexdigit())
138}