1use crate::core::GitReference;
2use crate::util::errors::{CargoResult, CargoResultExt};
3use crate::util::paths;
4use crate::util::process_builder::process;
5use crate::util::{network, Config, IntoUrl, Progress};
6use curl::easy::{Easy, List};
7use git2::{self, ObjectType};
8use log::{debug, info};
9use serde::ser;
10use serde::Serialize;
11use std::env;
12use std::fmt;
13use std::fs::File;
14use std::mem;
15use std::path::{Path, PathBuf};
16use std::process::Command;
17use url::Url;
18
19#[derive(PartialEq, Clone, Debug)]
20pub struct GitRevision(git2::Oid);
21
22impl ser::Serialize for GitRevision {
23 fn serialize<S: ser::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
24 serialize_str(self, s)
25 }
26}
27
28fn serialize_str<T, S>(t: &T, s: S) -> Result<S::Ok, S::Error>
29where
30 T: fmt::Display,
31 S: ser::Serializer,
32{
33 s.collect_str(t)
34}
35
36impl fmt::Display for GitRevision {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 fmt::Display::fmt(&self.0, f)
39 }
40}
41
42pub struct GitShortID(git2::Buf);
43
44impl GitShortID {
45 pub fn as_str(&self) -> &str {
46 self.0.as_str().unwrap()
47 }
48}
49
50#[derive(PartialEq, Clone, Debug, Serialize)]
53pub struct GitRemote {
54 #[serde(serialize_with = "serialize_str")]
55 url: Url,
56}
57
58#[derive(Serialize)]
61pub struct GitDatabase {
62 remote: GitRemote,
63 path: PathBuf,
64 #[serde(skip_serializing)]
65 repo: git2::Repository,
66}
67
68#[derive(Serialize)]
72pub struct GitCheckout<'a> {
73 database: &'a GitDatabase,
74 location: PathBuf,
75 revision: GitRevision,
76 #[serde(skip_serializing)]
77 repo: git2::Repository,
78}
79
80impl GitRemote {
83 pub fn new(url: &Url) -> GitRemote {
84 GitRemote { url: url.clone() }
85 }
86
87 pub fn url(&self) -> &Url {
88 &self.url
89 }
90
91 pub fn rev_for(&self, path: &Path, reference: &GitReference) -> CargoResult<GitRevision> {
92 reference.resolve(&self.db_at(path)?.repo)
93 }
94
95 pub fn checkout(
96 &self,
97 into: &Path,
98 reference: &GitReference,
99 cargo_config: &Config,
100 ) -> CargoResult<(GitDatabase, GitRevision)> {
101 let mut repo_and_rev = None;
102 if let Ok(mut repo) = git2::Repository::open(into) {
103 self.fetch_into(&mut repo, cargo_config)
104 .chain_err(|| format!("failed to fetch into {}", into.display()))?;
105 if let Ok(rev) = reference.resolve(&repo) {
106 repo_and_rev = Some((repo, rev));
107 }
108 }
109 let (repo, rev) = match repo_and_rev {
110 Some(pair) => pair,
111 None => {
112 let repo = self
113 .clone_into(into, cargo_config)
114 .chain_err(|| format!("failed to clone into: {}", into.display()))?;
115 let rev = reference.resolve(&repo)?;
116 (repo, rev)
117 }
118 };
119
120 Ok((
121 GitDatabase {
122 remote: self.clone(),
123 path: into.to_path_buf(),
124 repo,
125 },
126 rev,
127 ))
128 }
129
130 pub fn db_at(&self, db_path: &Path) -> CargoResult<GitDatabase> {
131 let repo = git2::Repository::open(db_path)?;
132 Ok(GitDatabase {
133 remote: self.clone(),
134 path: db_path.to_path_buf(),
135 repo,
136 })
137 }
138
139 fn fetch_into(&self, dst: &mut git2::Repository, cargo_config: &Config) -> CargoResult<()> {
140 let refspec = "refs/heads/*:refs/heads/*";
142 fetch(dst, self.url.as_str(), refspec, cargo_config)
143 }
144
145 fn clone_into(&self, dst: &Path, cargo_config: &Config) -> CargoResult<git2::Repository> {
146 if dst.exists() {
147 paths::remove_dir_all(dst)?;
148 }
149 paths::create_dir_all(dst)?;
150 let mut repo = init(dst, true)?;
151 fetch(
152 &mut repo,
153 self.url.as_str(),
154 "refs/heads/*:refs/heads/*",
155 cargo_config,
156 )?;
157 Ok(repo)
158 }
159}
160
161impl GitDatabase {
162 pub fn copy_to(
163 &self,
164 rev: GitRevision,
165 dest: &Path,
166 cargo_config: &Config,
167 ) -> CargoResult<GitCheckout<'_>> {
168 let mut checkout = None;
169 if let Ok(repo) = git2::Repository::open(dest) {
170 let mut co = GitCheckout::new(dest, self, rev.clone(), repo);
171 if !co.is_fresh() {
172 co.fetch(cargo_config)?;
177 if co.has_object() {
178 co.reset(cargo_config)?;
179 assert!(co.is_fresh());
180 checkout = Some(co);
181 }
182 } else {
183 checkout = Some(co);
184 }
185 };
186 let checkout = match checkout {
187 Some(c) => c,
188 None => GitCheckout::clone_into(dest, self, rev, cargo_config)?,
189 };
190 checkout.update_submodules(cargo_config)?;
191 Ok(checkout)
192 }
193
194 pub fn to_short_id(&self, revision: &GitRevision) -> CargoResult<GitShortID> {
195 let obj = self.repo.find_object(revision.0, None)?;
196 Ok(GitShortID(obj.short_id()?))
197 }
198
199 pub fn has_ref(&self, reference: &str) -> CargoResult<()> {
200 self.repo.revparse_single(reference)?;
201 Ok(())
202 }
203}
204
205impl GitReference {
206 fn resolve(&self, repo: &git2::Repository) -> CargoResult<GitRevision> {
207 let id = match *self {
208 GitReference::Tag(ref s) => (|| -> CargoResult<git2::Oid> {
209 let refname = format!("refs/tags/{}", s);
210 let id = repo.refname_to_id(&refname)?;
211 let obj = repo.find_object(id, None)?;
212 let obj = obj.peel(ObjectType::Commit)?;
213 Ok(obj.id())
214 })()
215 .chain_err(|| format!("failed to find tag `{}`", s))?,
216 GitReference::Branch(ref s) => {
217 let b = repo
218 .find_branch(s, git2::BranchType::Local)
219 .chain_err(|| format!("failed to find branch `{}`", s))?;
220 b.get()
221 .target()
222 .ok_or_else(|| anyhow::format_err!("branch `{}` did not have a target", s))?
223 }
224 GitReference::Rev(ref s) => {
225 let obj = repo.revparse_single(s)?;
226 match obj.as_tag() {
227 Some(tag) => tag.target_id(),
228 None => obj.id(),
229 }
230 }
231 };
232 Ok(GitRevision(id))
233 }
234}
235
236impl<'a> GitCheckout<'a> {
237 fn new(
238 path: &Path,
239 database: &'a GitDatabase,
240 revision: GitRevision,
241 repo: git2::Repository,
242 ) -> GitCheckout<'a> {
243 GitCheckout {
244 location: path.to_path_buf(),
245 database,
246 revision,
247 repo,
248 }
249 }
250
251 fn clone_into(
252 into: &Path,
253 database: &'a GitDatabase,
254 revision: GitRevision,
255 config: &Config,
256 ) -> CargoResult<GitCheckout<'a>> {
257 let dirname = into.parent().unwrap();
258 paths::create_dir_all(&dirname)?;
259 if into.exists() {
260 paths::remove_dir_all(into)?;
261 }
262
263 let git_config = git2::Config::new()?;
267
268 let url = database.path.into_url()?;
275 let mut repo = None;
276 with_fetch_options(&git_config, url.as_str(), config, &mut |fopts| {
277 let mut checkout = git2::build::CheckoutBuilder::new();
278 checkout.dry_run(); let r = git2::build::RepoBuilder::new()
281 .clone_local(git2::build::CloneLocal::Local)
284 .with_checkout(checkout)
285 .fetch_options(fopts)
286 .clone(url.as_str(), into)?;
288 repo = Some(r);
289 Ok(())
290 })?;
291 let repo = repo.unwrap();
292
293 let checkout = GitCheckout::new(into, database, revision, repo);
294 checkout.reset(config)?;
295 Ok(checkout)
296 }
297
298 fn is_fresh(&self) -> bool {
299 match self.repo.revparse_single("HEAD") {
300 Ok(ref head) if head.id() == self.revision.0 => {
301 self.location.join(".cargo-ok").exists()
303 }
304 _ => false,
305 }
306 }
307
308 fn fetch(&mut self, cargo_config: &Config) -> CargoResult<()> {
309 info!("fetch {}", self.repo.path().display());
310 let url = self.database.path.into_url()?;
311 let refspec = "refs/heads/*:refs/heads/*";
312 fetch(&mut self.repo, url.as_str(), refspec, cargo_config)?;
313 Ok(())
314 }
315
316 fn has_object(&self) -> bool {
317 self.repo.find_object(self.revision.0, None).is_ok()
318 }
319
320 fn reset(&self, config: &Config) -> CargoResult<()> {
321 let ok_file = self.location.join(".cargo-ok");
330 let _ = paths::remove_file(&ok_file);
331 info!("reset {} to {}", self.repo.path().display(), self.revision);
332 let object = self.repo.find_object(self.revision.0, None)?;
333 reset(&self.repo, &object, config)?;
334 File::create(ok_file)?;
335 Ok(())
336 }
337
338 fn update_submodules(&self, cargo_config: &Config) -> CargoResult<()> {
339 return update_submodules(&self.repo, cargo_config);
340
341 fn update_submodules(repo: &git2::Repository, cargo_config: &Config) -> CargoResult<()> {
342 info!("update submodules for: {:?}", repo.workdir().unwrap());
343
344 for mut child in repo.submodules()? {
345 update_submodule(repo, &mut child, cargo_config).chain_err(|| {
346 format!(
347 "failed to update submodule `{}`",
348 child.name().unwrap_or("")
349 )
350 })?;
351 }
352 Ok(())
353 }
354
355 fn update_submodule(
356 parent: &git2::Repository,
357 child: &mut git2::Submodule<'_>,
358 cargo_config: &Config,
359 ) -> CargoResult<()> {
360 child.init(false)?;
361 let url = child.url().ok_or_else(|| {
362 anyhow::format_err!("non-utf8 url for submodule {:?}?", child.path())
363 })?;
364
365 let head = match child.head_id() {
368 Some(head) => head,
369 None => return Ok(()),
370 };
371
372 let head_and_repo = child.open().and_then(|repo| {
377 let target = repo.head()?.target();
378 Ok((target, repo))
379 });
380 let mut repo = match head_and_repo {
381 Ok((head, repo)) => {
382 if child.head_id() == head {
383 return update_submodules(&repo, cargo_config);
384 }
385 repo
386 }
387 Err(..) => {
388 let path = parent.workdir().unwrap().join(child.path());
389 let _ = paths::remove_dir_all(&path);
390 init(&path, false)?
391 }
392 };
393 let refspec = "refs/heads/*:refs/heads/*";
395 cargo_config
396 .shell()
397 .status("Updating", format!("git submodule `{}`", url))?;
398 fetch(&mut repo, url, refspec, cargo_config).chain_err(|| {
399 format!(
400 "failed to fetch submodule `{}` from {}",
401 child.name().unwrap_or(""),
402 url
403 )
404 })?;
405
406 let obj = repo.find_object(head, None)?;
407 reset(&repo, &obj, cargo_config)?;
408 update_submodules(&repo, cargo_config)
409 }
410 }
411}
412
413fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F) -> CargoResult<T>
441where
442 F: FnMut(&mut git2::Credentials<'_>) -> CargoResult<T>,
443{
444 let mut cred_helper = git2::CredentialHelper::new(url);
445 cred_helper.config(cfg);
446
447 let mut ssh_username_requested = false;
448 let mut cred_helper_bad = None;
449 let mut ssh_agent_attempts = Vec::new();
450 let mut any_attempts = false;
451 let mut tried_sshkey = false;
452
453 let mut res = f(&mut |url, username, allowed| {
454 any_attempts = true;
455 if allowed.contains(git2::CredentialType::USERNAME) {
474 debug_assert!(username.is_none());
475 ssh_username_requested = true;
476 return Err(git2::Error::from_str("gonna try usernames later"));
477 }
478
479 if allowed.contains(git2::CredentialType::SSH_KEY) && !tried_sshkey {
488 tried_sshkey = true;
493 let username = username.unwrap();
494 debug_assert!(!ssh_username_requested);
495 ssh_agent_attempts.push(username.to_string());
496 return git2::Cred::ssh_key_from_agent(username);
497 }
498
499 if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) && cred_helper_bad.is_none()
510 {
511 let r = git2::Cred::credential_helper(cfg, url, username);
512 cred_helper_bad = Some(r.is_err());
513 return r;
514 }
515
516 if allowed.contains(git2::CredentialType::DEFAULT) {
519 return git2::Cred::default();
520 }
521
522 Err(git2::Error::from_str("no authentication available"))
524 });
525
526 if ssh_username_requested {
538 debug_assert!(res.is_err());
539 let mut attempts = Vec::new();
540 attempts.push("git".to_string());
541 if let Ok(s) = env::var("USER").or_else(|_| env::var("USERNAME")) {
542 attempts.push(s);
543 }
544 if let Some(ref s) = cred_helper.username {
545 attempts.push(s.clone());
546 }
547
548 while let Some(s) = attempts.pop() {
549 let mut attempts = 0;
554 res = f(&mut |_url, username, allowed| {
555 if allowed.contains(git2::CredentialType::USERNAME) {
556 return git2::Cred::username(&s);
557 }
558 if allowed.contains(git2::CredentialType::SSH_KEY) {
559 debug_assert_eq!(Some(&s[..]), username);
560 attempts += 1;
561 if attempts == 1 {
562 ssh_agent_attempts.push(s.to_string());
563 return git2::Cred::ssh_key_from_agent(&s);
564 }
565 }
566 Err(git2::Error::from_str("no authentication available"))
567 });
568
569 if attempts != 2 {
582 break;
583 }
584 }
585 }
586
587 if res.is_ok() || !any_attempts {
588 return res.map_err(From::from);
589 }
590
591 let res = res.map_err(anyhow::Error::from).chain_err(|| {
595 let mut msg = "failed to authenticate when downloading \
596 repository"
597 .to_string();
598 if !ssh_agent_attempts.is_empty() {
599 let names = ssh_agent_attempts
600 .iter()
601 .map(|s| format!("`{}`", s))
602 .collect::<Vec<_>>()
603 .join(", ");
604 msg.push_str(&format!(
605 "\nattempted ssh-agent authentication, but \
606 none of the usernames {} succeeded",
607 names
608 ));
609 }
610 if let Some(failed_cred_helper) = cred_helper_bad {
611 if failed_cred_helper {
612 msg.push_str(
613 "\nattempted to find username/password via \
614 git's `credential.helper` support, but failed",
615 );
616 } else {
617 msg.push_str(
618 "\nattempted to find username/password via \
619 `credential.helper`, but maybe the found \
620 credentials were incorrect",
621 );
622 }
623 }
624 msg
625 })?;
626 Ok(res)
627}
628
629fn reset(repo: &git2::Repository, obj: &git2::Object<'_>, config: &Config) -> CargoResult<()> {
630 let mut pb = Progress::new("Checkout", config);
631 let mut opts = git2::build::CheckoutBuilder::new();
632 opts.progress(|_, cur, max| {
633 drop(pb.tick(cur, max));
634 });
635 repo.reset(obj, git2::ResetType::Hard, Some(&mut opts))?;
636 Ok(())
637}
638
639pub fn with_fetch_options(
640 git_config: &git2::Config,
641 url: &str,
642 config: &Config,
643 cb: &mut dyn FnMut(git2::FetchOptions<'_>) -> CargoResult<()>,
644) -> CargoResult<()> {
645 let mut progress = Progress::new("Fetch", config);
646 network::with_retry(config, || {
647 with_authentication(url, git_config, |f| {
648 let mut rcb = git2::RemoteCallbacks::new();
649 rcb.credentials(f);
650
651 rcb.transfer_progress(|stats| {
652 progress
653 .tick(stats.indexed_objects(), stats.total_objects())
654 .is_ok()
655 });
656
657 let mut opts = git2::FetchOptions::new();
660 opts.remote_callbacks(rcb)
661 .download_tags(git2::AutotagOption::All);
662 cb(opts)
663 })?;
664 Ok(())
665 })
666}
667
668pub fn fetch(
669 repo: &mut git2::Repository,
670 url: &str,
671 refspec: &str,
672 config: &Config,
673) -> CargoResult<()> {
674 if config.frozen() {
675 anyhow::bail!(
676 "attempting to update a git repository, but --frozen \
677 was specified"
678 )
679 }
680 if !config.network_allowed() {
681 anyhow::bail!("can't update a git repository in the offline mode")
682 }
683
684 if let Ok(url) = Url::parse(url) {
688 if url.host_str() == Some("github.com") {
689 if let Ok(oid) = repo.refname_to_id("refs/remotes/origin/master") {
690 let mut handle = config.http()?.borrow_mut();
691 debug!("attempting GitHub fast path for {}", url);
692 if github_up_to_date(&mut handle, &url, &oid) {
693 return Ok(());
694 } else {
695 debug!("fast path failed, falling back to a git fetch");
696 }
697 }
698 }
699 }
700
701 maybe_gc_repo(repo)?;
706
707 if let Some(true) = config.net_config()?.git_fetch_with_cli {
714 return fetch_with_cli(repo, url, refspec, config);
715 }
716
717 debug!("doing a fetch for {}", url);
718 let git_config = git2::Config::open_default()?;
719 with_fetch_options(&git_config, url, config, &mut |mut opts| {
720 let mut repo_reinitialized = false;
731 loop {
732 debug!("initiating fetch of {} from {}", refspec, url);
733 let res = repo
734 .remote_anonymous(url)?
735 .fetch(&[refspec], Some(&mut opts), None);
736 let err = match res {
737 Ok(()) => break,
738 Err(e) => e,
739 };
740 debug!("fetch failed: {}", err);
741
742 if !repo_reinitialized && err.class() == git2::ErrorClass::Reference {
743 repo_reinitialized = true;
744 debug!(
745 "looks like this is a corrupt repository, reinitializing \
746 and trying again"
747 );
748 if reinitialize(repo).is_ok() {
749 continue;
750 }
751 }
752
753 return Err(err.into());
754 }
755 Ok(())
756 })
757}
758
759fn fetch_with_cli(
760 repo: &mut git2::Repository,
761 url: &str,
762 refspec: &str,
763 config: &Config,
764) -> CargoResult<()> {
765 let mut cmd = process("git");
766 cmd.arg("fetch")
767 .arg("--tags") .arg("--force") .arg("--update-head-ok") .arg(url)
771 .arg(refspec)
772 .env_remove("GIT_DIR")
777 .env_remove("GIT_WORK_TREE")
780 .env_remove("GIT_INDEX_FILE")
781 .env_remove("GIT_OBJECT_DIRECTORY")
782 .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
783 .cwd(repo.path());
784 config
785 .shell()
786 .verbose(|s| s.status("Running", &cmd.to_string()))?;
787 cmd.exec_with_output()?;
788 Ok(())
789}
790
791fn maybe_gc_repo(repo: &mut git2::Repository) -> CargoResult<()> {
805 let entries = match repo.path().join("objects/pack").read_dir() {
808 Ok(e) => e.count(),
809 Err(_) => {
810 debug!("skipping gc as pack dir appears gone");
811 return Ok(());
812 }
813 };
814 let max = env::var("__CARGO_PACKFILE_LIMIT")
815 .ok()
816 .and_then(|s| s.parse::<usize>().ok())
817 .unwrap_or(100);
818 if entries < max {
819 debug!("skipping gc as there's only {} pack files", entries);
820 return Ok(());
821 }
822
823 match Command::new("git")
828 .arg("gc")
829 .current_dir(repo.path())
830 .output()
831 {
832 Ok(out) => {
833 debug!(
834 "git-gc status: {}\n\nstdout ---\n{}\nstderr ---\n{}",
835 out.status,
836 String::from_utf8_lossy(&out.stdout),
837 String::from_utf8_lossy(&out.stderr)
838 );
839 if out.status.success() {
840 let new = git2::Repository::open(repo.path())?;
841 mem::replace(repo, new);
842 return Ok(());
843 }
844 }
845 Err(e) => debug!("git-gc failed to spawn: {}", e),
846 }
847
848 reinitialize(repo)
850}
851
852fn reinitialize(repo: &mut git2::Repository) -> CargoResult<()> {
853 let path = repo.path().to_path_buf();
858 debug!("reinitializing git repo at {:?}", path);
859 let tmp = path.join("tmp");
860 let bare = !repo.path().ends_with(".git");
861 *repo = init(&tmp, false)?;
862 for entry in path.read_dir()? {
863 let entry = entry?;
864 if entry.file_name().to_str() == Some("tmp") {
865 continue;
866 }
867 let path = entry.path();
868 drop(paths::remove_file(&path).or_else(|_| paths::remove_dir_all(&path)));
869 }
870 *repo = init(&path, bare)?;
871 paths::remove_dir_all(&tmp)?;
872 Ok(())
873}
874
875fn init(path: &Path, bare: bool) -> CargoResult<git2::Repository> {
876 let mut opts = git2::RepositoryInitOptions::new();
877 opts.external_template(false);
881 opts.bare(bare);
882 Ok(git2::Repository::init_opts(&path, &opts)?)
883}
884
885fn github_up_to_date(handle: &mut Easy, url: &Url, oid: &git2::Oid) -> bool {
902 macro_rules! r#try {
903 ($e:expr) => {
904 match $e {
905 Some(e) => e,
906 None => return false,
907 }
908 };
909 }
910
911 let mut pieces = r#try!(url.path_segments());
914 let username = r#try!(pieces.next());
915 let repo = r#try!(pieces.next());
916 if pieces.next().is_some() {
917 return false;
918 }
919
920 let url = format!(
921 "https://api.github.com/repos/{}/{}/commits/master",
922 username, repo
923 );
924 r#try!(handle.get(true).ok());
925 r#try!(handle.url(&url).ok());
926 r#try!(handle.useragent("cargo").ok());
927 let mut headers = List::new();
928 r#try!(headers.append("Accept: application/vnd.github.3.sha").ok());
929 r#try!(headers.append(&format!("If-None-Match: \"{}\"", oid)).ok());
930 r#try!(handle.http_headers(headers).ok());
931 r#try!(handle.perform().ok());
932
933 r#try!(handle.response_code().ok()) == 304
934}