1use crate::GitReference;
23
24use snarkvm::algorithms::crypto_hash::sha256;
25
26use leo_errors::Result;
27
28use std::{
29 path::{Path, PathBuf},
30 sync::atomic::{AtomicU64, Ordering},
31};
32
33const CHECKOUTS_SUBDIR: &str = "git/checkouts";
34const TMP_SUBDIR: &str = "git/tmp";
35
36pub(crate) fn checkouts_root(home: &Path) -> PathBuf {
38 home.join(CHECKOUTS_SUBDIR)
39}
40
41pub(crate) fn checkout_dir(home: &Path, url: &str, commit: &str) -> PathBuf {
44 let digest = sha256(url.as_bytes());
45 let url_hash: String = digest.iter().take(8).map(|b| format!("{b:02x}")).collect();
46 let repo: String = url
48 .trim_end_matches('/')
49 .rsplit('/')
50 .next()
51 .unwrap_or(url)
52 .trim_end_matches(".git")
53 .chars()
54 .filter(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'))
55 .take(32)
56 .collect();
57 let repo = if repo.is_empty() { "repo".to_string() } else { repo };
58 checkouts_root(home).join(format!("{repo}-{url_hash}")).join(commit)
59}
60
61pub fn resolve(
67 home: &Path,
68 name: &str,
69 url: &str,
70 reference: &GitReference,
71 locked_commit: Option<&str>,
72 offline: bool,
73) -> Result<(PathBuf, String)> {
74 if let Some(commit) = locked_commit
77 && (offline || !reference.is_mutable())
78 {
79 let dir = checkout_dir(home, url, commit);
80 if dir.is_dir() {
81 return Ok((dir, commit.to_string()));
82 }
83 }
84
85 if offline {
86 return Err(crate::errors::git_offline_unavailable(name, url).into());
87 }
88
89 let tmp = unique_dir(&home.join(TMP_SUBDIR), "clone");
92 let result = clone_resolve_checkout(home, name, url, reference, locked_commit, &tmp);
93 let _ = std::fs::remove_dir_all(&tmp);
94 result
95}
96
97fn clone_resolve_checkout(
98 home: &Path,
99 name: &str,
100 url: &str,
101 reference: &GitReference,
102 locked_commit: Option<&str>,
103 tmp: &Path,
104) -> Result<(PathBuf, String)> {
105 let _ = std::fs::remove_dir_all(tmp);
106 if let Some(parent) = tmp.parent() {
107 std::fs::create_dir_all(parent).map_err(|e| crate::errors::git_error(name, url, e))?;
108 }
109
110 let parsed = gix::url::parse(url.into()).map_err(|e| crate::errors::git_error(name, url, e))?;
112 let mut prepare = gix::prepare_clone(parsed, tmp)
113 .map_err(|e| crate::errors::git_error(name, url, e))?
114 .configure_connection(|connection| {
115 #[allow(clippy::result_large_err)] connection.set_credentials(|_action| Ok(None));
119 Ok(())
120 });
121 let (checkout, _) = prepare
122 .fetch_then_checkout(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
123 .map_err(|e| crate::errors::git_error(name, url, e))?;
124 let repo = checkout.persist();
126
127 let revspec = match (locked_commit, reference) {
128 (Some(commit), reference) if !reference.is_mutable() => commit.to_string(),
130 (_, GitReference::DefaultBranch) => "HEAD".to_string(),
131 (_, GitReference::Branch(branch)) => format!("origin/{branch}"),
132 (_, GitReference::Tag(tag)) => format!("refs/tags/{tag}"),
133 (_, GitReference::Rev(rev)) => rev.clone(),
134 };
135 let id = repo
136 .rev_parse_single(revspec.as_str())
137 .map_err(|e| crate::errors::git_reference_error(name, url, &revspec, e))?;
138 let commit = id.detach().to_string();
139
140 let dir = checkout_dir(home, url, &commit);
141 if !dir.is_dir() {
142 let staging = staging_dir(&dir);
144 let renamed =
145 checkout_tree(&repo, id, &staging).and_then(|()| std::fs::rename(&staging, &dir).map_err(Into::into));
146 let _ = std::fs::remove_dir_all(&staging);
147 if let Err(e) = renamed
149 && !dir.is_dir()
150 {
151 return Err(crate::errors::git_error(name, url, e).into());
152 }
153 }
154
155 Ok((dir, commit))
156}
157
158fn checkout_tree(
160 repo: &gix::Repository,
161 id: gix::Id<'_>,
162 dir: &Path,
163) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
164 let tree = id.object()?.peel_to_tree()?;
165 let mut index = repo.index_from_tree(&tree.id())?;
166 let mut opts = repo.checkout_options(gix::worktree::stack::state::attributes::Source::IdMapping)?;
167 opts.destination_is_initially_empty = true;
168
169 let _ = std::fs::remove_dir_all(dir);
171 std::fs::create_dir_all(dir)?;
172
173 gix::worktree::state::checkout(
174 &mut index,
175 dir,
176 repo.objects.clone().into_arc()?,
177 &gix::progress::Discard,
178 &gix::progress::Discard,
179 &gix::interrupt::IS_INTERRUPTED,
180 opts,
181 )?;
182 Ok(())
183}
184
185fn unique_dir(parent: &Path, prefix: &str) -> PathBuf {
187 static COUNTER: AtomicU64 = AtomicU64::new(0);
188 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
189 parent.join(format!("{prefix}-{}-{seq}", std::process::id()))
190}
191
192fn staging_dir(final_dir: &Path) -> PathBuf {
194 unique_dir(final_dir.parent().unwrap_or(final_dir), ".staging")
196}