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 let Some(true) = gctx.net_config()?.git_fetch_with_cli {
1106 fetch_with_cli(repo, remote_url, &refspecs, tags, shallow, gctx)
1107 } else if gctx.cli_unstable().gitoxide.map_or(false, |git| git.fetch) {
1108 fetch_with_gitoxide(repo, remote_url, refspecs, tags, shallow, gctx)
1109 } else {
1110 fetch_with_libgit2(repo, remote_url, refspecs, tags, shallow, gctx)
1111 };
1112
1113 if fast_path_rev {
1114 if let Some(oid) = oid_to_fetch {
1115 return result.with_context(|| format!("revision {} not found", oid));
1116 }
1117 }
1118 result
1119}
1120
1121fn has_shallow_lock_file(err: &crate::sources::git::fetch::Error) -> bool {
1126 matches!(
1127 err,
1128 gix::env::collate::fetch::Error::Fetch(gix::remote::fetch::Error::Fetch(
1129 gix::protocol::fetch::Error::LockShallowFile(_)
1130 ))
1131 )
1132}
1133
1134fn git_version() -> Option<GitVersion> {
1135 #[tracing::instrument(skip_all)]
1136 fn git_version() -> Option<GitVersion> {
1137 use std::process::Stdio;
1138
1139 let output = std::process::Command::new("git")
1140 .arg("--version")
1141 .stdin(Stdio::null())
1142 .output()
1143 .ok()?;
1144 if !output.status.success() {
1145 return None;
1146 }
1147 let Ok(stdout) = String::from_utf8(output.stdout) else {
1148 return None;
1149 };
1150 let default_version = GitVersion {
1152 major: 2,
1153 minor: 0,
1154 patch: 0,
1155 };
1156 Some(GitVersion::from_version_stdout(&stdout).unwrap_or(default_version))
1157 }
1158
1159 static CACHE: std::sync::OnceLock<Option<GitVersion>> = std::sync::OnceLock::new();
1160 *CACHE.get_or_init(git_version)
1161}
1162
1163#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1164#[allow(unused)]
1165struct GitVersion {
1166 major: usize,
1167 minor: usize,
1168 patch: usize,
1169}
1170
1171impl GitVersion {
1172 fn from_version_stdout(stdout: &str) -> Result<Self, anyhow::Error> {
1173 let Some(version) = stdout.strip_prefix("git version ") else {
1175 anyhow::bail!("unrecognized `git --version` output: {stdout}")
1176 };
1177 let (version, _) = version.split_once(" ").unwrap_or((version, ""));
1178 let (version, _) = version.split_once("\n").unwrap_or((version, ""));
1179 version.parse()
1180 }
1181}
1182
1183impl std::str::FromStr for GitVersion {
1184 type Err = anyhow::Error;
1185
1186 fn from_str(version: &str) -> Result<Self, Self::Err> {
1187 let unreleased = "GIT";
1188
1189 let s = version;
1190 let (major, s) = s.split_once(".").unwrap_or((s, ""));
1191 let mut major: usize = major.parse().map_err(|_err| {
1192 anyhow::format_err!("unrecognized major version `{major}` in `{version}`")
1193 })?;
1194 let (minor, s) = s.split_once(".").unwrap_or((s, ""));
1195 let mut minor: usize = if minor == unreleased {
1196 0
1197 } else {
1198 minor.parse().map_err(|_err| {
1199 anyhow::format_err!("unrecognized minor version `{minor}` in `{version}`")
1200 })?
1201 };
1202 let (patch, s) = s.split_once(".").unwrap_or((s, ""));
1203 let mut patch: usize = if patch == unreleased {
1204 0
1205 } else {
1206 patch.parse().map_err(|_err| {
1207 anyhow::format_err!("unrecognized patch version `{patch}` in `{version}`")
1208 })?
1209 };
1210
1211 let more = s;
1212 if !more.is_empty() {
1213 if let Some(sub) = patch.checked_sub(1) {
1216 patch = sub;
1217 } else if let Some(sub) = minor.checked_sub(1) {
1218 minor = sub;
1219 } else if let Some(sub) = major.checked_sub(1) {
1220 major = sub;
1221 }
1222 }
1223
1224 Ok(Self {
1225 major,
1226 minor,
1227 patch,
1228 })
1229 }
1230}
1231
1232#[tracing::instrument(skip(repo, gctx))]
1244fn fetch_with_cli(
1245 repo: &git2::Repository,
1246 url: &str,
1247 refspecs: &[String],
1248 tags: bool,
1249 shallow: gix::remote::fetch::Shallow,
1250 gctx: &GlobalContext,
1251) -> CargoResult<()> {
1252 debug!(target: "git-fetch", backend = "git-cli");
1253
1254 let Some(git_version) = git_version() else {
1255 anyhow::bail!(
1256 "`git` is not available
1257help: to still use `git` for fetching, please install it
1258help: to use Cargo's native git support, re-try with `net.git-fetch-with-cli = false`
1259https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
1260 );
1261 };
1262
1263 let mut cmd = ProcessBuilder::new("git");
1264 cmd.arg("-c").arg("core.fsmonitor=false");
1266 cmd.arg("-c").arg("advice.fetchShowForcedUpdates=false");
1268 let max_retries = gctx
1270 .net_config()?
1271 .retry
1272 .unwrap_or(network::retry::MAX_RETRY_DEFAULT);
1273 cmd.arg("-c").arg(format!("http.maxRetries={max_retries}"));
1274 cmd.arg("-c").arg("http.retryAfter=1"); cmd.arg("-c").arg(format!(
1276 "http.maxRetryTime={}",
1277 network::retry::MAX_RETRY_SLEEP_S
1278 ));
1279
1280 cmd.arg("fetch");
1281 if tags {
1282 cmd.arg("--tags");
1283 } else {
1284 cmd.arg("--no-tags");
1285 }
1286 if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1287 let depth = 0i32.saturating_add_unsigned(depth.get());
1288 cmd.arg(format!("--depth={depth}"));
1289 }
1290
1291 let progress_config = gctx.progress_config();
1292 let progress = match progress_config.when {
1293 ProgressWhen::Always => true,
1294 ProgressWhen::Never => false,
1295 ProgressWhen::Auto => {
1296 let width = progress_config
1298 .width
1299 .or_else(|| gctx.shell().err_width().progress_max_width());
1300 gctx.shell().progress_supported() && width.is_some()
1301 }
1302 };
1303 if gctx.shell().verbosity() == Verbosity::Verbose {
1304 cmd.arg("--verbose");
1305 } else if progress {
1306 cmd.arg("--progress");
1307 let min_version_porcelain = GitVersion {
1309 major: 2,
1310 minor: 41,
1311 patch: 0,
1312 };
1313 if min_version_porcelain <= git_version {
1314 cmd.arg("--porcelain").stdout(Stdio::Null);
1316 }
1317 } else {
1318 cmd.arg("--quiet");
1319 }
1320
1321 let min_version_no_show_forced_update = GitVersion {
1323 major: 2,
1324 minor: 23,
1325 patch: 0,
1326 };
1327 if min_version_no_show_forced_update <= git_version {
1328 cmd.arg("--no-show-forced-updates");
1330 }
1331
1332 cmd.arg("--force") .arg("--update-head-ok") .arg("--recurse-submodules=no") .arg(url)
1336 .args(refspecs)
1337 .env("GIT_DIR", repo.path())
1341 .env_remove("GIT_WORK_TREE")
1344 .env_remove("GIT_INDEX_FILE")
1345 .env_remove("GIT_OBJECT_DIRECTORY")
1346 .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
1347 .cwd(repo.path());
1348 gctx.shell()
1349 .verbose(|s| s.status("Running", &cmd.to_string()))?;
1350 let min_version_max_retries = GitVersion {
1352 major: 2,
1353 minor: 54,
1354 patch: 0,
1355 };
1356 network::retry::with_retry(gctx, || {
1357 cmd.exec().map_err(|_error| {
1358 let spurious = git_version < min_version_max_retries;
1359 let pr_hint = note_github_pull_request(url).unwrap_or_default();
1360 let ssh_config_hint = note_ssh_config(gctx).unwrap_or_default().unwrap_or_default();
1361 let git_config_hint = note_git_config(gctx).unwrap_or_default().unwrap_or_default();
1362 let with_depth = if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1363 format!(" with depth={depth}")
1364 } else {
1365 "".to_owned()
1366 };
1367 GitCliError::new(anyhow::format_err!("`git fetch` failed for {url}{with_depth}{pr_hint}{ssh_config_hint}{git_config_hint}"))
1368 .spurious(spurious)
1369 .workaround(
1370 "help: re-try with `net.git-fetch-with-cli = false` to see if it resolves the problem
1371https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
1372 )
1373 .into()
1374 })
1375 })?;
1376
1377 Ok(())
1378}
1379
1380#[tracing::instrument(skip(repo, gctx))]
1381fn fetch_with_gitoxide(
1382 repo: &mut git2::Repository,
1383 remote_url: &str,
1384 refspecs: Vec<String>,
1385 tags: bool,
1386 shallow: gix::remote::fetch::Shallow,
1387 gctx: &GlobalContext,
1388) -> CargoResult<()> {
1389 debug!(target: "git-fetch", backend = "gitoxide");
1390
1391 let git2_repo = repo;
1392 let config_overrides = cargo_config_to_gitoxide_overrides(gctx)?;
1393 let repo_reinitialized = AtomicBool::default();
1394 let res = oxide::with_retry_and_progress(
1395 git2_repo.path(),
1396 gctx,
1397 remote_url,
1398 &|repo_path,
1399 should_interrupt,
1400 mut progress,
1401 url_for_authentication: &mut dyn FnMut(&gix::bstr::BStr)| {
1402 loop {
1413 let res = oxide::open_repo(
1414 repo_path,
1415 config_overrides.clone(),
1416 oxide::OpenMode::ForFetch,
1417 )
1418 .map_err(crate::sources::git::fetch::Error::from)
1419 .and_then(|repo| {
1420 debug!("initiating fetch of {refspecs:?} from {remote_url}");
1421 let url_for_authentication = &mut *url_for_authentication;
1422 let remote = repo
1423 .remote_at(remote_url)?
1424 .with_fetch_tags(if tags {
1425 gix::remote::fetch::Tags::All
1426 } else {
1427 gix::remote::fetch::Tags::Included
1428 })
1429 .with_refspecs(
1430 refspecs.iter().map(|s| s.as_str()),
1431 gix::remote::Direction::Fetch,
1432 )
1433 .map_err(crate::sources::git::fetch::Error::Other)?;
1434 let url = remote
1435 .url(gix::remote::Direction::Fetch)
1436 .expect("set at init")
1437 .to_owned();
1438 let connection = remote.connect(gix::remote::Direction::Fetch)?;
1439 let mut authenticate = connection.configured_credentials(url)?;
1440 let connection = connection.with_credentials(
1441 move |action: gix::protocol::credentials::helper::Action| {
1442 if let Some(url) = action
1443 .context()
1444 .and_then(|gctx| gctx.url.as_ref().filter(|url| *url != remote_url))
1445 {
1446 url_for_authentication(url.as_ref());
1447 }
1448 authenticate(action)
1449 },
1450 );
1451 let outcome = connection
1452 .prepare_fetch(&mut progress, gix::remote::ref_map::Options::default())?
1453 .with_shallow(shallow.clone())
1454 .receive(&mut progress, should_interrupt)?;
1455 Ok(outcome)
1456 });
1457 let err = match res {
1458 Ok(_) => break,
1459 Err(e) => e,
1460 };
1461 debug!("fetch failed: {}", err);
1462
1463 if !repo_reinitialized.load(Ordering::Relaxed)
1464 && err.is_corrupted()
1469 || has_shallow_lock_file(&err)
1470 {
1471 repo_reinitialized.store(true, Ordering::Relaxed);
1472 debug!(
1473 "looks like this is a corrupt repository, reinitializing \
1474 and trying again"
1475 );
1476 if oxide::reinitialize(repo_path).is_ok() {
1477 continue;
1478 }
1479 }
1480
1481 return Err(err.into());
1482 }
1483 Ok(())
1484 },
1485 );
1486 if repo_reinitialized.load(Ordering::Relaxed) {
1487 *git2_repo = git2::Repository::open(git2_repo.path())?;
1488 }
1489 res
1490}
1491
1492#[tracing::instrument(skip(repo, gctx))]
1493fn fetch_with_libgit2(
1494 repo: &mut git2::Repository,
1495 remote_url: &str,
1496 refspecs: Vec<String>,
1497 tags: bool,
1498 shallow: gix::remote::fetch::Shallow,
1499 gctx: &GlobalContext,
1500) -> CargoResult<()> {
1501 debug!(target: "git-fetch", backend = "libgit2");
1502
1503 static INIT: Once = Once::new();
1504 INIT.call_once(|| {
1505 unsafe { init_git_transports(gctx) };
1519 });
1520
1521 let git_config = git2::Config::open_default()?;
1522 with_fetch_options(&git_config, remote_url, gctx, &mut |mut opts| {
1523 if tags {
1524 opts.download_tags(git2::AutotagOption::All);
1525 }
1526 if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1527 opts.depth(0i32.saturating_add_unsigned(depth.get()));
1528 }
1529 let mut repo_reinitialized = false;
1540 loop {
1541 debug!("initiating fetch of {refspecs:?} from {remote_url}");
1542 let res = repo
1543 .remote_anonymous(remote_url)?
1544 .fetch(&refspecs, Some(&mut opts), None);
1545 let err = match res {
1546 Ok(()) => break,
1547 Err(e) => e,
1548 };
1549 debug!("fetch failed: {}", err);
1550
1551 if !repo_reinitialized && matches!(err.class(), ErrorClass::Reference | ErrorClass::Odb)
1552 {
1553 repo_reinitialized = true;
1554 debug!(
1555 "looks like this is a corrupt repository, reinitializing \
1556 and trying again"
1557 );
1558 if reinitialize(repo).is_ok() {
1559 continue;
1560 }
1561 }
1562
1563 return Err(err.into());
1564 }
1565 Ok(())
1566 })
1567}
1568
1569#[tracing::instrument(skip_all)]
1579unsafe fn init_git_transports(gctx: &GlobalContext) {
1580 match needs_custom_http_transport(gctx) {
1581 Ok(true) => {}
1582 _ => return,
1583 }
1584
1585 let handle = match http_handle(gctx) {
1586 Ok(handle) => handle,
1587 Err(..) => return,
1588 };
1589
1590 unsafe {
1591 git2_curl::register(handle);
1592 }
1593}
1594
1595fn maybe_gc_repo(repo: &mut git2::Repository, gctx: &GlobalContext) -> CargoResult<()> {
1613 let mut cmd = Command::new("git");
1621 if let Ok(limit) = gctx.get_env("__CARGO_PACKFILE_LIMIT") {
1622 cmd.arg(format!("-c gc.autoPackLimit={}", limit));
1623 }
1624 cmd.arg("gc")
1625 .arg("--auto")
1626 .env("GIT_DIR", repo.path())
1628 .current_dir(repo.path());
1629
1630 match cmd.output() {
1631 Ok(out) => {
1632 debug!(
1633 "git-gc --auto status: {}\n\nstdout ---\n{}\nstderr ---\n{}",
1634 out.status,
1635 String::from_utf8_lossy(&out.stdout),
1636 String::from_utf8_lossy(&out.stderr)
1637 );
1638 if out.status.success() {
1639 let new = git2::Repository::open(repo.path())?;
1640 *repo = new;
1641 return Ok(());
1642 }
1643 }
1644 Err(e) => debug!("git-gc --auto failed to spawn: {}", e),
1645 }
1646
1647 reinitialize(repo)
1649}
1650
1651fn clean_repo_temp_files(repo: &git2::Repository) {
1666 let path = repo.path().join("objects/pack/pack_git2_*");
1667 let Some(pattern) = path.to_str() else {
1668 tracing::warn!("cannot convert {path:?} to a string");
1669 return;
1670 };
1671 let Ok(paths) = glob::glob(pattern) else {
1672 return;
1673 };
1674 for path in paths {
1675 if let Ok(path) = path {
1676 match paths::remove_file(&path) {
1677 Ok(_) => tracing::debug!("removed stale temp git file {path:?}"),
1678 Err(e) => {
1679 tracing::warn!("failed to remove {path:?} while cleaning temp files: {e}")
1680 }
1681 }
1682 }
1683 }
1684}
1685
1686fn reinitialize(repo: &mut git2::Repository) -> CargoResult<()> {
1689 let path = repo.path().to_path_buf();
1694 debug!("reinitializing git repo at {:?}", path);
1695 let tmp = path.join("tmp");
1696 let bare = !repo.path().ends_with(".git");
1697 *repo = init(&tmp, false)?;
1698 for entry in path.read_dir()? {
1699 let entry = entry?;
1700 if entry.file_name().to_str() == Some("tmp") {
1701 continue;
1702 }
1703 let path = entry.path();
1704 drop(paths::remove_file(&path).or_else(|_| paths::remove_dir_all(&path)));
1705 }
1706 *repo = init(&path, bare)?;
1707 paths::remove_dir_all(&tmp)?;
1708 Ok(())
1709}
1710
1711fn init(path: &Path, bare: bool) -> CargoResult<git2::Repository> {
1713 let mut opts = git2::RepositoryInitOptions::new();
1714 opts.external_template(false);
1718 opts.bare(bare);
1719 Ok(git2::Repository::init_opts(&path, &opts)?)
1720}
1721
1722enum FastPathRev {
1724 UpToDate,
1727 NeedsFetch(Oid),
1730 Indeterminate,
1733}
1734
1735#[tracing::instrument(skip(repo, gctx))]
1749fn github_fast_path(
1750 repo: &git2::Repository,
1751 url: &str,
1752 reference: &GitReference,
1753 gctx: &GlobalContext,
1754) -> CargoResult<FastPathRev> {
1755 let url = Url::parse(url)?;
1756 if !is_github(&url) {
1757 return Ok(FastPathRev::Indeterminate);
1758 }
1759
1760 let local_object = resolve_ref(reference, repo).ok();
1761
1762 let github_branch_name = match reference {
1763 GitReference::Branch(branch) => branch,
1764 GitReference::Tag(tag) => tag,
1765 GitReference::DefaultBranch => "HEAD",
1766 GitReference::Rev(rev) => {
1767 if rev.starts_with("refs/") {
1768 rev
1769 } else if looks_like_commit_hash(rev) {
1770 if let Some(local_object) = local_object {
1788 if is_short_hash_of(rev, local_object) {
1789 debug!("github fast path already has {local_object}");
1790 return Ok(FastPathRev::UpToDate);
1791 }
1792 }
1793 if let Some(oid) = rev_to_oid(rev) {
1798 debug!("github fast path is already a full commit hash {rev}");
1799 return Ok(FastPathRev::NeedsFetch(oid));
1800 }
1801 rev
1802 } else {
1803 debug!("can't use github fast path with `rev = \"{}\"`", rev);
1804 return Ok(FastPathRev::Indeterminate);
1805 }
1806 }
1807 };
1808
1809 let mut pieces = url
1812 .path_segments()
1813 .ok_or_else(|| anyhow!("no path segments on url"))?;
1814 let username = pieces
1815 .next()
1816 .ok_or_else(|| anyhow!("couldn't find username"))?;
1817 let repository = pieces
1818 .next()
1819 .ok_or_else(|| anyhow!("couldn't find repository name"))?;
1820 if pieces.next().is_some() {
1821 anyhow::bail!("too many segments on URL");
1822 }
1823
1824 let repository = repository.strip_suffix(".git").unwrap_or(repository);
1827
1828 let url = format!(
1829 "https://api.github.com/repos/{}/{}/commits/{}",
1830 username, repository, github_branch_name,
1831 );
1832 debug!("attempting GitHub fast path for {}", url);
1833 let mut request =
1834 Request::get(url).header(http::header::ACCEPT, "application/vnd.github.3.sha");
1835 if let Some(local_object) = local_object {
1836 request = request.header(http::header::IF_NONE_MATCH, &format!("\"{local_object}\""));
1837 }
1838 let response = gctx
1839 .http_async()?
1840 .request_blocking(request.body(Vec::new())?)?;
1841 let response_code = response.status();
1842 if response_code == StatusCode::NOT_MODIFIED {
1843 debug!("github fast path up-to-date");
1844 Ok(FastPathRev::UpToDate)
1845 } else if response_code == StatusCode::OK
1846 && let Some(oid_to_fetch) = rev_to_oid(str::from_utf8(&response.body())?)
1847 {
1848 debug!("github fast path fetch {oid_to_fetch}");
1850 Ok(FastPathRev::NeedsFetch(oid_to_fetch))
1851 } else {
1852 debug!("github fast path bad response code {response_code}");
1856 Ok(FastPathRev::Indeterminate)
1857 }
1858}
1859
1860fn is_github(url: &Url) -> bool {
1862 url.host_str() == Some("github.com")
1863}
1864
1865pub(crate) fn note_github_pull_request(url: &str) -> Option<String> {
1867 if let Ok(url) = url.parse::<Url>()
1868 && is_github(&url)
1869 {
1870 let path_segments = url
1871 .path_segments()
1872 .map(|p| p.into_iter().collect::<Vec<_>>())
1873 .unwrap_or_default();
1874 if let [owner, repo, "pull", pr_number, ..] = path_segments[..] {
1875 let repo_url = format!("https://github.com/{owner}/{repo}.git");
1876 let rev = format!("refs/pull/{pr_number}/head");
1877 return Some(format!(
1878 concat!(
1879 "\n\nnote: GitHub url {} is not a repository\n",
1880 "help: to specify a pull request as a dependency, replace the dependency with:\n",
1881 " `git = \"{}\" rev = \"{}\"`",
1882 ),
1883 url, repo_url, rev
1884 ));
1885 }
1886 }
1887
1888 None
1889}
1890
1891pub(crate) fn note_ssh_config(gctx: &GlobalContext) -> CargoResult<Option<&'static str>> {
1892 let net_config = gctx.net_config()?;
1893 if let Some(ssh_config) = net_config.ssh.as_ref()
1894 && ssh_config.known_hosts.is_some()
1895 {
1896 Ok(Some(
1897 "
1898note: `cargo`s `net.ssh.known-hosts` is not applied to `git`, check your system SSH configuration to ensure it is set there",
1899 ))
1900 } else {
1901 Ok(None)
1902 }
1903}
1904
1905pub(crate) fn note_git_config(gctx: &GlobalContext) -> CargoResult<Option<String>> {
1906 let mut fields = Vec::new();
1907
1908 let http_config = gctx.http_config()?;
1910 if http_config.proxy.is_some() {
1911 fields.push("http.proxy (git: http.proxy)");
1912 }
1913 if http_config.low_speed_limit.is_some() {
1914 fields.push("http.low-speed-limit (git: http.lowSpeedLimit)");
1915 }
1916 if http_config.timeout.is_some() {
1917 fields.push("http.timeout (git: http.lowSpeedTime)");
1918 }
1919 if http_config.cainfo.is_some() {
1920 fields.push("http.cainfo (git: http.sslCAInfo)");
1921 }
1922 if http_config.proxy_cainfo.is_some() {
1923 fields.push("http.proxy-cainfo (git: http.proxySSLCAInfo)");
1924 }
1925 if http_config.check_revoke.is_some() {
1926 fields.push("http.check-revoke (git: http.schannelCheckRevoke)");
1927 }
1928 if http_config.multiplexing.is_some() {
1929 fields.push("http.multiplexing (git: http.version)");
1930 }
1931 if http_config.ssl_version.is_some() {
1932 fields.push("http.ssl-version (git: http.sslVersion)");
1933 }
1934
1935 if !fields.is_empty() {
1936 Ok(Some(format!("
1937note: `cargo` does not apply the following config to `git`, check your git configuration to ensure they are set there
1938 {}", fields.join("\n "))))
1939 } else {
1940 Ok(None)
1941 }
1942}
1943
1944fn looks_like_commit_hash(rev: &str) -> bool {
1946 rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit())
1947}
1948
1949fn is_short_hash_of(rev: &str, oid: Oid) -> bool {
1951 let long_hash = oid.to_string();
1952 match long_hash.get(..rev.len()) {
1953 Some(truncated_long_hash) => truncated_long_hash.eq_ignore_ascii_case(rev),
1954 None => false,
1955 }
1956}
1957
1958#[cfg(test)]
1959mod tests {
1960 use super::*;
1961
1962 #[test]
1963 fn github_fast_path_full_hash_returns_needs_fetch() {
1964 let temp_dir = tempfile::TempDir::new().unwrap();
1965 let repo = git2::Repository::init_bare(temp_dir.path()).unwrap();
1966 let full_hash = "c9040898c9183ddbb9402dcbf749ed06d6ea90ad";
1967 let reference = GitReference::Rev(full_hash.to_string());
1968 let gctx = GlobalContext::default().unwrap();
1969 let expected_oid = rev_to_oid(full_hash).unwrap();
1970
1971 let result =
1972 github_fast_path(&repo, "https://github.com/user/repo", &reference, &gctx).unwrap();
1973
1974 assert!(matches!(result, FastPathRev::NeedsFetch(oid) if oid == expected_oid));
1975 }
1976
1977 #[test]
1978 fn test_absolute_submodule_url() {
1979 let cases = [
1980 (
1981 "ssh://git@gitub.com/rust-lang/cargo",
1982 "git@github.com:rust-lang/cargo.git",
1983 "git@github.com:rust-lang/cargo.git",
1984 ),
1985 (
1986 "ssh://git@gitub.com/rust-lang/cargo",
1987 "./",
1988 "ssh://git@gitub.com/rust-lang/cargo/",
1989 ),
1990 (
1991 "ssh://git@gitub.com/rust-lang/cargo",
1992 "../",
1993 "ssh://git@gitub.com/rust-lang/",
1994 ),
1995 (
1996 "ssh://git@gitub.com/rust-lang/cargo",
1997 "./foo",
1998 "ssh://git@gitub.com/rust-lang/cargo/foo",
1999 ),
2000 (
2001 "ssh://git@gitub.com/rust-lang/cargo/",
2002 "./foo",
2003 "ssh://git@gitub.com/rust-lang/cargo/foo",
2004 ),
2005 (
2006 "ssh://git@gitub.com/rust-lang/cargo/",
2007 "../foo",
2008 "ssh://git@gitub.com/rust-lang/foo",
2009 ),
2010 (
2011 "ssh://git@gitub.com/rust-lang/cargo",
2012 "../foo",
2013 "ssh://git@gitub.com/rust-lang/foo",
2014 ),
2015 (
2016 "ssh://git@gitub.com/rust-lang/cargo",
2017 "../foo/bar/../baz",
2018 "ssh://git@gitub.com/rust-lang/foo/baz",
2019 ),
2020 (
2021 "git@github.com:rust-lang/cargo.git",
2022 "ssh://git@gitub.com/rust-lang/cargo",
2023 "ssh://git@gitub.com/rust-lang/cargo",
2024 ),
2025 (
2026 "git@github.com:rust-lang/cargo.git",
2027 "./",
2028 "git@github.com:rust-lang/cargo.git/./",
2029 ),
2030 (
2031 "git@github.com:rust-lang/cargo.git",
2032 "../",
2033 "git@github.com:rust-lang/cargo.git/../",
2034 ),
2035 (
2036 "git@github.com:rust-lang/cargo.git",
2037 "./foo",
2038 "git@github.com:rust-lang/cargo.git/./foo",
2039 ),
2040 (
2041 "git@github.com:rust-lang/cargo.git/",
2042 "./foo",
2043 "git@github.com:rust-lang/cargo.git/./foo",
2044 ),
2045 (
2046 "git@github.com:rust-lang/cargo.git",
2047 "../foo",
2048 "git@github.com:rust-lang/cargo.git/../foo",
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/bar/../baz",
2058 "git@github.com:rust-lang/cargo.git/../foo/bar/../baz",
2059 ),
2060 ];
2061
2062 for (base_url, submodule_url, expected) in cases {
2063 let url = absolute_submodule_url(base_url, submodule_url).unwrap();
2064 assert_eq!(
2065 expected, url,
2066 "base `{base_url}`; submodule `{submodule_url}`"
2067 );
2068 }
2069 }
2070}
2071
2072pub(super) fn rev_to_oid(rev: &str) -> Option<Oid> {
2083 Oid::from_str(rev)
2084 .ok()
2085 .filter(|oid| oid.as_bytes().len() * 2 == rev.len())
2086}
2087
2088#[cfg(test)]
2089mod test {
2090 use super::*;
2091
2092 #[test]
2093 fn git_version_empty() {
2094 let input = "";
2095 GitVersion::from_version_stdout(input).unwrap_err();
2096 }
2097
2098 #[test]
2099 fn git_version_version() {
2100 let input = "git version 2.19.0";
2101 let actual = GitVersion::from_version_stdout(input).unwrap();
2102 let expected = GitVersion {
2103 major: 2,
2104 minor: 19,
2105 patch: 0,
2106 };
2107 assert_eq!(actual, expected);
2108 }
2109
2110 #[test]
2111 fn git_version_version_nl() {
2112 let input = "git version 2.19.0\n";
2113 let actual = GitVersion::from_version_stdout(input).unwrap();
2114 let expected = GitVersion {
2115 major: 2,
2116 minor: 19,
2117 patch: 0,
2118 };
2119 assert_eq!(actual, expected);
2120 }
2121
2122 #[test]
2123 fn git_version_version_platform() {
2124 let input = "git version 2.19.0 (Apple Git-117)";
2125 let actual = GitVersion::from_version_stdout(input).unwrap();
2126 let expected = GitVersion {
2127 major: 2,
2128 minor: 19,
2129 patch: 0,
2130 };
2131 assert_eq!(actual, expected);
2132 }
2133
2134 #[test]
2135 fn git_version_version_vendor() {
2136 let input = "git version 2.19.0.windows.1";
2137 let actual = GitVersion::from_version_stdout(input).unwrap();
2138 let expected = GitVersion {
2139 major: 2,
2140 minor: 18,
2141 patch: 0,
2142 };
2143 assert_eq!(actual, expected);
2144 }
2145
2146 #[test]
2147 fn git_version_version_rc() {
2148 let input = "git version 2.19.0.rc0";
2149 let actual = GitVersion::from_version_stdout(input).unwrap();
2150 let expected = GitVersion {
2151 major: 2,
2152 minor: 18,
2153 patch: 0,
2154 };
2155 assert_eq!(actual, expected);
2156 }
2157
2158 #[test]
2159 fn git_version_version_unreleased() {
2160 let input = "git version 2.55.GIT";
2161 let actual = GitVersion::from_version_stdout(input).unwrap();
2162 let expected = GitVersion {
2163 major: 2,
2164 minor: 55,
2165 patch: 0,
2166 };
2167 assert_eq!(actual, expected);
2168 }
2169}