1use crate::core::GitReference;
4use crate::core::SourceId;
5use crate::core::global_cache_tracker;
6use crate::core::{Dependency, Package, PackageId};
7use crate::sources::IndexSummary;
8use crate::sources::RecursivePathSource;
9use crate::sources::git::utils::GitDatabase;
10use crate::sources::git::utils::GitRemote;
11use crate::sources::git::utils::rev_to_oid;
12use crate::sources::source::MaybePackage;
13use crate::sources::source::QueryKind;
14use crate::sources::source::Source;
15use crate::util::GlobalContext;
16use crate::util::cache_lock::CacheLockMode;
17use crate::util::errors::CargoResult;
18use crate::util::hex::short_hash;
19use crate::util::interning::InternedString;
20use anyhow::Context as _;
21use cargo_util::paths::exclude_from_backups_and_indexing;
22use std::fmt::{self, Debug, Formatter};
23use std::task::Poll;
24use tracing::trace;
25use url::Url;
26
27pub struct GitSource<'gctx> {
71 remote: GitRemote,
73 locked_rev: Revision,
77 source_id: SourceId,
79 path_source: Option<RecursivePathSource<'gctx>>,
84 short_id: 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,
142 source_id,
143 path_source: None,
144 short_id: 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.url()
156 }
157
158 pub fn read_packages(&mut self) -> CargoResult<Vec<Package>> {
162 if self.path_source.is_none() {
163 self.invalidate_cache();
164 self.block_until_ready()?;
165 }
166 self.path_source.as_mut().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.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, 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.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
248#[derive(Clone, Debug)]
252enum Revision {
253 Deferred(GitReference),
257 Locked(git2::Oid),
259}
260
261impl Revision {
262 fn new(rev: &str) -> Revision {
263 match rev_to_oid(rev) {
264 Some(oid) => Revision::Locked(oid),
265 None => Revision::Deferred(GitReference::Rev(rev.to_string())),
266 }
267 }
268}
269
270impl From<GitReference> for Revision {
271 fn from(value: GitReference) -> Self {
272 Revision::Deferred(value)
273 }
274}
275
276impl From<Revision> for GitReference {
277 fn from(value: Revision) -> Self {
278 match value {
279 Revision::Deferred(git_ref) => git_ref,
280 Revision::Locked(oid) => GitReference::Rev(oid.to_string()),
281 }
282 }
283}
284
285fn ident(id: &SourceId) -> String {
288 let ident = id
289 .canonical_url()
290 .raw_canonicalized_url()
291 .path_segments()
292 .and_then(|s| s.rev().next())
293 .unwrap_or("");
294
295 let ident = if ident.is_empty() { "_empty" } else { ident };
296
297 format!("{}-{}", ident, short_hash(id.canonical_url()))
298}
299
300fn ident_shallow(id: &SourceId, is_shallow: bool) -> String {
307 let mut ident = ident(id);
308 if is_shallow {
309 ident.push_str("-shallow");
310 }
311 ident
312}
313
314impl<'gctx> Debug for GitSource<'gctx> {
315 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
316 write!(f, "git repo at {}", self.source_id.url())?;
317 match &self.locked_rev {
318 Revision::Deferred(git_ref) => match git_ref.pretty_ref(true) {
319 Some(s) => write!(f, " ({})", s),
320 None => Ok(()),
321 },
322 Revision::Locked(oid) => write!(f, " ({oid})"),
323 }
324 }
325}
326
327impl<'gctx> Source for GitSource<'gctx> {
328 fn query(
329 &mut self,
330 dep: &Dependency,
331 kind: QueryKind,
332 f: &mut dyn FnMut(IndexSummary),
333 ) -> Poll<CargoResult<()>> {
334 if let Some(src) = self.path_source.as_mut() {
335 src.query(dep, kind, f)
336 } else {
337 Poll::Pending
338 }
339 }
340
341 fn supports_checksums(&self) -> bool {
342 false
343 }
344
345 fn requires_precise(&self) -> bool {
346 true
347 }
348
349 fn source_id(&self) -> SourceId {
350 self.source_id
351 }
352
353 fn block_until_ready(&mut self) -> CargoResult<()> {
354 if self.path_source.is_some() {
355 self.mark_used()?;
356 return Ok(());
357 }
358
359 let git_fs = self.gctx.git_path();
360 let _ = git_fs.create_dir();
363 let git_path = self
364 .gctx
365 .assert_package_cache_locked(CacheLockMode::DownloadExclusive, &git_fs);
366
367 exclude_from_backups_and_indexing(&git_path);
376
377 let (db, actual_rev) = self.fetch_db(false)?;
378
379 let short_id = db.to_short_id(actual_rev)?;
383
384 let checkout_path = self
388 .gctx
389 .git_checkouts_path()
390 .join(&self.ident)
391 .join(short_id.as_str());
392 let checkout_path = checkout_path.into_path_unlocked();
393 db.copy_to(actual_rev, &checkout_path, self.gctx, self.quiet)?;
394
395 let source_id = self
396 .source_id
397 .with_git_precise(Some(actual_rev.to_string()));
398 let path_source = RecursivePathSource::new(&checkout_path, source_id, self.gctx);
399
400 self.path_source = Some(path_source);
401 self.short_id = Some(short_id.as_str().into());
402 self.locked_rev = Revision::Locked(actual_rev);
403 self.path_source.as_mut().unwrap().load()?;
404
405 self.mark_used()?;
406 Ok(())
407 }
408
409 fn download(&mut self, id: PackageId) -> CargoResult<MaybePackage> {
410 trace!(
411 "getting packages for package ID `{}` from `{:?}`",
412 id, self.remote
413 );
414 self.mark_used()?;
415 self.path_source
416 .as_mut()
417 .expect("BUG: `update()` must be called before `get()`")
418 .download(id)
419 }
420
421 fn finish_download(&mut self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
422 panic!("no download should have started")
423 }
424
425 fn fingerprint(&self, _pkg: &Package) -> CargoResult<String> {
426 match &self.locked_rev {
427 Revision::Locked(oid) => Ok(oid.to_string()),
428 _ => unreachable!("locked_rev must be resolved when computing fingerprint"),
429 }
430 }
431
432 fn describe(&self) -> String {
433 format!("Git repository {}", self.source_id)
434 }
435
436 fn add_to_yanked_whitelist(&mut self, _pkgs: &[PackageId]) {}
437
438 fn is_yanked(&mut self, _pkg: PackageId) -> Poll<CargoResult<bool>> {
439 Poll::Ready(Ok(false))
440 }
441
442 fn invalidate_cache(&mut self) {}
443
444 fn set_quiet(&mut self, quiet: bool) {
445 self.quiet = quiet;
446 }
447}
448
449#[cfg(test)]
450mod test {
451 use super::ident;
452 use crate::core::{GitReference, SourceId};
453 use crate::util::IntoUrl;
454
455 #[test]
456 pub fn test_url_to_path_ident_with_path() {
457 let ident = ident(&src("https://github.com/carlhuda/cargo"));
458 assert!(ident.starts_with("cargo-"));
459 }
460
461 #[test]
462 pub fn test_url_to_path_ident_without_path() {
463 let ident = ident(&src("https://github.com"));
464 assert!(ident.starts_with("_empty-"));
465 }
466
467 #[test]
468 fn test_canonicalize_idents_by_stripping_trailing_url_slash() {
469 let ident1 = ident(&src("https://github.com/PistonDevelopers/piston/"));
470 let ident2 = ident(&src("https://github.com/PistonDevelopers/piston"));
471 assert_eq!(ident1, ident2);
472 }
473
474 #[test]
475 fn test_canonicalize_idents_by_lowercasing_github_urls() {
476 let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
477 let ident2 = ident(&src("https://github.com/pistondevelopers/piston"));
478 assert_eq!(ident1, ident2);
479 }
480
481 #[test]
482 fn test_canonicalize_idents_by_stripping_dot_git() {
483 let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
484 let ident2 = ident(&src("https://github.com/PistonDevelopers/piston.git"));
485 assert_eq!(ident1, ident2);
486 }
487
488 #[test]
489 fn test_canonicalize_idents_different_protocols() {
490 let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
491 let ident2 = ident(&src("git://github.com/PistonDevelopers/piston"));
492 assert_eq!(ident1, ident2);
493 }
494
495 fn src(s: &str) -> SourceId {
496 SourceId::for_git(&s.into_url().unwrap(), GitReference::DefaultBranch).unwrap()
497 }
498}