Skip to main content

cargo/sources/git/
utils.rs

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