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 gix_url = gix::url::parse(gix::bstr::BStr::new(url.as_bytes())).ok()?;
609 if gix_url.serialize_alternative_form && gix_url.scheme == gix::url::Scheme::Ssh {
610 Some(ssh_url_for_source_id(gix_url))
611 } else {
612 None
613 }
614}
615
616pub(super) fn ssh_url_for_source_id(mut url: gix::Url) -> String {
623 url.serialize_alternative_form = false;
624 if !url.path.starts_with(b"/") && !url.path.starts_with(b"~") {
625 url.path.insert(0, b'/');
626 }
627 url.to_bstring().to_string()
628}
629
630fn with_authentication<T, F>(
658 gctx: &GlobalContext,
659 url: &str,
660 cfg: &git2::Config,
661 mut f: F,
662) -> CargoResult<T>
663where
664 F: FnMut(&mut git2::Credentials<'_>) -> CargoResult<T>,
665{
666 let mut cred_helper = git2::CredentialHelper::new(url);
667 cred_helper.config(cfg);
668
669 let mut ssh_username_requested = false;
670 let mut cred_helper_bad = None;
671 let mut ssh_agent_attempts = Vec::new();
672 let mut any_attempts = false;
673 let mut tried_sshkey = false;
674 let mut url_attempt = None;
675
676 let orig_url = url;
677 let mut res = f(&mut |url, username, allowed| {
678 any_attempts = true;
679 if url != orig_url {
680 url_attempt = Some(url.to_string());
681 }
682 if allowed.contains(git2::CredentialType::USERNAME) {
701 debug_assert!(username.is_none());
702 ssh_username_requested = true;
703 return Err(git2::Error::from_str("gonna try usernames later"));
704 }
705
706 if allowed.contains(git2::CredentialType::SSH_KEY) && !tried_sshkey {
715 tried_sshkey = true;
720 let username = username.unwrap();
721 debug_assert!(!ssh_username_requested);
722 ssh_agent_attempts.push(username.to_string());
723 return git2::Cred::ssh_key_from_agent(username);
724 }
725
726 if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) && cred_helper_bad.is_none()
737 {
738 let r = git2::Cred::credential_helper(cfg, url, username);
739 cred_helper_bad = Some(r.is_err());
740 return r;
741 }
742
743 if allowed.contains(git2::CredentialType::DEFAULT) {
746 return git2::Cred::default();
747 }
748
749 Err(git2::Error::from_str("no authentication methods succeeded"))
751 });
752
753 if ssh_username_requested {
765 debug_assert!(res.is_err());
766 let mut attempts = vec![String::from("git")];
767 if let Ok(s) = gctx.get_env("USER").or_else(|_| gctx.get_env("USERNAME")) {
768 attempts.push(s.to_string());
769 }
770 if let Some(ref s) = cred_helper.username {
771 attempts.push(s.clone());
772 }
773
774 while let Some(s) = attempts.pop() {
775 let mut attempts = 0;
780 res = f(&mut |_url, username, allowed| {
781 if allowed.contains(git2::CredentialType::USERNAME) {
782 return git2::Cred::username(&s);
783 }
784 if allowed.contains(git2::CredentialType::SSH_KEY) {
785 debug_assert_eq!(Some(&s[..]), username);
786 attempts += 1;
787 if attempts == 1 {
788 ssh_agent_attempts.push(s.to_string());
789 return git2::Cred::ssh_key_from_agent(&s);
790 }
791 }
792 Err(git2::Error::from_str("no authentication methods succeeded"))
793 });
794
795 if attempts != 2 {
808 break;
809 }
810 }
811 }
812 let mut err = match res {
813 Ok(e) => return Ok(e),
814 Err(e) => e,
815 };
816
817 if any_attempts {
821 let mut msg = "failed to authenticate when downloading \
822 repository"
823 .to_string();
824
825 if let Some(attempt) = &url_attempt {
826 if url != attempt {
827 msg.push_str(": ");
828 msg.push_str(attempt);
829 }
830 }
831 msg.push('\n');
832 if !ssh_agent_attempts.is_empty() {
833 let names = ssh_agent_attempts
834 .iter()
835 .map(|s| format!("`{}`", s))
836 .collect::<Vec<_>>()
837 .join(", ");
838 msg.push_str(&format!(
839 "\n* attempted ssh-agent authentication, but \
840 no usernames succeeded: {}",
841 names
842 ));
843 }
844 if let Some(failed_cred_helper) = cred_helper_bad {
845 if failed_cred_helper {
846 msg.push_str(
847 "\n* attempted to find username/password via \
848 git's `credential.helper` support, but failed",
849 );
850 } else {
851 msg.push_str(
852 "\n* attempted to find username/password via \
853 `credential.helper`, but maybe the found \
854 credentials were incorrect",
855 );
856 }
857 }
858 msg.push_str("\n\n");
859 msg.push_str("if the git CLI succeeds then `net.git-fetch-with-cli` may help here\n");
860 msg.push_str("https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli");
861 err = err.context(msg);
862
863 } else if let Some(e) = err.downcast_ref::<git2::Error>() {
867 match e.class() {
868 ErrorClass::Net
869 | ErrorClass::Ssl
870 | ErrorClass::Submodule
871 | ErrorClass::FetchHead
872 | ErrorClass::Ssh
873 | ErrorClass::Http => {
874 let msg = format!(
875 concat!(
876 "network failure seems to have happened\n",
877 "if a proxy or similar is necessary `net.git-fetch-with-cli` may help here\n",
878 "https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
879 "{}"
880 ),
881 note_github_pull_request(url).unwrap_or_default()
882 );
883 err = err.context(msg);
884 }
885 ErrorClass::Callback => {
886 err = anyhow::format_err!("{}", e.message());
891 }
892 _ => {}
893 }
894 }
895
896 Err(err)
897}
898
899#[tracing::instrument(skip_all)]
903fn reset(repo: &git2::Repository, obj: &git2::Object<'_>, gctx: &GlobalContext) -> CargoResult<()> {
904 let mut pb = Progress::new("Checkout", gctx);
905 let mut opts = git2::build::CheckoutBuilder::new();
906 opts.progress(|_, cur, max| {
907 drop(pb.tick(cur, max, ""));
908 });
909 debug!("doing reset");
910 repo.reset(obj, git2::ResetType::Hard, Some(&mut opts))?;
911 debug!("reset done");
912 Ok(())
913}
914
915pub fn with_fetch_options(
924 git_config: &git2::Config,
925 url: &str,
926 gctx: &GlobalContext,
927 cb: &mut dyn FnMut(git2::FetchOptions<'_>) -> CargoResult<()>,
928) -> CargoResult<()> {
929 let mut progress = Progress::new("Fetch", gctx);
930 let ssh_config = gctx.net_config()?.ssh.as_ref();
931 let config_known_hosts = ssh_config.and_then(|ssh| ssh.known_hosts.as_ref());
932 let diagnostic_home_config = gctx.diagnostic_home_config();
933 network::retry::with_retry(gctx, || {
934 let mut check_cb_result = Ok(());
937 let auth_result = with_authentication(gctx, url, git_config, |f| {
938 let port = Url::parse(url).ok().and_then(|url| url.port());
939 let mut last_update = Instant::now();
940 let mut rcb = git2::RemoteCallbacks::new();
941 let mut counter = MetricsCounter::<10>::new(0, last_update);
944 rcb.credentials(f);
945 rcb.certificate_check(|cert, host| {
946 match super::known_hosts::certificate_check(
947 gctx,
948 cert,
949 host,
950 port,
951 config_known_hosts,
952 &diagnostic_home_config,
953 ) {
954 Ok(status) => Ok(status),
955 Err(e) => {
956 check_cb_result = Err(e);
957 Err(git2::Error::from_str(
960 "invalid or unknown remote ssh hostkey",
961 ))
962 }
963 }
964 });
965 rcb.transfer_progress(|stats| {
966 let indexed_deltas = stats.indexed_deltas();
967 let msg = if indexed_deltas > 0 {
968 format!(
970 ", ({}/{}) resolving deltas",
971 indexed_deltas,
972 stats.total_deltas()
973 )
974 } else {
975 let now = Instant::now();
987 if now - last_update > Duration::from_millis(300) {
989 counter.add(stats.received_bytes(), now);
990 last_update = now;
991 }
992 let rate = HumanBytes(counter.rate() as u64);
993 format!(", {rate:.2}/s")
994 };
995 progress
996 .tick(stats.indexed_objects(), stats.total_objects(), &msg)
997 .is_ok()
998 });
999
1000 let mut opts = git2::FetchOptions::new();
1003 opts.remote_callbacks(rcb);
1004 cb(opts)
1005 });
1006 if auth_result.is_err() {
1007 check_cb_result?;
1008 }
1009 auth_result?;
1010 Ok(())
1011 })
1012}
1013
1014#[tracing::instrument(skip_all)]
1028pub fn fetch(
1029 repo: &mut git2::Repository,
1030 remote_url: &str,
1031 manifest_reference: &GitReference,
1032 locked_reference: &GitReference,
1033 gctx: &GlobalContext,
1034 remote_kind: RemoteKind,
1035) -> CargoResult<()> {
1036 if let Some(offline_flag) = gctx.offline_flag() {
1037 anyhow::bail!(
1038 "attempting to update a git repository, but {offline_flag} \
1039 was specified"
1040 )
1041 }
1042
1043 let shallow = remote_kind.to_shallow_setting(repo.is_shallow(), gctx);
1044
1045 let mut fast_path_rev: bool = false;
1047
1048 let oid_to_fetch = match github_fast_path(repo, remote_url, locked_reference, gctx) {
1049 Ok(FastPathRev::UpToDate) => return Ok(()),
1050 Ok(FastPathRev::NeedsFetch(rev)) => Some(rev),
1051 Ok(FastPathRev::Indeterminate) => None,
1052 Err(e) => {
1053 debug!("failed to check github {:?}", e);
1054 None
1055 }
1056 };
1057
1058 maybe_gc_repo(repo, gctx)?;
1059
1060 clean_repo_temp_files(repo);
1061
1062 let mut refspecs = Vec::new();
1065 let mut tags = false;
1066 match locked_reference {
1070 GitReference::Branch(b) => {
1073 refspecs.push(format!("+refs/heads/{0}:refs/remotes/origin/{0}", b));
1074 }
1075
1076 GitReference::Tag(t) => {
1077 refspecs.push(format!("+refs/tags/{0}:refs/remotes/origin/tags/{0}", t));
1078 }
1079
1080 GitReference::DefaultBranch => {
1081 refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
1082 }
1083
1084 GitReference::Rev(rev) => {
1085 if rev.starts_with("refs/") {
1086 refspecs.push(format!("+{0}:{0}", rev));
1087 } else if let Some(oid_to_fetch) = oid_to_fetch {
1088 fast_path_rev = true;
1089 refspecs.push(format!("+{0}:refs/commit/{0}", oid_to_fetch));
1090 } else if !matches!(shallow, gix::remote::fetch::Shallow::NoChange)
1091 && rev_to_oid(rev).is_some()
1092 {
1093 refspecs.push(format!("+{0}:refs/remotes/origin/HEAD", rev));
1100 } else if let GitReference::Rev(rev) = manifest_reference
1101 && rev.starts_with("refs/")
1102 {
1103 refspecs.push(format!("+{0}:{0}", rev));
1106 } else {
1107 refspecs.push(String::from("+refs/heads/*:refs/remotes/origin/*"));
1111 refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD"));
1112 tags = true;
1113 }
1114 }
1115 }
1116
1117 debug!("doing a fetch for {remote_url}");
1118 let result = if gctx
1119 .net_config()?
1120 .git_fetch_with_cli
1121 .unwrap_or_else(|| git_version().is_some())
1122 {
1123 fetch_with_cli(repo, remote_url, &refspecs, tags, shallow, gctx)
1124 } else {
1125 if gctx.net_config()?.git_fetch_with_cli.is_none() && git_version().is_none() {
1126 gctx.shell().warn(
1127 "no compatible `git` found
1128note: this may become a hard error in the future, see <https://github.com/rust-lang/cargo/issues/17227>
1129help: to still use `git` for fetching, install it
1130help: to use builtin native git support, re-try with `net.git-fetch-with-cli = false`
1131https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
1132 )?;
1133 }
1134 if gctx.cli_unstable().gitoxide.map_or(false, |git| git.fetch) {
1135 fetch_with_gitoxide(repo, remote_url, refspecs, tags, shallow, gctx)
1136 } else {
1137 fetch_with_libgit2(repo, remote_url, refspecs, tags, shallow, gctx)
1138 }
1139 };
1140
1141 if fast_path_rev {
1142 if let Some(oid) = oid_to_fetch {
1143 return result.with_context(|| format!("revision {} not found", oid));
1144 }
1145 }
1146 result
1147}
1148
1149fn has_shallow_lock_file(err: &crate::sources::git::fetch::Error, repo_path: &Path) -> bool {
1154 err.downcast_any_ref::<std::io::Error>()
1155 .is_some_and(|err| err.kind() == std::io::ErrorKind::AlreadyExists)
1156 && repo_path.join("shallow.lock").is_file()
1157}
1158
1159fn git_version() -> Option<GitVersion> {
1160 #[tracing::instrument(skip_all)]
1161 fn git_version() -> Option<GitVersion> {
1162 use std::process::Stdio;
1163
1164 let output = std::process::Command::new("git")
1165 .arg("--version")
1166 .stdin(Stdio::null())
1167 .output()
1168 .ok()?;
1169 if !output.status.success() {
1170 return None;
1171 }
1172 let Ok(stdout) = String::from_utf8(output.stdout) else {
1173 return None;
1174 };
1175 let default_version = GitVersion {
1177 major: 2,
1178 minor: 0,
1179 patch: 0,
1180 };
1181 Some(GitVersion::from_version_stdout(&stdout).unwrap_or(default_version))
1182 }
1183
1184 static CACHE: std::sync::OnceLock<Option<GitVersion>> = std::sync::OnceLock::new();
1185 *CACHE.get_or_init(git_version)
1186}
1187
1188#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1189#[allow(unused)]
1190struct GitVersion {
1191 major: usize,
1192 minor: usize,
1193 patch: usize,
1194}
1195
1196impl GitVersion {
1197 fn from_version_stdout(stdout: &str) -> Result<Self, anyhow::Error> {
1198 let Some(version) = stdout.strip_prefix("git version ") else {
1200 anyhow::bail!("unrecognized `git --version` output: {stdout}")
1201 };
1202 let (version, _) = version.split_once(" ").unwrap_or((version, ""));
1203 let (version, _) = version.split_once("\n").unwrap_or((version, ""));
1204 version.parse()
1205 }
1206}
1207
1208impl std::str::FromStr for GitVersion {
1209 type Err = anyhow::Error;
1210
1211 fn from_str(version: &str) -> Result<Self, Self::Err> {
1212 let unreleased = "GIT";
1213
1214 let s = version;
1215 let (major, s) = s.split_once(".").unwrap_or((s, ""));
1216 let mut major: usize = major.parse().map_err(|_err| {
1217 anyhow::format_err!("unrecognized major version `{major}` in `{version}`")
1218 })?;
1219 let (minor, s) = s.split_once(".").unwrap_or((s, ""));
1220 let mut minor: usize = if minor == unreleased {
1221 0
1222 } else {
1223 minor.parse().map_err(|_err| {
1224 anyhow::format_err!("unrecognized minor version `{minor}` in `{version}`")
1225 })?
1226 };
1227 let (patch, s) = s.split_once(".").unwrap_or((s, ""));
1228 let mut patch: usize = if patch == unreleased {
1229 0
1230 } else {
1231 patch.parse().map_err(|_err| {
1232 anyhow::format_err!("unrecognized patch version `{patch}` in `{version}`")
1233 })?
1234 };
1235
1236 let more = s;
1237 if !more.is_empty() {
1238 if let Some(sub) = patch.checked_sub(1) {
1241 patch = sub;
1242 } else if let Some(sub) = minor.checked_sub(1) {
1243 minor = sub;
1244 } else if let Some(sub) = major.checked_sub(1) {
1245 major = sub;
1246 }
1247 }
1248
1249 Ok(Self {
1250 major,
1251 minor,
1252 patch,
1253 })
1254 }
1255}
1256
1257#[tracing::instrument(skip(repo, gctx))]
1269fn fetch_with_cli(
1270 repo: &git2::Repository,
1271 url: &str,
1272 refspecs: &[String],
1273 tags: bool,
1274 shallow: gix::remote::fetch::Shallow,
1275 gctx: &GlobalContext,
1276) -> CargoResult<()> {
1277 debug!(target: "git-fetch", backend = "git-cli");
1278
1279 let Some(git_version) = git_version() else {
1280 anyhow::bail!(
1281 "`git` is not available
1282help: to still use `git` for fetching, please install it
1283help: to use Cargo's native git support, re-try with `net.git-fetch-with-cli = false`
1284https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
1285 );
1286 };
1287
1288 let mut cmd = ProcessBuilder::new("git");
1289 cmd.arg("-c").arg("core.fsmonitor=false");
1291 cmd.arg("-c").arg("advice.fetchShowForcedUpdates=false");
1293 let max_retries = gctx
1295 .net_config()?
1296 .retry
1297 .unwrap_or(network::retry::MAX_RETRY_DEFAULT);
1298 cmd.arg("-c").arg(format!("http.maxRetries={max_retries}"));
1299 cmd.arg("-c").arg("http.retryAfter=1"); cmd.arg("-c").arg(format!(
1301 "http.maxRetryTime={}",
1302 network::retry::MAX_RETRY_SLEEP_S
1303 ));
1304
1305 cmd.arg("fetch");
1306 if tags {
1307 cmd.arg("--tags");
1308 } else {
1309 cmd.arg("--no-tags");
1310 }
1311 if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1312 let depth = 0i32.saturating_add_unsigned(depth.get());
1313 cmd.arg(format!("--depth={depth}"));
1314 }
1315
1316 let progress_config = gctx.progress_config();
1317 let progress = match progress_config.when {
1318 ProgressWhen::Always => true,
1319 ProgressWhen::Never => false,
1320 ProgressWhen::Auto => {
1321 let width = progress_config
1323 .width
1324 .or_else(|| gctx.shell().err_width().progress_max_width());
1325 gctx.shell().progress_supported() && width.is_some()
1326 }
1327 };
1328 if gctx.shell().verbosity() == Verbosity::Verbose {
1329 cmd.arg("--verbose");
1330 } else if progress {
1331 cmd.arg("--progress");
1332 let min_version_porcelain = GitVersion {
1334 major: 2,
1335 minor: 41,
1336 patch: 0,
1337 };
1338 if min_version_porcelain <= git_version {
1339 cmd.arg("--porcelain").stdout(Stdio::Null);
1341 }
1342 } else {
1343 cmd.arg("--quiet");
1344 }
1345
1346 let min_version_no_show_forced_update = GitVersion {
1348 major: 2,
1349 minor: 23,
1350 patch: 0,
1351 };
1352 if min_version_no_show_forced_update <= git_version {
1353 cmd.arg("--no-show-forced-updates");
1355 }
1356
1357 cmd.arg("--force") .arg("--update-head-ok") .arg("--recurse-submodules=no") .arg(url)
1361 .args(refspecs)
1362 .env("GIT_DIR", repo.path())
1366 .env_remove("GIT_WORK_TREE")
1369 .env_remove("GIT_INDEX_FILE")
1370 .env_remove("GIT_OBJECT_DIRECTORY")
1371 .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
1372 .cwd(repo.path());
1373 gctx.shell()
1374 .verbose(|s| s.status("Running", &cmd.to_string()))?;
1375 let min_version_max_retries = GitVersion {
1377 major: 2,
1378 minor: 54,
1379 patch: 0,
1380 };
1381 network::retry::with_retry(gctx, || {
1382 cmd.exec().map_err(|_error| {
1383 let spurious = git_version < min_version_max_retries;
1384 let pr_hint = note_github_pull_request(url).unwrap_or_default();
1385 let ssh_config_hint = note_ssh_config(gctx).unwrap_or_default().unwrap_or_default();
1386 let git_config_hint = note_git_config(gctx).unwrap_or_default().unwrap_or_default();
1387 let with_depth = if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1388 format!(" with depth={depth}")
1389 } else {
1390 "".to_owned()
1391 };
1392 GitCliError::new(anyhow::format_err!("`git fetch` failed for {url}{with_depth}{pr_hint}{ssh_config_hint}{git_config_hint}"))
1393 .spurious(spurious)
1394 .workaround(
1395 "help: re-try with `net.git-fetch-with-cli = false` to see if it resolves the problem
1396https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
1397 )
1398 .into()
1399 })
1400 })?;
1401
1402 Ok(())
1403}
1404
1405#[tracing::instrument(skip(repo, gctx))]
1406fn fetch_with_gitoxide(
1407 repo: &mut git2::Repository,
1408 remote_url: &str,
1409 refspecs: Vec<String>,
1410 tags: bool,
1411 shallow: gix::remote::fetch::Shallow,
1412 gctx: &GlobalContext,
1413) -> CargoResult<()> {
1414 debug!(target: "git-fetch", backend = "gitoxide");
1415
1416 let git2_repo = repo;
1417 let config_overrides = cargo_config_to_gitoxide_overrides(gctx)?;
1418 let repo_reinitialized = AtomicBool::default();
1419 let res = oxide::with_retry_and_progress(
1420 git2_repo.path(),
1421 gctx,
1422 remote_url,
1423 &|repo_path,
1424 should_interrupt,
1425 mut progress,
1426 url_for_authentication: &mut dyn FnMut(&gix::bstr::BStr)| {
1427 loop {
1438 let res = oxide::open_repo(
1439 repo_path,
1440 config_overrides.clone(),
1441 oxide::OpenMode::ForFetch,
1442 )
1443 .and_then(|repo| {
1444 debug!("initiating fetch of {refspecs:?} from {remote_url}");
1445 let url_for_authentication = &mut *url_for_authentication;
1446 let remote = repo
1447 .remote_at(remote_url)?
1448 .with_fetch_tags(if tags {
1449 gix::remote::fetch::Tags::All
1450 } else {
1451 gix::remote::fetch::Tags::Included
1452 })
1453 .with_refspecs(
1454 refspecs.iter().map(|s| s.as_str()),
1455 gix::remote::Direction::Fetch,
1456 )?;
1457 let url = remote
1458 .url(gix::remote::Direction::Fetch)
1459 .expect("set at init")
1460 .to_owned();
1461 let connection = remote.connect(gix::remote::Direction::Fetch)?;
1462 let mut authenticate = connection.configured_credentials(url)?;
1463 let connection = connection.with_credentials(
1464 move |action: gix::protocol::credentials::helper::Action| {
1465 if let Some(url) = action
1466 .context()
1467 .and_then(|gctx| gctx.url.as_ref().filter(|url| *url != remote_url))
1468 {
1469 url_for_authentication(url.as_ref());
1470 }
1471 authenticate(action)
1472 .map_err(|err| gix::Error::from_error(oxide::CredentialError(err)))
1473 },
1474 );
1475 let outcome = connection
1476 .prepare_fetch(&mut progress, gix::remote::ref_map::Options::default())?
1477 .with_shallow(shallow.clone())
1478 .receive(&mut progress, should_interrupt)?;
1479 Ok(outcome)
1480 });
1481 let err = match res {
1482 Ok(_) => break,
1483 Err(e) => e,
1484 };
1485 debug!("fetch failed: {}", err);
1486
1487 if !repo_reinitialized.load(Ordering::Relaxed)
1488 && err.is_corrupted()
1493 || has_shallow_lock_file(&err, repo_path)
1494 {
1495 repo_reinitialized.store(true, Ordering::Relaxed);
1496 debug!(
1497 "looks like this is a corrupt repository, reinitializing \
1498 and trying again"
1499 );
1500 if oxide::reinitialize(repo_path).is_ok() {
1501 continue;
1502 }
1503 }
1504
1505 return Err(err.into());
1506 }
1507 Ok(())
1508 },
1509 );
1510 if repo_reinitialized.load(Ordering::Relaxed) {
1511 *git2_repo = git2::Repository::open(git2_repo.path())?;
1512 }
1513 res
1514}
1515
1516#[tracing::instrument(skip(repo, gctx))]
1517fn fetch_with_libgit2(
1518 repo: &mut git2::Repository,
1519 remote_url: &str,
1520 refspecs: Vec<String>,
1521 tags: bool,
1522 shallow: gix::remote::fetch::Shallow,
1523 gctx: &GlobalContext,
1524) -> CargoResult<()> {
1525 debug!(target: "git-fetch", backend = "libgit2");
1526
1527 static INIT: Once = Once::new();
1528 INIT.call_once(|| {
1529 unsafe { init_git_transports(gctx) };
1543 });
1544
1545 let git_config = git2::Config::open_default()?;
1546 with_fetch_options(&git_config, remote_url, gctx, &mut |mut opts| {
1547 if tags {
1548 opts.download_tags(git2::AutotagOption::All);
1549 }
1550 if let gix::remote::fetch::Shallow::DepthAtRemote(depth) = shallow {
1551 opts.depth(0i32.saturating_add_unsigned(depth.get()));
1552 }
1553 let mut repo_reinitialized = false;
1564 loop {
1565 debug!("initiating fetch of {refspecs:?} from {remote_url}");
1566 let res = repo
1567 .remote_anonymous(remote_url)?
1568 .fetch(&refspecs, Some(&mut opts), None);
1569 let err = match res {
1570 Ok(()) => break,
1571 Err(e) => e,
1572 };
1573 debug!("fetch failed: {}", err);
1574
1575 if !repo_reinitialized && matches!(err.class(), ErrorClass::Reference | ErrorClass::Odb)
1576 {
1577 repo_reinitialized = true;
1578 debug!(
1579 "looks like this is a corrupt repository, reinitializing \
1580 and trying again"
1581 );
1582 if reinitialize(repo).is_ok() {
1583 continue;
1584 }
1585 }
1586
1587 return Err(err.into());
1588 }
1589 Ok(())
1590 })
1591}
1592
1593#[tracing::instrument(skip_all)]
1603unsafe fn init_git_transports(gctx: &GlobalContext) {
1604 match needs_custom_http_transport(gctx) {
1605 Ok(true) => {}
1606 _ => return,
1607 }
1608
1609 let handle = match http_handle(gctx) {
1610 Ok(handle) => handle,
1611 Err(..) => return,
1612 };
1613
1614 unsafe {
1615 git2_curl::register(handle);
1616 }
1617}
1618
1619fn maybe_gc_repo(repo: &mut git2::Repository, gctx: &GlobalContext) -> CargoResult<()> {
1637 let mut cmd = Command::new("git");
1645 if let Ok(limit) = gctx.get_env("__CARGO_PACKFILE_LIMIT") {
1646 cmd.arg(format!("-c gc.autoPackLimit={}", limit));
1647 }
1648 cmd.arg("gc")
1649 .arg("--auto")
1650 .env("GIT_DIR", repo.path())
1652 .current_dir(repo.path());
1653
1654 match cmd.output() {
1655 Ok(out) => {
1656 debug!(
1657 "git-gc --auto status: {}\n\nstdout ---\n{}\nstderr ---\n{}",
1658 out.status,
1659 String::from_utf8_lossy(&out.stdout),
1660 String::from_utf8_lossy(&out.stderr)
1661 );
1662 if out.status.success() {
1663 let new = git2::Repository::open(repo.path())?;
1664 *repo = new;
1665 return Ok(());
1666 }
1667 }
1668 Err(e) => debug!("git-gc --auto failed to spawn: {}", e),
1669 }
1670
1671 reinitialize(repo)
1673}
1674
1675fn clean_repo_temp_files(repo: &git2::Repository) {
1690 let path = repo.path().join("objects/pack/pack_git2_*");
1691 let Some(pattern) = path.to_str() else {
1692 tracing::warn!("cannot convert {path:?} to a string");
1693 return;
1694 };
1695 let Ok(paths) = glob::glob(pattern) else {
1696 return;
1697 };
1698 for path in paths {
1699 if let Ok(path) = path {
1700 match paths::remove_file(&path) {
1701 Ok(_) => tracing::debug!("removed stale temp git file {path:?}"),
1702 Err(e) => {
1703 tracing::warn!("failed to remove {path:?} while cleaning temp files: {e}")
1704 }
1705 }
1706 }
1707 }
1708}
1709
1710fn reinitialize(repo: &mut git2::Repository) -> CargoResult<()> {
1713 let path = repo.path().to_path_buf();
1718 debug!("reinitializing git repo at {:?}", path);
1719 let tmp = path.join("tmp");
1720 let bare = !repo.path().ends_with(".git");
1721 *repo = init(&tmp, false)?;
1722 for entry in path.read_dir()? {
1723 let entry = entry?;
1724 if entry.file_name().to_str() == Some("tmp") {
1725 continue;
1726 }
1727 let path = entry.path();
1728 drop(paths::remove_file(&path).or_else(|_| paths::remove_dir_all(&path)));
1729 }
1730 *repo = init(&path, bare)?;
1731 paths::remove_dir_all(&tmp)?;
1732 Ok(())
1733}
1734
1735fn init(path: &Path, bare: bool) -> CargoResult<git2::Repository> {
1737 let mut opts = git2::RepositoryInitOptions::new();
1738 opts.external_template(false);
1742 opts.bare(bare);
1743 Ok(git2::Repository::init_opts(&path, &opts)?)
1744}
1745
1746enum FastPathRev {
1748 UpToDate,
1751 NeedsFetch(Oid),
1754 Indeterminate,
1757}
1758
1759#[tracing::instrument(skip(repo, gctx))]
1773fn github_fast_path(
1774 repo: &git2::Repository,
1775 url: &str,
1776 reference: &GitReference,
1777 gctx: &GlobalContext,
1778) -> CargoResult<FastPathRev> {
1779 let url = Url::parse(url)?;
1780 if !is_github(&url) {
1781 return Ok(FastPathRev::Indeterminate);
1782 }
1783
1784 let local_object = resolve_ref(reference, repo).ok();
1785
1786 let github_branch_name = match reference {
1787 GitReference::Branch(branch) => branch,
1788 GitReference::Tag(tag) => tag,
1789 GitReference::DefaultBranch => "HEAD",
1790 GitReference::Rev(rev) => {
1791 if rev.starts_with("refs/") {
1792 rev
1793 } else if looks_like_commit_hash(rev) {
1794 if let Some(local_object) = local_object {
1812 if is_short_hash_of(rev, local_object) {
1813 debug!("github fast path already has {local_object}");
1814 return Ok(FastPathRev::UpToDate);
1815 }
1816 }
1817 if let Some(oid) = rev_to_oid(rev) {
1822 debug!("github fast path is already a full commit hash {rev}");
1823 return Ok(FastPathRev::NeedsFetch(oid));
1824 }
1825 rev
1826 } else {
1827 debug!("can't use github fast path with `rev = \"{}\"`", rev);
1828 return Ok(FastPathRev::Indeterminate);
1829 }
1830 }
1831 };
1832
1833 let mut pieces = url
1836 .path_segments()
1837 .ok_or_else(|| anyhow!("no path segments on url"))?;
1838 let username = pieces
1839 .next()
1840 .ok_or_else(|| anyhow!("couldn't find username"))?;
1841 let repository = pieces
1842 .next()
1843 .ok_or_else(|| anyhow!("couldn't find repository name"))?;
1844 if pieces.next().is_some() {
1845 anyhow::bail!("too many segments on URL");
1846 }
1847
1848 let repository = repository.strip_suffix(".git").unwrap_or(repository);
1851
1852 let url = format!(
1853 "https://api.github.com/repos/{}/{}/commits/{}",
1854 username, repository, github_branch_name,
1855 );
1856 debug!("attempting GitHub fast path for {}", url);
1857 let mut request =
1858 Request::get(url).header(http::header::ACCEPT, "application/vnd.github.3.sha");
1859 if let Some(local_object) = local_object {
1860 request = request.header(http::header::IF_NONE_MATCH, &format!("\"{local_object}\""));
1861 }
1862 let response = gctx
1863 .http_async()?
1864 .request_blocking(request.body(Vec::new())?)?;
1865 let response_code = response.status();
1866 if response_code == StatusCode::NOT_MODIFIED {
1867 debug!("github fast path up-to-date");
1868 Ok(FastPathRev::UpToDate)
1869 } else if response_code == StatusCode::OK
1870 && let Some(oid_to_fetch) = rev_to_oid(str::from_utf8(&response.body())?)
1871 {
1872 debug!("github fast path fetch {oid_to_fetch}");
1874 Ok(FastPathRev::NeedsFetch(oid_to_fetch))
1875 } else {
1876 debug!("github fast path bad response code {response_code}");
1880 Ok(FastPathRev::Indeterminate)
1881 }
1882}
1883
1884fn is_github(url: &Url) -> bool {
1886 url.host_str() == Some("github.com")
1887}
1888
1889pub(crate) fn note_github_pull_request(url: &str) -> Option<String> {
1891 if let Ok(url) = url.parse::<Url>()
1892 && is_github(&url)
1893 {
1894 let path_segments = url
1895 .path_segments()
1896 .map(|p| p.into_iter().collect::<Vec<_>>())
1897 .unwrap_or_default();
1898 if let [owner, repo, "pull", pr_number, ..] = path_segments[..] {
1899 let repo_url = format!("https://github.com/{owner}/{repo}.git");
1900 let rev = format!("refs/pull/{pr_number}/head");
1901 return Some(format!(
1902 concat!(
1903 "\n\nnote: GitHub url {} is not a repository\n",
1904 "help: to specify a pull request as a dependency, replace the dependency with:\n",
1905 " `git = \"{}\" rev = \"{}\"`",
1906 ),
1907 url, repo_url, rev
1908 ));
1909 }
1910 }
1911
1912 None
1913}
1914
1915pub(crate) fn note_ssh_config(gctx: &GlobalContext) -> CargoResult<Option<&'static str>> {
1916 let net_config = gctx.net_config()?;
1917 if let Some(ssh_config) = net_config.ssh.as_ref()
1918 && ssh_config.known_hosts.is_some()
1919 {
1920 Ok(Some(
1921 "
1922note: `cargo`s `net.ssh.known-hosts` is not applied to `git`, check your system SSH configuration to ensure it is set there",
1923 ))
1924 } else {
1925 Ok(None)
1926 }
1927}
1928
1929pub(crate) fn note_git_config(gctx: &GlobalContext) -> CargoResult<Option<String>> {
1930 let mut fields = Vec::new();
1931
1932 let http_config = gctx.http_config()?;
1934 if http_config.proxy.is_some() {
1935 fields.push("http.proxy (git: http.proxy)");
1936 }
1937 if http_config.low_speed_limit.is_some() {
1938 fields.push("http.low-speed-limit (git: http.lowSpeedLimit)");
1939 }
1940 if http_config.timeout.is_some() {
1941 fields.push("http.timeout (git: http.lowSpeedTime)");
1942 }
1943 if http_config.cainfo.is_some() {
1944 fields.push("http.cainfo (git: http.sslCAInfo)");
1945 }
1946 if http_config.proxy_cainfo.is_some() {
1947 fields.push("http.proxy-cainfo (git: http.proxySSLCAInfo)");
1948 }
1949 if http_config.check_revoke.is_some() {
1950 fields.push("http.check-revoke (git: http.schannelCheckRevoke)");
1951 }
1952 if http_config.multiplexing.is_some() {
1953 fields.push("http.multiplexing (git: http.version)");
1954 }
1955 if http_config.ssl_version.is_some() {
1956 fields.push("http.ssl-version (git: http.sslVersion)");
1957 }
1958
1959 if !fields.is_empty() {
1960 Ok(Some(format!("
1961note: `cargo` does not apply the following config to `git`, check your git configuration to ensure they are set there
1962 {}", fields.join("\n "))))
1963 } else {
1964 Ok(None)
1965 }
1966}
1967
1968fn looks_like_commit_hash(rev: &str) -> bool {
1970 rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit())
1971}
1972
1973fn is_short_hash_of(rev: &str, oid: Oid) -> bool {
1975 let long_hash = oid.to_string();
1976 match long_hash.get(..rev.len()) {
1977 Some(truncated_long_hash) => truncated_long_hash.eq_ignore_ascii_case(rev),
1978 None => false,
1979 }
1980}
1981
1982#[cfg(test)]
1983mod tests {
1984 use super::*;
1985
1986 #[test]
1987 fn scp_to_ssh_produces_parseable_cache_urls() {
1988 assert!(
1989 "git@github.com:foo/bar.git".into_url().is_err(),
1990 "SourceId needs a WHATWG URL, which cannot represent SCP syntax"
1991 );
1992 for (input, expected) in [
1993 (
1994 "git@github.com:foo/bar.git",
1995 "ssh://git@github.com/foo/bar.git",
1996 ),
1997 (
1998 "git@github.com:/foo/bar.git",
1999 "ssh://git@github.com/foo/bar.git",
2000 ),
2001 ("git@github.com:~/bar.git", "ssh://git@github.com/~/bar.git"),
2002 (
2003 "git@github.com:~other/bar.git",
2004 "ssh://git@github.com/~other/bar.git",
2005 ),
2006 ("host:foo/bar.git", "ssh://host/foo/bar.git"),
2007 ] {
2008 let url = scp_to_ssh(input).unwrap();
2009 assert_eq!(
2010 url, expected,
2011 "conversion must preserve the pre-update cache URL"
2012 );
2013 let cache_url = url.as_str().into_url().unwrap();
2014 let source_id = SourceId::for_git(&cache_url, GitReference::DefaultBranch).unwrap();
2015 assert_eq!(source_id.url().as_str(), expected);
2016 }
2017 assert!(scp_to_ssh("https://github.com/foo/bar.git").is_none());
2018 }
2019
2020 #[test]
2021 fn shallow_lock_recovery_requires_existing_lock_and_matching_io_error() {
2022 use gix::error::ErrorExt;
2023
2024 let dir = tempfile::TempDir::new().unwrap();
2025 let locked = std::io::Error::from(std::io::ErrorKind::AlreadyExists).raise();
2026 assert!(!has_shallow_lock_file(&locked, dir.path()));
2027 std::fs::write(dir.path().join("shallow.lock"), []).unwrap();
2028 assert!(has_shallow_lock_file(&locked, dir.path()));
2029 let denied = std::io::Error::from(std::io::ErrorKind::PermissionDenied).raise();
2030 assert!(!has_shallow_lock_file(&denied, dir.path()));
2031 }
2032
2033 #[test]
2034 fn github_fast_path_full_hash_returns_needs_fetch() {
2035 let temp_dir = tempfile::TempDir::new().unwrap();
2036 let repo = git2::Repository::init_bare(temp_dir.path()).unwrap();
2037 let full_hash = "c9040898c9183ddbb9402dcbf749ed06d6ea90ad";
2038 let reference = GitReference::Rev(full_hash.to_string());
2039 let gctx = GlobalContext::default().unwrap();
2040 let expected_oid = rev_to_oid(full_hash).unwrap();
2041
2042 let result =
2043 github_fast_path(&repo, "https://github.com/user/repo", &reference, &gctx).unwrap();
2044
2045 assert!(matches!(result, FastPathRev::NeedsFetch(oid) if oid == expected_oid));
2046 }
2047
2048 #[test]
2049 fn test_absolute_submodule_url() {
2050 let cases = [
2051 (
2052 "ssh://git@gitub.com/rust-lang/cargo",
2053 "git@github.com:rust-lang/cargo.git",
2054 "git@github.com:rust-lang/cargo.git",
2055 ),
2056 (
2057 "ssh://git@gitub.com/rust-lang/cargo",
2058 "./",
2059 "ssh://git@gitub.com/rust-lang/cargo/",
2060 ),
2061 (
2062 "ssh://git@gitub.com/rust-lang/cargo",
2063 "../",
2064 "ssh://git@gitub.com/rust-lang/",
2065 ),
2066 (
2067 "ssh://git@gitub.com/rust-lang/cargo",
2068 "./foo",
2069 "ssh://git@gitub.com/rust-lang/cargo/foo",
2070 ),
2071 (
2072 "ssh://git@gitub.com/rust-lang/cargo/",
2073 "./foo",
2074 "ssh://git@gitub.com/rust-lang/cargo/foo",
2075 ),
2076 (
2077 "ssh://git@gitub.com/rust-lang/cargo/",
2078 "../foo",
2079 "ssh://git@gitub.com/rust-lang/foo",
2080 ),
2081 (
2082 "ssh://git@gitub.com/rust-lang/cargo",
2083 "../foo",
2084 "ssh://git@gitub.com/rust-lang/foo",
2085 ),
2086 (
2087 "ssh://git@gitub.com/rust-lang/cargo",
2088 "../foo/bar/../baz",
2089 "ssh://git@gitub.com/rust-lang/foo/baz",
2090 ),
2091 (
2092 "git@github.com:rust-lang/cargo.git",
2093 "ssh://git@gitub.com/rust-lang/cargo",
2094 "ssh://git@gitub.com/rust-lang/cargo",
2095 ),
2096 (
2097 "git@github.com:rust-lang/cargo.git",
2098 "./",
2099 "git@github.com:rust-lang/cargo.git/./",
2100 ),
2101 (
2102 "git@github.com:rust-lang/cargo.git",
2103 "../",
2104 "git@github.com:rust-lang/cargo.git/../",
2105 ),
2106 (
2107 "git@github.com:rust-lang/cargo.git",
2108 "./foo",
2109 "git@github.com:rust-lang/cargo.git/./foo",
2110 ),
2111 (
2112 "git@github.com:rust-lang/cargo.git/",
2113 "./foo",
2114 "git@github.com:rust-lang/cargo.git/./foo",
2115 ),
2116 (
2117 "git@github.com:rust-lang/cargo.git",
2118 "../foo",
2119 "git@github.com:rust-lang/cargo.git/../foo",
2120 ),
2121 (
2122 "git@github.com:rust-lang/cargo.git/",
2123 "../foo",
2124 "git@github.com:rust-lang/cargo.git/../foo",
2125 ),
2126 (
2127 "git@github.com:rust-lang/cargo.git",
2128 "../foo/bar/../baz",
2129 "git@github.com:rust-lang/cargo.git/../foo/bar/../baz",
2130 ),
2131 ];
2132
2133 for (base_url, submodule_url, expected) in cases {
2134 let url = absolute_submodule_url(base_url, submodule_url).unwrap();
2135 assert_eq!(
2136 expected, url,
2137 "base `{base_url}`; submodule `{submodule_url}`"
2138 );
2139 }
2140 }
2141
2142 #[test]
2143 fn test_scp_to_ssh() {
2144 assert_eq!(
2146 scp_to_ssh("git@github.com:rust-lang/cargo.git"),
2147 Some("ssh://git@github.com/rust-lang/cargo.git".to_string()),
2148 );
2149 assert_eq!(
2150 scp_to_ssh("git@github.com:foo/bar.git"),
2151 Some("ssh://git@github.com/foo/bar.git".to_string()),
2152 );
2153 assert_eq!(
2155 scp_to_ssh("git@github.com:~user/repo.git"),
2156 Some("ssh://git@github.com/~user/repo.git".to_string()),
2157 );
2158 assert_eq!(scp_to_ssh("ssh://git@github.com/rust-lang/cargo.git"), None);
2160 assert_eq!(scp_to_ssh("https://github.com/rust-lang/cargo.git"), None);
2161 assert_eq!(scp_to_ssh("not-a-url"), None);
2163 }
2164}
2165
2166pub(super) fn rev_to_oid(rev: &str) -> Option<Oid> {
2177 Oid::from_str(rev)
2178 .ok()
2179 .filter(|oid| oid.as_bytes().len() * 2 == rev.len())
2180}
2181
2182#[cfg(test)]
2183mod test {
2184 use super::*;
2185
2186 #[test]
2187 fn git_version_empty() {
2188 let input = "";
2189 GitVersion::from_version_stdout(input).unwrap_err();
2190 }
2191
2192 #[test]
2193 fn git_version_version() {
2194 let input = "git version 2.19.0";
2195 let actual = GitVersion::from_version_stdout(input).unwrap();
2196 let expected = GitVersion {
2197 major: 2,
2198 minor: 19,
2199 patch: 0,
2200 };
2201 assert_eq!(actual, expected);
2202 }
2203
2204 #[test]
2205 fn git_version_version_nl() {
2206 let input = "git version 2.19.0\n";
2207 let actual = GitVersion::from_version_stdout(input).unwrap();
2208 let expected = GitVersion {
2209 major: 2,
2210 minor: 19,
2211 patch: 0,
2212 };
2213 assert_eq!(actual, expected);
2214 }
2215
2216 #[test]
2217 fn git_version_version_platform() {
2218 let input = "git version 2.19.0 (Apple Git-117)";
2219 let actual = GitVersion::from_version_stdout(input).unwrap();
2220 let expected = GitVersion {
2221 major: 2,
2222 minor: 19,
2223 patch: 0,
2224 };
2225 assert_eq!(actual, expected);
2226 }
2227
2228 #[test]
2229 fn git_version_version_vendor() {
2230 let input = "git version 2.19.0.windows.1";
2231 let actual = GitVersion::from_version_stdout(input).unwrap();
2232 let expected = GitVersion {
2233 major: 2,
2234 minor: 18,
2235 patch: 0,
2236 };
2237 assert_eq!(actual, expected);
2238 }
2239
2240 #[test]
2241 fn git_version_version_rc() {
2242 let input = "git version 2.19.0.rc0";
2243 let actual = GitVersion::from_version_stdout(input).unwrap();
2244 let expected = GitVersion {
2245 major: 2,
2246 minor: 18,
2247 patch: 0,
2248 };
2249 assert_eq!(actual, expected);
2250 }
2251
2252 #[test]
2253 fn git_version_version_unreleased() {
2254 let input = "git version 2.55.GIT";
2255 let actual = GitVersion::from_version_stdout(input).unwrap();
2256 let expected = GitVersion {
2257 major: 2,
2258 minor: 55,
2259 patch: 0,
2260 };
2261 assert_eq!(actual, expected);
2262 }
2263}