Skip to main content

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, Stdio, 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 for cache/SourceId identity, not fetching.
607fn scp_to_ssh(url: &str) -> Option<String> {
608    let 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        Some(ssh_url_for_source_id(gix_url))
611    } else {
612        None
613    }
614}
615
616/// Serializes an SSH URL as a WHATWG URL for [`SourceId`] and cache identity.
617///
618/// gix preserves relative SCP paths because `host:repo` and `ssh://host/repo`
619/// fetch different repositories. SourceId cannot represent SCP syntax, so make
620/// the path absolute here and retain the original URL for fetching. Tilde paths
621/// already have a lossless `ssh://` form and need no adjustment.
622pub(super) fn ssh_url_for_source_id(mut url: gix::Url) -> String {
623    url.serialize_alternative_form = false;
624    if !url.path.starts_with(b"/") && !url.path.starts_with(b"~") {
625        url.path.insert(0, b'/');
626    }
627    url.to_bstring().to_string()
628}
629
630/// Prepare the authentication callbacks for cloning a git repository.
631///
632/// The main purpose of this function is to construct the "authentication
633/// callback" which is used to clone a repository. This callback will attempt to
634/// find the right authentication on the system (without user input) and will
635/// guide libgit2 in doing so.
636///
637/// The callback is provided `allowed` types of credentials, and we try to do as
638/// much as possible based on that:
639///
640/// * Prioritize SSH keys from the local ssh agent as they're likely the most
641///   reliable. The username here is prioritized from the credential
642///   callback, then from whatever is configured in git itself, and finally
643///   we fall back to the generic user of `git`.
644///
645/// * If a username/password is allowed, then we fallback to git2-rs's
646///   implementation of the credential helper. This is what is configured
647///   with `credential.helper` in git, and is the interface for the macOS
648///   keychain, for example.
649///
650/// * After the above two have failed, we just kinda grapple attempting to
651///   return *something*.
652///
653/// If any form of authentication fails, libgit2 will repeatedly ask us for
654/// credentials until we give it a reason to not do so. To ensure we don't
655/// just sit here looping forever we keep track of authentications we've
656/// attempted and we don't try the same ones again.
657fn with_authentication<T, F>(
658    gctx: &GlobalContext,
659    url: &str,
660    cfg: &git2::Config,
661    mut f: F,
662) -> CargoResult<T>
663where
664    F: FnMut(&mut git2::Credentials<'_>) -> CargoResult<T>,
665{
666    let mut cred_helper = git2::CredentialHelper::new(url);
667    cred_helper.config(cfg);
668
669    let mut ssh_username_requested = false;
670    let mut cred_helper_bad = None;
671    let mut ssh_agent_attempts = Vec::new();
672    let mut any_attempts = false;
673    let mut tried_sshkey = false;
674    let mut url_attempt = None;
675
676    let orig_url = url;
677    let mut res = f(&mut |url, username, allowed| {
678        any_attempts = true;
679        if url != orig_url {
680            url_attempt = Some(url.to_string());
681        }
682        // libgit2's "USERNAME" authentication actually means that it's just
683        // asking us for a username to keep going. This is currently only really
684        // used for SSH authentication and isn't really an authentication type.
685        // The logic currently looks like:
686        //
687        //      let user = ...;
688        //      if (user.is_null())
689        //          user = callback(USERNAME, null, ...);
690        //
691        //      callback(SSH_KEY, user, ...)
692        //
693        // So if we're being called here then we know that (a) we're using ssh
694        // authentication and (b) no username was specified in the URL that
695        // we're trying to clone. We need to guess an appropriate username here,
696        // but that may involve a few attempts. Unfortunately we can't switch
697        // usernames during one authentication session with libgit2, so to
698        // handle this we bail out of this authentication session after setting
699        // the flag `ssh_username_requested`, and then we handle this below.
700        if allowed.contains(git2::CredentialType::USERNAME) {
701            debug_assert!(username.is_none());
702            ssh_username_requested = true;
703            return Err(git2::Error::from_str("gonna try usernames later"));
704        }
705
706        // An "SSH_KEY" authentication indicates that we need some sort of SSH
707        // authentication. This can currently either come from the ssh-agent
708        // process or from a raw in-memory SSH key. Cargo only supports using
709        // ssh-agent currently.
710        //
711        // If we get called with this then the only way that should be possible
712        // is if a username is specified in the URL itself (e.g., `username` is
713        // Some), hence the unwrap() here. We try custom usernames down below.
714        if allowed.contains(git2::CredentialType::SSH_KEY) && !tried_sshkey {
715            // If ssh-agent authentication fails, libgit2 will keep
716            // calling this callback asking for other authentication
717            // methods to try. Make sure we only try ssh-agent once,
718            // to avoid looping forever.
719            tried_sshkey = true;
720            let username = username.unwrap();
721            debug_assert!(!ssh_username_requested);
722            ssh_agent_attempts.push(username.to_string());
723            return git2::Cred::ssh_key_from_agent(username);
724        }
725
726        // Sometimes libgit2 will ask for a username/password in plaintext. This
727        // is where Cargo would have an interactive prompt if we supported it,
728        // but we currently don't! Right now the only way we support fetching a
729        // plaintext password is through the `credential.helper` support, so
730        // fetch that here.
731        //
732        // If ssh-agent authentication fails, libgit2 will keep calling this
733        // callback asking for other authentication methods to try. Check
734        // cred_helper_bad to make sure we only try the git credential helper
735        // once, to avoid looping forever.
736        if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) && cred_helper_bad.is_none()
737        {
738            let r = git2::Cred::credential_helper(cfg, url, username);
739            cred_helper_bad = Some(r.is_err());
740            return r;
741        }
742
743        // I'm... not sure what the DEFAULT kind of authentication is, but seems
744        // easy to support?
745        if allowed.contains(git2::CredentialType::DEFAULT) {
746            return git2::Cred::default();
747        }
748
749        // Whelp, we tried our best
750        Err(git2::Error::from_str("no authentication methods succeeded"))
751    });
752
753    // Ok, so if it looks like we're going to be doing ssh authentication, we
754    // want to try a few different usernames as one wasn't specified in the URL
755    // for us to use. In order, we'll try:
756    //
757    // * A credential helper's username for this URL, if available.
758    // * This account's username.
759    // * "git"
760    //
761    // We have to restart the authentication session each time (due to
762    // constraints in libssh2 I guess? maybe this is inherent to ssh?), so we
763    // call our callback, `f`, in a loop here.
764    if ssh_username_requested {
765        debug_assert!(res.is_err());
766        let mut attempts = vec![String::from("git")];
767        if let Ok(s) = gctx.get_env("USER").or_else(|_| gctx.get_env("USERNAME")) {
768            attempts.push(s.to_string());
769        }
770        if let Some(ref s) = cred_helper.username {
771            attempts.push(s.clone());
772        }
773
774        while let Some(s) = attempts.pop() {
775            // We should get `USERNAME` first, where we just return our attempt,
776            // and then after that we should get `SSH_KEY`. If the first attempt
777            // fails we'll get called again, but we don't have another option so
778            // we bail out.
779            let mut attempts = 0;
780            res = f(&mut |_url, username, allowed| {
781                if allowed.contains(git2::CredentialType::USERNAME) {
782                    return git2::Cred::username(&s);
783                }
784                if allowed.contains(git2::CredentialType::SSH_KEY) {
785                    debug_assert_eq!(Some(&s[..]), username);
786                    attempts += 1;
787                    if attempts == 1 {
788                        ssh_agent_attempts.push(s.to_string());
789                        return git2::Cred::ssh_key_from_agent(&s);
790                    }
791                }
792                Err(git2::Error::from_str("no authentication methods succeeded"))
793            });
794
795            // If we made two attempts then that means:
796            //
797            // 1. A username was requested, we returned `s`.
798            // 2. An ssh key was requested, we returned to look up `s` in the
799            //    ssh agent.
800            // 3. For whatever reason that lookup failed, so we were asked again
801            //    for another mode of authentication.
802            //
803            // Essentially, if `attempts == 2` then in theory the only error was
804            // that this username failed to authenticate (e.g., no other network
805            // errors happened). Otherwise something else is funny so we bail
806            // out.
807            if attempts != 2 {
808                break;
809            }
810        }
811    }
812    let mut err = match res {
813        Ok(e) => return Ok(e),
814        Err(e) => e,
815    };
816
817    // In the case of an authentication failure (where we tried something) then
818    // we try to give a more helpful error message about precisely what we
819    // tried.
820    if any_attempts {
821        let mut msg = "failed to authenticate when downloading \
822                       repository"
823            .to_string();
824
825        if let Some(attempt) = &url_attempt {
826            if url != attempt {
827                msg.push_str(": ");
828                msg.push_str(attempt);
829            }
830        }
831        msg.push('\n');
832        if !ssh_agent_attempts.is_empty() {
833            let names = ssh_agent_attempts
834                .iter()
835                .map(|s| format!("`{}`", s))
836                .collect::<Vec<_>>()
837                .join(", ");
838            msg.push_str(&format!(
839                "\n* attempted ssh-agent authentication, but \
840                 no usernames succeeded: {}",
841                names
842            ));
843        }
844        if let Some(failed_cred_helper) = cred_helper_bad {
845            if failed_cred_helper {
846                msg.push_str(
847                    "\n* attempted to find username/password via \
848                     git's `credential.helper` support, but failed",
849                );
850            } else {
851                msg.push_str(
852                    "\n* attempted to find username/password via \
853                     `credential.helper`, but maybe the found \
854                     credentials were incorrect",
855                );
856            }
857        }
858        msg.push_str("\n\n");
859        msg.push_str("if the git CLI succeeds then `net.git-fetch-with-cli` may help here\n");
860        msg.push_str("https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli");
861        err = err.context(msg);
862
863        // Otherwise if we didn't even get to the authentication phase them we may
864        // have failed to set up a connection, in these cases hint on the
865        // `net.git-fetch-with-cli` configuration option.
866    } else if let Some(e) = err.downcast_ref::<git2::Error>() {
867        match e.class() {
868            ErrorClass::Net
869            | ErrorClass::Ssl
870            | ErrorClass::Submodule
871            | ErrorClass::FetchHead
872            | ErrorClass::Ssh
873            | ErrorClass::Http => {
874                let msg = format!(
875                    concat!(
876                        "network failure seems to have happened\n",
877                        "if a proxy or similar is necessary `net.git-fetch-with-cli` may help here\n",
878                        "https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
879                        "{}"
880                    ),
881                    note_github_pull_request(url).unwrap_or_default()
882                );
883                err = err.context(msg);
884            }
885            ErrorClass::Callback => {
886                // This unwraps the git2 error. We're using the callback error
887                // specifically to convey errors from Rust land through the C
888                // callback interface. We don't need the `; class=Callback
889                // (26)` that gets tacked on to the git2 error message.
890                err = anyhow::format_err!("{}", e.message());
891            }
892            _ => {}
893        }
894    }
895
896    Err(err)
897}
898
899/// `git reset --hard` to the given `obj` for the `repo`.
900///
901/// The `obj` is a commit-ish to which the head should be moved.
902#[tracing::instrument(skip_all)]
903fn reset(repo: &git2::Repository, obj: &git2::Object<'_>, gctx: &GlobalContext) -> CargoResult<()> {
904    let mut pb = Progress::new("Checkout", gctx);
905    let mut opts = git2::build::CheckoutBuilder::new();
906    opts.progress(|_, cur, max| {
907        drop(pb.tick(cur, max, ""));
908    });
909    debug!("doing reset");
910    repo.reset(obj, git2::ResetType::Hard, Some(&mut opts))?;
911    debug!("reset done");
912    Ok(())
913}
914
915/// Prepares the callbacks for fetching a git repository.
916///
917/// The main purpose of this function is to construct everything before a fetch.
918/// This will attempt to setup a progress bar, the authentication for git,
919/// ssh known hosts check, and the network retry mechanism.
920///
921/// The callback is provided a fetch options, which can be used by the actual
922/// git fetch.
923pub fn with_fetch_options(
924    git_config: &git2::Config,
925    url: &str,
926    gctx: &GlobalContext,
927    cb: &mut dyn FnMut(git2::FetchOptions<'_>) -> CargoResult<()>,
928) -> CargoResult<()> {
929    let mut progress = Progress::new("Fetch", gctx);
930    let ssh_config = gctx.net_config()?.ssh.as_ref();
931    let config_known_hosts = ssh_config.and_then(|ssh| ssh.known_hosts.as_ref());
932    let diagnostic_home_config = gctx.diagnostic_home_config();
933    network::retry::with_retry(gctx, || {
934        // Hack: libgit2 disallows overriding the error from check_cb since v1.8.0,
935        // so we store the error additionally and unwrap it later
936        let mut check_cb_result = Ok(());
937        let auth_result = with_authentication(gctx, url, git_config, |f| {
938            let port = Url::parse(url).ok().and_then(|url| url.port());
939            let mut last_update = Instant::now();
940            let mut rcb = git2::RemoteCallbacks::new();
941            // We choose `N=10` here to make a `300ms * 10slots ~= 3000ms`
942            // sliding window for tracking the data transfer rate (in bytes/s).
943            let mut counter = MetricsCounter::<10>::new(0, last_update);
944            rcb.credentials(f);
945            rcb.certificate_check(|cert, host| {
946                match super::known_hosts::certificate_check(
947                    gctx,
948                    cert,
949                    host,
950                    port,
951                    config_known_hosts,
952                    &diagnostic_home_config,
953                ) {
954                    Ok(status) => Ok(status),
955                    Err(e) => {
956                        check_cb_result = Err(e);
957                        // This is not really used because it'll be overridden by libgit2
958                        // See https://github.com/libgit2/libgit2/commit/9a9f220119d9647a352867b24b0556195cb26548
959                        Err(git2::Error::from_str(
960                            "invalid or unknown remote ssh hostkey",
961                        ))
962                    }
963                }
964            });
965            rcb.transfer_progress(|stats| {
966                let indexed_deltas = stats.indexed_deltas();
967                let msg = if indexed_deltas > 0 {
968                    // Resolving deltas.
969                    format!(
970                        ", ({}/{}) resolving deltas",
971                        indexed_deltas,
972                        stats.total_deltas()
973                    )
974                } else {
975                    // Receiving objects.
976                    //
977                    // # Caveat
978                    //
979                    // Progress bar relies on git2 calling `transfer_progress`
980                    // to update its transfer rate, but we cannot guarantee a
981                    // periodic call of that callback. Thus if we don't receive
982                    // any data for, say, 10 seconds, the rate will get stuck
983                    // and never go down to 0B/s.
984                    // In the future, we need to find away to update the rate
985                    // even when the callback is not called.
986                    let now = Instant::now();
987                    // Scrape a `received_bytes` to the counter every 300ms.
988                    if now - last_update > Duration::from_millis(300) {
989                        counter.add(stats.received_bytes(), now);
990                        last_update = now;
991                    }
992                    let rate = HumanBytes(counter.rate() as u64);
993                    format!(", {rate:.2}/s")
994                };
995                progress
996                    .tick(stats.indexed_objects(), stats.total_objects(), &msg)
997                    .is_ok()
998            });
999
1000            // Create a local anonymous remote in the repository to fetch the
1001            // url
1002            let mut opts = git2::FetchOptions::new();
1003            opts.remote_callbacks(rcb);
1004            cb(opts)
1005        });
1006        if auth_result.is_err() {
1007            check_cb_result?;
1008        }
1009        auth_result?;
1010        Ok(())
1011    })
1012}
1013
1014/// Attempts to fetch the given git `reference` for a Git repository.
1015///
1016/// This is the main entry for git clone/fetch. It does the followings:
1017///
1018/// * Turns [`GitReference`] into refspecs accordingly.
1019/// * Dispatches `git fetch` using libgit2, gitoxide, or git CLI.
1020///
1021/// The `remote_url` argument is the git remote URL where we want to fetch from.
1022///
1023/// The `remote_kind` argument is a thing for [`-Zgitoxide`] shallow clones
1024/// at this time. It could be extended when libgit2 supports shallow clones.
1025///
1026/// [`-Zgitoxide`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#gitoxide
1027#[tracing::instrument(skip_all)]
1028pub fn fetch(
1029    repo: &mut git2::Repository,
1030    remote_url: &str,
1031    manifest_reference: &GitReference,
1032    locked_reference: &GitReference,
1033    gctx: &GlobalContext,
1034    remote_kind: RemoteKind,
1035) -> CargoResult<()> {
1036    if let Some(offline_flag) = gctx.offline_flag() {
1037        anyhow::bail!(
1038            "attempting to update a git repository, but {offline_flag} \
1039             was specified"
1040        )
1041    }
1042
1043    let shallow = remote_kind.to_shallow_setting(repo.is_shallow(), gctx);
1044
1045    // Flag to keep track if the rev is a full commit hash
1046    let mut fast_path_rev: bool = false;
1047
1048    let oid_to_fetch = match github_fast_path(repo, remote_url, locked_reference, gctx) {
1049        Ok(FastPathRev::UpToDate) => return Ok(()),
1050        Ok(FastPathRev::NeedsFetch(rev)) => Some(rev),
1051        Ok(FastPathRev::Indeterminate) => None,
1052        Err(e) => {
1053            debug!("failed to check github {:?}", e);
1054            None
1055        }
1056    };
1057
1058    maybe_gc_repo(repo, gctx)?;
1059
1060    clean_repo_temp_files(repo);
1061
1062    // Translate the reference desired here into an actual list of refspecs
1063    // which need to get fetched. Additionally record if we're fetching tags.
1064    let mut refspecs = Vec::new();
1065    let mut tags = false;
1066    // The `+` symbol on the refspec means to allow a forced (fast-forward)
1067    // update which is needed if there is ever a force push that requires a
1068    // fast-forward.
1069    match locked_reference {
1070        // For branches and tags we can fetch simply one reference and copy it
1071        // locally, no need to fetch other branches/tags.
1072        GitReference::Branch(b) => {
1073            refspecs.push(format!("+refs/heads/{0}:refs/remotes/origin/{0}", b));
1074        }
1075
1076        GitReference::Tag(t) => {
1077            refspecs.push(format!("+refs/tags/{0}:refs/remotes/origin/tags/{0}", t));
1078        }
1079
1080        GitReference::DefaultBranch => {
1081            refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
1082        }
1083
1084        GitReference::Rev(rev) => {
1085            if rev.starts_with("refs/") {
1086                refspecs.push(format!("+{0}:{0}", rev));
1087            } else if let Some(oid_to_fetch) = oid_to_fetch {
1088                fast_path_rev = true;
1089                refspecs.push(format!("+{0}:refs/commit/{0}", oid_to_fetch));
1090            } else if !matches!(shallow, gix::remote::fetch::Shallow::NoChange)
1091                && rev_to_oid(rev).is_some()
1092            {
1093                // There is a specific commit to fetch and we will do so in shallow-mode only
1094                // to not disturb the previous logic.
1095                // Note that with typical settings for shallowing, we will just fetch a single `rev`
1096                // as single commit.
1097                // The reason we write to `refs/remotes/origin/HEAD` is that it's of special significance
1098                // when during `GitReference::resolve()`, but otherwise it shouldn't matter.
1099                refspecs.push(format!("+{0}:refs/remotes/origin/HEAD", rev));
1100            } else if let GitReference::Rev(rev) = manifest_reference
1101                && rev.starts_with("refs/")
1102            {
1103                // If the lockfile has a commit. we can't directly fetch it (unless we're talking
1104                // to GitHub), so we fetch the ref associated with it from the manifest.
1105                refspecs.push(format!("+{0}:{0}", rev));
1106            } else {
1107                // We don't know what the rev will point to. To handle this
1108                // situation we fetch all branches and tags, and then we pray
1109                // it's somewhere in there.
1110                refspecs.push(String::from("+refs/heads/*:refs/remotes/origin/*"));
1111                refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
1112                tags = true;
1113            }
1114        }
1115    }
1116
1117    debug!("doing a fetch for {remote_url}");
1118    let result = if gctx
1119        .net_config()?
1120        .git_fetch_with_cli
1121        .unwrap_or_else(|| git_version().is_some())
1122    {
1123        fetch_with_cli(repo, remote_url, &refspecs, tags, shallow, gctx)
1124    } else {
1125        if gctx.net_config()?.git_fetch_with_cli.is_none() && git_version().is_none() {
1126            gctx.shell().warn(
1127                "no compatible `git` found
1128note: this may become a hard error in the future, see <https://github.com/rust-lang/cargo/issues/17227>
1129help: to still use `git` for fetching, install it
1130help: to use builtin native git support, re-try with `net.git-fetch-with-cli = false`
1131https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
1132            )?;
1133        }
1134        if gctx.cli_unstable().gitoxide.map_or(false, |git| git.fetch) {
1135            fetch_with_gitoxide(repo, remote_url, refspecs, tags, shallow, gctx)
1136        } else {
1137            fetch_with_libgit2(repo, remote_url, refspecs, tags, shallow, gctx)
1138        }
1139    };
1140
1141    if fast_path_rev {
1142        if let Some(oid) = oid_to_fetch {
1143            return result.with_context(|| format!("revision {} not found", oid));
1144        }
1145    }
1146    result
1147}
1148
1149/// `gitoxide` uses shallow locks to assure consistency when fetching to and to avoid races, and to write
1150/// files atomically.
1151/// Cargo has its own lock files and doesn't need that mechanism for race protection, so a stray lock means
1152/// a signal interrupted a previous shallow fetch and doesn't mean a race is happening.
1153fn has_shallow_lock_file(err: &crate::sources::git::fetch::Error, repo_path: &Path) -> bool {
1154    err.downcast_any_ref::<std::io::Error>()
1155        .is_some_and(|err| err.kind() == std::io::ErrorKind::AlreadyExists)
1156        && repo_path.join("shallow.lock").is_file()
1157}
1158
1159fn git_version() -> Option<GitVersion> {
1160    #[tracing::instrument(skip_all)]
1161    fn git_version() -> Option<GitVersion> {
1162        use std::process::Stdio;
1163
1164        let output = std::process::Command::new("git")
1165            .arg("--version")
1166            .stdin(Stdio::null())
1167            .output()
1168            .ok()?;
1169        if !output.status.success() {
1170            return None;
1171        }
1172        let Ok(stdout) = String::from_utf8(output.stdout) else {
1173            return None;
1174        };
1175        // All else fails, we can at least report the lowest common denominator of what we support
1176        let default_version = GitVersion {
1177            major: 2,
1178            minor: 0,
1179            patch: 0,
1180        };
1181        Some(GitVersion::from_version_stdout(&stdout).unwrap_or(default_version))
1182    }
1183
1184    static CACHE: std::sync::OnceLock<Option<GitVersion>> = std::sync::OnceLock::new();
1185    *CACHE.get_or_init(git_version)
1186}
1187
1188#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1189#[allow(unused)]
1190struct GitVersion {
1191    major: usize,
1192    minor: usize,
1193    patch: usize,
1194}
1195
1196impl GitVersion {
1197    fn from_version_stdout(stdout: &str) -> Result<Self, anyhow::Error> {
1198        // See https://github.com/git/git/blob/f78ce2f7b6df702f93d40b85d6bda92a3f65da79/help.c#L779-L784
1199        let Some(version) = stdout.strip_prefix("git version ") else {
1200            anyhow::bail!("unrecognized `git --version` output: {stdout}")
1201        };
1202        let (version, _) = version.split_once(" ").unwrap_or((version, ""));
1203        let (version, _) = version.split_once("\n").unwrap_or((version, ""));
1204        version.parse()
1205    }
1206}
1207
1208impl std::str::FromStr for GitVersion {
1209    type Err = anyhow::Error;
1210
1211    fn from_str(version: &str) -> Result<Self, Self::Err> {
1212        let unreleased = "GIT";
1213
1214        let s = version;
1215        let (major, s) = s.split_once(".").unwrap_or((s, ""));
1216        let mut major: usize = major.parse().map_err(|_err| {
1217            anyhow::format_err!("unrecognized major version `{major}` in `{version}`")
1218        })?;
1219        let (minor, s) = s.split_once(".").unwrap_or((s, ""));
1220        let mut minor: usize = if minor == unreleased {
1221            0
1222        } else {
1223            minor.parse().map_err(|_err| {
1224                anyhow::format_err!("unrecognized minor version `{minor}` in `{version}`")
1225            })?
1226        };
1227        let (patch, s) = s.split_once(".").unwrap_or((s, ""));
1228        let mut patch: usize = if patch == unreleased {
1229            0
1230        } else {
1231            patch.parse().map_err(|_err| {
1232                anyhow::format_err!("unrecognized patch version `{patch}` in `{version}`")
1233            })?
1234        };
1235
1236        let more = s;
1237        if !more.is_empty() {
1238            // `more` can be either pre-release or post-release.
1239            // The difference should be minimal, so be conservative and assume they are pre-release
1240            if let Some(sub) = patch.checked_sub(1) {
1241                patch = sub;
1242            } else if let Some(sub) = minor.checked_sub(1) {
1243                minor = sub;
1244            } else if let Some(sub) = major.checked_sub(1) {
1245                major = sub;
1246            }
1247        }
1248
1249        Ok(Self {
1250            major,
1251            minor,
1252            patch,
1253        })
1254    }
1255}
1256
1257/// Attempts to use `git` CLI installed on the system to fetch a repository,
1258/// when the config value [`net.git-fetch-with-cli`][1] is set.
1259///
1260/// Unfortunately `libgit2` is notably lacking in the realm of authentication
1261/// when compared to the `git` command line. As a result, allow an escape
1262/// hatch for users that would prefer to use `git`-the-CLI for fetching
1263/// repositories instead of `libgit2`-the-library. This should make more
1264/// flavors of authentication possible while also still giving us all the
1265/// speed and portability of using `libgit2`.
1266///
1267/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/config.html#netgit-fetch-with-cli
1268#[tracing::instrument(skip(repo, gctx))]
1269fn fetch_with_cli(
1270    repo: &git2::Repository,
1271    url: &str,
1272    refspecs: &[String],
1273    tags: bool,
1274    shallow: gix::remote::fetch::Shallow,
1275    gctx: &GlobalContext,
1276) -> CargoResult<()> {
1277    debug!(target: "git-fetch", backend = "git-cli");
1278
1279    let Some(git_version) = git_version() else {
1280        anyhow::bail!(
1281            "`git` is not available
1282help: to still use `git` for fetching, please install it
1283help: to use Cargo's native git support, re-try with `net.git-fetch-with-cli = false`
1284https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
1285        );
1286    };
1287
1288    let mut cmd = ProcessBuilder::new("git");
1289    // Avoid potential for unused work that may also hang (#15775)
1290    cmd.arg("-c").arg("core.fsmonitor=false");
1291    // Avoid warnings with `--no-show-forced-updates`
1292    cmd.arg("-c").arg("advice.fetchShowForcedUpdates=false");
1293    // Have 429's retried in git, where supported
1294    let max_retries = gctx
1295        .net_config()?
1296        .retry
1297        .unwrap_or(network::retry::MAX_RETRY_DEFAULT);
1298    cmd.arg("-c").arg(format!("http.maxRetries={max_retries}"));
1299    cmd.arg("-c").arg("http.retryAfter=1"); // `network::retry` has a growing value starting with 500ms, so choosing 1s
1300    cmd.arg("-c").arg(format!(
1301        "http.maxRetryTime={}",
1302        network::retry::MAX_RETRY_SLEEP_S
1303    ));
1304
1305    cmd.arg("fetch");
1306    if tags {
1307        cmd.arg("--tags");
1308    } else {
1309        cmd.arg("--no-tags");
1310    }
1311    if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1312        let depth = 0i32.saturating_add_unsigned(depth.get());
1313        cmd.arg(format!("--depth={depth}"));
1314    }
1315
1316    let progress_config = gctx.progress_config();
1317    let progress = match progress_config.when {
1318        ProgressWhen::Always => true,
1319        ProgressWhen::Never => false,
1320        ProgressWhen::Auto => {
1321            // Recreate the same conditions used with `Progress`
1322            let width = progress_config
1323                .width
1324                .or_else(|| gctx.shell().err_width().progress_max_width());
1325            gctx.shell().progress_supported() && width.is_some()
1326        }
1327    };
1328    if gctx.shell().verbosity() == Verbosity::Verbose {
1329        cmd.arg("--verbose");
1330    } else if progress {
1331        cmd.arg("--progress");
1332        // https://github.com/git/git/blob/1630431f326e15fcde608827b5ff38422528eb59/Documentation/RelNotes/2.41.0.adoc?plain=1#L113-L114
1333        let min_version_porcelain = GitVersion {
1334            major: 2,
1335            minor: 41,
1336            patch: 0,
1337        };
1338        if min_version_porcelain <= git_version {
1339            // Move ref update status to `stdout` and silence it
1340            cmd.arg("--porcelain").stdout(Stdio::Null);
1341        }
1342    } else {
1343        cmd.arg("--quiet");
1344    }
1345
1346    // https://github.com/git/git/blob/1630431f326e15fcde608827b5ff38422528eb59/Documentation/RelNotes/2.23.0.adoc?plain=1#L56-L59
1347    let min_version_no_show_forced_update = GitVersion {
1348        major: 2,
1349        minor: 23,
1350        patch: 0,
1351    };
1352    if min_version_no_show_forced_update <= git_version {
1353        // skip unneeded expensive calculations
1354        cmd.arg("--no-show-forced-updates");
1355    }
1356
1357    cmd.arg("--force") // handle force pushes
1358        .arg("--update-head-ok") // see discussion in #2078
1359        .arg("--recurse-submodules=no") // we handle this, incompatible with `--porcelain`
1360        .arg(url)
1361        .args(refspecs)
1362        // If cargo is run by git (for example, the `exec` command in `git
1363        // rebase`), the GIT_DIR is set by git and will point to the wrong
1364        // location. This makes sure GIT_DIR is always the repository path.
1365        .env("GIT_DIR", repo.path())
1366        // The reset of these may not be necessary, but I'm including them
1367        // just to be extra paranoid and avoid any issues.
1368        .env_remove("GIT_WORK_TREE")
1369        .env_remove("GIT_INDEX_FILE")
1370        .env_remove("GIT_OBJECT_DIRECTORY")
1371        .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
1372        .cwd(repo.path());
1373    gctx.shell()
1374        .verbose(|s| s.status("Running", &cmd.to_string()))?;
1375    // https://github.com/git/git/blob/1630431f326e15fcde608827b5ff38422528eb59/Documentation/RelNotes/2.54.0.adoc?plain=1#L107
1376    let min_version_max_retries = GitVersion {
1377        major: 2,
1378        minor: 54,
1379        patch: 0,
1380    };
1381    network::retry::with_retry(gctx, || {
1382        cmd.exec().map_err(|_error| {
1383            let spurious = git_version < min_version_max_retries;
1384            let pr_hint = note_github_pull_request(url).unwrap_or_default();
1385            let ssh_config_hint = note_ssh_config(gctx).unwrap_or_default().unwrap_or_default();
1386            let git_config_hint = note_git_config(gctx).unwrap_or_default().unwrap_or_default();
1387            let with_depth = if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1388                format!(" with depth={depth}")
1389            } else {
1390                "".to_owned()
1391            };
1392            GitCliError::new(anyhow::format_err!("`git fetch` failed for {url}{with_depth}{pr_hint}{ssh_config_hint}{git_config_hint}"))
1393                .spurious(spurious)
1394                .workaround(
1395                    "help: re-try with `net.git-fetch-with-cli = false` to see if it resolves the problem
1396https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
1397                )
1398                .into()
1399        })
1400    })?;
1401
1402    Ok(())
1403}
1404
1405#[tracing::instrument(skip(repo, gctx))]
1406fn fetch_with_gitoxide(
1407    repo: &mut git2::Repository,
1408    remote_url: &str,
1409    refspecs: Vec<String>,
1410    tags: bool,
1411    shallow: gix::remote::fetch::Shallow,
1412    gctx: &GlobalContext,
1413) -> CargoResult<()> {
1414    debug!(target: "git-fetch", backend = "gitoxide");
1415
1416    let git2_repo = repo;
1417    let config_overrides = cargo_config_to_gitoxide_overrides(gctx)?;
1418    let repo_reinitialized = AtomicBool::default();
1419    let res = oxide::with_retry_and_progress(
1420        git2_repo.path(),
1421        gctx,
1422        remote_url,
1423        &|repo_path,
1424          should_interrupt,
1425          mut progress,
1426          url_for_authentication: &mut dyn FnMut(&gix::bstr::BStr)| {
1427            // The `fetch` operation here may fail spuriously due to a corrupt
1428            // repository. It could also fail, however, for a whole slew of other
1429            // reasons (aka network related reasons). We want Cargo to automatically
1430            // recover from corrupt repositories, but we don't want Cargo to stomp
1431            // over other legitimate errors.
1432            //
1433            // Consequently we save off the error of the `fetch` operation and if it
1434            // looks like a "corrupt repo" error then we blow away the repo and try
1435            // again. If it looks like any other kind of error, or if we've already
1436            // blown away the repository, then we want to return the error as-is.
1437            loop {
1438                let res = oxide::open_repo(
1439                    repo_path,
1440                    config_overrides.clone(),
1441                    oxide::OpenMode::ForFetch,
1442                )
1443                .and_then(|repo| {
1444                    debug!("initiating fetch of {refspecs:?} from {remote_url}");
1445                    let url_for_authentication = &mut *url_for_authentication;
1446                    let remote = repo
1447                        .remote_at(remote_url)?
1448                        .with_fetch_tags(if tags {
1449                            gix::remote::fetch::Tags::All
1450                        } else {
1451                            gix::remote::fetch::Tags::Included
1452                        })
1453                        .with_refspecs(
1454                            refspecs.iter().map(|s| s.as_str()),
1455                            gix::remote::Direction::Fetch,
1456                        )?;
1457                    let url = remote
1458                        .url(gix::remote::Direction::Fetch)
1459                        .expect("set at init")
1460                        .to_owned();
1461                    let connection = remote.connect(gix::remote::Direction::Fetch)?;
1462                    let mut authenticate = connection.configured_credentials(url)?;
1463                    let connection = connection.with_credentials(
1464                        move |action: gix::protocol::credentials::helper::Action| {
1465                            if let Some(url) = action
1466                                .context()
1467                                .and_then(|gctx| gctx.url.as_ref().filter(|url| *url != remote_url))
1468                            {
1469                                url_for_authentication(url.as_ref());
1470                            }
1471                            authenticate(action)
1472                                .map_err(|err| gix::Error::from_error(oxide::CredentialError(err)))
1473                        },
1474                    );
1475                    let outcome = connection
1476                        .prepare_fetch(&mut progress, gix::remote::ref_map::Options::default())?
1477                        .with_shallow(shallow.clone())
1478                        .receive(&mut progress, should_interrupt)?;
1479                    Ok(outcome)
1480                });
1481                let err = match res {
1482                    Ok(_) => break,
1483                    Err(e) => e,
1484                };
1485                debug!("fetch failed: {}", err);
1486
1487                if !repo_reinitialized.load(Ordering::Relaxed)
1488                        // We check for errors that could occur if the configuration, refs or odb files are corrupted.
1489                        // We don't check for errors related to writing as `gitoxide` is expected to create missing leading
1490                        // folder before writing files into it, or else not even open a directory as git repository (which is
1491                        // also handled here).
1492                        && err.is_corrupted()
1493                    || has_shallow_lock_file(&err, repo_path)
1494                {
1495                    repo_reinitialized.store(true, Ordering::Relaxed);
1496                    debug!(
1497                        "looks like this is a corrupt repository, reinitializing \
1498                     and trying again"
1499                    );
1500                    if oxide::reinitialize(repo_path).is_ok() {
1501                        continue;
1502                    }
1503                }
1504
1505                return Err(err.into());
1506            }
1507            Ok(())
1508        },
1509    );
1510    if repo_reinitialized.load(Ordering::Relaxed) {
1511        *git2_repo = git2::Repository::open(git2_repo.path())?;
1512    }
1513    res
1514}
1515
1516#[tracing::instrument(skip(repo, gctx))]
1517fn fetch_with_libgit2(
1518    repo: &mut git2::Repository,
1519    remote_url: &str,
1520    refspecs: Vec<String>,
1521    tags: bool,
1522    shallow: gix::remote::fetch::Shallow,
1523    gctx: &GlobalContext,
1524) -> CargoResult<()> {
1525    debug!(target: "git-fetch", backend = "libgit2");
1526
1527    static INIT: Once = Once::new();
1528    INIT.call_once(|| {
1529        // SAFETY:
1530        //
1531        // The unsafety of the registration function derives from two aspects:
1532        //
1533        // 1. This call must be synchronized with all other registration calls as
1534        //    well as construction of new transports.
1535        // 2. The argument is leaked.
1536        //
1537        // We're clear on point (1) because this is the only `register` call we make in the
1538        // cargo-binary and cargo-library is not officially supported and it is under a lock.
1539        // Technically, `git2_curl` also has a lock but that isn't part of their API guarantees.
1540        //
1541        // We're mostly clear on point (2) because we'd only free it after everything is done anyway
1542        unsafe { init_git_transports(gctx) };
1543    });
1544
1545    let git_config = git2::Config::open_default()?;
1546    with_fetch_options(&git_config, remote_url, gctx, &mut |mut opts| {
1547        if tags {
1548            opts.download_tags(git2::AutotagOption::All);
1549        }
1550        if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1551            opts.depth(0i32.saturating_add_unsigned(depth.get()));
1552        }
1553        // The `fetch` operation here may fail spuriously due to a corrupt
1554        // repository. It could also fail, however, for a whole slew of other
1555        // reasons (aka network related reasons). We want Cargo to automatically
1556        // recover from corrupt repositories, but we don't want Cargo to stomp
1557        // over other legitimate errors.
1558        //
1559        // Consequently we save off the error of the `fetch` operation and if it
1560        // looks like a "corrupt repo" error then we blow away the repo and try
1561        // again. If it looks like any other kind of error, or if we've already
1562        // blown away the repository, then we want to return the error as-is.
1563        let mut repo_reinitialized = false;
1564        loop {
1565            debug!("initiating fetch of {refspecs:?} from {remote_url}");
1566            let res = repo
1567                .remote_anonymous(remote_url)?
1568                .fetch(&refspecs, Some(&mut opts), None);
1569            let err = match res {
1570                Ok(()) => break,
1571                Err(e) => e,
1572            };
1573            debug!("fetch failed: {}", err);
1574
1575            if !repo_reinitialized && matches!(err.class(), ErrorClass::Reference | ErrorClass::Odb)
1576            {
1577                repo_reinitialized = true;
1578                debug!(
1579                    "looks like this is a corrupt repository, reinitializing \
1580                     and trying again"
1581                );
1582                if reinitialize(repo).is_ok() {
1583                    continue;
1584                }
1585            }
1586
1587            return Err(err.into());
1588        }
1589        Ok(())
1590    })
1591}
1592
1593/// Configure libgit2 to use libcurl if necessary.
1594///
1595/// If the user has a non-default network configuration, then libgit2 will be
1596/// configured to use libcurl instead of the built-in networking support so
1597/// that those configuration settings can be used.
1598///
1599/// # Safety
1600///
1601/// See [git2_curl::register]
1602#[tracing::instrument(skip_all)]
1603unsafe fn init_git_transports(gctx: &GlobalContext) {
1604    match needs_custom_http_transport(gctx) {
1605        Ok(true) => {}
1606        _ => return,
1607    }
1608
1609    let handle = match http_handle(gctx) {
1610        Ok(handle) => handle,
1611        Err(..) => return,
1612    };
1613
1614    unsafe {
1615        git2_curl::register(handle);
1616    }
1617}
1618
1619/// Attempts to `git gc` a repository.
1620///
1621/// Cargo has a bunch of long-lived git repositories in its global cache and
1622/// some, like the index, are updated very frequently. Right now each update
1623/// creates a new "pack file" inside the git database, and over time this can
1624/// cause bad performance and bad current behavior in libgit2.
1625///
1626/// One pathological use case today is where libgit2 opens hundreds of file
1627/// descriptors, getting us dangerously close to blowing out the OS limits of
1628/// how many fds we can have open. This is detailed in [#4403].
1629///
1630/// Instead of trying to be clever about when gc is needed, we just run
1631/// `git gc --auto` and let git figure it out. It checks its own thresholds
1632/// (gc.auto, gc.autoPackLimit) and either does the work or exits quickly.
1633/// If git isn't installed, no worries - we skip it.
1634///
1635/// [#4403]: https://github.com/rust-lang/cargo/issues/4403
1636fn maybe_gc_repo(repo: &mut git2::Repository, gctx: &GlobalContext) -> CargoResult<()> {
1637    // Let git decide whether gc is actually needed based on its own thresholds
1638    // (gc.auto, gc.autoPackLimit). This avoids duplicating git's internal logic
1639    // for deciding when housekeeping is needed.
1640    //
1641    // For testing purposes, __CARGO_PACKFILE_LIMIT can be set to override
1642    // gc.autoPackLimit, which has the same meaning. This lets tests force gc
1643    // to run by setting a low threshold without depending on git's defaults.
1644    let mut cmd = Command::new("git");
1645    if let Ok(limit) = gctx.get_env("__CARGO_PACKFILE_LIMIT") {
1646        cmd.arg(format!("-c gc.autoPackLimit={}", limit));
1647    }
1648    cmd.arg("gc")
1649        .arg("--auto")
1650        // Explicitly set `GIT_DIR` so `safe.bareRepository=explicit` doesn't reject it.
1651        .env("GIT_DIR", repo.path())
1652        .current_dir(repo.path());
1653
1654    match cmd.output() {
1655        Ok(out) => {
1656            debug!(
1657                "git-gc --auto status: {}\n\nstdout ---\n{}\nstderr ---\n{}",
1658                out.status,
1659                String::from_utf8_lossy(&out.stdout),
1660                String::from_utf8_lossy(&out.stderr)
1661            );
1662            if out.status.success() {
1663                let new = git2::Repository::open(repo.path())?;
1664                *repo = new;
1665                return Ok(());
1666            }
1667        }
1668        Err(e) => debug!("git-gc --auto failed to spawn: {}", e),
1669    }
1670
1671    // Alright all else failed, let's start over.
1672    reinitialize(repo)
1673}
1674
1675/// Removes temporary files left from previous activity.
1676///
1677/// If libgit2 is interrupted while indexing pack files, it will leave behind
1678/// some temporary files that it doesn't clean up. These can be quite large in
1679/// size, so this tries to clean things up.
1680///
1681/// This intentionally ignores errors. This is only an opportunistic cleaning,
1682/// and we don't really care if there are issues (there's unlikely anything
1683/// that can be done).
1684///
1685/// The git CLI has similar behavior (its temp files look like
1686/// `objects/pack/tmp_pack_9kUSA8`). Those files are normally deleted via `git
1687/// prune` which is run by `git gc`. However, it doesn't know about libgit2's
1688/// filenames, so they never get cleaned up.
1689fn clean_repo_temp_files(repo: &git2::Repository) {
1690    let path = repo.path().join("objects/pack/pack_git2_*");
1691    let Some(pattern) = path.to_str() else {
1692        tracing::warn!("cannot convert {path:?} to a string");
1693        return;
1694    };
1695    let Ok(paths) = glob::glob(pattern) else {
1696        return;
1697    };
1698    for path in paths {
1699        if let Ok(path) = path {
1700            match paths::remove_file(&path) {
1701                Ok(_) => tracing::debug!("removed stale temp git file {path:?}"),
1702                Err(e) => {
1703                    tracing::warn!("failed to remove {path:?} while cleaning temp files: {e}")
1704                }
1705            }
1706        }
1707    }
1708}
1709
1710/// Reinitializes a given Git repository. This is useful when a Git repository
1711/// seems corrupted and we want to start over.
1712fn reinitialize(repo: &mut git2::Repository) -> CargoResult<()> {
1713    // Here we want to drop the current repository object pointed to by `repo`,
1714    // so we initialize temporary repository in a sub-folder, blow away the
1715    // existing git folder, and then recreate the git repo. Finally we blow away
1716    // the `tmp` folder we allocated.
1717    let path = repo.path().to_path_buf();
1718    debug!("reinitializing git repo at {:?}", path);
1719    let tmp = path.join("tmp");
1720    let bare = !repo.path().ends_with(".git");
1721    *repo = init(&tmp, false)?;
1722    for entry in path.read_dir()? {
1723        let entry = entry?;
1724        if entry.file_name().to_str() == Some("tmp") {
1725            continue;
1726        }
1727        let path = entry.path();
1728        drop(paths::remove_file(&path).or_else(|_| paths::remove_dir_all(&path)));
1729    }
1730    *repo = init(&path, bare)?;
1731    paths::remove_dir_all(&tmp)?;
1732    Ok(())
1733}
1734
1735/// Initializes a Git repository at `path`.
1736fn init(path: &Path, bare: bool) -> CargoResult<git2::Repository> {
1737    let mut opts = git2::RepositoryInitOptions::new();
1738    // Skip anything related to templates, they just call all sorts of issues as
1739    // we really don't want to use them yet they insist on being used. See #6240
1740    // for an example issue that comes up.
1741    opts.external_template(false);
1742    opts.bare(bare);
1743    Ok(git2::Repository::init_opts(&path, &opts)?)
1744}
1745
1746/// The result of GitHub fast path check. See [`github_fast_path`] for more.
1747enum FastPathRev {
1748    /// The local rev (determined by `reference.resolve(repo)`) is already up to
1749    /// date with what this rev resolves to on GitHub's server.
1750    UpToDate,
1751    /// The following SHA must be fetched in order for the local rev to become
1752    /// up to date.
1753    NeedsFetch(Oid),
1754    /// Don't know whether local rev is up to date. We'll fetch _all_ branches
1755    /// and tags from the server and see what happens.
1756    Indeterminate,
1757}
1758
1759/// Attempts GitHub's special fast path for testing if we've already got an
1760/// up-to-date copy of the repository.
1761///
1762/// Updating the index is done pretty regularly so we want it to be as fast as
1763/// possible. For registries hosted on GitHub (like the crates.io index) there's
1764/// a fast path available to use[^1] to tell us that there's no updates to be
1765/// made.
1766///
1767/// Note that this function should never cause an actual failure because it's
1768/// just a fast path. As a result, a caller should ignore `Err` returned from
1769/// this function and move forward on the normal path.
1770///
1771/// [^1]: <https://developer.github.com/v3/repos/commits/#get-the-sha-1-of-a-commit-reference>
1772#[tracing::instrument(skip(repo, gctx))]
1773fn github_fast_path(
1774    repo: &git2::Repository,
1775    url: &str,
1776    reference: &GitReference,
1777    gctx: &GlobalContext,
1778) -> CargoResult<FastPathRev> {
1779    let url = Url::parse(url)?;
1780    if !is_github(&url) {
1781        return Ok(FastPathRev::Indeterminate);
1782    }
1783
1784    let local_object = resolve_ref(reference, repo).ok();
1785
1786    let github_branch_name = match reference {
1787        GitReference::Branch(branch) => branch,
1788        GitReference::Tag(tag) => tag,
1789        GitReference::DefaultBranch => "HEAD",
1790        GitReference::Rev(rev) => {
1791            if rev.starts_with("refs/") {
1792                rev
1793            } else if looks_like_commit_hash(rev) {
1794                // `revparse_single` (used by `resolve`) is the only way to turn
1795                // short hash -> long hash, but it also parses other things,
1796                // like branch and tag names, which might coincidentally be
1797                // valid hex.
1798                //
1799                // We only return early if `rev` is a prefix of the object found
1800                // by `revparse_single`. Don't bother talking to GitHub in that
1801                // case, since commit hashes are permanent. If a commit with the
1802                // requested hash is already present in the local clone, its
1803                // contents must be the same as what is on the server for that
1804                // hash.
1805                //
1806                // If `rev` is not found locally by `revparse_single`, we'll
1807                // need GitHub to resolve it and get a hash. If `rev` is found
1808                // but is not a short hash of the found object, it's probably a
1809                // branch and we also need to get a hash from GitHub, in case
1810                // the branch has moved.
1811                if let Some(local_object) = local_object {
1812                    if is_short_hash_of(rev, local_object) {
1813                        debug!("github fast path already has {local_object}");
1814                        return Ok(FastPathRev::UpToDate);
1815                    }
1816                }
1817                // If `rev` is a full commit hash, the only thing it can resolve
1818                // to is itself. Don't bother talking to GitHub in that case
1819                // either. (This ensures that we always attempt to fetch the
1820                // commit directly even if we can't reach the GitHub API.)
1821                if let Some(oid) = rev_to_oid(rev) {
1822                    debug!("github fast path is already a full commit hash {rev}");
1823                    return Ok(FastPathRev::NeedsFetch(oid));
1824                }
1825                rev
1826            } else {
1827                debug!("can't use github fast path with `rev = \"{}\"`", rev);
1828                return Ok(FastPathRev::Indeterminate);
1829            }
1830        }
1831    };
1832
1833    // This expects GitHub urls in the form `github.com/user/repo` and nothing
1834    // else
1835    let mut pieces = url
1836        .path_segments()
1837        .ok_or_else(|| anyhow!("no path segments on url"))?;
1838    let username = pieces
1839        .next()
1840        .ok_or_else(|| anyhow!("couldn't find username"))?;
1841    let repository = pieces
1842        .next()
1843        .ok_or_else(|| anyhow!("couldn't find repository name"))?;
1844    if pieces.next().is_some() {
1845        anyhow::bail!("too many segments on URL");
1846    }
1847
1848    // Trim off the `.git` from the repository, if present, since that's
1849    // optional for GitHub and won't work when we try to use the API as well.
1850    let repository = repository.strip_suffix(".git").unwrap_or(repository);
1851
1852    let url = format!(
1853        "https://api.github.com/repos/{}/{}/commits/{}",
1854        username, repository, github_branch_name,
1855    );
1856    debug!("attempting GitHub fast path for {}", url);
1857    let mut request =
1858        Request::get(url).header(http::header::ACCEPT, "application/vnd.github.3.sha");
1859    if let Some(local_object) = local_object {
1860        request = request.header(http::header::IF_NONE_MATCH, &format!("\"{local_object}\""));
1861    }
1862    let response = gctx
1863        .http_async()?
1864        .request_blocking(request.body(Vec::new())?)?;
1865    let response_code = response.status();
1866    if response_code == StatusCode::NOT_MODIFIED {
1867        debug!("github fast path up-to-date");
1868        Ok(FastPathRev::UpToDate)
1869    } else if response_code == StatusCode::OK
1870        && let Some(oid_to_fetch) = rev_to_oid(str::from_utf8(&response.body())?)
1871    {
1872        // response expected to be a full hash hexstring (40 or 64 chars)
1873        debug!("github fast path fetch {oid_to_fetch}");
1874        Ok(FastPathRev::NeedsFetch(oid_to_fetch))
1875    } else {
1876        // Usually response_code == 404 if the repository does not exist, and
1877        // response_code == 422 if exists but GitHub is unable to resolve the
1878        // requested rev.
1879        debug!("github fast path bad response code {response_code}");
1880        Ok(FastPathRev::Indeterminate)
1881    }
1882}
1883
1884/// Whether a `url` is one from GitHub.
1885fn is_github(url: &Url) -> bool {
1886    url.host_str() == Some("github.com")
1887}
1888
1889// Give some messages on GitHub PR URL given as is
1890pub(crate) fn note_github_pull_request(url: &str) -> Option<String> {
1891    if let Ok(url) = url.parse::<Url>()
1892        && is_github(&url)
1893    {
1894        let path_segments = url
1895            .path_segments()
1896            .map(|p| p.into_iter().collect::<Vec<_>>())
1897            .unwrap_or_default();
1898        if let [owner, repo, "pull", pr_number, ..] = path_segments[..] {
1899            let repo_url = format!("https://github.com/{owner}/{repo}.git");
1900            let rev = format!("refs/pull/{pr_number}/head");
1901            return Some(format!(
1902                concat!(
1903                    "\n\nnote: GitHub url {} is not a repository\n",
1904                    "help: to specify a pull request as a dependency, replace the dependency with:\n",
1905                    "       `git = \"{}\" rev = \"{}\"`",
1906                ),
1907                url, repo_url, rev
1908            ));
1909        }
1910    }
1911
1912    None
1913}
1914
1915pub(crate) fn note_ssh_config(gctx: &GlobalContext) -> CargoResult<Option<&'static str>> {
1916    let net_config = gctx.net_config()?;
1917    if let Some(ssh_config) = net_config.ssh.as_ref()
1918        && ssh_config.known_hosts.is_some()
1919    {
1920        Ok(Some(
1921            "
1922note: `cargo`s `net.ssh.known-hosts` is not applied to `git`, check your system SSH configuration to ensure it is set there",
1923        ))
1924    } else {
1925        Ok(None)
1926    }
1927}
1928
1929pub(crate) fn note_git_config(gctx: &GlobalContext) -> CargoResult<Option<String>> {
1930    let mut fields = Vec::new();
1931
1932    // As this is for errors, the focus is on things that impact functionality
1933    let http_config = gctx.http_config()?;
1934    if http_config.proxy.is_some() {
1935        fields.push("http.proxy (git: http.proxy)");
1936    }
1937    if http_config.low_speed_limit.is_some() {
1938        fields.push("http.low-speed-limit (git: http.lowSpeedLimit)");
1939    }
1940    if http_config.timeout.is_some() {
1941        fields.push("http.timeout (git: http.lowSpeedTime)");
1942    }
1943    if http_config.cainfo.is_some() {
1944        fields.push("http.cainfo (git: http.sslCAInfo)");
1945    }
1946    if http_config.proxy_cainfo.is_some() {
1947        fields.push("http.proxy-cainfo (git: http.proxySSLCAInfo)");
1948    }
1949    if http_config.check_revoke.is_some() {
1950        fields.push("http.check-revoke (git: http.schannelCheckRevoke)");
1951    }
1952    if http_config.multiplexing.is_some() {
1953        fields.push("http.multiplexing (git: http.version)");
1954    }
1955    if http_config.ssl_version.is_some() {
1956        fields.push("http.ssl-version (git: http.sslVersion)");
1957    }
1958
1959    if !fields.is_empty() {
1960        Ok(Some(format!("
1961note: `cargo` does not apply the following config to `git`, check your git configuration to ensure they are set there
1962    {}", fields.join("\n    "))))
1963    } else {
1964        Ok(None)
1965    }
1966}
1967
1968/// Whether a `rev` looks like a commit hash (ASCII hex digits).
1969fn looks_like_commit_hash(rev: &str) -> bool {
1970    rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit())
1971}
1972
1973/// Whether `rev` is a shorter hash of `oid`.
1974fn is_short_hash_of(rev: &str, oid: Oid) -> bool {
1975    let long_hash = oid.to_string();
1976    match long_hash.get(..rev.len()) {
1977        Some(truncated_long_hash) => truncated_long_hash.eq_ignore_ascii_case(rev),
1978        None => false,
1979    }
1980}
1981
1982#[cfg(test)]
1983mod tests {
1984    use super::*;
1985
1986    #[test]
1987    fn scp_to_ssh_produces_parseable_cache_urls() {
1988        assert!(
1989            "git@github.com:foo/bar.git".into_url().is_err(),
1990            "SourceId needs a WHATWG URL, which cannot represent SCP syntax"
1991        );
1992        for (input, expected) in [
1993            (
1994                "git@github.com:foo/bar.git",
1995                "ssh://git@github.com/foo/bar.git",
1996            ),
1997            (
1998                "git@github.com:/foo/bar.git",
1999                "ssh://git@github.com/foo/bar.git",
2000            ),
2001            ("git@github.com:~/bar.git", "ssh://git@github.com/~/bar.git"),
2002            (
2003                "git@github.com:~other/bar.git",
2004                "ssh://git@github.com/~other/bar.git",
2005            ),
2006            ("host:foo/bar.git", "ssh://host/foo/bar.git"),
2007        ] {
2008            let url = scp_to_ssh(input).unwrap();
2009            assert_eq!(
2010                url, expected,
2011                "conversion must preserve the pre-update cache URL"
2012            );
2013            let cache_url = url.as_str().into_url().unwrap();
2014            let source_id = SourceId::for_git(&cache_url, GitReference::DefaultBranch).unwrap();
2015            assert_eq!(source_id.url().as_str(), expected);
2016        }
2017        assert!(scp_to_ssh("https://github.com/foo/bar.git").is_none());
2018    }
2019
2020    #[test]
2021    fn shallow_lock_recovery_requires_existing_lock_and_matching_io_error() {
2022        use gix::error::ErrorExt;
2023
2024        let dir = tempfile::TempDir::new().unwrap();
2025        let locked = std::io::Error::from(std::io::ErrorKind::AlreadyExists).raise();
2026        assert!(!has_shallow_lock_file(&locked, dir.path()));
2027        std::fs::write(dir.path().join("shallow.lock"), []).unwrap();
2028        assert!(has_shallow_lock_file(&locked, dir.path()));
2029        let denied = std::io::Error::from(std::io::ErrorKind::PermissionDenied).raise();
2030        assert!(!has_shallow_lock_file(&denied, dir.path()));
2031    }
2032
2033    #[test]
2034    fn github_fast_path_full_hash_returns_needs_fetch() {
2035        let temp_dir = tempfile::TempDir::new().unwrap();
2036        let repo = git2::Repository::init_bare(temp_dir.path()).unwrap();
2037        let full_hash = "c9040898c9183ddbb9402dcbf749ed06d6ea90ad";
2038        let reference = GitReference::Rev(full_hash.to_string());
2039        let gctx = GlobalContext::default().unwrap();
2040        let expected_oid = rev_to_oid(full_hash).unwrap();
2041
2042        let result =
2043            github_fast_path(&repo, "https://github.com/user/repo", &reference, &gctx).unwrap();
2044
2045        assert!(matches!(result, FastPathRev::NeedsFetch(oid) if oid == expected_oid));
2046    }
2047
2048    #[test]
2049    fn test_absolute_submodule_url() {
2050        let cases = [
2051            (
2052                "ssh://git@gitub.com/rust-lang/cargo",
2053                "git@github.com:rust-lang/cargo.git",
2054                "git@github.com:rust-lang/cargo.git",
2055            ),
2056            (
2057                "ssh://git@gitub.com/rust-lang/cargo",
2058                "./",
2059                "ssh://git@gitub.com/rust-lang/cargo/",
2060            ),
2061            (
2062                "ssh://git@gitub.com/rust-lang/cargo",
2063                "../",
2064                "ssh://git@gitub.com/rust-lang/",
2065            ),
2066            (
2067                "ssh://git@gitub.com/rust-lang/cargo",
2068                "./foo",
2069                "ssh://git@gitub.com/rust-lang/cargo/foo",
2070            ),
2071            (
2072                "ssh://git@gitub.com/rust-lang/cargo/",
2073                "./foo",
2074                "ssh://git@gitub.com/rust-lang/cargo/foo",
2075            ),
2076            (
2077                "ssh://git@gitub.com/rust-lang/cargo/",
2078                "../foo",
2079                "ssh://git@gitub.com/rust-lang/foo",
2080            ),
2081            (
2082                "ssh://git@gitub.com/rust-lang/cargo",
2083                "../foo",
2084                "ssh://git@gitub.com/rust-lang/foo",
2085            ),
2086            (
2087                "ssh://git@gitub.com/rust-lang/cargo",
2088                "../foo/bar/../baz",
2089                "ssh://git@gitub.com/rust-lang/foo/baz",
2090            ),
2091            (
2092                "git@github.com:rust-lang/cargo.git",
2093                "ssh://git@gitub.com/rust-lang/cargo",
2094                "ssh://git@gitub.com/rust-lang/cargo",
2095            ),
2096            (
2097                "git@github.com:rust-lang/cargo.git",
2098                "./",
2099                "git@github.com:rust-lang/cargo.git/./",
2100            ),
2101            (
2102                "git@github.com:rust-lang/cargo.git",
2103                "../",
2104                "git@github.com:rust-lang/cargo.git/../",
2105            ),
2106            (
2107                "git@github.com:rust-lang/cargo.git",
2108                "./foo",
2109                "git@github.com:rust-lang/cargo.git/./foo",
2110            ),
2111            (
2112                "git@github.com:rust-lang/cargo.git/",
2113                "./foo",
2114                "git@github.com:rust-lang/cargo.git/./foo",
2115            ),
2116            (
2117                "git@github.com:rust-lang/cargo.git",
2118                "../foo",
2119                "git@github.com:rust-lang/cargo.git/../foo",
2120            ),
2121            (
2122                "git@github.com:rust-lang/cargo.git/",
2123                "../foo",
2124                "git@github.com:rust-lang/cargo.git/../foo",
2125            ),
2126            (
2127                "git@github.com:rust-lang/cargo.git",
2128                "../foo/bar/../baz",
2129                "git@github.com:rust-lang/cargo.git/../foo/bar/../baz",
2130            ),
2131        ];
2132
2133        for (base_url, submodule_url, expected) in cases {
2134            let url = absolute_submodule_url(base_url, submodule_url).unwrap();
2135            assert_eq!(
2136                expected, url,
2137                "base `{base_url}`; submodule `{submodule_url}`"
2138            );
2139        }
2140    }
2141
2142    #[test]
2143    fn test_scp_to_ssh() {
2144        // Standard SCP-like URLs should be converted to ssh:// form.
2145        assert_eq!(
2146            scp_to_ssh("git@github.com:rust-lang/cargo.git"),
2147            Some("ssh://git@github.com/rust-lang/cargo.git".to_string()),
2148        );
2149        assert_eq!(
2150            scp_to_ssh("git@github.com:foo/bar.git"),
2151            Some("ssh://git@github.com/foo/bar.git".to_string()),
2152        );
2153        // A path starting with '~' should also be converted.
2154        assert_eq!(
2155            scp_to_ssh("git@github.com:~user/repo.git"),
2156            Some("ssh://git@github.com/~user/repo.git".to_string()),
2157        );
2158        // Non-SCP URLs should not be converted and return None.
2159        assert_eq!(scp_to_ssh("ssh://git@github.com/rust-lang/cargo.git"), None);
2160        assert_eq!(scp_to_ssh("https://github.com/rust-lang/cargo.git"), None);
2161        // Unparsable input should also return None.
2162        assert_eq!(scp_to_ssh("not-a-url"), None);
2163    }
2164}
2165
2166/// Turns a full commit hash revision into an oid.
2167///
2168/// Git object ID is supposed to be a hex string of 20 (SHA1) or 32 (SHA256) bytes.
2169/// Its length must be double to the underlying bytes (40 or 64),
2170/// otherwise libgit2 would happily zero-pad the returned oid.
2171///
2172/// See:
2173///
2174/// * <https://github.com/rust-lang/cargo/issues/13188>
2175/// * <https://github.com/rust-lang/cargo/issues/13968>
2176pub(super) fn rev_to_oid(rev: &str) -> Option<Oid> {
2177    Oid::from_str(rev)
2178        .ok()
2179        .filter(|oid| oid.as_bytes().len() * 2 == rev.len())
2180}
2181
2182#[cfg(test)]
2183mod test {
2184    use super::*;
2185
2186    #[test]
2187    fn git_version_empty() {
2188        let input = "";
2189        GitVersion::from_version_stdout(input).unwrap_err();
2190    }
2191
2192    #[test]
2193    fn git_version_version() {
2194        let input = "git version 2.19.0";
2195        let actual = GitVersion::from_version_stdout(input).unwrap();
2196        let expected = GitVersion {
2197            major: 2,
2198            minor: 19,
2199            patch: 0,
2200        };
2201        assert_eq!(actual, expected);
2202    }
2203
2204    #[test]
2205    fn git_version_version_nl() {
2206        let input = "git version 2.19.0\n";
2207        let actual = GitVersion::from_version_stdout(input).unwrap();
2208        let expected = GitVersion {
2209            major: 2,
2210            minor: 19,
2211            patch: 0,
2212        };
2213        assert_eq!(actual, expected);
2214    }
2215
2216    #[test]
2217    fn git_version_version_platform() {
2218        let input = "git version 2.19.0 (Apple Git-117)";
2219        let actual = GitVersion::from_version_stdout(input).unwrap();
2220        let expected = GitVersion {
2221            major: 2,
2222            minor: 19,
2223            patch: 0,
2224        };
2225        assert_eq!(actual, expected);
2226    }
2227
2228    #[test]
2229    fn git_version_version_vendor() {
2230        let input = "git version 2.19.0.windows.1";
2231        let actual = GitVersion::from_version_stdout(input).unwrap();
2232        let expected = GitVersion {
2233            major: 2,
2234            minor: 18,
2235            patch: 0,
2236        };
2237        assert_eq!(actual, expected);
2238    }
2239
2240    #[test]
2241    fn git_version_version_rc() {
2242        let input = "git version 2.19.0.rc0";
2243        let actual = GitVersion::from_version_stdout(input).unwrap();
2244        let expected = GitVersion {
2245            major: 2,
2246            minor: 18,
2247            patch: 0,
2248        };
2249        assert_eq!(actual, expected);
2250    }
2251
2252    #[test]
2253    fn git_version_version_unreleased() {
2254        let input = "git version 2.55.GIT";
2255        let actual = GitVersion::from_version_stdout(input).unwrap();
2256        let expected = GitVersion {
2257            major: 2,
2258            minor: 55,
2259            patch: 0,
2260        };
2261        assert_eq!(actual, expected);
2262    }
2263}