1use std::borrow::Cow;
2use std::path::PathBuf;
3use std::str::FromStr;
4use std::sync::Arc;
5
6use dashmap::DashMap;
7use dashmap::mapref::one::Ref;
8use fs_err::tokio as fs;
9use reqwest_middleware::ClientWithMiddleware;
10use tracing::debug;
11
12use uv_cache_key::{RepositoryUrl, cache_digest};
13use uv_fs::{LockedFile, LockedFileError, LockedFileMode};
14use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl};
15use uv_static::EnvVars;
16use uv_version::version;
17
18use crate::{
19 Fetch, GitSource, Reporter,
20 rate_limit::{GITHUB_RATE_LIMIT_STATUS, is_github_rate_limited},
21};
22
23#[derive(Debug, thiserror::Error)]
24pub enum GitResolverError {
25 #[error(transparent)]
26 Io(#[from] std::io::Error),
27 #[error(transparent)]
28 LockedFile(#[from] LockedFileError),
29 #[error(transparent)]
30 Join(#[from] tokio::task::JoinError),
31 #[error("Git operation failed")]
32 Git(#[source] anyhow::Error),
33 #[error(transparent)]
34 Reqwest(#[from] reqwest::Error),
35 #[error(transparent)]
36 ReqwestMiddleware(#[from] reqwest_middleware::Error),
37}
38
39#[derive(Default, Clone)]
41pub struct GitResolver(Arc<DashMap<RepositoryReference, GitOid>>);
42
43impl GitResolver {
44 pub fn insert(&self, reference: RepositoryReference, sha: GitOid) {
46 self.0.insert(reference, sha);
47 }
48
49 fn get(&self, reference: &RepositoryReference) -> Option<Ref<'_, RepositoryReference, GitOid>> {
51 self.0.get(reference)
52 }
53
54 pub fn get_precise(&self, url: &GitUrl) -> Option<GitOid> {
56 if let Some(precise) = url.precise() {
58 return Some(precise);
59 }
60
61 let reference = RepositoryReference::from(url);
63 if let Some(precise) = self.get(&reference) {
64 return Some(*precise);
65 }
66
67 None
68 }
69
70 pub async fn github_fast_path(
75 &self,
76 url: &GitUrl,
77 client: &ClientWithMiddleware,
78 ) -> Result<Option<GitOid>, GitResolverError> {
79 if std::env::var_os(EnvVars::UV_NO_GITHUB_FAST_PATH).is_some() {
80 return Ok(None);
81 }
82
83 if let Some(precise) = self.get_precise(url) {
85 return Ok(Some(precise));
86 }
87
88 let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(url.repository())
90 else {
91 return Ok(None);
92 };
93
94 if GITHUB_RATE_LIMIT_STATUS.is_active() {
96 debug!("Rate-limited by GitHub. Skipping GitHub fast path attempt for: {url}");
97 return Ok(None);
98 }
99
100 let rev = url.reference().as_rev();
102
103 let github_api_base_url = std::env::var(EnvVars::UV_GITHUB_FAST_PATH_URL)
104 .unwrap_or("https://api.github.com/repos".to_owned());
105 let github_api_url = format!("{github_api_base_url}/{owner}/{repo}/commits/{rev}");
106
107 debug!("Querying GitHub for commit at: {github_api_url}");
108 let mut request = client.get(&github_api_url);
109 request = request.header("Accept", "application/vnd.github.3.sha");
110 request = request.header(
111 "User-Agent",
112 format!("uv/{} (+https://github.com/astral-sh/uv)", version()),
113 );
114
115 let response = request.send().await?;
116 let status = response.status();
117 if !status.is_success() {
118 debug!(
121 "GitHub API request failed for: {github_api_url} ({})",
122 response.status()
123 );
124
125 if is_github_rate_limited(&response) {
126 GITHUB_RATE_LIMIT_STATUS.activate();
128 }
129
130 return Ok(None);
131 }
132
133 let precise = response.text().await?;
135 let precise =
136 GitOid::from_str(&precise).map_err(|err| GitResolverError::Git(err.into()))?;
137
138 self.insert(RepositoryReference::from(url), precise);
141
142 Ok(Some(precise))
143 }
144
145 pub async fn fetch(
147 &self,
148 url: &GitUrl,
149 disable_ssl: bool,
150 offline: bool,
151 cache: PathBuf,
152 reporter: Option<Arc<dyn Reporter>>,
153 ) -> Result<Fetch, GitResolverError> {
154 debug!("Fetching source distribution from Git: {url}");
155
156 let reference = RepositoryReference::from(url);
157
158 let url = {
161 if let Some(precise) = self.get(&reference) {
162 Cow::Owned(url.clone().with_precise(*precise))
163 } else {
164 Cow::Borrowed(url)
165 }
166 };
167
168 let lock_dir = cache.join("locks");
170 fs::create_dir_all(&lock_dir).await?;
171 let repository_url = RepositoryUrl::new(url.repository());
172 let _lock = LockedFile::acquire(
173 lock_dir.join(cache_digest(&repository_url)),
174 LockedFileMode::Exclusive,
175 &repository_url,
176 )
177 .await?;
178
179 let source = if let Some(reporter) = reporter {
181 GitSource::new(url.as_ref().clone(), cache, offline).with_reporter(reporter)
182 } else {
183 GitSource::new(url.as_ref().clone(), cache, offline)
184 };
185
186 let source = if disable_ssl {
188 source.dangerous()
189 } else {
190 source
191 };
192
193 let fetch = tokio::task::spawn_blocking(move || source.fetch())
194 .await?
195 .map_err(GitResolverError::Git)?;
196
197 if let Some(precise) = fetch.git().precise() {
200 self.insert(reference, precise);
201 }
202
203 Ok(fetch)
204 }
205
206 pub fn precise(&self, url: GitUrl) -> Option<GitUrl> {
219 let reference = RepositoryReference::from(&url);
220 let precise = self.get(&reference)?;
221 Some(url.with_precise(*precise))
222 }
223
224 pub fn same_ref(&self, a: &GitUrl, b: &GitUrl) -> bool {
226 let a_ref = RepositoryReference::from(a);
228
229 let b_ref = RepositoryReference::from(b);
231
232 if a_ref.url != b_ref.url {
234 return false;
235 }
236
237 if a_ref.reference == b_ref.reference {
239 return true;
240 }
241
242 let Some(a_precise) = a.precise().or_else(|| self.get(&a_ref).map(|sha| *sha)) else {
244 return false;
245 };
246
247 let Some(b_precise) = b.precise().or_else(|| self.get(&b_ref).map(|sha| *sha)) else {
248 return false;
249 };
250
251 a_precise == b_precise
252 }
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Hash)]
256pub struct ResolvedRepositoryReference {
257 pub reference: RepositoryReference,
260 pub sha: GitOid,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Hash)]
265pub struct RepositoryReference {
266 pub url: RepositoryUrl,
268 pub reference: GitReference,
270}
271
272impl From<&GitUrl> for RepositoryReference {
273 fn from(git: &GitUrl) -> Self {
274 Self {
275 url: RepositoryUrl::new(git.repository()),
276 reference: git.reference().clone(),
277 }
278 }
279}