use chrono::prelude::*;
use color_eyre::eyre::{eyre, Result};
use git2::Cred;
use git2::{Branch, Commit, Repository};
use git_url_parse::GitUrl;
use hex::ToHex;
use log::debug;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
#[doc(hidden)]
pub mod clone;
#[doc(hidden)]
pub mod info;
#[doc(hidden)]
pub mod types;
#[doc(inline)]
pub use crate::types::*;
impl GitCommitMeta {
pub fn new<I: ToHex + AsRef<[u8]>>(id: I) -> GitCommitMeta {
GitCommitMeta {
id: hex::encode(id),
message: None,
timestamp: None,
}
}
pub fn with_timestamp(mut self, time: i64) -> Self {
let naive_datetime = NaiveDateTime::from_timestamp(time, 0);
let datetime: DateTime<Utc> = DateTime::from_utc(naive_datetime, Utc);
self.timestamp = Some(datetime);
self
}
pub fn with_message(mut self, msg: Option<String>) -> Self {
self.message = msg;
self
}
}
impl From<Repository> for GitRepo {
fn from(repo: Repository) -> Self {
GitRepo::open(repo.path().to_path_buf(), None, None)
.expect("Failed to convert Repository to GitRepo")
}
}
impl GitRepo {
pub fn open(
path: PathBuf,
branch: Option<String>,
commit_id: Option<String>,
) -> Result<GitRepo> {
let local_repo = GitRepo::to_repository_from_path(path.clone())?;
let remote_url = GitRepo::git_remote_from_repo(&local_repo)?;
let working_branch_name = GitRepo::get_git2_branch(&local_repo, &branch)?
.name()?
.expect("Unable to extract branch name")
.to_string();
if let Some(_c) = &commit_id {
if local_repo.is_shallow() {
return Err(eyre!("Can't open by commit on shallow clones"));
}
}
let commit =
GitRepo::get_git2_commit(&local_repo, &Some(working_branch_name.clone()), &commit_id)?;
Ok(GitRepo::new(remote_url)?
.with_path(path)
.with_branch(Some(working_branch_name))
.with_git2_commit(commit))
}
pub fn with_path(mut self, path: PathBuf) -> Self {
self.path = Some(fs::canonicalize(path).expect("Directory was not found"));
self
}
pub fn with_branch(mut self, branch: Option<String>) -> Self {
if let Some(b) = branch {
self.branch = Some(b.into());
}
self
}
pub fn with_commit(mut self, commit_id: Option<String>) -> Self {
self = GitRepo::open(self.path.expect("No path set"), self.branch, commit_id)
.expect("Unable to open GitRepo with commit id");
self
}
pub fn with_git2_commit(mut self, commit: Option<Commit>) -> Self {
match commit {
Some(c) => {
let commit_msg = c.message().unwrap_or_default().to_string();
let commit = GitCommitMeta::new(c.id())
.with_message(Some(commit_msg))
.with_timestamp(c.time().seconds());
self.head = Some(commit);
self
}
None => {
self.head = None;
self
}
}
}
pub fn with_credentials(mut self, creds: Option<GitCredentials>) -> Self {
self.credentials = creds;
self
}
pub fn new<S: AsRef<str>>(url: S) -> Result<GitRepo> {
Ok(GitRepo {
url: GitUrl::parse(url.as_ref()).expect("url failed to parse as GitUrl"),
credentials: None,
head: None,
branch: None,
path: None,
})
}
pub fn to_repository(&self) -> Result<Repository, git2::Error> {
GitRepo::to_repository_from_path(
self.path.clone().expect("No path set to open").as_os_str(),
)
}
fn to_repository_from_path<P: AsRef<Path>>(path: P) -> Result<Repository, git2::Error> {
Repository::open(path.as_ref().as_os_str())
}
fn get_git2_commit<'repo>(
r: &'repo Repository,
branch: &Option<String>,
commit_id: &Option<String>,
) -> Result<Option<Commit<'repo>>> {
let working_branch = GitRepo::get_git2_branch(r, branch)?;
match commit_id {
Some(id) => {
debug!("Commit provided. Using {}", id);
let commit = r.find_commit(git2::Oid::from_str(id)?)?;
Ok(Some(commit))
}
None => {
debug!("No commit provided. Attempting to use HEAD commit from remote branch");
match working_branch.upstream() {
Ok(upstream_branch) => {
let working_ref = upstream_branch.into_reference();
let commit = working_ref
.peel_to_commit()
.expect("Unable to retrieve HEAD commit object from remote branch");
let _ =
GitRepo::is_commit_in_branch(r, &commit, &Branch::wrap(working_ref));
Ok(Some(commit))
}
Err(_e) => {
debug!("No remote branch found. Using HEAD commit from local branch");
let working_ref = working_branch.into_reference();
let commit = working_ref
.peel_to_commit()
.expect("Unable to retrieve HEAD commit object from local branch");
let _ =
GitRepo::is_commit_in_branch(r, &commit, &Branch::wrap(working_ref));
Ok(Some(commit))
}
}
}
}
}
fn build_git2_remotecallback(&self) -> git2::RemoteCallbacks {
if let Some(cred) = self.credentials.clone() {
debug!("Before building callback: {:?}", &cred);
match cred {
GitCredentials::SshKey {
username,
public_key,
private_key,
passphrase,
} => {
let mut cb = git2::RemoteCallbacks::new();
cb.credentials(
move |_, _, _| match (public_key.clone(), passphrase.clone()) {
(None, None) => {
Ok(Cred::ssh_key(&username, None, private_key.as_path(), None)
.expect("Could not create credentials object for ssh key"))
}
(None, Some(pp)) => Ok(Cred::ssh_key(
&username,
None,
private_key.as_path(),
Some(pp.as_ref()),
)
.expect("Could not create credentials object for ssh key")),
(Some(pk), None) => Ok(Cred::ssh_key(
&username,
Some(pk.as_path()),
private_key.as_path(),
None,
)
.expect("Could not create credentials object for ssh key")),
(Some(pk), Some(pp)) => Ok(Cred::ssh_key(
&username,
Some(pk.as_path()),
private_key.as_path(),
Some(pp.as_ref()),
)
.expect("Could not create credentials object for ssh key")),
},
);
cb
}
GitCredentials::UserPassPlaintext { username, password } => {
let mut cb = git2::RemoteCallbacks::new();
cb.credentials(move |_, _, _| {
Cred::userpass_plaintext(username.as_str(), password.as_str())
});
cb
}
}
} else {
git2::RemoteCallbacks::new()
}
}
pub fn is_shallow(&self) -> bool {
let repo = self.to_repository().expect("Could not read repo");
repo.is_shallow()
}
}