1use crate::sources::IndexSummary;
4use crate::sources::RecursivePathSource;
5use crate::sources::git::utils::GitDatabase;
6use crate::sources::git::utils::GitRemote;
7use crate::sources::git::utils::rev_to_oid;
8use crate::sources::source::MaybePackage;
9use crate::sources::source::QueryKind;
10use crate::sources::source::Source;
11use crate::util::GlobalContext;
12use crate::util::cache_lock::CacheLockMode;
13use crate::util::errors::CargoResult;
14use crate::util::hex::short_hash;
15use crate::util::interning::InternedString;
16use crate::workspace::GitReference;
17use crate::workspace::SourceId;
18use crate::workspace::global_cache_tracker;
19use crate::workspace::{Dependency, Package, PackageId};
20use anyhow::Context as _;
21use cargo_util::paths::exclude_from_backups_and_indexing;
22use std::cell::RefCell;
23use std::fmt::{self, Debug, Formatter};
24use tracing::trace;
25use url::Url;
26
27pub struct GitSource<'gctx> {
71 remote: GitRemote,
73 locked_rev: RefCell<Revision>,
77 source_id: RefCell<SourceId>,
79 path_source: RefCell<Option<RecursivePathSource<'gctx>>>,
84 short_id: RefCell<Option<InternedString>>,
92 ident: InternedString,
95 gctx: &'gctx GlobalContext,
96 quiet: bool,
98}
99
100impl<'gctx> GitSource<'gctx> {
101 pub fn new(source_id: SourceId, gctx: &'gctx GlobalContext) -> CargoResult<GitSource<'gctx>> {
103 let remote = GitRemote::new(source_id.url());
104 Self::new_with_remote(source_id, remote, gctx)
105 }
106
107 pub(super) fn new_for_submodule(
111 source_id: SourceId,
112 fetch_url: String,
113 gctx: &'gctx GlobalContext,
114 ) -> CargoResult<GitSource<'gctx>> {
115 let remote = GitRemote::new_from_str(fetch_url);
116 Self::new_with_remote(source_id, remote, gctx)
117 }
118
119 fn new_with_remote(
120 source_id: SourceId,
121 remote: GitRemote,
122 gctx: &'gctx GlobalContext,
123 ) -> CargoResult<GitSource<'gctx>> {
124 assert!(source_id.is_git(), "id is not git, id={}", source_id);
125
126 let locked_rev = source_id
128 .precise_git_fragment()
129 .map(|s| Revision::new(s.into()))
130 .unwrap_or_else(|| source_id.git_reference().unwrap().clone().into());
131
132 let ident = ident_shallow(
133 &source_id,
134 gctx.cli_unstable()
135 .git
136 .map_or(false, |features| features.shallow_deps),
137 );
138
139 let source = GitSource {
140 remote,
141 locked_rev: RefCell::new(locked_rev),
142 source_id: RefCell::new(source_id),
143 path_source: RefCell::new(None),
144 short_id: RefCell::new(None),
145 ident: ident.into(),
146 gctx,
147 quiet: false,
148 };
149
150 Ok(source)
151 }
152
153 pub fn url(&self) -> Url {
155 self.source_id.borrow().url().clone()
156 }
157
158 pub fn read_packages(&self) -> CargoResult<Vec<Package>> {
162 if self.path_source.borrow().is_none() {
163 self.invalidate_cache();
164 self.update()?;
165 }
166 self.path_source.borrow().as_ref().unwrap().read_packages()
167 }
168
169 fn mark_used(&self) -> CargoResult<()> {
170 self.gctx
171 .deferred_global_last_use()?
172 .mark_git_checkout_used(global_cache_tracker::GitCheckout {
173 encoded_git_name: self.ident,
174 short_name: self.short_id.borrow().expect("update before download"),
175 size: None,
176 });
177 Ok(())
178 }
179
180 pub(crate) fn fetch_db(&self, is_submodule: bool) -> CargoResult<(GitDatabase, git2::Oid)> {
186 let db_path = self.gctx.git_db_path().join(&self.ident);
187 let db_path = db_path.into_path_unlocked();
188
189 let db = self.remote.db_at(&db_path).ok();
190
191 let (db, actual_rev) = match (&*self.locked_rev.borrow(), db) {
192 (Revision::Locked(oid), Some(db)) if db.contains(*oid) => (db, *oid),
195
196 (Revision::Deferred(git_ref), Some(db)) if !self.gctx.network_allowed() => {
200 let offline_flag = self
201 .gctx
202 .offline_flag()
203 .expect("always present when `!network_allowed`");
204 let rev = db.resolve(&git_ref).with_context(|| {
205 format!(
206 "failed to lookup reference in preexisting repository, and \
207 can't check for updates in offline mode ({offline_flag})"
208 )
209 })?;
210 (db, rev)
211 }
212
213 (locked_rev, db) => {
218 if let Some(offline_flag) = self.gctx.offline_flag() {
219 anyhow::bail!(
220 "can't checkout from '{}': you are in the offline mode ({offline_flag})",
221 self.remote.url()
222 );
223 }
224
225 if !self.quiet {
226 let scope = if is_submodule {
227 "submodule"
228 } else {
229 "repository"
230 };
231 self.gctx
232 .shell()
233 .status("Updating", format!("git {scope} `{}`", self.remote.url()))?;
234 }
235
236 trace!("updating git source `{:?}`", self.remote);
237
238 let locked_rev = locked_rev.clone().into();
239 let manifest_reference = self.source_id.borrow().git_reference().unwrap();
240 self.remote
241 .checkout(&db_path, db, manifest_reference, &locked_rev, self.gctx)?
242 }
243 };
244 Ok((db, actual_rev))
245 }
246
247 fn update(&self) -> CargoResult<()> {
248 if self.path_source.borrow().is_some() {
249 self.mark_used()?;
250 return Ok(());
251 }
252
253 let git_fs = self.gctx.git_path();
254 let _ = git_fs.create_dir();
257 let git_path = self
258 .gctx
259 .assert_package_cache_locked(CacheLockMode::DownloadExclusive, &git_fs);
260
261 exclude_from_backups_and_indexing(&git_path);
270
271 let (db, actual_rev) = self.fetch_db(false)?;
272
273 let short_id = db.to_short_id(actual_rev)?;
277
278 let checkout_path = self
282 .gctx
283 .git_checkouts_path()
284 .join(&self.ident)
285 .join(short_id.as_str());
286 let checkout_path = checkout_path.into_path_unlocked();
287 db.copy_to(actual_rev, &checkout_path, self.gctx, self.quiet)?;
288
289 let source_id = self
290 .source_id
291 .borrow()
292 .with_git_precise(Some(actual_rev.to_string()));
293 let path_source = RecursivePathSource::new(&checkout_path, source_id, self.gctx);
294
295 self.path_source.replace(Some(path_source));
296 self.short_id.replace(Some(short_id.as_str().into()));
297 self.locked_rev.replace(Revision::Locked(actual_rev));
298 self.path_source.borrow().as_ref().unwrap().load()?;
299
300 self.mark_used()?;
301 Ok(())
302 }
303}
304
305#[derive(Clone, Debug)]
309enum Revision {
310 Deferred(GitReference),
314 Locked(git2::Oid),
316}
317
318impl Revision {
319 fn new(rev: &str) -> Revision {
320 match rev_to_oid(rev) {
321 Some(oid) => Revision::Locked(oid),
322 None => Revision::Deferred(GitReference::Rev(rev.to_string())),
323 }
324 }
325}
326
327impl From<GitReference> for Revision {
328 fn from(value: GitReference) -> Self {
329 Revision::Deferred(value)
330 }
331}
332
333impl From<Revision> for GitReference {
334 fn from(value: Revision) -> Self {
335 match value {
336 Revision::Deferred(git_ref) => git_ref,
337 Revision::Locked(oid) => GitReference::Rev(oid.to_string()),
338 }
339 }
340}
341
342fn ident(id: &SourceId) -> String {
345 let ident = id
346 .canonical_url()
347 .raw_canonicalized_url()
348 .path_segments()
349 .and_then(|s| s.rev().next())
350 .unwrap_or("");
351
352 let ident = if ident.is_empty() { "_empty" } else { ident };
353
354 format!("{}-{}", ident, short_hash(id.canonical_url()))
355}
356
357fn ident_shallow(id: &SourceId, is_shallow: bool) -> String {
364 let mut ident = ident(id);
365 if is_shallow {
366 ident.push_str("-shallow");
367 }
368 ident
369}
370
371impl<'gctx> Debug for GitSource<'gctx> {
372 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
373 write!(f, "git repo at {}", self.source_id.borrow().url())?;
374 match &*self.locked_rev.borrow() {
375 Revision::Deferred(git_ref) => match git_ref.pretty_ref(true) {
376 Some(s) => write!(f, " ({})", s),
377 None => Ok(()),
378 },
379 Revision::Locked(oid) => write!(f, " ({oid})"),
380 }
381 }
382}
383
384#[async_trait::async_trait(?Send)]
385impl<'gctx> Source for GitSource<'gctx> {
386 async fn query(
387 &self,
388 dep: &Dependency,
389 kind: QueryKind,
390 f: &mut dyn FnMut(IndexSummary),
391 ) -> CargoResult<()> {
392 if self.path_source.borrow().is_none() {
393 self.update()?;
394 }
395 let src = self.path_source.borrow();
396 let src = src.as_ref().unwrap();
397 src.query(dep, kind, f).await
398 }
399
400 fn supports_checksums(&self) -> bool {
401 false
402 }
403
404 fn requires_precise(&self) -> bool {
405 true
406 }
407
408 fn source_id(&self) -> SourceId {
409 *self.source_id.borrow()
410 }
411
412 async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
413 trace!(
414 "getting packages for package ID `{}` from `{:?}`",
415 id, self.remote
416 );
417 self.mark_used()?;
418 self.path_source
419 .borrow_mut()
420 .as_mut()
421 .expect("BUG: `update()` must be called before `get()`")
422 .download(id)
423 .await
424 }
425
426 async fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
427 panic!("no download should have started")
428 }
429
430 fn fingerprint(&self, _pkg: &Package) -> CargoResult<String> {
431 match &*self.locked_rev.borrow() {
432 Revision::Locked(oid) => Ok(oid.to_string()),
433 _ => unreachable!("locked_rev must be resolved when computing fingerprint"),
434 }
435 }
436
437 fn describe(&self) -> String {
438 format!("Git repository {}", self.source_id.borrow())
439 }
440
441 fn invalidate_cache(&self) {}
442
443 fn set_quiet(&mut self, quiet: bool) {
444 self.quiet = quiet;
445 }
446}
447
448#[cfg(test)]
449mod test {
450 use super::ident;
451 use crate::util::IntoUrl;
452 use crate::workspace::{GitReference, SourceId};
453
454 #[test]
455 pub fn test_url_to_path_ident_with_path() {
456 let ident = ident(&src("https://github.com/carlhuda/cargo"));
457 assert!(ident.starts_with("cargo-"));
458 }
459
460 #[test]
461 pub fn test_url_to_path_ident_without_path() {
462 let ident = ident(&src("https://github.com"));
463 assert!(ident.starts_with("_empty-"));
464 }
465
466 #[test]
467 fn test_canonicalize_idents_by_stripping_trailing_url_slash() {
468 let ident1 = ident(&src("https://github.com/PistonDevelopers/piston/"));
469 let ident2 = ident(&src("https://github.com/PistonDevelopers/piston"));
470 assert_eq!(ident1, ident2);
471 }
472
473 #[test]
474 fn test_canonicalize_idents_by_lowercasing_github_urls() {
475 let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
476 let ident2 = ident(&src("https://github.com/pistondevelopers/piston"));
477 assert_eq!(ident1, ident2);
478 }
479
480 #[test]
481 fn test_canonicalize_idents_by_stripping_dot_git() {
482 let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
483 let ident2 = ident(&src("https://github.com/PistonDevelopers/piston.git"));
484 assert_eq!(ident1, ident2);
485 }
486
487 #[test]
488 fn test_canonicalize_idents_different_protocols() {
489 let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
490 let ident2 = ident(&src("git://github.com/PistonDevelopers/piston"));
491 assert_eq!(ident1, ident2);
492 }
493
494 #[test]
495 fn test_canonicalize_idents_does_not_strip_dot_git_for_sparse() {
496 let ident1 = ident(&src("sparse+https://crates.io/fake-registry"));
497 let ident2 = ident(&src("sparse+https://crates.io/fake-registry.git"));
498 assert_ne!(ident1, ident2);
499 }
500
501 fn src(s: &str) -> SourceId {
502 SourceId::for_git(&s.into_url().unwrap(), GitReference::DefaultBranch).unwrap()
503 }
504}