use chrono::prelude::*;
use color_eyre::eyre::Result;
use git2::Cred;
use git2::{Branch, BranchType, Commit, ObjectType, Repository};
use git_url_parse::GitUrl;
use hex::ToHex;
use log::debug;
use std::path::Path;
use std::path::PathBuf;
pub mod clone;
pub mod info;
#[derive(Clone, Debug)]
pub enum GitCredentials {
SshKey {
username: String,
public_key: Option<PathBuf>,
private_key: PathBuf,
passphrase: Option<String>,
},
UserPassPlaintext {
username: String,
password: String,
},
}
#[derive(Clone, Debug)]
pub struct GitRepoCloner {
pub url: GitUrl,
pub credentials: Option<GitCredentials>,
pub branch: Option<String>,
pub path: Option<PathBuf>,
}
#[derive(Clone, Debug, Default)]
pub struct GitRepo {
pub url: GitUrl,
pub head: Option<GitCommitMeta>,
pub credentials: Option<GitCredentials>,
pub branch: Option<String>,
pub path: Option<PathBuf>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct GitCommitMeta {
pub id: String,
pub message: Option<String>,
pub epoch_time: Option<DateTime<Utc>>,
}
impl GitCommitMeta {
pub fn new<I: ToHex + AsRef<[u8]>>(id: I) -> GitCommitMeta {
GitCommitMeta {
id: hex::encode(id),
message: None,
epoch_time: 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.epoch_time = Some(datetime);
self
}
pub fn with_message(mut self, msg: Option<String>) -> Self {
self.message = msg;
self
}
}
impl GitRepo {
fn get_local_repo_from_path<P: AsRef<Path>>(path: P) -> Result<Repository, git2::Error> {
Repository::open(path.as_ref().as_os_str())
}
fn _get_remote_url<'repo>(r: &'repo Repository) -> Result<String> {
let remote_name = GitRepo::_get_remote_name(&r)?;
let remote_url: String = r
.find_remote(&remote_name)?
.url()
.expect("Unable to extract repo url from remote")
.chars()
.collect();
Ok(remote_url)
}
fn _get_remote_name<'repo>(r: &'repo Repository) -> Result<String> {
let remote_name = r
.branch_upstream_remote(
r.head()
.and_then(|h| h.resolve())?
.name()
.expect("branch name is valid utf8"),
)
.map(|b| b.as_str().expect("valid utf8").to_string())
.unwrap_or_else(|_| "origin".into());
debug!("Remote name: {:?}", &remote_name);
Ok(remote_name)
}
pub fn git_remote_from_path(path: &Path) -> Result<String> {
let r = GitRepo::get_local_repo_from_path(path)?;
GitRepo::_get_remote_url(&r)
}
fn git_remote_from_repo(local_repo: &Repository) -> Result<String> {
GitRepo::_get_remote_url(&local_repo)
}
fn get_working_branch<'repo>(
r: &'repo Repository,
local_branch: &Option<String>,
) -> Result<Branch<'repo>> {
match local_branch {
Some(branch) => {
let b = r.find_branch(&branch, BranchType::Local)?;
debug!("Returning given branch: {:?}", &b.name());
Ok(b)
}
None => {
let head = r.head();
let local_branch = Branch::wrap(head?);
debug!("Returning HEAD branch: {:?}", local_branch.name()?);
match r.find_branch(
local_branch
.name()?
.expect("Unable to return local branch name"),
BranchType::Local,
) {
Ok(b) => Ok(b),
Err(e) => Err(e.into()),
}
}
}
}
fn is_commit_in_branch<'repo>(r: &'repo Repository, commit: &Commit, branch: &Branch) -> bool {
let branch_head = branch.get().peel_to_commit();
if branch_head.is_err() {
return false;
}
let branch_head = branch_head.expect("Unable to extract branch HEAD commit");
if branch_head.id() == commit.id() {
return true;
}
let check_commit_in_branch = r.graph_descendant_of(branch_head.id(), commit.id());
if check_commit_in_branch.is_err() {
return false;
}
check_commit_in_branch.expect("Unable to determine if commit exists within branch")
}
fn get_target_commit<'repo>(
r: &'repo Repository,
branch: &Option<String>,
commit_id: &Option<String>,
) -> Result<Commit<'repo>> {
let working_branch = GitRepo::get_working_branch(r, branch)?;
match commit_id {
Some(id) => {
let working_ref = working_branch.into_reference();
debug!("Commit provided. Using {}", id);
let oid = git2::Oid::from_str(id)?;
let obj = r.find_object(oid, ObjectType::from_str("commit"))?;
let commit = obj
.into_commit()
.expect("Unable to convert commit id into commit object");
let _ = GitRepo::is_commit_in_branch(r, &commit, &Branch::wrap(working_ref));
Ok(commit)
}
None => {
debug!("No commit provided. Using HEAD commit from remote branch");
let upstream_branch = working_branch.upstream()?;
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(commit)
}
}
}
pub fn open(
path: PathBuf,
branch: Option<String>,
commit_id: Option<String>,
) -> Result<GitRepo> {
let local_repo = GitRepo::get_local_repo_from_path(path.clone())?;
let remote_url = GitRepo::git_remote_from_repo(&local_repo)?;
let working_branch_name = GitRepo::get_working_branch(&local_repo, &branch)?
.name()?
.expect("Unable to extract branch name")
.to_string();
let commit = GitRepo::get_target_commit(
&local_repo,
&Some(working_branch_name.clone()),
&commit_id,
)?;
Ok(GitRepo::new(remote_url)?
.with_path(path)
.with_branch(working_branch_name)
.with_commit(commit))
}
pub fn with_path(mut self, path: PathBuf) -> Self {
self.path = Some(path);
self
}
pub fn with_branch<S: Into<String>>(mut self, branch: S) -> Self {
self.branch = Some(branch.into());
self
}
pub fn with_commit(mut self, commit: Commit) -> Self {
let commit_msg = commit.clone().message().unwrap_or_default().to_string();
let commit = GitCommitMeta::new(commit.id())
.with_message(Some(commit_msg))
.with_timestamp(commit.time().seconds());
self.head = Some(commit);
self
}
pub fn with_credentials(mut self, creds: GitCredentials) -> Self {
self.credentials = Some(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 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()
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}