use std::path::{Path, PathBuf};
use clap::ArgEnum;
use git2::{IndexAddOption, PushOptions};
type Result<T> = std::result::Result<T, git2::Error>;
const HEAD: &str = "HEAD";
#[derive(ArgEnum, Clone)]
pub enum AuthenticationMethod {
SshAgent,
SshKey { path: PathBuf, passphrase: String },
}
pub trait Repository: Send {
fn stage<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>;
fn stage_all(&self) -> Result<()>;
fn commit(&self, message: &str) -> Result<()>;
fn push(&self, authentication_method: AuthenticationMethod) -> Result<()>;
}
pub struct WatchedRepository(git2::Repository);
impl WatchedRepository {
pub fn new<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
Ok(Self(git2::Repository::open(path)?))
}
}
impl Repository for WatchedRepository {
fn stage<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let mut index = self.0.index()?;
index.add_path(
path.as_ref()
.strip_prefix(self.0.path().parent().unwrap())
.unwrap(),
)?;
index.write()?;
Ok(())
}
fn stage_all(&self) -> Result<()> {
let mut index = self.0.index()?;
index.add_all(["*"].iter(), IndexAddOption::CHECK_PATHSPEC, None)?;
index.write()?;
Ok(())
}
fn commit(&self, message: &str) -> Result<()> {
let repo = &self.0;
let tree_oid = repo.index()?.write_tree()?;
let tree = repo.find_tree(tree_oid)?;
let config = repo.config()?;
let name = config.get_string("user.name")?;
let email = config.get_string("user.email")?;
let signature = git2::Signature::now(&name, &email)?;
let parent_commit = repo.head()?.resolve()?.peel_to_commit()?;
repo.commit(
Some(HEAD),
&signature,
&signature,
message,
&tree,
&[&parent_commit],
)?;
Ok(())
}
fn push(&self, authentication_method: AuthenticationMethod) -> Result<()> {
let repo = &self.0;
let mut remote = repo.find_remote("origin")?;
let head = repo.head()?;
let refspecs: &[&str] = &[head.name().unwrap()];
let mut remote_callbacks = git2::RemoteCallbacks::new();
match authentication_method {
AuthenticationMethod::SshAgent => {
remote_callbacks.credentials(|_url, username_from_url, _allowed_types| {
git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
});
}
AuthenticationMethod::SshKey {
path: private_key_path,
passphrase: key_passphrase,
} => {
remote_callbacks.credentials(move |_url, username_from_url, _allowed_types| {
git2::Cred::ssh_key(
username_from_url.unwrap(),
Some(&private_key_path.clone().with_extension("pub")),
&private_key_path,
Some(&key_passphrase.clone()),
)
});
}
};
remote_callbacks.push_update_reference(|refname, status| {
if let Some(status_message) = status {
log::error!("error pushing reference {}", refname);
Err(git2::Error::from_str(status_message))
} else {
Ok(())
}
});
let mut push_options = PushOptions::new();
push_options.remote_callbacks(remote_callbacks);
remote.push(refspecs, Some(&mut push_options))?;
Ok(())
}
}
pub struct DummyRepository;
impl Repository for DummyRepository {
fn stage<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
log::info!("staged file {}", path.as_ref().display());
Ok(())
}
fn stage_all(&self) -> Result<()> {
log::info!("staged all files");
Ok(())
}
fn commit(&self, message: &str) -> Result<()> {
log::info!("commited staged files with message: {}", message);
Ok(())
}
fn push(&self, _authentication_method: AuthenticationMethod) -> Result<()> {
log::info!("pushed files to remote");
Ok(())
}
}