cargo/sources/git/utils.rs
1//! Utilities for handling git repositories, mainly around
2//! authentication/cloning.
3
4use crate::context::ProgressWhen;
5use crate::sources::git::fetch::RemoteKind;
6use crate::sources::git::oxide;
7use crate::sources::git::oxide::cargo_config_to_gitoxide_overrides;
8use crate::sources::git::source::GitSource;
9use crate::sources::source::Source as _;
10use crate::util::HumanBytes;
11use crate::util::errors::{CargoResult, GitCliError};
12use crate::util::network::http::http_handle;
13use crate::util::network::http::needs_custom_http_transport;
14use crate::util::{GlobalContext, IntoUrl, MetricsCounter, Progress, network};
15use crate::workspace::{GitReference, SourceId};
16
17use anyhow::{Context as _, anyhow};
18use cargo_util::{ProcessBuilder, paths};
19use cargo_util_terminal::Verbosity;
20use git2::{ErrorClass, ObjectType, Oid};
21use http::{Request, StatusCode};
22use tracing::{debug, info};
23use url::Url;
24
25use std::borrow::Cow;
26use std::path::{Path, PathBuf};
27use std::process::Command;
28use std::str;
29use std::sync::Once;
30use std::sync::atomic::{AtomicBool, Ordering};
31use std::time::{Duration, Instant};
32
33/// A file indicates that if present, `git reset` has been done and a repo
34/// checkout is ready to go. See [`GitCheckout::reset`] for why we need this.
35const CHECKOUT_READY_LOCK: &str = ".cargo-ok";
36
37/// A remote repository. It gets cloned into a local [`GitDatabase`].
38#[derive(PartialEq, Clone, Debug)]
39pub struct GitRemote {
40 /// URL to a remote repository.
41 ///
42 /// This may differ from the [`SourceId`] URL when the original URL
43 /// can't be represented as a WHATWG [`Url`], for example SCP-like URLs.
44 /// See <https://github.com/rust-lang/cargo/issues/16740>.
45 url: String,
46}
47
48/// A local clone of a remote repository's database. Multiple [`GitCheckout`]s
49/// can be cloned from a single [`GitDatabase`].
50pub struct GitDatabase {
51 /// The remote repository where this database is fetched from.
52 remote: GitRemote,
53 /// Path to the root of the underlying Git repository on the local filesystem.
54 path: PathBuf,
55 /// Underlying Git repository instance for this database.
56 repo: git2::Repository,
57}
58
59/// A local checkout of a particular revision from a [`GitDatabase`].
60pub struct GitCheckout<'a> {
61 /// The git database where this checkout is cloned from.
62 database: &'a GitDatabase,
63 /// Path to the root of the underlying Git repository on the local filesystem.
64 path: PathBuf,
65 /// The git revision this checkout is for.
66 revision: git2::Oid,
67 /// Underlying Git repository instance for this checkout.
68 repo: git2::Repository,
69}
70
71impl GitRemote {
72 /// Creates an instance for a remote repository URL.
73 pub fn new(url: &Url) -> GitRemote {
74 GitRemote {
75 url: url.as_str().to_owned(),
76 }
77 }
78
79 /// Creates an instance with an URL that may not be a valid WHATWG URL.
80 ///
81 /// This is needed because [`SourceId`] hasn't yet supported SCP-like URLs.
82 pub(super) fn new_from_str(url: String) -> GitRemote {
83 GitRemote { url }
84 }
85
86 /// Gets the remote repository URL.
87 pub fn url(&self) -> &str {
88 &self.url
89 }
90
91 /// Fetches and checkouts to a reference or a revision from this remote
92 /// into a local path.
93 ///
94 /// This ensures that it gets the up-to-date commit when a named reference
95 /// is given (tag, branch, refs/*). Thus, network connection is involved.
96 ///
97 /// If we have a previous instance of [`GitDatabase`] then fetch into that
98 /// if we can. If that can successfully load our revision then we've
99 /// populated the database with the latest version of `reference`, so
100 /// return that database and the rev we resolve to.
101 pub fn checkout(
102 &self,
103 into: &Path,
104 db: Option<GitDatabase>,
105 manifest_reference: &GitReference,
106 reference: &GitReference,
107 gctx: &GlobalContext,
108 ) -> CargoResult<(GitDatabase, git2::Oid)> {
109 if let Some(mut db) = db {
110 fetch(
111 &mut db.repo,
112 self.url(),
113 manifest_reference,
114 reference,
115 gctx,
116 RemoteKind::GitDependency,
117 )
118 .with_context(|| format!("failed to fetch into: {}", into.display()))?;
119
120 if let Some(rev) = resolve_ref(reference, &db.repo).ok() {
121 return Ok((db, rev));
122 }
123 }
124
125 // Otherwise start from scratch to handle corrupt git repositories.
126 // After our fetch (which is interpreted as a clone now) we do the same
127 // resolution to figure out what we cloned.
128 if into.exists() {
129 paths::remove_dir_all(into)?;
130 }
131 paths::create_dir_all(into)?;
132 let mut repo = init(into, true)?;
133 fetch(
134 &mut repo,
135 self.url(),
136 manifest_reference,
137 reference,
138 gctx,
139 RemoteKind::GitDependency,
140 )
141 .with_context(|| format!("failed to clone into: {}", into.display()))?;
142 let rev = resolve_ref(reference, &repo)?;
143
144 Ok((
145 GitDatabase {
146 remote: self.clone(),
147 path: into.to_path_buf(),
148 repo,
149 },
150 rev,
151 ))
152 }
153
154 /// Creates a [`GitDatabase`] of this remote at `db_path`.
155 pub fn db_at(&self, db_path: &Path) -> CargoResult<GitDatabase> {
156 let repo = git2::Repository::open(db_path)?;
157 Ok(GitDatabase {
158 remote: self.clone(),
159 path: db_path.to_path_buf(),
160 repo,
161 })
162 }
163}
164
165impl GitDatabase {
166 /// Checkouts to a revision at `dest`ination from this database.
167 #[tracing::instrument(skip(self, gctx))]
168 pub fn copy_to(
169 &self,
170 rev: git2::Oid,
171 dest: &Path,
172 gctx: &GlobalContext,
173 quiet: bool,
174 ) -> CargoResult<GitCheckout<'_>> {
175 // If the existing checkout exists, and it is fresh, use it.
176 // A non-fresh checkout can happen if the checkout operation was
177 // interrupted. In that case, the checkout gets deleted and a new
178 // clone is created.
179 let checkout = match git2::Repository::open(dest)
180 .ok()
181 .map(|repo| GitCheckout::new(self, rev, repo))
182 .filter(|co| co.is_fresh())
183 {
184 Some(co) => co,
185 None => {
186 let (checkout, guard) = GitCheckout::clone_into(dest, self, rev, gctx)?;
187 checkout.update_submodules(gctx, quiet)?;
188 guard.mark_ok()?;
189 checkout
190 }
191 };
192
193 Ok(checkout)
194 }
195
196 /// Get a short OID for a `revision`, 7 chars or more if ambiguous.
197 ///
198 /// Like [`git2::Object::short_id`]
199 /// but ignores the user's `core.abbrev` git config.
200 pub fn to_short_id(&self, rev: git2::Oid) -> CargoResult<String> {
201 const MIN_ABBREV_LEN: usize = 7; // this is git/libgit2's default
202 let odb = self.repo.odb()?;
203 let mut len = MIN_ABBREV_LEN;
204 let mut hex = rev.to_string();
205 // quasi- re-implementation of
206 // https://github.com/libgit2/libgit2/blob/26055f5af74ab/src/libgit2/object.c#L523-L573
207 while len < hex.len() {
208 match odb.exists_prefix(rev, len) {
209 Ok(_) => break,
210 Err(err) if err.code() == git2::ErrorCode::Ambiguous => len += 1,
211 Err(err) => return Err(err.into()),
212 }
213 }
214 hex.truncate(len);
215 Ok(hex)
216 }
217
218 /// Checks if the database contains the object of this `oid`..
219 pub fn contains(&self, oid: git2::Oid) -> bool {
220 self.repo.revparse_single(&oid.to_string()).is_ok()
221 }
222
223 /// [`resolve_ref`]s this reference with this database.
224 pub fn resolve(&self, r: &GitReference) -> CargoResult<git2::Oid> {
225 resolve_ref(r, &self.repo)
226 }
227}
228
229/// Resolves [`GitReference`] to an object ID with objects the `repo` currently has.
230pub fn resolve_ref(gitref: &GitReference, repo: &git2::Repository) -> CargoResult<git2::Oid> {
231 let id = match gitref {
232 // Note that we resolve the named tag here in sync with where it's
233 // fetched into via `fetch` below.
234 GitReference::Tag(s) => (|| -> CargoResult<git2::Oid> {
235 let refname = format!("refs/remotes/origin/tags/{}", s);
236 let id = repo.refname_to_id(&refname)?;
237 let obj = repo.find_object(id, None)?;
238 let obj = obj.peel(ObjectType::Commit)?;
239 Ok(obj.id())
240 })()
241 .with_context(|| format!("failed to find tag `{}`", s))?,
242
243 // Resolve the remote name since that's all we're configuring in
244 // `fetch` below.
245 GitReference::Branch(s) => {
246 let name = format!("origin/{}", s);
247 let b = repo
248 .find_branch(&name, git2::BranchType::Remote)
249 .with_context(|| format!("failed to find branch `{}`", s))?;
250 b.get()
251 .target()
252 .ok_or_else(|| anyhow::format_err!("branch `{}` did not have a target", s))?
253 }
254
255 // We'll be using the HEAD commit
256 GitReference::DefaultBranch => {
257 let head_id = repo.refname_to_id("refs/remotes/origin/HEAD")?;
258 let head = repo.find_object(head_id, None)?;
259 head.peel(ObjectType::Commit)?.id()
260 }
261
262 GitReference::Rev(s) => {
263 let obj = repo.revparse_single(s)?;
264 match obj.as_tag() {
265 Some(tag) => tag.target_id(),
266 None => obj.id(),
267 }
268 }
269 };
270 Ok(id)
271}
272
273impl<'a> GitCheckout<'a> {
274 /// Creates an instance of [`GitCheckout`]. This doesn't imply the checkout
275 /// is done. Use [`GitCheckout::is_fresh`] to check.
276 ///
277 /// * The `database` is where this checkout is from.
278 /// * The `repo` will be the checked out Git repository.
279 fn new(
280 database: &'a GitDatabase,
281 revision: git2::Oid,
282 repo: git2::Repository,
283 ) -> GitCheckout<'a> {
284 let path = repo.workdir().unwrap_or_else(|| repo.path());
285 GitCheckout {
286 path: path.to_path_buf(),
287 database,
288 revision,
289 repo,
290 }
291 }
292
293 /// Gets the remote repository URL.
294 fn remote_url(&self) -> &str {
295 self.database.remote.url()
296 }
297
298 /// Clone a repo for a `revision` into a local path from a `database`.
299 /// This is a filesystem-to-filesystem clone.
300 fn clone_into(
301 into: &Path,
302 database: &'a GitDatabase,
303 revision: git2::Oid,
304 gctx: &GlobalContext,
305 ) -> CargoResult<(GitCheckout<'a>, CheckoutGuard)> {
306 let dirname = into.parent().unwrap();
307 paths::create_dir_all(&dirname)?;
308 if into.exists() {
309 paths::remove_dir_all(into)?;
310 }
311
312 // we're doing a local filesystem-to-filesystem clone so there should
313 // be no need to respect global configuration options, so pass in
314 // an empty instance of `git2::Config` below.
315 let git_config = git2::Config::new()?;
316
317 // Clone the repository, but make sure we use the "local" option in
318 // libgit2 which will attempt to use hardlinks to set up the database.
319 // This should speed up the clone operation quite a bit if it works.
320 //
321 // Note that we still use the same fetch options because while we don't
322 // need authentication information we may want progress bars and such.
323 let url = database.path.into_url()?;
324 let mut repo = None;
325 with_fetch_options(&git_config, url.as_str(), gctx, &mut |fopts| {
326 let mut checkout = git2::build::CheckoutBuilder::new();
327 checkout.dry_run(); // we'll do this below during a `reset`
328
329 let r = git2::build::RepoBuilder::new()
330 // use hard links and/or copy the database, we're doing a
331 // filesystem clone so this'll speed things up quite a bit.
332 .clone_local(git2::build::CloneLocal::Local)
333 .with_checkout(checkout)
334 .fetch_options(fopts)
335 .clone(url.as_str(), into)?;
336 // `git2` doesn't seem to handle shallow repos correctly when doing
337 // a local clone. Fortunately all that's needed is the copy of the
338 // one file that defines the shallow boundary, the commits which
339 // have their parents omitted as part of the shallow clone.
340 //
341 // TODO(git2): remove this when git2 supports shallow clone correctly
342 if database.repo.is_shallow() {
343 std::fs::copy(
344 database.repo.path().join("shallow"),
345 r.path().join("shallow"),
346 )?;
347 }
348 repo = Some(r);
349 Ok(())
350 })?;
351 let repo = repo.unwrap();
352
353 let checkout = GitCheckout::new(database, revision, repo);
354 let guard = checkout.reset(gctx)?;
355 Ok((checkout, guard))
356 }
357
358 /// Checks if the `HEAD` of this checkout points to the expected revision.
359 fn is_fresh(&self) -> bool {
360 match self.repo.revparse_single("HEAD") {
361 Ok(ref head) if head.id() == self.revision => {
362 // See comments in reset() for why we check this
363 self.path.join(CHECKOUT_READY_LOCK).exists()
364 }
365 _ => false,
366 }
367 }
368
369 /// Similar to [`reset()`]. This roughly performs `git reset --hard` to the
370 /// revision of this checkout, with additional interrupt protection by a
371 /// dummy file [`CHECKOUT_READY_LOCK`].
372 ///
373 /// If we're interrupted while performing a `git reset` (e.g., we die
374 /// because of a signal) Cargo needs to be sure to try to check out this
375 /// repo again on the next go-round.
376 ///
377 /// To enable this we have a dummy file in our checkout, [`.cargo-ok`],
378 /// which if present means that the repo has been successfully reset and is
379 /// ready to go. Hence if we start to do a reset, we make sure this file
380 /// *doesn't* exist. The caller of [`reset`] has an option to perform additional operations
381 /// (e.g. submodule update) before marking the check-out as ready.
382 ///
383 /// [`.cargo-ok`]: CHECKOUT_READY_LOCK
384 fn reset(&self, gctx: &GlobalContext) -> CargoResult<CheckoutGuard> {
385 let guard = CheckoutGuard::guard(&self.path);
386 info!("reset {} to {}", self.repo.path().display(), self.revision);
387
388 // Ensure libgit2 won't mess with newlines when we vendor.
389 if let Ok(mut git_config) = self.repo.config() {
390 git_config.set_bool("core.autocrlf", false)?;
391 }
392
393 let object = self.repo.find_object(self.revision, None)?;
394 reset(&self.repo, &object, gctx)?;
395
396 Ok(guard)
397 }
398
399 /// Like `git submodule update --recursive` but for this git checkout.
400 ///
401 /// This function respects `submodule.<name>.update = none`[^1] git config.
402 /// Submodules set to `none` won't be fetched.
403 ///
404 /// [^1]: <https://git-scm.com/docs/git-submodule#Documentation/git-submodule.txt-none>
405 fn update_submodules(&self, gctx: &GlobalContext, quiet: bool) -> CargoResult<()> {
406 return update_submodules(&self.repo, gctx, quiet, self.remote_url());
407
408 /// Recursive helper for [`GitCheckout::update_submodules`].
409 fn update_submodules(
410 repo: &git2::Repository,
411 gctx: &GlobalContext,
412 quiet: bool,
413 parent_remote_url: &str,
414 ) -> CargoResult<()> {
415 debug!("update submodules for: {:?}", repo.workdir().unwrap());
416
417 for mut child in repo.submodules()? {
418 update_submodule(repo, &mut child, gctx, quiet, parent_remote_url).with_context(
419 || {
420 format!(
421 "failed to update submodule `{}`",
422 child.name().unwrap_or("")
423 )
424 },
425 )?;
426 }
427 Ok(())
428 }
429
430 /// Update a single Git submodule, and recurse into its submodules.
431 fn update_submodule(
432 parent: &git2::Repository,
433 child: &mut git2::Submodule<'_>,
434 gctx: &GlobalContext,
435 quiet: bool,
436 parent_remote_url: &str,
437 ) -> CargoResult<()> {
438 child.init(false)?;
439
440 let child_url_str = child
441 .url()
442 .with_context(|| {
443 format!("failed to update submodule `{}`", child.path().display())
444 })?
445 .ok_or_else(|| {
446 anyhow::format_err!(
447 "unable to update submodule `{}` without a path",
448 child.name().unwrap_or("")
449 )
450 })?;
451
452 // Skip the submodule if the config says not to update it.
453 if child.update_strategy() == git2::SubmoduleUpdate::None {
454 gctx.shell().status(
455 "Skipping",
456 format!(
457 "git submodule `{}` due to update strategy in .gitmodules",
458 child_url_str
459 ),
460 )?;
461 return Ok(());
462 }
463
464 let child_remote_url = absolute_submodule_url(parent_remote_url, child_url_str)?;
465
466 // A submodule which is listed in .gitmodules but not actually
467 // checked out will not have a head id, so we should ignore it.
468 let Some(head) = child.head_id() else {
469 return Ok(());
470 };
471
472 // If the submodule hasn't been checked out yet, we need to
473 // clone it. If it has been checked out and the head is the same
474 // as the submodule's head, then we can skip an update and keep
475 // recursing.
476 let head_and_repo = child.open().and_then(|repo| {
477 let target = repo.head()?.target();
478 Ok((target, repo))
479 });
480 let repo = match head_and_repo {
481 Ok((head, repo)) => {
482 if child.head_id() == head {
483 return update_submodules(&repo, gctx, quiet, &child_remote_url);
484 }
485 repo
486 }
487 Err(..) => {
488 let path = parent.workdir().unwrap().join(child.path());
489 let _ = paths::remove_dir_all(&path);
490 init(&path, false)?
491 }
492 };
493 // Fetch submodule database and checkout to target revision
494 let reference = GitReference::Rev(head.to_string());
495
496 // SCP-like URL is not a WHATWG Standard URL.
497 // `url` crate can't parse SCP-like URLs.
498 // We convert to `ssh://` for SourceId,
499 // but preserve the original URL for fetch to maintain correct semantics
500 // See <https://github.com/rust-lang/cargo/issues/16740>
501 let (source_url, fetch_url) = match child_remote_url.as_ref().into_url() {
502 Ok(url) => (url, None),
503 Err(_) => {
504 let ssh_url = scp_to_ssh(&child_remote_url)
505 .ok_or_else(|| anyhow::format_err!("invalid url `{child_remote_url}`"))?
506 .as_str()
507 .into_url()?;
508 (ssh_url, Some(child_remote_url.into_owned()))
509 }
510 };
511
512 // GitSource created from SourceId without git precise will result to
513 // locked_rev being Deferred and fetch_db always try to fetch if online
514 let source_id =
515 SourceId::for_git(&source_url, reference)?.with_git_precise(Some(head.to_string()));
516
517 let mut source = match &fetch_url {
518 Some(url) => GitSource::new_for_submodule(source_id, url.to_owned(), gctx)?,
519 None => GitSource::new(source_id, gctx)?,
520 };
521 source.set_quiet(quiet);
522
523 let (db, actual_rev) = source.fetch_db(true).with_context(|| {
524 let name = child.name().unwrap_or("");
525 let url = fetch_url.unwrap_or_else(|| source_url.to_string());
526 format!("failed to fetch submodule `{name}` from {url}")
527 })?;
528 db.copy_to(actual_rev, repo.path(), gctx, quiet)?;
529 Ok(())
530 }
531 }
532}
533
534/// See [`GitCheckout::reset`] for rationale on this type.
535#[must_use]
536struct CheckoutGuard {
537 ok_file: PathBuf,
538}
539
540impl CheckoutGuard {
541 fn guard(path: &Path) -> Self {
542 let ok_file = path.join(CHECKOUT_READY_LOCK);
543 let _ = paths::remove_file(&ok_file);
544 Self { ok_file }
545 }
546
547 fn mark_ok(self) -> CargoResult<()> {
548 let _ = paths::create(self.ok_file)?;
549 Ok(())
550 }
551}
552
553/// Constructs an absolute URL for a child submodule URL with its parent base URL.
554///
555/// Git only assumes a submodule URL is a relative path if it starts with `./`
556/// or `../` [^1]. To fetch the correct repo, we need to construct an absolute
557/// submodule URL.
558///
559/// At this moment it comes with some limitations:
560///
561/// * GitHub doesn't accept non-normalized URLs with relative paths.
562/// (`ssh://git@github.com/rust-lang/cargo.git/relative/..` is invalid)
563/// * `url` crate cannot parse SCP-like URLs.
564/// (`git@github.com:rust-lang/cargo.git` is not a valid WHATWG URL)
565///
566/// To overcome these, this patch always tries [`Url::parse`] first to normalize
567/// the path. If it couldn't, append the relative path and/or convert SCP-like URLs
568/// to ssh:// format as the last resorts and pray the remote git service supports
569/// non-normalized URLs.
570///
571/// See also rust-lang/cargo#12404 and rust-lang/cargo#12295.
572///
573/// [^1]: <https://git-scm.com/docs/git-submodule>
574fn absolute_submodule_url<'s>(base_url: &str, submodule_url: &'s str) -> CargoResult<Cow<'s, str>> {
575 let absolute_url = if ["./", "../"].iter().any(|p| submodule_url.starts_with(p)) {
576 match Url::parse(base_url) {
577 Ok(mut base_url) => {
578 let path = base_url.path();
579 if !path.ends_with('/') {
580 base_url.set_path(&format!("{path}/"));
581 }
582 let absolute_url = base_url.join(submodule_url).with_context(|| {
583 format!(
584 "failed to parse relative child submodule url `{submodule_url}` \
585 using parent base url `{base_url}`"
586 )
587 })?;
588 Cow::from(absolute_url.to_string())
589 }
590 Err(_) => {
591 let mut absolute_url = base_url.to_string();
592 if !absolute_url.ends_with('/') {
593 absolute_url.push('/');
594 }
595 absolute_url.push_str(submodule_url);
596 Cow::from(absolute_url)
597 }
598 }
599 } else {
600 Cow::from(submodule_url)
601 };
602
603 Ok(absolute_url)
604}
605
606/// Converts an SCP-like URL to `ssh://` format.
607fn scp_to_ssh(url: &str) -> Option<String> {
608 let mut gix_url = gix::url::parse(gix::bstr::BStr::new(url.as_bytes())).ok()?;
609 if gix_url.serialize_alternative_form && gix_url.scheme == gix::url::Scheme::Ssh {
610 gix_url.serialize_alternative_form = false;
611 Some(gix_url.to_bstring().to_string())
612 } else {
613 None
614 }
615}
616
617/// Prepare the authentication callbacks for cloning a git repository.
618///
619/// The main purpose of this function is to construct the "authentication
620/// callback" which is used to clone a repository. This callback will attempt to
621/// find the right authentication on the system (without user input) and will
622/// guide libgit2 in doing so.
623///
624/// The callback is provided `allowed` types of credentials, and we try to do as
625/// much as possible based on that:
626///
627/// * Prioritize SSH keys from the local ssh agent as they're likely the most
628/// reliable. The username here is prioritized from the credential
629/// callback, then from whatever is configured in git itself, and finally
630/// we fall back to the generic user of `git`.
631///
632/// * If a username/password is allowed, then we fallback to git2-rs's
633/// implementation of the credential helper. This is what is configured
634/// with `credential.helper` in git, and is the interface for the macOS
635/// keychain, for example.
636///
637/// * After the above two have failed, we just kinda grapple attempting to
638/// return *something*.
639///
640/// If any form of authentication fails, libgit2 will repeatedly ask us for
641/// credentials until we give it a reason to not do so. To ensure we don't
642/// just sit here looping forever we keep track of authentications we've
643/// attempted and we don't try the same ones again.
644fn with_authentication<T, F>(
645 gctx: &GlobalContext,
646 url: &str,
647 cfg: &git2::Config,
648 mut f: F,
649) -> CargoResult<T>
650where
651 F: FnMut(&mut git2::Credentials<'_>) -> CargoResult<T>,
652{
653 let mut cred_helper = git2::CredentialHelper::new(url);
654 cred_helper.config(cfg);
655
656 let mut ssh_username_requested = false;
657 let mut cred_helper_bad = None;
658 let mut ssh_agent_attempts = Vec::new();
659 let mut any_attempts = false;
660 let mut tried_sshkey = false;
661 let mut url_attempt = None;
662
663 let orig_url = url;
664 let mut res = f(&mut |url, username, allowed| {
665 any_attempts = true;
666 if url != orig_url {
667 url_attempt = Some(url.to_string());
668 }
669 // libgit2's "USERNAME" authentication actually means that it's just
670 // asking us for a username to keep going. This is currently only really
671 // used for SSH authentication and isn't really an authentication type.
672 // The logic currently looks like:
673 //
674 // let user = ...;
675 // if (user.is_null())
676 // user = callback(USERNAME, null, ...);
677 //
678 // callback(SSH_KEY, user, ...)
679 //
680 // So if we're being called here then we know that (a) we're using ssh
681 // authentication and (b) no username was specified in the URL that
682 // we're trying to clone. We need to guess an appropriate username here,
683 // but that may involve a few attempts. Unfortunately we can't switch
684 // usernames during one authentication session with libgit2, so to
685 // handle this we bail out of this authentication session after setting
686 // the flag `ssh_username_requested`, and then we handle this below.
687 if allowed.contains(git2::CredentialType::USERNAME) {
688 debug_assert!(username.is_none());
689 ssh_username_requested = true;
690 return Err(git2::Error::from_str("gonna try usernames later"));
691 }
692
693 // An "SSH_KEY" authentication indicates that we need some sort of SSH
694 // authentication. This can currently either come from the ssh-agent
695 // process or from a raw in-memory SSH key. Cargo only supports using
696 // ssh-agent currently.
697 //
698 // If we get called with this then the only way that should be possible
699 // is if a username is specified in the URL itself (e.g., `username` is
700 // Some), hence the unwrap() here. We try custom usernames down below.
701 if allowed.contains(git2::CredentialType::SSH_KEY) && !tried_sshkey {
702 // If ssh-agent authentication fails, libgit2 will keep
703 // calling this callback asking for other authentication
704 // methods to try. Make sure we only try ssh-agent once,
705 // to avoid looping forever.
706 tried_sshkey = true;
707 let username = username.unwrap();
708 debug_assert!(!ssh_username_requested);
709 ssh_agent_attempts.push(username.to_string());
710 return git2::Cred::ssh_key_from_agent(username);
711 }
712
713 // Sometimes libgit2 will ask for a username/password in plaintext. This
714 // is where Cargo would have an interactive prompt if we supported it,
715 // but we currently don't! Right now the only way we support fetching a
716 // plaintext password is through the `credential.helper` support, so
717 // fetch that here.
718 //
719 // If ssh-agent authentication fails, libgit2 will keep calling this
720 // callback asking for other authentication methods to try. Check
721 // cred_helper_bad to make sure we only try the git credential helper
722 // once, to avoid looping forever.
723 if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) && cred_helper_bad.is_none()
724 {
725 let r = git2::Cred::credential_helper(cfg, url, username);
726 cred_helper_bad = Some(r.is_err());
727 return r;
728 }
729
730 // I'm... not sure what the DEFAULT kind of authentication is, but seems
731 // easy to support?
732 if allowed.contains(git2::CredentialType::DEFAULT) {
733 return git2::Cred::default();
734 }
735
736 // Whelp, we tried our best
737 Err(git2::Error::from_str("no authentication methods succeeded"))
738 });
739
740 // Ok, so if it looks like we're going to be doing ssh authentication, we
741 // want to try a few different usernames as one wasn't specified in the URL
742 // for us to use. In order, we'll try:
743 //
744 // * A credential helper's username for this URL, if available.
745 // * This account's username.
746 // * "git"
747 //
748 // We have to restart the authentication session each time (due to
749 // constraints in libssh2 I guess? maybe this is inherent to ssh?), so we
750 // call our callback, `f`, in a loop here.
751 if ssh_username_requested {
752 debug_assert!(res.is_err());
753 let mut attempts = vec![String::from("git")];
754 if let Ok(s) = gctx.get_env("USER").or_else(|_| gctx.get_env("USERNAME")) {
755 attempts.push(s.to_string());
756 }
757 if let Some(ref s) = cred_helper.username {
758 attempts.push(s.clone());
759 }
760
761 while let Some(s) = attempts.pop() {
762 // We should get `USERNAME` first, where we just return our attempt,
763 // and then after that we should get `SSH_KEY`. If the first attempt
764 // fails we'll get called again, but we don't have another option so
765 // we bail out.
766 let mut attempts = 0;
767 res = f(&mut |_url, username, allowed| {
768 if allowed.contains(git2::CredentialType::USERNAME) {
769 return git2::Cred::username(&s);
770 }
771 if allowed.contains(git2::CredentialType::SSH_KEY) {
772 debug_assert_eq!(Some(&s[..]), username);
773 attempts += 1;
774 if attempts == 1 {
775 ssh_agent_attempts.push(s.to_string());
776 return git2::Cred::ssh_key_from_agent(&s);
777 }
778 }
779 Err(git2::Error::from_str("no authentication methods succeeded"))
780 });
781
782 // If we made two attempts then that means:
783 //
784 // 1. A username was requested, we returned `s`.
785 // 2. An ssh key was requested, we returned to look up `s` in the
786 // ssh agent.
787 // 3. For whatever reason that lookup failed, so we were asked again
788 // for another mode of authentication.
789 //
790 // Essentially, if `attempts == 2` then in theory the only error was
791 // that this username failed to authenticate (e.g., no other network
792 // errors happened). Otherwise something else is funny so we bail
793 // out.
794 if attempts != 2 {
795 break;
796 }
797 }
798 }
799 let mut err = match res {
800 Ok(e) => return Ok(e),
801 Err(e) => e,
802 };
803
804 // In the case of an authentication failure (where we tried something) then
805 // we try to give a more helpful error message about precisely what we
806 // tried.
807 if any_attempts {
808 let mut msg = "failed to authenticate when downloading \
809 repository"
810 .to_string();
811
812 if let Some(attempt) = &url_attempt {
813 if url != attempt {
814 msg.push_str(": ");
815 msg.push_str(attempt);
816 }
817 }
818 msg.push('\n');
819 if !ssh_agent_attempts.is_empty() {
820 let names = ssh_agent_attempts
821 .iter()
822 .map(|s| format!("`{}`", s))
823 .collect::<Vec<_>>()
824 .join(", ");
825 msg.push_str(&format!(
826 "\n* attempted ssh-agent authentication, but \
827 no usernames succeeded: {}",
828 names
829 ));
830 }
831 if let Some(failed_cred_helper) = cred_helper_bad {
832 if failed_cred_helper {
833 msg.push_str(
834 "\n* attempted to find username/password via \
835 git's `credential.helper` support, but failed",
836 );
837 } else {
838 msg.push_str(
839 "\n* attempted to find username/password via \
840 `credential.helper`, but maybe the found \
841 credentials were incorrect",
842 );
843 }
844 }
845 msg.push_str("\n\n");
846 msg.push_str("if the git CLI succeeds then `net.git-fetch-with-cli` may help here\n");
847 msg.push_str("https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli");
848 err = err.context(msg);
849
850 // Otherwise if we didn't even get to the authentication phase them we may
851 // have failed to set up a connection, in these cases hint on the
852 // `net.git-fetch-with-cli` configuration option.
853 } else if let Some(e) = err.downcast_ref::<git2::Error>() {
854 match e.class() {
855 ErrorClass::Net
856 | ErrorClass::Ssl
857 | ErrorClass::Submodule
858 | ErrorClass::FetchHead
859 | ErrorClass::Ssh
860 | ErrorClass::Http => {
861 let msg = format!(
862 concat!(
863 "network failure seems to have happened\n",
864 "if a proxy or similar is necessary `net.git-fetch-with-cli` may help here\n",
865 "https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
866 "{}"
867 ),
868 note_github_pull_request(url).unwrap_or_default()
869 );
870 err = err.context(msg);
871 }
872 ErrorClass::Callback => {
873 // This unwraps the git2 error. We're using the callback error
874 // specifically to convey errors from Rust land through the C
875 // callback interface. We don't need the `; class=Callback
876 // (26)` that gets tacked on to the git2 error message.
877 err = anyhow::format_err!("{}", e.message());
878 }
879 _ => {}
880 }
881 }
882
883 Err(err)
884}
885
886/// `git reset --hard` to the given `obj` for the `repo`.
887///
888/// The `obj` is a commit-ish to which the head should be moved.
889#[tracing::instrument(skip_all)]
890fn reset(repo: &git2::Repository, obj: &git2::Object<'_>, gctx: &GlobalContext) -> CargoResult<()> {
891 let mut pb = Progress::new("Checkout", gctx);
892 let mut opts = git2::build::CheckoutBuilder::new();
893 opts.progress(|_, cur, max| {
894 drop(pb.tick(cur, max, ""));
895 });
896 debug!("doing reset");
897 repo.reset(obj, git2::ResetType::Hard, Some(&mut opts))?;
898 debug!("reset done");
899 Ok(())
900}
901
902/// Prepares the callbacks for fetching a git repository.
903///
904/// The main purpose of this function is to construct everything before a fetch.
905/// This will attempt to setup a progress bar, the authentication for git,
906/// ssh known hosts check, and the network retry mechanism.
907///
908/// The callback is provided a fetch options, which can be used by the actual
909/// git fetch.
910pub fn with_fetch_options(
911 git_config: &git2::Config,
912 url: &str,
913 gctx: &GlobalContext,
914 cb: &mut dyn FnMut(git2::FetchOptions<'_>) -> CargoResult<()>,
915) -> CargoResult<()> {
916 let mut progress = Progress::new("Fetch", gctx);
917 let ssh_config = gctx.net_config()?.ssh.as_ref();
918 let config_known_hosts = ssh_config.and_then(|ssh| ssh.known_hosts.as_ref());
919 let diagnostic_home_config = gctx.diagnostic_home_config();
920 network::retry::with_retry(gctx, || {
921 // Hack: libgit2 disallows overriding the error from check_cb since v1.8.0,
922 // so we store the error additionally and unwrap it later
923 let mut check_cb_result = Ok(());
924 let auth_result = with_authentication(gctx, url, git_config, |f| {
925 let port = Url::parse(url).ok().and_then(|url| url.port());
926 let mut last_update = Instant::now();
927 let mut rcb = git2::RemoteCallbacks::new();
928 // We choose `N=10` here to make a `300ms * 10slots ~= 3000ms`
929 // sliding window for tracking the data transfer rate (in bytes/s).
930 let mut counter = MetricsCounter::<10>::new(0, last_update);
931 rcb.credentials(f);
932 rcb.certificate_check(|cert, host| {
933 match super::known_hosts::certificate_check(
934 gctx,
935 cert,
936 host,
937 port,
938 config_known_hosts,
939 &diagnostic_home_config,
940 ) {
941 Ok(status) => Ok(status),
942 Err(e) => {
943 check_cb_result = Err(e);
944 // This is not really used because it'll be overridden by libgit2
945 // See https://github.com/libgit2/libgit2/commit/9a9f220119d9647a352867b24b0556195cb26548
946 Err(git2::Error::from_str(
947 "invalid or unknown remote ssh hostkey",
948 ))
949 }
950 }
951 });
952 rcb.transfer_progress(|stats| {
953 let indexed_deltas = stats.indexed_deltas();
954 let msg = if indexed_deltas > 0 {
955 // Resolving deltas.
956 format!(
957 ", ({}/{}) resolving deltas",
958 indexed_deltas,
959 stats.total_deltas()
960 )
961 } else {
962 // Receiving objects.
963 //
964 // # Caveat
965 //
966 // Progress bar relies on git2 calling `transfer_progress`
967 // to update its transfer rate, but we cannot guarantee a
968 // periodic call of that callback. Thus if we don't receive
969 // any data for, say, 10 seconds, the rate will get stuck
970 // and never go down to 0B/s.
971 // In the future, we need to find away to update the rate
972 // even when the callback is not called.
973 let now = Instant::now();
974 // Scrape a `received_bytes` to the counter every 300ms.
975 if now - last_update > Duration::from_millis(300) {
976 counter.add(stats.received_bytes(), now);
977 last_update = now;
978 }
979 let rate = HumanBytes(counter.rate() as u64);
980 format!(", {rate:.2}/s")
981 };
982 progress
983 .tick(stats.indexed_objects(), stats.total_objects(), &msg)
984 .is_ok()
985 });
986
987 // Create a local anonymous remote in the repository to fetch the
988 // url
989 let mut opts = git2::FetchOptions::new();
990 opts.remote_callbacks(rcb);
991 cb(opts)
992 });
993 if auth_result.is_err() {
994 check_cb_result?;
995 }
996 auth_result?;
997 Ok(())
998 })
999}
1000
1001/// Attempts to fetch the given git `reference` for a Git repository.
1002///
1003/// This is the main entry for git clone/fetch. It does the followings:
1004///
1005/// * Turns [`GitReference`] into refspecs accordingly.
1006/// * Dispatches `git fetch` using libgit2, gitoxide, or git CLI.
1007///
1008/// The `remote_url` argument is the git remote URL where we want to fetch from.
1009///
1010/// The `remote_kind` argument is a thing for [`-Zgitoxide`] shallow clones
1011/// at this time. It could be extended when libgit2 supports shallow clones.
1012///
1013/// [`-Zgitoxide`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#gitoxide
1014#[tracing::instrument(skip_all)]
1015pub fn fetch(
1016 repo: &mut git2::Repository,
1017 remote_url: &str,
1018 manifest_reference: &GitReference,
1019 locked_reference: &GitReference,
1020 gctx: &GlobalContext,
1021 remote_kind: RemoteKind,
1022) -> CargoResult<()> {
1023 if let Some(offline_flag) = gctx.offline_flag() {
1024 anyhow::bail!(
1025 "attempting to update a git repository, but {offline_flag} \
1026 was specified"
1027 )
1028 }
1029
1030 let shallow = remote_kind.to_shallow_setting(repo.is_shallow(), gctx);
1031
1032 // Flag to keep track if the rev is a full commit hash
1033 let mut fast_path_rev: bool = false;
1034
1035 let oid_to_fetch = match github_fast_path(repo, remote_url, locked_reference, gctx) {
1036 Ok(FastPathRev::UpToDate) => return Ok(()),
1037 Ok(FastPathRev::NeedsFetch(rev)) => Some(rev),
1038 Ok(FastPathRev::Indeterminate) => None,
1039 Err(e) => {
1040 debug!("failed to check github {:?}", e);
1041 None
1042 }
1043 };
1044
1045 maybe_gc_repo(repo, gctx)?;
1046
1047 clean_repo_temp_files(repo);
1048
1049 // Translate the reference desired here into an actual list of refspecs
1050 // which need to get fetched. Additionally record if we're fetching tags.
1051 let mut refspecs = Vec::new();
1052 let mut tags = false;
1053 // The `+` symbol on the refspec means to allow a forced (fast-forward)
1054 // update which is needed if there is ever a force push that requires a
1055 // fast-forward.
1056 match locked_reference {
1057 // For branches and tags we can fetch simply one reference and copy it
1058 // locally, no need to fetch other branches/tags.
1059 GitReference::Branch(b) => {
1060 refspecs.push(format!("+refs/heads/{0}:refs/remotes/origin/{0}", b));
1061 }
1062
1063 GitReference::Tag(t) => {
1064 refspecs.push(format!("+refs/tags/{0}:refs/remotes/origin/tags/{0}", t));
1065 }
1066
1067 GitReference::DefaultBranch => {
1068 refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
1069 }
1070
1071 GitReference::Rev(rev) => {
1072 if rev.starts_with("refs/") {
1073 refspecs.push(format!("+{0}:{0}", rev));
1074 } else if let Some(oid_to_fetch) = oid_to_fetch {
1075 fast_path_rev = true;
1076 refspecs.push(format!("+{0}:refs/commit/{0}", oid_to_fetch));
1077 } else if !matches!(shallow, gix::remote::fetch::Shallow::NoChange)
1078 && rev_to_oid(rev).is_some()
1079 {
1080 // There is a specific commit to fetch and we will do so in shallow-mode only
1081 // to not disturb the previous logic.
1082 // Note that with typical settings for shallowing, we will just fetch a single `rev`
1083 // as single commit.
1084 // The reason we write to `refs/remotes/origin/HEAD` is that it's of special significance
1085 // when during `GitReference::resolve()`, but otherwise it shouldn't matter.
1086 refspecs.push(format!("+{0}:refs/remotes/origin/HEAD", rev));
1087 } else if let GitReference::Rev(rev) = manifest_reference
1088 && rev.starts_with("refs/")
1089 {
1090 // If the lockfile has a commit. we can't directly fetch it (unless we're talking
1091 // to GitHub), so we fetch the ref associated with it from the manifest.
1092 refspecs.push(format!("+{0}:{0}", rev));
1093 } else {
1094 // We don't know what the rev will point to. To handle this
1095 // situation we fetch all branches and tags, and then we pray
1096 // it's somewhere in there.
1097 refspecs.push(String::from("+refs/heads/*:refs/remotes/origin/*"));
1098 refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
1099 tags = true;
1100 }
1101 }
1102 }
1103
1104 debug!("doing a fetch for {remote_url}");
1105 let result = if let Some(true) = gctx.net_config()?.git_fetch_with_cli {
1106 fetch_with_cli(repo, remote_url, &refspecs, tags, shallow, gctx)
1107 } else if gctx.cli_unstable().gitoxide.map_or(false, |git| git.fetch) {
1108 fetch_with_gitoxide(repo, remote_url, refspecs, tags, shallow, gctx)
1109 } else {
1110 fetch_with_libgit2(repo, remote_url, refspecs, tags, shallow, gctx)
1111 };
1112
1113 if fast_path_rev {
1114 if let Some(oid) = oid_to_fetch {
1115 return result.with_context(|| format!("revision {} not found", oid));
1116 }
1117 }
1118 result
1119}
1120
1121/// `gitoxide` uses shallow locks to assure consistency when fetching to and to avoid races, and to write
1122/// files atomically.
1123/// Cargo has its own lock files and doesn't need that mechanism for race protection, so a stray lock means
1124/// a signal interrupted a previous shallow fetch and doesn't mean a race is happening.
1125fn has_shallow_lock_file(err: &crate::sources::git::fetch::Error) -> bool {
1126 matches!(
1127 err,
1128 gix::env::collate::fetch::Error::Fetch(gix::remote::fetch::Error::Fetch(
1129 gix::protocol::fetch::Error::LockShallowFile(_)
1130 ))
1131 )
1132}
1133
1134/// Attempts to use `git` CLI installed on the system to fetch a repository,
1135/// when the config value [`net.git-fetch-with-cli`][1] is set.
1136///
1137/// Unfortunately `libgit2` is notably lacking in the realm of authentication
1138/// when compared to the `git` command line. As a result, allow an escape
1139/// hatch for users that would prefer to use `git`-the-CLI for fetching
1140/// repositories instead of `libgit2`-the-library. This should make more
1141/// flavors of authentication possible while also still giving us all the
1142/// speed and portability of using `libgit2`.
1143///
1144/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/config.html#netgit-fetch-with-cli
1145#[tracing::instrument(skip(repo, gctx))]
1146fn fetch_with_cli(
1147 repo: &git2::Repository,
1148 url: &str,
1149 refspecs: &[String],
1150 tags: bool,
1151 shallow: gix::remote::fetch::Shallow,
1152 gctx: &GlobalContext,
1153) -> CargoResult<()> {
1154 debug!(target: "git-fetch", backend = "git-cli");
1155
1156 let mut cmd = ProcessBuilder::new("git");
1157 // Avoid potential for unused work that may also hang (#15775)
1158 cmd.arg("-c").arg("core.fsmonitor=false");
1159
1160 cmd.arg("fetch");
1161 if tags {
1162 cmd.arg("--tags");
1163 } else {
1164 cmd.arg("--no-tags");
1165 }
1166 if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1167 let depth = 0i32.saturating_add_unsigned(depth.get());
1168 cmd.arg(format!("--depth={depth}"));
1169 }
1170
1171 let progress_config = gctx.progress_config();
1172 let progress = match progress_config.when {
1173 ProgressWhen::Always => true,
1174 ProgressWhen::Never => false,
1175 ProgressWhen::Auto => {
1176 // Recreate the same conditions used with `Progress`
1177 let width = progress_config
1178 .width
1179 .or_else(|| gctx.shell().err_width().progress_max_width());
1180 gctx.shell().progress_supported() && width.is_some()
1181 }
1182 };
1183 if gctx.shell().verbosity() == Verbosity::Verbose {
1184 cmd.arg("--verbose");
1185 } else if !progress {
1186 cmd.arg("--quiet");
1187 }
1188
1189 cmd.arg("--force") // handle force pushes
1190 .arg("--update-head-ok") // see discussion in #2078
1191 .arg(url)
1192 .args(refspecs)
1193 // If cargo is run by git (for example, the `exec` command in `git
1194 // rebase`), the GIT_DIR is set by git and will point to the wrong
1195 // location. This makes sure GIT_DIR is always the repository path.
1196 .env("GIT_DIR", repo.path())
1197 // The reset of these may not be necessary, but I'm including them
1198 // just to be extra paranoid and avoid any issues.
1199 .env_remove("GIT_WORK_TREE")
1200 .env_remove("GIT_INDEX_FILE")
1201 .env_remove("GIT_OBJECT_DIRECTORY")
1202 .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
1203 .cwd(repo.path());
1204 gctx.shell()
1205 .verbose(|s| s.status("Running", &cmd.to_string()))?;
1206 network::retry::with_retry(gctx, || {
1207 cmd.exec().map_err(|error| {
1208 GitCliError::new(error)
1209 .spurious(true)
1210 .workaround(
1211 "help: re-try with `net.git-fetch-with-cli = false` to see if it resolves the problem
1212https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
1213 )
1214 .into()
1215 })
1216 })?;
1217
1218 Ok(())
1219}
1220
1221#[tracing::instrument(skip(repo, gctx))]
1222fn fetch_with_gitoxide(
1223 repo: &mut git2::Repository,
1224 remote_url: &str,
1225 refspecs: Vec<String>,
1226 tags: bool,
1227 shallow: gix::remote::fetch::Shallow,
1228 gctx: &GlobalContext,
1229) -> CargoResult<()> {
1230 debug!(target: "git-fetch", backend = "gitoxide");
1231
1232 let git2_repo = repo;
1233 let config_overrides = cargo_config_to_gitoxide_overrides(gctx)?;
1234 let repo_reinitialized = AtomicBool::default();
1235 let res = oxide::with_retry_and_progress(
1236 git2_repo.path(),
1237 gctx,
1238 remote_url,
1239 &|repo_path,
1240 should_interrupt,
1241 mut progress,
1242 url_for_authentication: &mut dyn FnMut(&gix::bstr::BStr)| {
1243 // The `fetch` operation here may fail spuriously due to a corrupt
1244 // repository. It could also fail, however, for a whole slew of other
1245 // reasons (aka network related reasons). We want Cargo to automatically
1246 // recover from corrupt repositories, but we don't want Cargo to stomp
1247 // over other legitimate errors.
1248 //
1249 // Consequently we save off the error of the `fetch` operation and if it
1250 // looks like a "corrupt repo" error then we blow away the repo and try
1251 // again. If it looks like any other kind of error, or if we've already
1252 // blown away the repository, then we want to return the error as-is.
1253 loop {
1254 let res = oxide::open_repo(
1255 repo_path,
1256 config_overrides.clone(),
1257 oxide::OpenMode::ForFetch,
1258 )
1259 .map_err(crate::sources::git::fetch::Error::from)
1260 .and_then(|repo| {
1261 debug!("initiating fetch of {refspecs:?} from {remote_url}");
1262 let url_for_authentication = &mut *url_for_authentication;
1263 let remote = repo
1264 .remote_at(remote_url)?
1265 .with_fetch_tags(if tags {
1266 gix::remote::fetch::Tags::All
1267 } else {
1268 gix::remote::fetch::Tags::Included
1269 })
1270 .with_refspecs(
1271 refspecs.iter().map(|s| s.as_str()),
1272 gix::remote::Direction::Fetch,
1273 )
1274 .map_err(crate::sources::git::fetch::Error::Other)?;
1275 let url = remote
1276 .url(gix::remote::Direction::Fetch)
1277 .expect("set at init")
1278 .to_owned();
1279 let connection = remote.connect(gix::remote::Direction::Fetch)?;
1280 let mut authenticate = connection.configured_credentials(url)?;
1281 let connection = connection.with_credentials(
1282 move |action: gix::protocol::credentials::helper::Action| {
1283 if let Some(url) = action
1284 .context()
1285 .and_then(|gctx| gctx.url.as_ref().filter(|url| *url != remote_url))
1286 {
1287 url_for_authentication(url.as_ref());
1288 }
1289 authenticate(action)
1290 },
1291 );
1292 let outcome = connection
1293 .prepare_fetch(&mut progress, gix::remote::ref_map::Options::default())?
1294 .with_shallow(shallow.clone())
1295 .receive(&mut progress, should_interrupt)?;
1296 Ok(outcome)
1297 });
1298 let err = match res {
1299 Ok(_) => break,
1300 Err(e) => e,
1301 };
1302 debug!("fetch failed: {}", err);
1303
1304 if !repo_reinitialized.load(Ordering::Relaxed)
1305 // We check for errors that could occur if the configuration, refs or odb files are corrupted.
1306 // We don't check for errors related to writing as `gitoxide` is expected to create missing leading
1307 // folder before writing files into it, or else not even open a directory as git repository (which is
1308 // also handled here).
1309 && err.is_corrupted()
1310 || has_shallow_lock_file(&err)
1311 {
1312 repo_reinitialized.store(true, Ordering::Relaxed);
1313 debug!(
1314 "looks like this is a corrupt repository, reinitializing \
1315 and trying again"
1316 );
1317 if oxide::reinitialize(repo_path).is_ok() {
1318 continue;
1319 }
1320 }
1321
1322 return Err(err.into());
1323 }
1324 Ok(())
1325 },
1326 );
1327 if repo_reinitialized.load(Ordering::Relaxed) {
1328 *git2_repo = git2::Repository::open(git2_repo.path())?;
1329 }
1330 res
1331}
1332
1333#[tracing::instrument(skip(repo, gctx))]
1334fn fetch_with_libgit2(
1335 repo: &mut git2::Repository,
1336 remote_url: &str,
1337 refspecs: Vec<String>,
1338 tags: bool,
1339 shallow: gix::remote::fetch::Shallow,
1340 gctx: &GlobalContext,
1341) -> CargoResult<()> {
1342 debug!(target: "git-fetch", backend = "libgit2");
1343
1344 static INIT: Once = Once::new();
1345 INIT.call_once(|| {
1346 // SAFETY:
1347 //
1348 // The unsafety of the registration function derives from two aspects:
1349 //
1350 // 1. This call must be synchronized with all other registration calls as
1351 // well as construction of new transports.
1352 // 2. The argument is leaked.
1353 //
1354 // We're clear on point (1) because this is the only `register` call we make in the
1355 // cargo-binary and cargo-library is not officially supported and it is under a lock.
1356 // Technically, `git2_curl` also has a lock but that isn't part of their API guarantees.
1357 //
1358 // We're mostly clear on point (2) because we'd only free it after everything is done anyway
1359 unsafe { init_git_transports(gctx) };
1360 });
1361
1362 let git_config = git2::Config::open_default()?;
1363 with_fetch_options(&git_config, remote_url, gctx, &mut |mut opts| {
1364 if tags {
1365 opts.download_tags(git2::AutotagOption::All);
1366 }
1367 if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1368 opts.depth(0i32.saturating_add_unsigned(depth.get()));
1369 }
1370 // The `fetch` operation here may fail spuriously due to a corrupt
1371 // repository. It could also fail, however, for a whole slew of other
1372 // reasons (aka network related reasons). We want Cargo to automatically
1373 // recover from corrupt repositories, but we don't want Cargo to stomp
1374 // over other legitimate errors.
1375 //
1376 // Consequently we save off the error of the `fetch` operation and if it
1377 // looks like a "corrupt repo" error then we blow away the repo and try
1378 // again. If it looks like any other kind of error, or if we've already
1379 // blown away the repository, then we want to return the error as-is.
1380 let mut repo_reinitialized = false;
1381 loop {
1382 debug!("initiating fetch of {refspecs:?} from {remote_url}");
1383 let res = repo
1384 .remote_anonymous(remote_url)?
1385 .fetch(&refspecs, Some(&mut opts), None);
1386 let err = match res {
1387 Ok(()) => break,
1388 Err(e) => e,
1389 };
1390 debug!("fetch failed: {}", err);
1391
1392 if !repo_reinitialized && matches!(err.class(), ErrorClass::Reference | ErrorClass::Odb)
1393 {
1394 repo_reinitialized = true;
1395 debug!(
1396 "looks like this is a corrupt repository, reinitializing \
1397 and trying again"
1398 );
1399 if reinitialize(repo).is_ok() {
1400 continue;
1401 }
1402 }
1403
1404 return Err(err.into());
1405 }
1406 Ok(())
1407 })
1408}
1409
1410/// Configure libgit2 to use libcurl if necessary.
1411///
1412/// If the user has a non-default network configuration, then libgit2 will be
1413/// configured to use libcurl instead of the built-in networking support so
1414/// that those configuration settings can be used.
1415///
1416/// # Safety
1417///
1418/// See [git2_curl::register]
1419#[tracing::instrument(skip_all)]
1420unsafe fn init_git_transports(gctx: &GlobalContext) {
1421 match needs_custom_http_transport(gctx) {
1422 Ok(true) => {}
1423 _ => return,
1424 }
1425
1426 let handle = match http_handle(gctx) {
1427 Ok(handle) => handle,
1428 Err(..) => return,
1429 };
1430
1431 unsafe {
1432 git2_curl::register(handle);
1433 }
1434}
1435
1436/// Attempts to `git gc` a repository.
1437///
1438/// Cargo has a bunch of long-lived git repositories in its global cache and
1439/// some, like the index, are updated very frequently. Right now each update
1440/// creates a new "pack file" inside the git database, and over time this can
1441/// cause bad performance and bad current behavior in libgit2.
1442///
1443/// One pathological use case today is where libgit2 opens hundreds of file
1444/// descriptors, getting us dangerously close to blowing out the OS limits of
1445/// how many fds we can have open. This is detailed in [#4403].
1446///
1447/// Instead of trying to be clever about when gc is needed, we just run
1448/// `git gc --auto` and let git figure it out. It checks its own thresholds
1449/// (gc.auto, gc.autoPackLimit) and either does the work or exits quickly.
1450/// If git isn't installed, no worries - we skip it.
1451///
1452/// [#4403]: https://github.com/rust-lang/cargo/issues/4403
1453fn maybe_gc_repo(repo: &mut git2::Repository, gctx: &GlobalContext) -> CargoResult<()> {
1454 // Let git decide whether gc is actually needed based on its own thresholds
1455 // (gc.auto, gc.autoPackLimit). This avoids duplicating git's internal logic
1456 // for deciding when housekeeping is needed.
1457 //
1458 // For testing purposes, __CARGO_PACKFILE_LIMIT can be set to override
1459 // gc.autoPackLimit, which has the same meaning. This lets tests force gc
1460 // to run by setting a low threshold without depending on git's defaults.
1461 let mut cmd = Command::new("git");
1462 if let Ok(limit) = gctx.get_env("__CARGO_PACKFILE_LIMIT") {
1463 cmd.arg(format!("-c gc.autoPackLimit={}", limit));
1464 }
1465 cmd.arg("gc")
1466 .arg("--auto")
1467 // Explicitly set `GIT_DIR` so `safe.bareRepository=explicit` doesn't reject it.
1468 .env("GIT_DIR", repo.path())
1469 .current_dir(repo.path());
1470
1471 match cmd.output() {
1472 Ok(out) => {
1473 debug!(
1474 "git-gc --auto status: {}\n\nstdout ---\n{}\nstderr ---\n{}",
1475 out.status,
1476 String::from_utf8_lossy(&out.stdout),
1477 String::from_utf8_lossy(&out.stderr)
1478 );
1479 if out.status.success() {
1480 let new = git2::Repository::open(repo.path())?;
1481 *repo = new;
1482 return Ok(());
1483 }
1484 }
1485 Err(e) => debug!("git-gc --auto failed to spawn: {}", e),
1486 }
1487
1488 // Alright all else failed, let's start over.
1489 reinitialize(repo)
1490}
1491
1492/// Removes temporary files left from previous activity.
1493///
1494/// If libgit2 is interrupted while indexing pack files, it will leave behind
1495/// some temporary files that it doesn't clean up. These can be quite large in
1496/// size, so this tries to clean things up.
1497///
1498/// This intentionally ignores errors. This is only an opportunistic cleaning,
1499/// and we don't really care if there are issues (there's unlikely anything
1500/// that can be done).
1501///
1502/// The git CLI has similar behavior (its temp files look like
1503/// `objects/pack/tmp_pack_9kUSA8`). Those files are normally deleted via `git
1504/// prune` which is run by `git gc`. However, it doesn't know about libgit2's
1505/// filenames, so they never get cleaned up.
1506fn clean_repo_temp_files(repo: &git2::Repository) {
1507 let path = repo.path().join("objects/pack/pack_git2_*");
1508 let Some(pattern) = path.to_str() else {
1509 tracing::warn!("cannot convert {path:?} to a string");
1510 return;
1511 };
1512 let Ok(paths) = glob::glob(pattern) else {
1513 return;
1514 };
1515 for path in paths {
1516 if let Ok(path) = path {
1517 match paths::remove_file(&path) {
1518 Ok(_) => tracing::debug!("removed stale temp git file {path:?}"),
1519 Err(e) => {
1520 tracing::warn!("failed to remove {path:?} while cleaning temp files: {e}")
1521 }
1522 }
1523 }
1524 }
1525}
1526
1527/// Reinitializes a given Git repository. This is useful when a Git repository
1528/// seems corrupted and we want to start over.
1529fn reinitialize(repo: &mut git2::Repository) -> CargoResult<()> {
1530 // Here we want to drop the current repository object pointed to by `repo`,
1531 // so we initialize temporary repository in a sub-folder, blow away the
1532 // existing git folder, and then recreate the git repo. Finally we blow away
1533 // the `tmp` folder we allocated.
1534 let path = repo.path().to_path_buf();
1535 debug!("reinitializing git repo at {:?}", path);
1536 let tmp = path.join("tmp");
1537 let bare = !repo.path().ends_with(".git");
1538 *repo = init(&tmp, false)?;
1539 for entry in path.read_dir()? {
1540 let entry = entry?;
1541 if entry.file_name().to_str() == Some("tmp") {
1542 continue;
1543 }
1544 let path = entry.path();
1545 drop(paths::remove_file(&path).or_else(|_| paths::remove_dir_all(&path)));
1546 }
1547 *repo = init(&path, bare)?;
1548 paths::remove_dir_all(&tmp)?;
1549 Ok(())
1550}
1551
1552/// Initializes a Git repository at `path`.
1553fn init(path: &Path, bare: bool) -> CargoResult<git2::Repository> {
1554 let mut opts = git2::RepositoryInitOptions::new();
1555 // Skip anything related to templates, they just call all sorts of issues as
1556 // we really don't want to use them yet they insist on being used. See #6240
1557 // for an example issue that comes up.
1558 opts.external_template(false);
1559 opts.bare(bare);
1560 Ok(git2::Repository::init_opts(&path, &opts)?)
1561}
1562
1563/// The result of GitHub fast path check. See [`github_fast_path`] for more.
1564enum FastPathRev {
1565 /// The local rev (determined by `reference.resolve(repo)`) is already up to
1566 /// date with what this rev resolves to on GitHub's server.
1567 UpToDate,
1568 /// The following SHA must be fetched in order for the local rev to become
1569 /// up to date.
1570 NeedsFetch(Oid),
1571 /// Don't know whether local rev is up to date. We'll fetch _all_ branches
1572 /// and tags from the server and see what happens.
1573 Indeterminate,
1574}
1575
1576/// Attempts GitHub's special fast path for testing if we've already got an
1577/// up-to-date copy of the repository.
1578///
1579/// Updating the index is done pretty regularly so we want it to be as fast as
1580/// possible. For registries hosted on GitHub (like the crates.io index) there's
1581/// a fast path available to use[^1] to tell us that there's no updates to be
1582/// made.
1583///
1584/// Note that this function should never cause an actual failure because it's
1585/// just a fast path. As a result, a caller should ignore `Err` returned from
1586/// this function and move forward on the normal path.
1587///
1588/// [^1]: <https://developer.github.com/v3/repos/commits/#get-the-sha-1-of-a-commit-reference>
1589#[tracing::instrument(skip(repo, gctx))]
1590fn github_fast_path(
1591 repo: &git2::Repository,
1592 url: &str,
1593 reference: &GitReference,
1594 gctx: &GlobalContext,
1595) -> CargoResult<FastPathRev> {
1596 let url = Url::parse(url)?;
1597 if !is_github(&url) {
1598 return Ok(FastPathRev::Indeterminate);
1599 }
1600
1601 let local_object = resolve_ref(reference, repo).ok();
1602
1603 let github_branch_name = match reference {
1604 GitReference::Branch(branch) => branch,
1605 GitReference::Tag(tag) => tag,
1606 GitReference::DefaultBranch => "HEAD",
1607 GitReference::Rev(rev) => {
1608 if rev.starts_with("refs/") {
1609 rev
1610 } else if looks_like_commit_hash(rev) {
1611 // `revparse_single` (used by `resolve`) is the only way to turn
1612 // short hash -> long hash, but it also parses other things,
1613 // like branch and tag names, which might coincidentally be
1614 // valid hex.
1615 //
1616 // We only return early if `rev` is a prefix of the object found
1617 // by `revparse_single`. Don't bother talking to GitHub in that
1618 // case, since commit hashes are permanent. If a commit with the
1619 // requested hash is already present in the local clone, its
1620 // contents must be the same as what is on the server for that
1621 // hash.
1622 //
1623 // If `rev` is not found locally by `revparse_single`, we'll
1624 // need GitHub to resolve it and get a hash. If `rev` is found
1625 // but is not a short hash of the found object, it's probably a
1626 // branch and we also need to get a hash from GitHub, in case
1627 // the branch has moved.
1628 if let Some(local_object) = local_object {
1629 if is_short_hash_of(rev, local_object) {
1630 debug!("github fast path already has {local_object}");
1631 return Ok(FastPathRev::UpToDate);
1632 }
1633 }
1634 // If `rev` is a full commit hash, the only thing it can resolve
1635 // to is itself. Don't bother talking to GitHub in that case
1636 // either. (This ensures that we always attempt to fetch the
1637 // commit directly even if we can't reach the GitHub API.)
1638 if let Some(oid) = rev_to_oid(rev) {
1639 debug!("github fast path is already a full commit hash {rev}");
1640 return Ok(FastPathRev::NeedsFetch(oid));
1641 }
1642 rev
1643 } else {
1644 debug!("can't use github fast path with `rev = \"{}\"`", rev);
1645 return Ok(FastPathRev::Indeterminate);
1646 }
1647 }
1648 };
1649
1650 // This expects GitHub urls in the form `github.com/user/repo` and nothing
1651 // else
1652 let mut pieces = url
1653 .path_segments()
1654 .ok_or_else(|| anyhow!("no path segments on url"))?;
1655 let username = pieces
1656 .next()
1657 .ok_or_else(|| anyhow!("couldn't find username"))?;
1658 let repository = pieces
1659 .next()
1660 .ok_or_else(|| anyhow!("couldn't find repository name"))?;
1661 if pieces.next().is_some() {
1662 anyhow::bail!("too many segments on URL");
1663 }
1664
1665 // Trim off the `.git` from the repository, if present, since that's
1666 // optional for GitHub and won't work when we try to use the API as well.
1667 let repository = repository.strip_suffix(".git").unwrap_or(repository);
1668
1669 let url = format!(
1670 "https://api.github.com/repos/{}/{}/commits/{}",
1671 username, repository, github_branch_name,
1672 );
1673 debug!("attempting GitHub fast path for {}", url);
1674 let mut request =
1675 Request::get(url).header(http::header::ACCEPT, "application/vnd.github.3.sha");
1676 if let Some(local_object) = local_object {
1677 request = request.header(http::header::IF_NONE_MATCH, &format!("\"{local_object}\""));
1678 }
1679 let response = gctx
1680 .http_async()?
1681 .request_blocking(request.body(Vec::new())?)?;
1682 let response_code = response.status();
1683 if response_code == StatusCode::NOT_MODIFIED {
1684 debug!("github fast path up-to-date");
1685 Ok(FastPathRev::UpToDate)
1686 } else if response_code == StatusCode::OK
1687 && let Some(oid_to_fetch) = rev_to_oid(str::from_utf8(&response.body())?)
1688 {
1689 // response expected to be a full hash hexstring (40 or 64 chars)
1690 debug!("github fast path fetch {oid_to_fetch}");
1691 Ok(FastPathRev::NeedsFetch(oid_to_fetch))
1692 } else {
1693 // Usually response_code == 404 if the repository does not exist, and
1694 // response_code == 422 if exists but GitHub is unable to resolve the
1695 // requested rev.
1696 debug!("github fast path bad response code {response_code}");
1697 Ok(FastPathRev::Indeterminate)
1698 }
1699}
1700
1701/// Whether a `url` is one from GitHub.
1702fn is_github(url: &Url) -> bool {
1703 url.host_str() == Some("github.com")
1704}
1705
1706// Give some messages on GitHub PR URL given as is
1707pub(crate) fn note_github_pull_request(url: &str) -> Option<String> {
1708 if let Ok(url) = url.parse::<Url>()
1709 && is_github(&url)
1710 {
1711 let path_segments = url
1712 .path_segments()
1713 .map(|p| p.into_iter().collect::<Vec<_>>())
1714 .unwrap_or_default();
1715 if let [owner, repo, "pull", pr_number, ..] = path_segments[..] {
1716 let repo_url = format!("https://github.com/{owner}/{repo}.git");
1717 let rev = format!("refs/pull/{pr_number}/head");
1718 return Some(format!(
1719 concat!(
1720 "\n\nnote: GitHub url {} is not a repository. \n",
1721 "help: Replace the dependency with \n",
1722 " `git = \"{}\" rev = \"{}\"` \n",
1723 " to specify pull requests as dependencies' revision."
1724 ),
1725 url, repo_url, rev
1726 ));
1727 }
1728 }
1729
1730 None
1731}
1732
1733/// Whether a `rev` looks like a commit hash (ASCII hex digits).
1734fn looks_like_commit_hash(rev: &str) -> bool {
1735 rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit())
1736}
1737
1738/// Whether `rev` is a shorter hash of `oid`.
1739fn is_short_hash_of(rev: &str, oid: Oid) -> bool {
1740 let long_hash = oid.to_string();
1741 match long_hash.get(..rev.len()) {
1742 Some(truncated_long_hash) => truncated_long_hash.eq_ignore_ascii_case(rev),
1743 None => false,
1744 }
1745}
1746
1747#[cfg(test)]
1748mod tests {
1749 use super::*;
1750
1751 #[test]
1752 fn github_fast_path_full_hash_returns_needs_fetch() {
1753 let temp_dir = tempfile::TempDir::new().unwrap();
1754 let repo = git2::Repository::init_bare(temp_dir.path()).unwrap();
1755 let full_hash = "c9040898c9183ddbb9402dcbf749ed06d6ea90ad";
1756 let reference = GitReference::Rev(full_hash.to_string());
1757 let gctx = GlobalContext::default().unwrap();
1758 let expected_oid = rev_to_oid(full_hash).unwrap();
1759
1760 let result =
1761 github_fast_path(&repo, "https://github.com/user/repo", &reference, &gctx).unwrap();
1762
1763 assert!(matches!(result, FastPathRev::NeedsFetch(oid) if oid == expected_oid));
1764 }
1765
1766 #[test]
1767 fn test_absolute_submodule_url() {
1768 let cases = [
1769 (
1770 "ssh://git@gitub.com/rust-lang/cargo",
1771 "git@github.com:rust-lang/cargo.git",
1772 "git@github.com:rust-lang/cargo.git",
1773 ),
1774 (
1775 "ssh://git@gitub.com/rust-lang/cargo",
1776 "./",
1777 "ssh://git@gitub.com/rust-lang/cargo/",
1778 ),
1779 (
1780 "ssh://git@gitub.com/rust-lang/cargo",
1781 "../",
1782 "ssh://git@gitub.com/rust-lang/",
1783 ),
1784 (
1785 "ssh://git@gitub.com/rust-lang/cargo",
1786 "./foo",
1787 "ssh://git@gitub.com/rust-lang/cargo/foo",
1788 ),
1789 (
1790 "ssh://git@gitub.com/rust-lang/cargo/",
1791 "./foo",
1792 "ssh://git@gitub.com/rust-lang/cargo/foo",
1793 ),
1794 (
1795 "ssh://git@gitub.com/rust-lang/cargo/",
1796 "../foo",
1797 "ssh://git@gitub.com/rust-lang/foo",
1798 ),
1799 (
1800 "ssh://git@gitub.com/rust-lang/cargo",
1801 "../foo",
1802 "ssh://git@gitub.com/rust-lang/foo",
1803 ),
1804 (
1805 "ssh://git@gitub.com/rust-lang/cargo",
1806 "../foo/bar/../baz",
1807 "ssh://git@gitub.com/rust-lang/foo/baz",
1808 ),
1809 (
1810 "git@github.com:rust-lang/cargo.git",
1811 "ssh://git@gitub.com/rust-lang/cargo",
1812 "ssh://git@gitub.com/rust-lang/cargo",
1813 ),
1814 (
1815 "git@github.com:rust-lang/cargo.git",
1816 "./",
1817 "git@github.com:rust-lang/cargo.git/./",
1818 ),
1819 (
1820 "git@github.com:rust-lang/cargo.git",
1821 "../",
1822 "git@github.com:rust-lang/cargo.git/../",
1823 ),
1824 (
1825 "git@github.com:rust-lang/cargo.git",
1826 "./foo",
1827 "git@github.com:rust-lang/cargo.git/./foo",
1828 ),
1829 (
1830 "git@github.com:rust-lang/cargo.git/",
1831 "./foo",
1832 "git@github.com:rust-lang/cargo.git/./foo",
1833 ),
1834 (
1835 "git@github.com:rust-lang/cargo.git",
1836 "../foo",
1837 "git@github.com:rust-lang/cargo.git/../foo",
1838 ),
1839 (
1840 "git@github.com:rust-lang/cargo.git/",
1841 "../foo",
1842 "git@github.com:rust-lang/cargo.git/../foo",
1843 ),
1844 (
1845 "git@github.com:rust-lang/cargo.git",
1846 "../foo/bar/../baz",
1847 "git@github.com:rust-lang/cargo.git/../foo/bar/../baz",
1848 ),
1849 ];
1850
1851 for (base_url, submodule_url, expected) in cases {
1852 let url = absolute_submodule_url(base_url, submodule_url).unwrap();
1853 assert_eq!(
1854 expected, url,
1855 "base `{base_url}`; submodule `{submodule_url}`"
1856 );
1857 }
1858 }
1859}
1860
1861/// Turns a full commit hash revision into an oid.
1862///
1863/// Git object ID is supposed to be a hex string of 20 (SHA1) or 32 (SHA256) bytes.
1864/// Its length must be double to the underlying bytes (40 or 64),
1865/// otherwise libgit2 would happily zero-pad the returned oid.
1866///
1867/// See:
1868///
1869/// * <https://github.com/rust-lang/cargo/issues/13188>
1870/// * <https://github.com/rust-lang/cargo/issues/13968>
1871pub(super) fn rev_to_oid(rev: &str) -> Option<Oid> {
1872 Oid::from_str(rev)
1873 .ok()
1874 .filter(|oid| oid.as_bytes().len() * 2 == rev.len())
1875}