use {Error, ResultExt};
use backend::Backend;
use git2::{self, Repository, Direction, AutotagOption};
use std::path::Path;
pub struct Git {
repo: Repository,
}
impl Git {
pub fn open(repo_path: &Path) -> Result<Git, Error> {
let repo = Repository::open(repo_path).
chain_err(|| format!("could not open '{}'", repo_path.display()))?;
Ok(Git { repo: repo })
}
pub fn setup(dest: &Path, source: &str) -> Result<Git, Error> {
ilog!("cloning from Git repository at '{}' to '{}'", dest.display(), source);
let repo = Repository::clone_recurse(source, dest).chain_err(|| format!("could not clone '{}'", source))?;
ilog!("successfully cloned Git repository");
Ok(Git { repo: repo })
}
pub fn open_or_create(dest: &Path, source: &str) -> Result<Git, Error> {
if dest.join(".git").exists() {
Git::open(dest)
} else {
Git::setup(dest, source)
}
}
fn is_worktree_dirty(&self) -> Result<bool, Error> {
Ok(self.repo.statuses(None)?.
iter().
any(|entry| !entry.status().is_empty()))
}
}
impl Backend for Git {
fn update(&mut self, _verbose: bool) -> Result<(), Error> {
if self.is_worktree_dirty()? {
fatal!("dotfiles repository needs to have a clean worktree ({})",
self.repo.path().display());
}
self::ensure_head_is_named_reference(&mut self.repo)?;
let mut original_head = self.repo.head()?;
assert!(original_head.is_branch(), "HEAD is not a branch");
let branch_name = original_head.shorthand().unwrap().to_owned();
let remote_name = if let Some(name) = self.repo.remotes()?.iter().next() {
name.expect("branch name is not valid utf-8").to_owned()
} else {
return Err("repository has no remotes set up".into());
};
let mut remote = self.repo.find_remote(&remote_name)?;
remote.connect(Direction::Fetch)?;
remote.download::<&str>(&[], None)?;
remote.disconnect()?;
remote.update_tips(None, true,
AutotagOption::Unspecified, None)?;
for mut submodule in self.repo.submodules()? {
submodule.update(true, None)?;
}
let remote_ref_name = format!("refs/remotes/{}/{}", remote_name, branch_name);
let remote_ref = self.repo.find_reference(&remote_ref_name)?;
let current_head = original_head.set_target(remote_ref.target().unwrap(), "updating branch for new dotfiles")?;
let original_oid = original_head.target().unwrap();
let current_oid = current_head.target().unwrap();
let current_oid_label = ¤t_oid.to_string()[0..7];
if original_head == current_head {
ilog!("already up-to-date with {} at {}", branch_name, current_oid_label);
} else {
let mut revwalk = self.repo.revwalk()?;
revwalk.push(current_oid)?;
revwalk.hide(original_oid)?;
ilog!("");
ilog!("Commits");
ilog!("-------");
for oid in revwalk {
let oid = oid?;
let commit = self.repo.find_commit(oid)?;
let oid_label = &oid.to_string()[..7];
ilog!("{} {}", oid_label, commit.message().unwrap().trim());
}
ilog!("");
ilog!("updated `{}` to {}", branch_name, current_oid_label);
}
Ok(())
}
}
fn ensure_head_is_named_reference(repo: &mut Repository) -> Result<(), git2::Error> {
let head = repo.head()?;
if head.is_branch() {
Ok(())
} else if head.is_note() {
panic!("HEAD is a note!");
} else if head.is_remote() {
panic!("HEAD is a remote!");
} else if head.is_tag() {
panic!("HEAD is a tag!");
} else {
panic!("an arbitrary commit is checked out");
}
}