1use 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
33const CHECKOUT_READY_LOCK: &str = ".cargo-ok";
36
37#[derive(PartialEq, Clone, Debug)]
39pub struct GitRemote {
40 url: String,
46}
47
48pub struct GitDatabase {
51 remote: GitRemote,
53 path: PathBuf,
55 repo: git2::Repository,
57}
58
59pub struct GitCheckout<'a> {
61 database: &'a GitDatabase,
63 path: PathBuf,
65 revision: git2::Oid,
67 repo: git2::Repository,
69}
70
71impl GitRemote {
72 pub fn new(url: &Url) -> GitRemote {
74 GitRemote {
75 url: url.as_str().to_owned(),
76 }
77 }
78
79 pub(super) fn new_from_str(url: String) -> GitRemote {
83 GitRemote { url }
84 }
85
86 pub fn url(&self) -> &str {
88 &self.url
89 }
90
91 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 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 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 #[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 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 pub fn to_short_id(&self, rev: git2::Oid) -> CargoResult<String> {
201 const MIN_ABBREV_LEN: usize = 7; let odb = self.repo.odb()?;
203 let mut len = MIN_ABBREV_LEN;
204 let mut hex = rev.to_string();
205 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 pub fn contains(&self, oid: git2::Oid) -> bool {
220 self.repo.revparse_single(&oid.to_string()).is_ok()
221 }
222
223 pub fn resolve(&self, r: &GitReference) -> CargoResult<git2::Oid> {
225 resolve_ref(r, &self.repo)
226 }
227}
228
229pub fn resolve_ref(gitref: &GitReference, repo: &git2::Repository) -> CargoResult<git2::Oid> {
231 let id = match gitref {
232 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 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 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 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 fn remote_url(&self) -> &str {
295 self.database.remote.url()
296 }
297
298 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 let git_config = git2::Config::new()?;
316
317 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(); let r = git2::build::RepoBuilder::new()
330 .clone_local(git2::build::CloneLocal::Local)
333 .with_checkout(checkout)
334 .fetch_options(fopts)
335 .clone(url.as_str(), into)?;
336 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 fn is_fresh(&self) -> bool {
360 match self.repo.revparse_single("HEAD") {
361 Ok(ref head) if head.id() == self.revision => {
362 self.path.join(CHECKOUT_READY_LOCK).exists()
364 }
365 _ => false,
366 }
367 }
368
369 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 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 fn update_submodules(&self, gctx: &GlobalContext, quiet: bool) -> CargoResult<()> {
406 return update_submodules(&self.repo, gctx, quiet, self.remote_url());
407
408 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 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 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 let Some(head) = child.head_id() else {
469 return Ok(());
470 };
471
472 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 let reference = GitReference::Rev(head.to_string());
495
496 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 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#[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
553fn 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
606fn scp_to_ssh(url: &str) -> Option<String> {
608 let mut gix_url = gix::url::parse(gix::bstr::BStr::new(url.as_bytes())).ok()?;
609 if gix_url.serialize_alternative_form && gix_url.scheme == gix::url::Scheme::Ssh {
610 gix_url.serialize_alternative_form = false;
611 Some(gix_url.to_bstring().to_string())
612 } else {
613 None
614 }
615}
616
617fn with_authentication<T, F>(
645 gctx: &GlobalContext,
646 url: &str,
647 cfg: &git2::Config,
648 mut f: F,
649) -> CargoResult<T>
650where
651 F: FnMut(&mut git2::Credentials<'_>) -> CargoResult<T>,
652{
653 let mut cred_helper = git2::CredentialHelper::new(url);
654 cred_helper.config(cfg);
655
656 let mut ssh_username_requested = false;
657 let mut cred_helper_bad = None;
658 let mut ssh_agent_attempts = Vec::new();
659 let mut any_attempts = false;
660 let mut tried_sshkey = false;
661 let mut url_attempt = None;
662
663 let orig_url = url;
664 let mut res = f(&mut |url, username, allowed| {
665 any_attempts = true;
666 if url != orig_url {
667 url_attempt = Some(url.to_string());
668 }
669 if allowed.contains(git2::CredentialType::USERNAME) {
688 debug_assert!(username.is_none());
689 ssh_username_requested = true;
690 return Err(git2::Error::from_str("gonna try usernames later"));
691 }
692
693 if allowed.contains(git2::CredentialType::SSH_KEY) && !tried_sshkey {
702 tried_sshkey = true;
707 let username = username.unwrap();
708 debug_assert!(!ssh_username_requested);
709 ssh_agent_attempts.push(username.to_string());
710 return git2::Cred::ssh_key_from_agent(username);
711 }
712
713 if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) && cred_helper_bad.is_none()
724 {
725 let r = git2::Cred::credential_helper(cfg, url, username);
726 cred_helper_bad = Some(r.is_err());
727 return r;
728 }
729
730 if allowed.contains(git2::CredentialType::DEFAULT) {
733 return git2::Cred::default();
734 }
735
736 Err(git2::Error::from_str("no authentication methods succeeded"))
738 });
739
740 if ssh_username_requested {
752 debug_assert!(res.is_err());
753 let mut attempts = vec![String::from("git")];
754 if let Ok(s) = gctx.get_env("USER").or_else(|_| gctx.get_env("USERNAME")) {
755 attempts.push(s.to_string());
756 }
757 if let Some(ref s) = cred_helper.username {
758 attempts.push(s.clone());
759 }
760
761 while let Some(s) = attempts.pop() {
762 let mut attempts = 0;
767 res = f(&mut |_url, username, allowed| {
768 if allowed.contains(git2::CredentialType::USERNAME) {
769 return git2::Cred::username(&s);
770 }
771 if allowed.contains(git2::CredentialType::SSH_KEY) {
772 debug_assert_eq!(Some(&s[..]), username);
773 attempts += 1;
774 if attempts == 1 {
775 ssh_agent_attempts.push(s.to_string());
776 return git2::Cred::ssh_key_from_agent(&s);
777 }
778 }
779 Err(git2::Error::from_str("no authentication methods succeeded"))
780 });
781
782 if attempts != 2 {
795 break;
796 }
797 }
798 }
799 let mut err = match res {
800 Ok(e) => return Ok(e),
801 Err(e) => e,
802 };
803
804 if any_attempts {
808 let mut msg = "failed to authenticate when downloading \
809 repository"
810 .to_string();
811
812 if let Some(attempt) = &url_attempt {
813 if url != attempt {
814 msg.push_str(": ");
815 msg.push_str(attempt);
816 }
817 }
818 msg.push('\n');
819 if !ssh_agent_attempts.is_empty() {
820 let names = ssh_agent_attempts
821 .iter()
822 .map(|s| format!("`{}`", s))
823 .collect::<Vec<_>>()
824 .join(", ");
825 msg.push_str(&format!(
826 "\n* attempted ssh-agent authentication, but \
827 no usernames succeeded: {}",
828 names
829 ));
830 }
831 if let Some(failed_cred_helper) = cred_helper_bad {
832 if failed_cred_helper {
833 msg.push_str(
834 "\n* attempted to find username/password via \
835 git's `credential.helper` support, but failed",
836 );
837 } else {
838 msg.push_str(
839 "\n* attempted to find username/password via \
840 `credential.helper`, but maybe the found \
841 credentials were incorrect",
842 );
843 }
844 }
845 msg.push_str("\n\n");
846 msg.push_str("if the git CLI succeeds then `net.git-fetch-with-cli` may help here\n");
847 msg.push_str("https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli");
848 err = err.context(msg);
849
850 } else if let Some(e) = err.downcast_ref::<git2::Error>() {
854 match e.class() {
855 ErrorClass::Net
856 | ErrorClass::Ssl
857 | ErrorClass::Submodule
858 | ErrorClass::FetchHead
859 | ErrorClass::Ssh
860 | ErrorClass::Http => {
861 let msg = format!(
862 concat!(
863 "network failure seems to have happened\n",
864 "if a proxy or similar is necessary `net.git-fetch-with-cli` may help here\n",
865 "https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
866 "{}"
867 ),
868 note_github_pull_request(url).unwrap_or_default()
869 );
870 err = err.context(msg);
871 }
872 ErrorClass::Callback => {
873 err = anyhow::format_err!("{}", e.message());
878 }
879 _ => {}
880 }
881 }
882
883 Err(err)
884}
885
886#[tracing::instrument(skip_all)]
890fn reset(repo: &git2::Repository, obj: &git2::Object<'_>, gctx: &GlobalContext) -> CargoResult<()> {
891 let mut pb = Progress::new("Checkout", gctx);
892 let mut opts = git2::build::CheckoutBuilder::new();
893 opts.progress(|_, cur, max| {
894 drop(pb.tick(cur, max, ""));
895 });
896 debug!("doing reset");
897 repo.reset(obj, git2::ResetType::Hard, Some(&mut opts))?;
898 debug!("reset done");
899 Ok(())
900}
901
902pub fn with_fetch_options(
911 git_config: &git2::Config,
912 url: &str,
913 gctx: &GlobalContext,
914 cb: &mut dyn FnMut(git2::FetchOptions<'_>) -> CargoResult<()>,
915) -> CargoResult<()> {
916 let mut progress = Progress::new("Fetch", gctx);
917 let ssh_config = gctx.net_config()?.ssh.as_ref();
918 let config_known_hosts = ssh_config.and_then(|ssh| ssh.known_hosts.as_ref());
919 let diagnostic_home_config = gctx.diagnostic_home_config();
920 network::retry::with_retry(gctx, || {
921 let mut check_cb_result = Ok(());
924 let auth_result = with_authentication(gctx, url, git_config, |f| {
925 let port = Url::parse(url).ok().and_then(|url| url.port());
926 let mut last_update = Instant::now();
927 let mut rcb = git2::RemoteCallbacks::new();
928 let mut counter = MetricsCounter::<10>::new(0, last_update);
931 rcb.credentials(f);
932 rcb.certificate_check(|cert, host| {
933 match super::known_hosts::certificate_check(
934 gctx,
935 cert,
936 host,
937 port,
938 config_known_hosts,
939 &diagnostic_home_config,
940 ) {
941 Ok(status) => Ok(status),
942 Err(e) => {
943 check_cb_result = Err(e);
944 Err(git2::Error::from_str(
947 "invalid or unknown remote ssh hostkey",
948 ))
949 }
950 }
951 });
952 rcb.transfer_progress(|stats| {
953 let indexed_deltas = stats.indexed_deltas();
954 let msg = if indexed_deltas > 0 {
955 format!(
957 ", ({}/{}) resolving deltas",
958 indexed_deltas,
959 stats.total_deltas()
960 )
961 } else {
962 let now = Instant::now();
974 if now - last_update > Duration::from_millis(300) {
976 counter.add(stats.received_bytes(), now);
977 last_update = now;
978 }
979 let rate = HumanBytes(counter.rate() as u64);
980 format!(", {rate:.2}/s")
981 };
982 progress
983 .tick(stats.indexed_objects(), stats.total_objects(), &msg)
984 .is_ok()
985 });
986
987 let mut opts = git2::FetchOptions::new();
990 opts.remote_callbacks(rcb);
991 cb(opts)
992 });
993 if auth_result.is_err() {
994 check_cb_result?;
995 }
996 auth_result?;
997 Ok(())
998 })
999}
1000
1001#[tracing::instrument(skip_all)]
1015pub fn fetch(
1016 repo: &mut git2::Repository,
1017 remote_url: &str,
1018 manifest_reference: &GitReference,
1019 locked_reference: &GitReference,
1020 gctx: &GlobalContext,
1021 remote_kind: RemoteKind,
1022) -> CargoResult<()> {
1023 if let Some(offline_flag) = gctx.offline_flag() {
1024 anyhow::bail!(
1025 "attempting to update a git repository, but {offline_flag} \
1026 was specified"
1027 )
1028 }
1029
1030 let shallow = remote_kind.to_shallow_setting(repo.is_shallow(), gctx);
1031
1032 let mut fast_path_rev: bool = false;
1034
1035 let oid_to_fetch = match github_fast_path(repo, remote_url, locked_reference, gctx) {
1036 Ok(FastPathRev::UpToDate) => return Ok(()),
1037 Ok(FastPathRev::NeedsFetch(rev)) => Some(rev),
1038 Ok(FastPathRev::Indeterminate) => None,
1039 Err(e) => {
1040 debug!("failed to check github {:?}", e);
1041 None
1042 }
1043 };
1044
1045 maybe_gc_repo(repo, gctx)?;
1046
1047 clean_repo_temp_files(repo);
1048
1049 let mut refspecs = Vec::new();
1052 let mut tags = false;
1053 match locked_reference {
1057 GitReference::Branch(b) => {
1060 refspecs.push(format!("+refs/heads/{0}:refs/remotes/origin/{0}", b));
1061 }
1062
1063 GitReference::Tag(t) => {
1064 refspecs.push(format!("+refs/tags/{0}:refs/remotes/origin/tags/{0}", t));
1065 }
1066
1067 GitReference::DefaultBranch => {
1068 refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
1069 }
1070
1071 GitReference::Rev(rev) => {
1072 if rev.starts_with("refs/") {
1073 refspecs.push(format!("+{0}:{0}", rev));
1074 } else if let Some(oid_to_fetch) = oid_to_fetch {
1075 fast_path_rev = true;
1076 refspecs.push(format!("+{0}:refs/commit/{0}", oid_to_fetch));
1077 } else if !matches!(shallow, gix::remote::fetch::Shallow::NoChange)
1078 && rev_to_oid(rev).is_some()
1079 {
1080 refspecs.push(format!("+{0}:refs/remotes/origin/HEAD", rev));
1087 } else if let GitReference::Rev(rev) = manifest_reference
1088 && rev.starts_with("refs/")
1089 {
1090 refspecs.push(format!("+{0}:{0}", rev));
1093 } else {
1094 refspecs.push(String::from("+refs/heads/*:refs/remotes/origin/*"));
1098 refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
1099 tags = true;
1100 }
1101 }
1102 }
1103
1104 debug!("doing a fetch for {remote_url}");
1105 let result = if gctx
1106 .net_config()?
1107 .git_fetch_with_cli
1108 .unwrap_or_else(|| git_version().is_some())
1109 {
1110 fetch_with_cli(repo, remote_url, &refspecs, tags, shallow, gctx)
1111 } else {
1112 if gctx.net_config()?.git_fetch_with_cli.is_none() && git_version().is_none() {
1113 gctx.shell().warn(
1114 "no compatible `git` found
1115note: this may become a hard error in the future, see <https://github.com/rust-lang/cargo/issues/17227>
1116help: to still use `git` for fetching, install it
1117help: to use builtin native git support, re-try with `net.git-fetch-with-cli = false`
1118https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
1119 )?;
1120 }
1121 if gctx.cli_unstable().gitoxide.map_or(false, |git| git.fetch) {
1122 fetch_with_gitoxide(repo, remote_url, refspecs, tags, shallow, gctx)
1123 } else {
1124 fetch_with_libgit2(repo, remote_url, refspecs, tags, shallow, gctx)
1125 }
1126 };
1127
1128 if fast_path_rev {
1129 if let Some(oid) = oid_to_fetch {
1130 return result.with_context(|| format!("revision {} not found", oid));
1131 }
1132 }
1133 result
1134}
1135
1136fn has_shallow_lock_file(err: &crate::sources::git::fetch::Error) -> bool {
1141 matches!(
1142 err,
1143 gix::env::collate::fetch::Error::Fetch(gix::remote::fetch::Error::Fetch(
1144 gix::protocol::fetch::Error::LockShallowFile(_)
1145 ))
1146 )
1147}
1148
1149fn git_version() -> Option<GitVersion> {
1150 #[tracing::instrument(skip_all)]
1151 fn git_version() -> Option<GitVersion> {
1152 use std::process::Stdio;
1153
1154 let output = std::process::Command::new("git")
1155 .arg("--version")
1156 .stdin(Stdio::null())
1157 .output()
1158 .ok()?;
1159 if !output.status.success() {
1160 return None;
1161 }
1162 let Ok(stdout) = String::from_utf8(output.stdout) else {
1163 return None;
1164 };
1165 let default_version = GitVersion {
1167 major: 2,
1168 minor: 0,
1169 patch: 0,
1170 };
1171 Some(GitVersion::from_version_stdout(&stdout).unwrap_or(default_version))
1172 }
1173
1174 static CACHE: std::sync::OnceLock<Option<GitVersion>> = std::sync::OnceLock::new();
1175 *CACHE.get_or_init(git_version)
1176}
1177
1178#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1179#[allow(unused)]
1180struct GitVersion {
1181 major: usize,
1182 minor: usize,
1183 patch: usize,
1184}
1185
1186impl GitVersion {
1187 fn from_version_stdout(stdout: &str) -> Result<Self, anyhow::Error> {
1188 let Some(version) = stdout.strip_prefix("git version ") else {
1190 anyhow::bail!("unrecognized `git --version` output: {stdout}")
1191 };
1192 let (version, _) = version.split_once(" ").unwrap_or((version, ""));
1193 let (version, _) = version.split_once("\n").unwrap_or((version, ""));
1194 version.parse()
1195 }
1196}
1197
1198impl std::str::FromStr for GitVersion {
1199 type Err = anyhow::Error;
1200
1201 fn from_str(version: &str) -> Result<Self, Self::Err> {
1202 let unreleased = "GIT";
1203
1204 let s = version;
1205 let (major, s) = s.split_once(".").unwrap_or((s, ""));
1206 let mut major: usize = major.parse().map_err(|_err| {
1207 anyhow::format_err!("unrecognized major version `{major}` in `{version}`")
1208 })?;
1209 let (minor, s) = s.split_once(".").unwrap_or((s, ""));
1210 let mut minor: usize = if minor == unreleased {
1211 0
1212 } else {
1213 minor.parse().map_err(|_err| {
1214 anyhow::format_err!("unrecognized minor version `{minor}` in `{version}`")
1215 })?
1216 };
1217 let (patch, s) = s.split_once(".").unwrap_or((s, ""));
1218 let mut patch: usize = if patch == unreleased {
1219 0
1220 } else {
1221 patch.parse().map_err(|_err| {
1222 anyhow::format_err!("unrecognized patch version `{patch}` in `{version}`")
1223 })?
1224 };
1225
1226 let more = s;
1227 if !more.is_empty() {
1228 if let Some(sub) = patch.checked_sub(1) {
1231 patch = sub;
1232 } else if let Some(sub) = minor.checked_sub(1) {
1233 minor = sub;
1234 } else if let Some(sub) = major.checked_sub(1) {
1235 major = sub;
1236 }
1237 }
1238
1239 Ok(Self {
1240 major,
1241 minor,
1242 patch,
1243 })
1244 }
1245}
1246
1247#[tracing::instrument(skip(repo, gctx))]
1259fn fetch_with_cli(
1260 repo: &git2::Repository,
1261 url: &str,
1262 refspecs: &[String],
1263 tags: bool,
1264 shallow: gix::remote::fetch::Shallow,
1265 gctx: &GlobalContext,
1266) -> CargoResult<()> {
1267 debug!(target: "git-fetch", backend = "git-cli");
1268
1269 let Some(git_version) = git_version() else {
1270 anyhow::bail!(
1271 "`git` is not available
1272help: to still use `git` for fetching, please install it
1273help: to use Cargo's native git support, re-try with `net.git-fetch-with-cli = false`
1274https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
1275 );
1276 };
1277
1278 let mut cmd = ProcessBuilder::new("git");
1279 cmd.arg("-c").arg("core.fsmonitor=false");
1281 cmd.arg("-c").arg("advice.fetchShowForcedUpdates=false");
1283 let max_retries = gctx
1285 .net_config()?
1286 .retry
1287 .unwrap_or(network::retry::MAX_RETRY_DEFAULT);
1288 cmd.arg("-c").arg(format!("http.maxRetries={max_retries}"));
1289 cmd.arg("-c").arg("http.retryAfter=1"); cmd.arg("-c").arg(format!(
1291 "http.maxRetryTime={}",
1292 network::retry::MAX_RETRY_SLEEP_S
1293 ));
1294
1295 cmd.arg("fetch");
1296 if tags {
1297 cmd.arg("--tags");
1298 } else {
1299 cmd.arg("--no-tags");
1300 }
1301 if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1302 let depth = 0i32.saturating_add_unsigned(depth.get());
1303 cmd.arg(format!("--depth={depth}"));
1304 }
1305
1306 let progress_config = gctx.progress_config();
1307 let progress = match progress_config.when {
1308 ProgressWhen::Always => true,
1309 ProgressWhen::Never => false,
1310 ProgressWhen::Auto => {
1311 let width = progress_config
1313 .width
1314 .or_else(|| gctx.shell().err_width().progress_max_width());
1315 gctx.shell().progress_supported() && width.is_some()
1316 }
1317 };
1318 if gctx.shell().verbosity() == Verbosity::Verbose {
1319 cmd.arg("--verbose");
1320 } else if progress {
1321 cmd.arg("--progress");
1322 let min_version_porcelain = GitVersion {
1324 major: 2,
1325 minor: 41,
1326 patch: 0,
1327 };
1328 if min_version_porcelain <= git_version {
1329 cmd.arg("--porcelain").stdout(Stdio::Null);
1331 }
1332 } else {
1333 cmd.arg("--quiet");
1334 }
1335
1336 let min_version_no_show_forced_update = GitVersion {
1338 major: 2,
1339 minor: 23,
1340 patch: 0,
1341 };
1342 if min_version_no_show_forced_update <= git_version {
1343 cmd.arg("--no-show-forced-updates");
1345 }
1346
1347 cmd.arg("--force") .arg("--update-head-ok") .arg("--recurse-submodules=no") .arg(url)
1351 .args(refspecs)
1352 .env("GIT_DIR", repo.path())
1356 .env_remove("GIT_WORK_TREE")
1359 .env_remove("GIT_INDEX_FILE")
1360 .env_remove("GIT_OBJECT_DIRECTORY")
1361 .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
1362 .cwd(repo.path());
1363 gctx.shell()
1364 .verbose(|s| s.status("Running", &cmd.to_string()))?;
1365 let min_version_max_retries = GitVersion {
1367 major: 2,
1368 minor: 54,
1369 patch: 0,
1370 };
1371 network::retry::with_retry(gctx, || {
1372 cmd.exec().map_err(|_error| {
1373 let spurious = git_version < min_version_max_retries;
1374 let pr_hint = note_github_pull_request(url).unwrap_or_default();
1375 let ssh_config_hint = note_ssh_config(gctx).unwrap_or_default().unwrap_or_default();
1376 let git_config_hint = note_git_config(gctx).unwrap_or_default().unwrap_or_default();
1377 let with_depth = if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1378 format!(" with depth={depth}")
1379 } else {
1380 "".to_owned()
1381 };
1382 GitCliError::new(anyhow::format_err!("`git fetch` failed for {url}{with_depth}{pr_hint}{ssh_config_hint}{git_config_hint}"))
1383 .spurious(spurious)
1384 .workaround(
1385 "help: re-try with `net.git-fetch-with-cli = false` to see if it resolves the problem
1386https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
1387 )
1388 .into()
1389 })
1390 })?;
1391
1392 Ok(())
1393}
1394
1395#[tracing::instrument(skip(repo, gctx))]
1396fn fetch_with_gitoxide(
1397 repo: &mut git2::Repository,
1398 remote_url: &str,
1399 refspecs: Vec<String>,
1400 tags: bool,
1401 shallow: gix::remote::fetch::Shallow,
1402 gctx: &GlobalContext,
1403) -> CargoResult<()> {
1404 debug!(target: "git-fetch", backend = "gitoxide");
1405
1406 let git2_repo = repo;
1407 let config_overrides = cargo_config_to_gitoxide_overrides(gctx)?;
1408 let repo_reinitialized = AtomicBool::default();
1409 let res = oxide::with_retry_and_progress(
1410 git2_repo.path(),
1411 gctx,
1412 remote_url,
1413 &|repo_path,
1414 should_interrupt,
1415 mut progress,
1416 url_for_authentication: &mut dyn FnMut(&gix::bstr::BStr)| {
1417 loop {
1428 let res = oxide::open_repo(
1429 repo_path,
1430 config_overrides.clone(),
1431 oxide::OpenMode::ForFetch,
1432 )
1433 .map_err(crate::sources::git::fetch::Error::from)
1434 .and_then(|repo| {
1435 debug!("initiating fetch of {refspecs:?} from {remote_url}");
1436 let url_for_authentication = &mut *url_for_authentication;
1437 let remote = repo
1438 .remote_at(remote_url)?
1439 .with_fetch_tags(if tags {
1440 gix::remote::fetch::Tags::All
1441 } else {
1442 gix::remote::fetch::Tags::Included
1443 })
1444 .with_refspecs(
1445 refspecs.iter().map(|s| s.as_str()),
1446 gix::remote::Direction::Fetch,
1447 )
1448 .map_err(crate::sources::git::fetch::Error::Other)?;
1449 let url = remote
1450 .url(gix::remote::Direction::Fetch)
1451 .expect("set at init")
1452 .to_owned();
1453 let connection = remote.connect(gix::remote::Direction::Fetch)?;
1454 let mut authenticate = connection.configured_credentials(url)?;
1455 let connection = connection.with_credentials(
1456 move |action: gix::protocol::credentials::helper::Action| {
1457 if let Some(url) = action
1458 .context()
1459 .and_then(|gctx| gctx.url.as_ref().filter(|url| *url != remote_url))
1460 {
1461 url_for_authentication(url.as_ref());
1462 }
1463 authenticate(action)
1464 },
1465 );
1466 let outcome = connection
1467 .prepare_fetch(&mut progress, gix::remote::ref_map::Options::default())?
1468 .with_shallow(shallow.clone())
1469 .receive(&mut progress, should_interrupt)?;
1470 Ok(outcome)
1471 });
1472 let err = match res {
1473 Ok(_) => break,
1474 Err(e) => e,
1475 };
1476 debug!("fetch failed: {}", err);
1477
1478 if !repo_reinitialized.load(Ordering::Relaxed)
1479 && err.is_corrupted()
1484 || has_shallow_lock_file(&err)
1485 {
1486 repo_reinitialized.store(true, Ordering::Relaxed);
1487 debug!(
1488 "looks like this is a corrupt repository, reinitializing \
1489 and trying again"
1490 );
1491 if oxide::reinitialize(repo_path).is_ok() {
1492 continue;
1493 }
1494 }
1495
1496 return Err(err.into());
1497 }
1498 Ok(())
1499 },
1500 );
1501 if repo_reinitialized.load(Ordering::Relaxed) {
1502 *git2_repo = git2::Repository::open(git2_repo.path())?;
1503 }
1504 res
1505}
1506
1507#[tracing::instrument(skip(repo, gctx))]
1508fn fetch_with_libgit2(
1509 repo: &mut git2::Repository,
1510 remote_url: &str,
1511 refspecs: Vec<String>,
1512 tags: bool,
1513 shallow: gix::remote::fetch::Shallow,
1514 gctx: &GlobalContext,
1515) -> CargoResult<()> {
1516 debug!(target: "git-fetch", backend = "libgit2");
1517
1518 static INIT: Once = Once::new();
1519 INIT.call_once(|| {
1520 unsafe { init_git_transports(gctx) };
1534 });
1535
1536 let git_config = git2::Config::open_default()?;
1537 with_fetch_options(&git_config, remote_url, gctx, &mut |mut opts| {
1538 if tags {
1539 opts.download_tags(git2::AutotagOption::All);
1540 }
1541 if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1542 opts.depth(0i32.saturating_add_unsigned(depth.get()));
1543 }
1544 let mut repo_reinitialized = false;
1555 loop {
1556 debug!("initiating fetch of {refspecs:?} from {remote_url}");
1557 let res = repo
1558 .remote_anonymous(remote_url)?
1559 .fetch(&refspecs, Some(&mut opts), None);
1560 let err = match res {
1561 Ok(()) => break,
1562 Err(e) => e,
1563 };
1564 debug!("fetch failed: {}", err);
1565
1566 if !repo_reinitialized && matches!(err.class(), ErrorClass::Reference | ErrorClass::Odb)
1567 {
1568 repo_reinitialized = true;
1569 debug!(
1570 "looks like this is a corrupt repository, reinitializing \
1571 and trying again"
1572 );
1573 if reinitialize(repo).is_ok() {
1574 continue;
1575 }
1576 }
1577
1578 return Err(err.into());
1579 }
1580 Ok(())
1581 })
1582}
1583
1584#[tracing::instrument(skip_all)]
1594unsafe fn init_git_transports(gctx: &GlobalContext) {
1595 match needs_custom_http_transport(gctx) {
1596 Ok(true) => {}
1597 _ => return,
1598 }
1599
1600 let handle = match http_handle(gctx) {
1601 Ok(handle) => handle,
1602 Err(..) => return,
1603 };
1604
1605 unsafe {
1606 git2_curl::register(handle);
1607 }
1608}
1609
1610fn maybe_gc_repo(repo: &mut git2::Repository, gctx: &GlobalContext) -> CargoResult<()> {
1628 let mut cmd = Command::new("git");
1636 if let Ok(limit) = gctx.get_env("__CARGO_PACKFILE_LIMIT") {
1637 cmd.arg(format!("-c gc.autoPackLimit={}", limit));
1638 }
1639 cmd.arg("gc")
1640 .arg("--auto")
1641 .env("GIT_DIR", repo.path())
1643 .current_dir(repo.path());
1644
1645 match cmd.output() {
1646 Ok(out) => {
1647 debug!(
1648 "git-gc --auto status: {}\n\nstdout ---\n{}\nstderr ---\n{}",
1649 out.status,
1650 String::from_utf8_lossy(&out.stdout),
1651 String::from_utf8_lossy(&out.stderr)
1652 );
1653 if out.status.success() {
1654 let new = git2::Repository::open(repo.path())?;
1655 *repo = new;
1656 return Ok(());
1657 }
1658 }
1659 Err(e) => debug!("git-gc --auto failed to spawn: {}", e),
1660 }
1661
1662 reinitialize(repo)
1664}
1665
1666fn clean_repo_temp_files(repo: &git2::Repository) {
1681 let path = repo.path().join("objects/pack/pack_git2_*");
1682 let Some(pattern) = path.to_str() else {
1683 tracing::warn!("cannot convert {path:?} to a string");
1684 return;
1685 };
1686 let Ok(paths) = glob::glob(pattern) else {
1687 return;
1688 };
1689 for path in paths {
1690 if let Ok(path) = path {
1691 match paths::remove_file(&path) {
1692 Ok(_) => tracing::debug!("removed stale temp git file {path:?}"),
1693 Err(e) => {
1694 tracing::warn!("failed to remove {path:?} while cleaning temp files: {e}")
1695 }
1696 }
1697 }
1698 }
1699}
1700
1701fn reinitialize(repo: &mut git2::Repository) -> CargoResult<()> {
1704 let path = repo.path().to_path_buf();
1709 debug!("reinitializing git repo at {:?}", path);
1710 let tmp = path.join("tmp");
1711 let bare = !repo.path().ends_with(".git");
1712 *repo = init(&tmp, false)?;
1713 for entry in path.read_dir()? {
1714 let entry = entry?;
1715 if entry.file_name().to_str() == Some("tmp") {
1716 continue;
1717 }
1718 let path = entry.path();
1719 drop(paths::remove_file(&path).or_else(|_| paths::remove_dir_all(&path)));
1720 }
1721 *repo = init(&path, bare)?;
1722 paths::remove_dir_all(&tmp)?;
1723 Ok(())
1724}
1725
1726fn init(path: &Path, bare: bool) -> CargoResult<git2::Repository> {
1728 let mut opts = git2::RepositoryInitOptions::new();
1729 opts.external_template(false);
1733 opts.bare(bare);
1734 Ok(git2::Repository::init_opts(&path, &opts)?)
1735}
1736
1737enum FastPathRev {
1739 UpToDate,
1742 NeedsFetch(Oid),
1745 Indeterminate,
1748}
1749
1750#[tracing::instrument(skip(repo, gctx))]
1764fn github_fast_path(
1765 repo: &git2::Repository,
1766 url: &str,
1767 reference: &GitReference,
1768 gctx: &GlobalContext,
1769) -> CargoResult<FastPathRev> {
1770 let url = Url::parse(url)?;
1771 if !is_github(&url) {
1772 return Ok(FastPathRev::Indeterminate);
1773 }
1774
1775 let local_object = resolve_ref(reference, repo).ok();
1776
1777 let github_branch_name = match reference {
1778 GitReference::Branch(branch) => branch,
1779 GitReference::Tag(tag) => tag,
1780 GitReference::DefaultBranch => "HEAD",
1781 GitReference::Rev(rev) => {
1782 if rev.starts_with("refs/") {
1783 rev
1784 } else if looks_like_commit_hash(rev) {
1785 if let Some(local_object) = local_object {
1803 if is_short_hash_of(rev, local_object) {
1804 debug!("github fast path already has {local_object}");
1805 return Ok(FastPathRev::UpToDate);
1806 }
1807 }
1808 if let Some(oid) = rev_to_oid(rev) {
1813 debug!("github fast path is already a full commit hash {rev}");
1814 return Ok(FastPathRev::NeedsFetch(oid));
1815 }
1816 rev
1817 } else {
1818 debug!("can't use github fast path with `rev = \"{}\"`", rev);
1819 return Ok(FastPathRev::Indeterminate);
1820 }
1821 }
1822 };
1823
1824 let mut pieces = url
1827 .path_segments()
1828 .ok_or_else(|| anyhow!("no path segments on url"))?;
1829 let username = pieces
1830 .next()
1831 .ok_or_else(|| anyhow!("couldn't find username"))?;
1832 let repository = pieces
1833 .next()
1834 .ok_or_else(|| anyhow!("couldn't find repository name"))?;
1835 if pieces.next().is_some() {
1836 anyhow::bail!("too many segments on URL");
1837 }
1838
1839 let repository = repository.strip_suffix(".git").unwrap_or(repository);
1842
1843 let url = format!(
1844 "https://api.github.com/repos/{}/{}/commits/{}",
1845 username, repository, github_branch_name,
1846 );
1847 debug!("attempting GitHub fast path for {}", url);
1848 let mut request =
1849 Request::get(url).header(http::header::ACCEPT, "application/vnd.github.3.sha");
1850 if let Some(local_object) = local_object {
1851 request = request.header(http::header::IF_NONE_MATCH, &format!("\"{local_object}\""));
1852 }
1853 let response = gctx
1854 .http_async()?
1855 .request_blocking(request.body(Vec::new())?)?;
1856 let response_code = response.status();
1857 if response_code == StatusCode::NOT_MODIFIED {
1858 debug!("github fast path up-to-date");
1859 Ok(FastPathRev::UpToDate)
1860 } else if response_code == StatusCode::OK
1861 && let Some(oid_to_fetch) = rev_to_oid(str::from_utf8(&response.body())?)
1862 {
1863 debug!("github fast path fetch {oid_to_fetch}");
1865 Ok(FastPathRev::NeedsFetch(oid_to_fetch))
1866 } else {
1867 debug!("github fast path bad response code {response_code}");
1871 Ok(FastPathRev::Indeterminate)
1872 }
1873}
1874
1875fn is_github(url: &Url) -> bool {
1877 url.host_str() == Some("github.com")
1878}
1879
1880pub(crate) fn note_github_pull_request(url: &str) -> Option<String> {
1882 if let Ok(url) = url.parse::<Url>()
1883 && is_github(&url)
1884 {
1885 let path_segments = url
1886 .path_segments()
1887 .map(|p| p.into_iter().collect::<Vec<_>>())
1888 .unwrap_or_default();
1889 if let [owner, repo, "pull", pr_number, ..] = path_segments[..] {
1890 let repo_url = format!("https://github.com/{owner}/{repo}.git");
1891 let rev = format!("refs/pull/{pr_number}/head");
1892 return Some(format!(
1893 concat!(
1894 "\n\nnote: GitHub url {} is not a repository\n",
1895 "help: to specify a pull request as a dependency, replace the dependency with:\n",
1896 " `git = \"{}\" rev = \"{}\"`",
1897 ),
1898 url, repo_url, rev
1899 ));
1900 }
1901 }
1902
1903 None
1904}
1905
1906pub(crate) fn note_ssh_config(gctx: &GlobalContext) -> CargoResult<Option<&'static str>> {
1907 let net_config = gctx.net_config()?;
1908 if let Some(ssh_config) = net_config.ssh.as_ref()
1909 && ssh_config.known_hosts.is_some()
1910 {
1911 Ok(Some(
1912 "
1913note: `cargo`s `net.ssh.known-hosts` is not applied to `git`, check your system SSH configuration to ensure it is set there",
1914 ))
1915 } else {
1916 Ok(None)
1917 }
1918}
1919
1920pub(crate) fn note_git_config(gctx: &GlobalContext) -> CargoResult<Option<String>> {
1921 let mut fields = Vec::new();
1922
1923 let http_config = gctx.http_config()?;
1925 if http_config.proxy.is_some() {
1926 fields.push("http.proxy (git: http.proxy)");
1927 }
1928 if http_config.low_speed_limit.is_some() {
1929 fields.push("http.low-speed-limit (git: http.lowSpeedLimit)");
1930 }
1931 if http_config.timeout.is_some() {
1932 fields.push("http.timeout (git: http.lowSpeedTime)");
1933 }
1934 if http_config.cainfo.is_some() {
1935 fields.push("http.cainfo (git: http.sslCAInfo)");
1936 }
1937 if http_config.proxy_cainfo.is_some() {
1938 fields.push("http.proxy-cainfo (git: http.proxySSLCAInfo)");
1939 }
1940 if http_config.check_revoke.is_some() {
1941 fields.push("http.check-revoke (git: http.schannelCheckRevoke)");
1942 }
1943 if http_config.multiplexing.is_some() {
1944 fields.push("http.multiplexing (git: http.version)");
1945 }
1946 if http_config.ssl_version.is_some() {
1947 fields.push("http.ssl-version (git: http.sslVersion)");
1948 }
1949
1950 if !fields.is_empty() {
1951 Ok(Some(format!("
1952note: `cargo` does not apply the following config to `git`, check your git configuration to ensure they are set there
1953 {}", fields.join("\n "))))
1954 } else {
1955 Ok(None)
1956 }
1957}
1958
1959fn looks_like_commit_hash(rev: &str) -> bool {
1961 rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit())
1962}
1963
1964fn is_short_hash_of(rev: &str, oid: Oid) -> bool {
1966 let long_hash = oid.to_string();
1967 match long_hash.get(..rev.len()) {
1968 Some(truncated_long_hash) => truncated_long_hash.eq_ignore_ascii_case(rev),
1969 None => false,
1970 }
1971}
1972
1973#[cfg(test)]
1974mod tests {
1975 use super::*;
1976
1977 #[test]
1978 fn github_fast_path_full_hash_returns_needs_fetch() {
1979 let temp_dir = tempfile::TempDir::new().unwrap();
1980 let repo = git2::Repository::init_bare(temp_dir.path()).unwrap();
1981 let full_hash = "c9040898c9183ddbb9402dcbf749ed06d6ea90ad";
1982 let reference = GitReference::Rev(full_hash.to_string());
1983 let gctx = GlobalContext::default().unwrap();
1984 let expected_oid = rev_to_oid(full_hash).unwrap();
1985
1986 let result =
1987 github_fast_path(&repo, "https://github.com/user/repo", &reference, &gctx).unwrap();
1988
1989 assert!(matches!(result, FastPathRev::NeedsFetch(oid) if oid == expected_oid));
1990 }
1991
1992 #[test]
1993 fn test_absolute_submodule_url() {
1994 let cases = [
1995 (
1996 "ssh://git@gitub.com/rust-lang/cargo",
1997 "git@github.com:rust-lang/cargo.git",
1998 "git@github.com:rust-lang/cargo.git",
1999 ),
2000 (
2001 "ssh://git@gitub.com/rust-lang/cargo",
2002 "./",
2003 "ssh://git@gitub.com/rust-lang/cargo/",
2004 ),
2005 (
2006 "ssh://git@gitub.com/rust-lang/cargo",
2007 "../",
2008 "ssh://git@gitub.com/rust-lang/",
2009 ),
2010 (
2011 "ssh://git@gitub.com/rust-lang/cargo",
2012 "./foo",
2013 "ssh://git@gitub.com/rust-lang/cargo/foo",
2014 ),
2015 (
2016 "ssh://git@gitub.com/rust-lang/cargo/",
2017 "./foo",
2018 "ssh://git@gitub.com/rust-lang/cargo/foo",
2019 ),
2020 (
2021 "ssh://git@gitub.com/rust-lang/cargo/",
2022 "../foo",
2023 "ssh://git@gitub.com/rust-lang/foo",
2024 ),
2025 (
2026 "ssh://git@gitub.com/rust-lang/cargo",
2027 "../foo",
2028 "ssh://git@gitub.com/rust-lang/foo",
2029 ),
2030 (
2031 "ssh://git@gitub.com/rust-lang/cargo",
2032 "../foo/bar/../baz",
2033 "ssh://git@gitub.com/rust-lang/foo/baz",
2034 ),
2035 (
2036 "git@github.com:rust-lang/cargo.git",
2037 "ssh://git@gitub.com/rust-lang/cargo",
2038 "ssh://git@gitub.com/rust-lang/cargo",
2039 ),
2040 (
2041 "git@github.com:rust-lang/cargo.git",
2042 "./",
2043 "git@github.com:rust-lang/cargo.git/./",
2044 ),
2045 (
2046 "git@github.com:rust-lang/cargo.git",
2047 "../",
2048 "git@github.com:rust-lang/cargo.git/../",
2049 ),
2050 (
2051 "git@github.com:rust-lang/cargo.git",
2052 "./foo",
2053 "git@github.com:rust-lang/cargo.git/./foo",
2054 ),
2055 (
2056 "git@github.com:rust-lang/cargo.git/",
2057 "./foo",
2058 "git@github.com:rust-lang/cargo.git/./foo",
2059 ),
2060 (
2061 "git@github.com:rust-lang/cargo.git",
2062 "../foo",
2063 "git@github.com:rust-lang/cargo.git/../foo",
2064 ),
2065 (
2066 "git@github.com:rust-lang/cargo.git/",
2067 "../foo",
2068 "git@github.com:rust-lang/cargo.git/../foo",
2069 ),
2070 (
2071 "git@github.com:rust-lang/cargo.git",
2072 "../foo/bar/../baz",
2073 "git@github.com:rust-lang/cargo.git/../foo/bar/../baz",
2074 ),
2075 ];
2076
2077 for (base_url, submodule_url, expected) in cases {
2078 let url = absolute_submodule_url(base_url, submodule_url).unwrap();
2079 assert_eq!(
2080 expected, url,
2081 "base `{base_url}`; submodule `{submodule_url}`"
2082 );
2083 }
2084 }
2085}
2086
2087pub(super) fn rev_to_oid(rev: &str) -> Option<Oid> {
2098 Oid::from_str(rev)
2099 .ok()
2100 .filter(|oid| oid.as_bytes().len() * 2 == rev.len())
2101}
2102
2103#[cfg(test)]
2104mod test {
2105 use super::*;
2106
2107 #[test]
2108 fn git_version_empty() {
2109 let input = "";
2110 GitVersion::from_version_stdout(input).unwrap_err();
2111 }
2112
2113 #[test]
2114 fn git_version_version() {
2115 let input = "git version 2.19.0";
2116 let actual = GitVersion::from_version_stdout(input).unwrap();
2117 let expected = GitVersion {
2118 major: 2,
2119 minor: 19,
2120 patch: 0,
2121 };
2122 assert_eq!(actual, expected);
2123 }
2124
2125 #[test]
2126 fn git_version_version_nl() {
2127 let input = "git version 2.19.0\n";
2128 let actual = GitVersion::from_version_stdout(input).unwrap();
2129 let expected = GitVersion {
2130 major: 2,
2131 minor: 19,
2132 patch: 0,
2133 };
2134 assert_eq!(actual, expected);
2135 }
2136
2137 #[test]
2138 fn git_version_version_platform() {
2139 let input = "git version 2.19.0 (Apple Git-117)";
2140 let actual = GitVersion::from_version_stdout(input).unwrap();
2141 let expected = GitVersion {
2142 major: 2,
2143 minor: 19,
2144 patch: 0,
2145 };
2146 assert_eq!(actual, expected);
2147 }
2148
2149 #[test]
2150 fn git_version_version_vendor() {
2151 let input = "git version 2.19.0.windows.1";
2152 let actual = GitVersion::from_version_stdout(input).unwrap();
2153 let expected = GitVersion {
2154 major: 2,
2155 minor: 18,
2156 patch: 0,
2157 };
2158 assert_eq!(actual, expected);
2159 }
2160
2161 #[test]
2162 fn git_version_version_rc() {
2163 let input = "git version 2.19.0.rc0";
2164 let actual = GitVersion::from_version_stdout(input).unwrap();
2165 let expected = GitVersion {
2166 major: 2,
2167 minor: 18,
2168 patch: 0,
2169 };
2170 assert_eq!(actual, expected);
2171 }
2172
2173 #[test]
2174 fn git_version_version_unreleased() {
2175 let input = "git version 2.55.GIT";
2176 let actual = GitVersion::from_version_stdout(input).unwrap();
2177 let expected = GitVersion {
2178 major: 2,
2179 minor: 55,
2180 patch: 0,
2181 };
2182 assert_eq!(actual, expected);
2183 }
2184}