use std::env;
use std::io;
use std::io::Write;
use std::str;
use std::path::Path;
use git2::{
Commit, Cred, Error, Index, IndexAddOption, ObjectType, Oid, PushOptions, RemoteCallbacks,
Repository, ResetType, Signature,
};
pub struct Repo {
path: String,
}
impl Repo {
pub fn new(path: &str) -> Repo {
Repo {
path: path.to_owned(),
}
}
fn git_credentials_callback(
_user: &str,
_user_from_url: Option<&str>,
_cred: git2::CredentialType,
) -> Result<git2::Cred, git2::Error> {
let user = _user_from_url.unwrap_or("git");
if _cred.contains(git2::CredentialType::USERNAME) {
return git2::Cred::username(user);
}
match env::var("GPM_SSH_KEY") {
Ok(k) => {
println!(
"authenticate with user {} and private key located in {}",
user, k
);
git2::Cred::ssh_key(user, None, std::path::Path::new(&k), None)
}
_ => {
let ssh_path = Path::new(&dirs::home_dir().unwrap())
.join(Path::new(".ssh/id_rsa"));
if !ssh_path.exists() {
panic!("No SSH Key found in ~/.ssh")
}
Cred::ssh_key(_user_from_url.unwrap(), None, &ssh_path, None)
}
}
}
#[allow(dead_code)]
fn reset(&self) -> Result<(), Box<Error>> {
let repo = match Repository::open(&self.path) {
Ok(repo) => repo,
Err(e) => panic!("Failed to open: {}", e),
};
repo.reset(
&repo.revparse_single("HEAD").unwrap(),
ResetType::Hard,
None,
)?;
Ok(())
}
pub fn find_last_commit<'repo>(
&self,
repo: &'repo Repository,
) -> Result<Commit<'repo>, Error> {
let obj = repo.head()?.resolve()?.peel(ObjectType::Commit)?;
match obj.into_commit() {
Ok(c) => Ok(c),
_ => Err(Error::from_str("commit error")),
}
}
pub fn is_ahead_behind_remote(&self) -> (bool, bool) {
let repo = Repository::open(&self.path).unwrap();
let mut remote = repo.find_remote("origin").unwrap();
self.do_fetch(&repo, &["master"], &mut remote).unwrap();
let head = repo.revparse_single("HEAD").unwrap().id();
println!("🏠 HEAD: {:?}", head);
if let Some((upstream, _)) = repo.revparse_ext("@{u}").ok() {
println!("📻️ Upstream: {:?}", upstream.id());
return match repo.graph_ahead_behind(head, upstream.id()) {
Ok((commits_ahead, commits_behind)) => {
(commits_ahead > 0, commits_behind > 0)
}
Err(_) => (false, false),
};
}
(false, false)
}
fn do_fetch<'a>(
&self,
repo: &'a git2::Repository,
refs: &[&str],
remote: &'a mut git2::Remote,
) -> Result<git2::AnnotatedCommit<'a>, git2::Error> {
let mut cb = git2::RemoteCallbacks::new();
cb.credentials(Repo::git_credentials_callback);
cb.transfer_progress(|stats| {
if stats.received_objects() == stats.total_objects() {
print!(
"Resolving deltas {}/{}\r",
stats.indexed_deltas(),
stats.total_deltas()
);
} else if stats.total_objects() > 0 {
print!(
"Received {}/{} objects ({}) in {} bytes\r",
stats.received_objects(),
stats.total_objects(),
stats.indexed_objects(),
stats.received_bytes()
);
}
io::stdout().flush().unwrap();
true
});
let mut fo = git2::FetchOptions::new();
fo.remote_callbacks(cb);
fo.download_tags(git2::AutotagOption::All);
println!("🪂 Fetching {} for repo", remote.name().unwrap());
remote.fetch(refs, Some(&mut fo), None)?;
let stats = remote.stats();
if stats.local_objects() > 0 {
println!(
"\rReceived {}/{} objects in {} bytes (used {} local objects)",
stats.indexed_objects(),
stats.total_objects(),
stats.received_bytes(),
stats.local_objects()
);
} else {
println!(
"\rReceived {}/{} objects in {} bytes",
stats.indexed_objects(),
stats.total_objects(),
stats.received_bytes()
);
}
let fetch_head = repo.find_reference("FETCH_HEAD")?;
Ok(repo.reference_to_annotated_commit(&fetch_head)?)
}
fn fast_forward(
&self,
repo: &Repository,
lb: &mut git2::Reference,
rc: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
let name = match lb.name() {
Some(s) => s.to_string(),
None => String::from_utf8_lossy(lb.name_bytes()).to_string(),
};
let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id());
println!("{}", msg);
lb.set_target(rc.id(), &msg)?;
repo.set_head(&name)?;
repo.checkout_head(Some(git2::build::CheckoutBuilder::default()
.force()))?;
Ok(())
}
fn normal_merge(
&self,
repo: &Repository,
local: &git2::AnnotatedCommit,
remote: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
let local_tree = repo.find_commit(local.id())?.tree()?;
let remote_tree = repo.find_commit(remote.id())?.tree()?;
let ancestor = repo
.find_commit(repo.merge_base(local.id(), remote.id())?)?
.tree()?;
let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;
if idx.has_conflicts() {
println!("⛔ Merge conficts detected...");
repo.checkout_index(Some(&mut idx), None)?;
return Ok(());
}
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
let msg = format!("Merge: {} into {}", remote.id(), local.id());
let sig = repo.signature()?;
let local_commit = repo.find_commit(local.id())?;
let remote_commit = repo.find_commit(remote.id())?;
let _merge_commit = repo.commit(
Some("HEAD"),
&sig,
&sig,
&msg,
&result_tree,
&[&local_commit, &remote_commit],
)?;
repo.checkout_head(None)?;
Ok(())
}
fn do_merge<'a>(
&self,
repo: &'a Repository,
remote_branch: &str,
fetch_commit: git2::AnnotatedCommit<'a>,
) -> Result<(), git2::Error> {
let analysis = repo.merge_analysis(&[&fetch_commit])?;
if analysis.0.is_fast_forward() {
println!("⏩ Doing a fast forward");
let refname = format!("refs/heads/{}", remote_branch);
match repo.find_reference(&refname) {
Ok(mut r) => {
self.fast_forward(&repo, &mut r, &fetch_commit)?;
}
Err(_) => {
repo.reference(
&refname,
fetch_commit.id(),
true,
&format!(
"Setting {} to {}",
remote_branch,
fetch_commit.id()
),
)?;
repo.set_head(&refname)?;
repo.checkout_head(Some(
git2::build::CheckoutBuilder::default()
.allow_conflicts(true)
.conflict_style_merge(true)
.force(),
))?;
}
};
} else if analysis.0.is_normal() {
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
self.normal_merge(&repo, &head_commit, &fetch_commit)?;
} else {
println!("Nothing to do...");
}
Ok(())
}
pub fn pull(&self) -> Result<(), Error> {
let repo = Repository::open(&self.path)?;
println!("🔍 Pulling update from Github");
let mut remote = repo.find_remote("origin")?;
let fetch_commit = self.do_fetch(&repo, &["master"], &mut remote)?;
self.do_merge(&repo, "master", fetch_commit)?;
Ok(())
}
#[allow(dead_code)]
pub fn check(&self) {
let repo_path = Path::new(&self.path);
if repo_path.exists() && repo_path.is_dir() {
self.reset().expect("Could not reset repo");
let _ = match self.pull() {
Ok(idx) => idx,
Err(e) => panic!("Failed to pull: {}", e),
};
}
}
fn get_repo(&self) -> Repository {
let repo_path = Path::new(&self.path);
match Repository::open(repo_path) {
Ok(r) => r,
Err(_) => panic!("Supplied path is not a git directory. Consider initializing it first"),
}
}
#[allow(dead_code)]
fn add_file(&self, file_path: &Path) -> Result<Index, Error> {
let repo = self.get_repo();
let mut index = repo.index()?;
index.add_path(file_path)?;
index.write().unwrap();
return Ok(index);
}
pub fn add_all(&self) -> Result<Index, Error> {
let repo = self.get_repo();
let mut index = repo.index()?;
index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None)?;
index.write().expect("Failed to write index to disk: {}");
return Ok(index);
}
pub fn commit(&self, mut index: Index, message: &str) -> Result<Oid, Error> {
let repo = self.get_repo();
let oid = index.write_tree_to(&repo)?;
let signature = Signature::now("Tri-sync CLI", "tri-sync@ibm.com")?;
let parent_commit = self.find_last_commit(&repo)?;
let tree = repo.find_tree(oid)?;
repo.commit(
Some("HEAD"),
&signature,
&signature,
message,
&tree,
&[&parent_commit],
)
}
pub fn find_remote_origin_url<'repo>(&self) -> Option<String> {
let repo = self.get_repo();
let url = repo.find_remote("origin");
match url {
Ok(u) => match u.url() {
Some(s) => Some(String::from(s)),
None => None,
},
Err(_) => None,
}
}
pub fn last_commit_id(&self) -> Result<String, Box<Error>> {
let repo = self.get_repo();
let commit_id = self.find_last_commit(&repo)?.id().to_string();
Ok(commit_id)
}
pub fn push(&self) -> Result<(), Error> {
let repo_path = Path::new(&self.path);
let mut push_opts = PushOptions::new();
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(Repo::git_credentials_callback);
push_opts.remote_callbacks(callbacks);
let repo = match Repository::open(repo_path) {
Ok(r) => r,
Err(e) => panic!("Could not open repository: {}", e),
};
return repo.find_remote("origin")?.push(
&["refs/heads/master:refs/heads/master"],
Some(&mut push_opts),
);
}
}