1use std::borrow::Cow;
2use std::path::PathBuf;
3use std::str::FromStr;
4use std::sync::Arc;
5
6use fs_err::tokio as fs;
7use papaya::{HashMap, ResizeMode};
8use reqwest_middleware::ClientWithMiddleware;
9use tracing::debug;
10
11use uv_cache_key::{RepositoryUrl, cache_digest};
12use uv_fs::{LockedFile, LockedFileError, LockedFileMode};
13use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl};
14use uv_static::EnvVars;
15use uv_version::version;
16
17use crate::{
18 Fetch, GitSource, Reporter,
19 rate_limit::{GITHUB_RATE_LIMIT_STATUS, is_github_rate_limited},
20};
21
22#[derive(Debug, thiserror::Error)]
23pub enum GitResolverError {
24 #[error(transparent)]
25 Io(#[from] std::io::Error),
26 #[error(transparent)]
27 LockedFile(#[from] LockedFileError),
28 #[error(transparent)]
29 Join(#[from] tokio::task::JoinError),
30 #[error("Git operation failed")]
31 Git(#[source] anyhow::Error),
32 #[error(transparent)]
33 Reqwest(#[from] reqwest::Error),
34 #[error(transparent)]
35 ReqwestMiddleware(#[from] reqwest_middleware::Error),
36}
37
38#[derive(Debug, Clone, Copy, Default)]
40pub struct GitHttpSettings {
41 disable_ssl: bool,
42 offline: bool,
43}
44
45impl GitHttpSettings {
46 #[must_use]
48 pub fn with_disabled_ssl(mut self, disable_ssl: bool) -> Self {
49 self.disable_ssl = disable_ssl;
50 self
51 }
52
53 #[must_use]
55 pub fn with_offline(mut self, offline: bool) -> Self {
56 self.offline = offline;
57 self
58 }
59}
60
61#[derive(Clone)]
63pub struct GitResolver(Arc<HashMap<RepositoryReference, GitOid>>);
64
65impl Default for GitResolver {
66 fn default() -> Self {
67 Self(Arc::new(
68 HashMap::builder().resize_mode(ResizeMode::Blocking).build(),
69 ))
70 }
71}
72
73impl GitResolver {
74 pub fn insert(&self, reference: RepositoryReference, sha: GitOid) {
76 self.0.pin().insert(reference, sha);
77 }
78
79 fn get(&self, reference: &RepositoryReference) -> Option<GitOid> {
81 self.0.pin().get(reference).copied()
82 }
83
84 pub fn get_precise(&self, url: &GitUrl) -> Option<GitOid> {
86 if let Some(precise) = url.precise() {
88 return Some(precise);
89 }
90
91 let reference = RepositoryReference::from(url);
93 if let Some(precise) = self.get(&reference) {
94 return Some(precise);
95 }
96
97 None
98 }
99
100 pub async fn github_fast_path(
105 &self,
106 url: &GitUrl,
107 client: &ClientWithMiddleware,
108 ) -> Result<Option<GitOid>, GitResolverError> {
109 if std::env::var_os(EnvVars::UV_NO_GITHUB_FAST_PATH).is_some() {
110 return Ok(None);
111 }
112
113 if let Some(precise) = self.get_precise(url) {
115 return Ok(Some(precise));
116 }
117
118 let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(url.repository())
120 else {
121 return Ok(None);
122 };
123
124 if GITHUB_RATE_LIMIT_STATUS.is_active() {
126 debug!("Rate-limited by GitHub. Skipping GitHub fast path attempt for: {url}");
127 return Ok(None);
128 }
129
130 let rev = url.reference().as_rev();
132
133 let github_api_base_url = std::env::var(EnvVars::UV_GITHUB_FAST_PATH_URL)
134 .unwrap_or("https://api.github.com/repos".to_owned());
135 let github_api_url = format!("{github_api_base_url}/{owner}/{repo}/commits/{rev}");
136
137 debug!("Querying GitHub for commit at: {github_api_url}");
138 let mut request = client.get(&github_api_url);
139 request = request.header("Accept", "application/vnd.github.3.sha");
140 request = request.header(
141 "User-Agent",
142 format!("uv/{} (+https://github.com/astral-sh/uv)", version()),
143 );
144
145 let response = request.send().await?;
146 let status = response.status();
147 if !status.is_success() {
148 debug!(
151 "GitHub API request failed for: {github_api_url} ({})",
152 response.status()
153 );
154
155 if is_github_rate_limited(&response) {
156 GITHUB_RATE_LIMIT_STATUS.activate();
158 }
159
160 return Ok(None);
161 }
162
163 let precise = response.text().await?;
165 let precise =
166 GitOid::from_str(&precise).map_err(|err| GitResolverError::Git(err.into()))?;
167 let url = url
168 .clone()
169 .with_precise(precise)
170 .map_err(|error| GitResolverError::Git(error.into()))?;
171
172 self.insert(RepositoryReference::from(&url), precise);
175
176 Ok(Some(precise))
177 }
178
179 pub async fn fetch(
181 &self,
182 url: &GitUrl,
183 http_settings: GitHttpSettings,
184 cache: PathBuf,
185 reporter: Option<Arc<dyn Reporter>>,
186 ) -> Result<Fetch, GitResolverError> {
187 debug!("Fetching source distribution from Git: {url}");
188
189 let reference = RepositoryReference::from(url);
190
191 let url = {
194 if let Some(precise) = self.get(&reference) {
195 Cow::Owned(
196 url.clone()
197 .with_precise(precise)
198 .map_err(|error| GitResolverError::Git(error.into()))?,
199 )
200 } else {
201 Cow::Borrowed(url)
202 }
203 };
204
205 let lock_dir = cache.join("locks");
207 fs::create_dir_all(&lock_dir).await?;
208 let repository_url = url.repository().clone();
209 let _lock = LockedFile::acquire(
210 lock_dir.join(cache_digest(&repository_url)),
211 LockedFileMode::Exclusive,
212 &repository_url,
213 )
214 .await?;
215
216 let source = if let Some(reporter) = reporter {
218 GitSource::new(url.as_ref().clone(), cache, http_settings.offline)
219 .with_reporter(reporter)
220 } else {
221 GitSource::new(url.as_ref().clone(), cache, http_settings.offline)
222 };
223
224 let source = if http_settings.disable_ssl {
226 source.dangerous()
227 } else {
228 source
229 };
230
231 let fetch = tokio::task::spawn_blocking(move || source.fetch())
232 .await?
233 .map_err(GitResolverError::Git)?;
234
235 if let Some(precise) = fetch.git().precise() {
238 self.insert(reference, precise);
239 }
240
241 Ok(fetch)
242 }
243
244 pub fn precise(&self, url: GitUrl) -> Option<GitUrl> {
257 let reference = RepositoryReference::from(&url);
258 let precise = self.get(&reference)?;
259 url.with_precise(precise).ok()
260 }
261
262 pub fn same_ref(&self, a: &GitUrl, b: &GitUrl) -> bool {
264 let a_ref = RepositoryReference::from(a);
266
267 let b_ref = RepositoryReference::from(b);
269
270 if a_ref.url != b_ref.url {
272 return false;
273 }
274
275 if a_ref.reference == b_ref.reference {
277 return true;
278 }
279
280 let Some(a_precise) = a.precise().or_else(|| self.get(&a_ref)) else {
282 return false;
283 };
284
285 let Some(b_precise) = b.precise().or_else(|| self.get(&b_ref)) else {
286 return false;
287 };
288
289 a_precise == b_precise
290 }
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Hash)]
294pub struct ResolvedRepositoryReference {
295 pub reference: RepositoryReference,
298 pub sha: GitOid,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Hash)]
303pub struct RepositoryReference {
304 pub url: RepositoryUrl,
306 pub reference: GitReference,
308}
309
310impl From<&GitUrl> for RepositoryReference {
311 fn from(git: &GitUrl) -> Self {
312 Self {
313 url: git.repository().clone(),
314 reference: git.reference().clone(),
315 }
316 }
317}