use crate::util::errors::Res;
use failure::{format_err, ResultExt};
use git2;
use std::{env, fs, path::Path};
use url::Url;
pub fn init(path: &Path) -> Res<()> {
git2::Repository::init(path)?;
Ok(())
}
pub fn clone(url: &Url, into: &Path) -> Res<git2::Repository> {
let git_config = git2::Config::open_default()?;
with_fetch_options(&git_config, &url, &mut |opts| {
let repo = git2::build::RepoBuilder::new()
.fetch_options(opts)
.clone(url.as_str(), into)?;
Ok(repo)
})
}
pub fn update_submodules(repo: &git2::Repository) -> Res<()> {
for mut child in repo.submodules()? {
update_submodule(repo, &mut child).with_context(|_| {
format!(
"failed to update submodule `{}`",
child.name().unwrap_or("")
)
})?;
}
Ok(())
}
fn update_submodule(parent: &git2::Repository, child: &mut git2::Submodule) -> Res<()> {
child.init(false)?;
let url = child
.url()
.ok_or_else(|| format_err!("non-utf8 url for submodule"))?;
let head = match child.head_id() {
Some(head) => head,
None => return Ok(()),
};
let head_and_repo = child.open().and_then(|repo| {
let target = repo.head()?.target();
Ok((target, repo))
});
let mut repo = match head_and_repo {
Ok((head, repo)) => {
if child.head_id() == head {
return update_submodules(&repo);
}
repo
}
Err(..) => {
let path = parent.workdir().unwrap().join(child.path());
let _ = fs::remove_dir_all(&path);
git2::Repository::init(&path)?
}
};
let refspec = "refs/heads/*:refs/heads/*";
let url = Url::parse(url)?;
fetch(&mut repo, &url, refspec).with_context(|_| {
format_err!(
"failed to fetch submodule `{}` from {}",
child.name().unwrap_or(""),
url
)
})?;
let obj = repo.find_object(head, None)?;
reset(&repo, &obj)?;
update_submodules(&repo)
}
pub fn fetch(repo: &mut git2::Repository, url: &Url, refspec: &str) -> Res<()> {
let mut repo_reinitialized = false;
let git_config = git2::Config::open_default()?;
with_fetch_options(&git_config, url, &mut |mut opts| {
loop {
let res = repo
.remote_anonymous(url.as_str())?
.fetch(&[refspec], Some(&mut opts), None);
let err = match res {
Ok(()) => break,
Err(e) => e,
};
if !repo_reinitialized && err.class() == git2::ErrorClass::Reference {
repo_reinitialized = true;
if reinitialize(repo).is_ok() {
continue;
}
}
return Err(err.into());
}
Ok(())
})
}
pub fn reset(repo: &git2::Repository, obj: &git2::Object) -> Res<()> {
let mut opts = git2::build::CheckoutBuilder::new();
repo.reset(obj, git2::ResetType::Hard, Some(&mut opts))?;
Ok(())
}
fn reinitialize(repo: &mut git2::Repository) -> Res<()> {
let path = repo.path().to_path_buf();
let tmp = path.join("tmp");
let bare = !repo.path().ends_with(".git");
*repo = git2::Repository::init(&tmp)?;
for entry in path.read_dir()? {
let entry = entry?;
if entry.file_name().to_str() == Some("tmp") {
continue;
}
let path = entry.path();
drop(fs::remove_file(&path).or_else(|_| fs::remove_dir_all(&path)));
}
if bare {
*repo = git2::Repository::init_bare(path)?;
} else {
*repo = git2::Repository::init(path)?;
}
fs::remove_dir_all(&tmp)?;
Ok(())
}
fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F) -> Res<T>
where
F: FnMut(&mut git2::Credentials) -> Res<T>,
{
let mut cred_helper = git2::CredentialHelper::new(url);
cred_helper.config(cfg);
let mut ssh_username_requested = false;
let mut cred_helper_bad = None;
let mut ssh_agent_attempts = Vec::new();
let mut any_attempts = false;
let mut tried_sshkey = false;
let mut res = f(&mut |url, username, allowed| {
any_attempts = true;
if allowed.contains(git2::CredentialType::USERNAME) {
debug_assert!(username.is_none());
ssh_username_requested = true;
return Err(git2::Error::from_str("gonna try usernames later"));
}
if allowed.contains(git2::CredentialType::SSH_KEY) && !tried_sshkey {
tried_sshkey = true;
let username = username.unwrap();
debug_assert!(!ssh_username_requested);
ssh_agent_attempts.push(username.to_string());
return git2::Cred::ssh_key_from_agent(username);
}
if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
let r = git2::Cred::credential_helper(cfg, url, username);
cred_helper_bad = Some(r.is_err());
return r;
}
if allowed.contains(git2::CredentialType::DEFAULT) {
return git2::Cred::default();
}
Err(git2::Error::from_str("no authentication available"))
});
if ssh_username_requested {
debug_assert!(res.is_err());
let mut attempts = Vec::new();
attempts.push("git".to_string());
if let Ok(s) = env::var("USER").or_else(|_| env::var("USERNAME")) {
attempts.push(s);
}
if let Some(ref s) = cred_helper.username {
attempts.push(s.clone());
}
while let Some(s) = attempts.pop() {
let mut attempts = 0;
res = f(&mut |_url, username, allowed| {
if allowed.contains(git2::CredentialType::USERNAME) {
return git2::Cred::username(&s);
}
if allowed.contains(git2::CredentialType::SSH_KEY) {
debug_assert_eq!(Some(&s[..]), username);
attempts += 1;
if attempts == 1 {
ssh_agent_attempts.push(s.to_string());
return git2::Cred::ssh_key_from_agent(&s);
}
}
Err(git2::Error::from_str("no authentication available"))
});
if attempts != 2 {
break;
}
}
}
if res.is_ok() || !any_attempts {
return res.map_err(From::from);
}
let res = res.with_context(|_| {
let mut msg = "failed to authenticate when downloading \
repository"
.to_string();
if !ssh_agent_attempts.is_empty() {
let names = ssh_agent_attempts
.iter()
.map(|s| format!("`{}`", s))
.collect::<Vec<_>>()
.join(", ");
msg.push_str(&format!(
"\nattempted ssh-agent authentication, but \
none of the usernames {} succeeded",
names
));
}
if let Some(failed_cred_helper) = cred_helper_bad {
if failed_cred_helper {
msg.push_str(
"\nattempted to find username/password via \
git's `credential.helper` support, but failed",
);
} else {
msg.push_str(
"\nattempted to find username/password via \
`credential.helper`, but maybe the found \
credentials were incorrect",
);
}
}
msg
})?;
Ok(res)
}
pub fn with_fetch_options<T>(
git_config: &git2::Config,
url: &Url,
cb: &mut FnMut(git2::FetchOptions) -> Res<T>,
) -> Res<T> {
with_authentication(url.as_str(), git_config, |f| {
let mut rcb = git2::RemoteCallbacks::new();
rcb.credentials(f);
let mut opts = git2::FetchOptions::new();
opts.remote_callbacks(rcb)
.download_tags(git2::AutotagOption::All);
cb(opts)
})
}