use error::*;
use prepare::GitWorkArea;
use std::ffi::OsStr;
use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CommitId(String);
impl CommitId {
pub fn new<I: ToString>(id: I) -> Self {
CommitId(id.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for CommitId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct GitContext {
gitdir: PathBuf,
config: Option<PathBuf>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Identity {
pub name: String,
pub email: String,
}
impl Identity {
pub fn new<N, E>(name: N, email: E) -> Self
where N: ToString,
E: ToString,
{
Identity {
name: name.to_string(),
email: email.to_string(),
}
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
Self::new(&self.name, &self.email)
}
}
impl Display for Identity {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} <{}>", self.name, self.email)
}
}
#[derive(Debug)]
pub enum MergeStatus {
NoCommonHistory,
AlreadyMerged,
Mergeable(Vec<CommitId>),
}
impl Display for MergeStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f,
"{}",
match *self {
MergeStatus::NoCommonHistory => "no common history",
MergeStatus::AlreadyMerged => "already merged",
MergeStatus::Mergeable(_) => "mergeable",
})
}
}
impl GitContext {
pub fn new<P>(gitdir: P) -> Self
where P: AsRef<Path>,
{
GitContext {
gitdir: gitdir.as_ref().to_path_buf(),
config: None,
}
}
pub fn new_with_config<P, C>(gitdir: P, config: C) -> Self
where P: AsRef<Path>,
C: AsRef<Path>,
{
GitContext {
gitdir: gitdir.as_ref().to_path_buf(),
config: Some(config.as_ref().to_path_buf()),
}
}
pub fn git(&self) -> Command {
let mut git = Command::new("git");
git.env("GIT_DIR", &self.gitdir);
self.config
.as_ref()
.map(|config| git.env("GIT_CONFIG", config));
git
}
pub fn fetch<R, I, N>(&self, remote: R, refnames: I) -> Result<()>
where R: AsRef<str>,
I: IntoIterator<Item = N>,
N: AsRef<OsStr>,
{
let fetch = self.git()
.arg("fetch")
.arg(remote.as_ref())
.args(&refnames.into_iter()
.collect::<Vec<_>>())
.output()
.chain_err(|| "failed to construct fetch command")?;
if !fetch.status.success() {
bail!(ErrorKind::Git(format!("fetch from {} failed: {}",
remote.as_ref(),
String::from_utf8_lossy(&fetch.stderr))));
}
Ok(())
}
pub fn fetch_into<R, N, T>(&self, remote: R, refname: N, target: T) -> Result<()>
where R: AsRef<str>,
N: AsRef<str>,
T: AsRef<str>,
{
self.fetch(remote,
&[&format!("{}:{}", refname.as_ref(), target.as_ref())])
}
pub fn force_fetch_into<R, N, T>(&self, remote: R, refname: N, target: T) -> Result<()>
where R: AsRef<str>,
N: AsRef<str>,
T: AsRef<str>,
{
self.fetch_into(remote.as_ref(),
format!("+{}", refname.as_ref()),
target.as_ref())
}
pub fn prepare(&self, rev: &CommitId) -> Result<GitWorkArea> {
GitWorkArea::new(self.clone(), rev)
}
pub fn reserve_ref<N>(&self, name: N, commit: &CommitId) -> Result<(String, usize)>
where N: AsRef<str>,
{
let ref_prefix = format!("refs/{}/heads", name.as_ref());
debug!(target: "git", "reserving ref under {}", ref_prefix);
loop {
let for_each_ref = self.git()
.arg("for-each-ref")
.arg("--format=%(refname)")
.arg("--")
.arg(&ref_prefix)
.output()
.chain_err(|| "failed to construct for-each-ref command")?;
if !for_each_ref.status.success() {
bail!(ErrorKind::Git(format!("listing all {} refs: {}",
ref_prefix,
String::from_utf8_lossy(&for_each_ref.stderr))));
}
let refs = String::from_utf8_lossy(&for_each_ref.stdout);
let nrefs = refs.lines().count();
let new_ref = format!("{}/{}", ref_prefix, nrefs);
debug!(target: "git", "trying to reserve ref {}", new_ref);
let lock_ref = self.git()
.arg("update-ref")
.arg(&new_ref)
.arg(commit.as_str())
.arg("0000000000000000000000000000000000000000")
.stdout(Stdio::null())
.output()
.chain_err(|| "failed to construct update-ref command")?;
if lock_ref.status.success() {
debug!(target: "git", "successfully reserved {}", new_ref);
return Ok((new_ref, nrefs));
}
let err = String::from_utf8_lossy(&lock_ref.stderr);
if err.contains("with nonexistent object") {
bail!(ErrorKind::InvalidRef("no such commit".to_string()));
} else if err.contains("not a valid SHA1") {
bail!(ErrorKind::InvalidRef("invalid SHA".to_string()));
}
}
}
pub fn reserve_refs<N>(&self, name: N, commit: &CommitId) -> Result<(String, String)>
where N: AsRef<str>,
{
let (new_ref, id) = self.reserve_ref(name.as_ref(), commit)?;
let new_base = format!("refs/{}/bases/{}", name.as_ref(), id);
debug!(target: "git", "successfully reserved {} and {}", new_ref, new_base);
Ok((new_ref, new_base))
}
pub fn mergeable(&self, base: &CommitId, topic: &CommitId) -> Result<MergeStatus> {
let merge_base = self.git()
.arg("merge-base")
.arg("--all") .arg(base.as_str())
.arg(topic.as_str())
.output()
.chain_err(|| "failed to construct merge-base command")?;
if !merge_base.status.success() {
return Ok(MergeStatus::NoCommonHistory);
}
let bases = String::from_utf8_lossy(&merge_base.stdout);
let bases = bases.split_whitespace()
.map(CommitId::new)
.collect::<Vec<_>>();
Ok(if Some(topic) == bases.first() {
MergeStatus::AlreadyMerged
} else {
MergeStatus::Mergeable(bases)
})
}
pub fn gitdir(&self) -> &Path {
&self.gitdir
}
}