use crates::chrono::{DateTime, Utc};
use crates::regex::Regex;
use crates::tempdir::TempDir;
use error::*;
use git::{CommitId, GitContext, Identity};
use std::borrow::Cow;
use std::collections::hash_map::HashMap;
use std::ffi::OsStr;
use std::fmt::{self, Debug};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::iter;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Debug)]
pub enum Conflict {
Path(PathBuf),
SubmoduleNotMerged(PathBuf),
SubmoduleNotPresent(PathBuf),
SubmoduleWithFix(PathBuf, CommitId),
}
impl Conflict {
pub fn path(&self) -> &Path {
match *self {
Conflict::Path(ref p) |
Conflict::SubmoduleNotMerged(ref p) |
Conflict::SubmoduleNotPresent(ref p) |
Conflict::SubmoduleWithFix(ref p, _) => p,
}
}
}
impl PartialEq for Conflict {
fn eq(&self, rhs: &Self) -> bool {
self.path() == rhs.path()
}
}
pub struct MergeCommand {
command: Command,
}
impl MergeCommand {
pub fn committer(&mut self, committer: &Identity) -> &mut Self {
self.command
.env("GIT_COMMITTER_NAME", &committer.name)
.env("GIT_COMMITTER_EMAIL", &committer.email);
self
}
pub fn author(&mut self, author: &Identity) -> &mut Self {
self.command
.env("GIT_AUTHOR_NAME", &author.name)
.env("GIT_AUTHOR_EMAIL", &author.email);
self
}
pub fn author_date(&mut self, when: &DateTime<Utc>) -> &mut Self {
self.command
.env("GIT_AUTHOR_DATE", when.to_rfc2822());
self
}
pub fn commit<M>(self, message: M) -> Result<CommitId>
where M: AsRef<str>,
{
self.commit_impl(message.as_ref())
}
fn commit_impl(mut self, message: &str) -> Result<CommitId> {
let mut commit_tree = self.command
.spawn()
.chain_err(|| "failed to construct commit-tree command")?;
{
let mut commit_tree_stdin =
commit_tree.stdin.as_mut().expect("expected commit-tree to have a stdin");
commit_tree_stdin.write_all(message.as_bytes())
.chain_err(|| {
ErrorKind::Git("failed to write the commit message to commit-tree".to_string())
})?;
}
let commit_tree = commit_tree.wait_with_output()
.chain_err(|| "failed to execute commit-tree command")?;
if !commit_tree.status.success() {
bail!(ErrorKind::Git(format!("failed to commit the merged tree: {}",
String::from_utf8_lossy(&commit_tree.stderr))));
}
let merge_commit = String::from_utf8_lossy(&commit_tree.stdout);
Ok(CommitId::new(merge_commit.trim()))
}
}
impl Debug for MergeCommand {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MergeCommand")
}
}
#[derive(Debug)]
pub enum MergeResult<'a> {
Conflict(Vec<Conflict>),
Ready(MergeCommand),
#[doc(hidden)]
_Phantom(PhantomData<&'a str>),
}
pub type SubmoduleConfig = HashMap<String, HashMap<String, String>>;
struct PreparingGitWorkArea {
context: GitContext,
dir: TempDir,
}
#[derive(Debug)]
pub struct GitWorkArea {
context: GitContext,
dir: TempDir,
submodule_config: SubmoduleConfig,
}
lazy_static! {
static ref SUBMODULE_CONFIG_RE: Regex =
Regex::new(r"^submodule\.(?P<name>.*)\.(?P<key>[^=]*)=(?P<value>.*)$").unwrap();
}
trait WorkareaGitContext {
fn cmd(&self) -> Command;
}
fn checkout<I, P>(ctx: &WorkareaGitContext, paths: I) -> Result<()>
where I: IntoIterator<Item = P>,
P: AsRef<OsStr>,
{
let ls_files = ctx.cmd()
.arg("ls-files")
.arg("--")
.args(paths.into_iter())
.output()
.chain_err(|| "failed to construct ls-files command")?;
if !ls_files.status.success() {
bail!(ErrorKind::Git(format!("listing paths in the index: {}",
String::from_utf8_lossy(&ls_files.stderr))));
}
checkout_files(ctx, &ls_files.stdout)
}
fn checkout_files(ctx: &WorkareaGitContext, files: &[u8]) -> Result<()> {
let mut checkout_index = ctx.cmd()
.arg("checkout-index")
.arg("-f")
.arg("-q")
.arg("--stdin")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.chain_err(|| "failed to construct checkout-index command")?;
checkout_index.stdin
.as_mut()
.expect("expected checkout-index to have a stdin")
.write_all(files)
.chain_err(|| ErrorKind::Git("writing to checkout-index".to_string()))?;
let res = checkout_index.wait().expect("expected checkout-index to execute successfully");
if !res.success() {
let mut stderr = String::new();
checkout_index.stderr
.as_mut()
.expect("expected checkout-index to have a stderr")
.read_to_string(&mut stderr)
.chain_err(|| "failed to read from checkout-index")?;
bail!(ErrorKind::Git(format!("running checkout-index: {}", stderr)));
}
let mut update_index = ctx.cmd()
.arg("update-index")
.arg("--stdin")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.chain_err(|| "failed to construct update-index command")?;
update_index.stdin
.as_mut()
.expect("expected update-index to have a stdin")
.write_all(files)
.chain_err(|| ErrorKind::Git("writing to update-index".to_string()))?;
let res = update_index.wait().expect("expected update-index to execute successfully");
if !res.success() {
let mut stderr = String::new();
update_index.stderr
.as_mut()
.expect("expected update-index to have a stderr")
.read_to_string(&mut stderr)
.chain_err(|| "failed to read from update-index")?;
bail!(ErrorKind::Git(format!("running update-index: {}", stderr)));
}
Ok(())
}
fn file_name(path: &Path) -> Cow<str> {
path.file_name().map_or_else(|| Cow::Borrowed("<unknown>"), OsStr::to_string_lossy)
}
impl PreparingGitWorkArea {
fn new(context: GitContext, rev: &CommitId) -> Result<Self> {
let tempdir = TempDir::new_in(context.gitdir(), "git-work-area")
.chain_err(|| "failed to create temporary directory")?;
let workarea = PreparingGitWorkArea {
context: context,
dir: tempdir,
};
debug!(target: "git.workarea",
"creating prepared workarea under {}",
workarea.dir.path().display());
fs::create_dir_all(workarea.work_tree())
.chain_err(|| "failed to create the workarea directory")?;
workarea.prepare(rev)?;
debug!(target: "git.workarea",
"created prepared workarea under {}",
file_name(workarea.dir.path()));
Ok(workarea)
}
fn prepare(&self, rev: &CommitId) -> Result<()> {
let res = self.git()
.arg("read-tree")
.arg("-i") .arg("-m") .arg(rev.as_str())
.output()
.chain_err(|| "failed to construct read-tree command")?;
if !res.status.success() {
bail!(ErrorKind::Git(format!("reading the tree from {}: {}",
rev,
String::from_utf8_lossy(&res.stderr))));
}
self.git()
.arg("update-index")
.arg("--refresh")
.arg("--ignore-missing")
.arg("--skip-worktree")
.stdout(Stdio::null())
.status()
.chain_err(|| "failed to construct update-index command")?;
checkout(self, iter::once(".gitmodules"))
}
fn git(&self) -> Command {
let mut git = self.context.git();
git.env("GIT_WORK_TREE", self.work_tree())
.env("GIT_INDEX_FILE", self.index());
git
}
fn query_submodules(&self) -> Result<SubmoduleConfig> {
let module_path = self.work_tree().join(".gitmodules");
if !module_path.exists() {
return Ok(SubmoduleConfig::new());
}
let config = self.git()
.arg("config")
.arg("-f")
.arg(module_path)
.arg("-l")
.output()
.chain_err(|| "failed to construct config command for submodules")?;
if !config.status.success() {
bail!(ErrorKind::Git(format!("reading the submodule configuration: {}",
String::from_utf8_lossy(&config.stderr))));
}
let config = String::from_utf8_lossy(&config.stdout);
let mut submodule_config = SubmoduleConfig::new();
let captures = config.lines()
.filter_map(|l| SUBMODULE_CONFIG_RE.captures(l));
for capture in captures {
submodule_config.entry(capture.name("name")
.expect("the submodule regex should have a 'name' group")
.as_str()
.to_string())
.or_insert_with(HashMap::new)
.insert(capture.name("key")
.expect("the submodule regex should have a 'key' group")
.as_str()
.to_string(),
capture.name("value")
.expect("the submodule regex should have a 'value' group")
.as_str()
.to_string());
}
let gitmoduledir = self.context.gitdir().join("modules");
Ok(submodule_config.into_iter()
.filter(|&(ref name, _)| gitmoduledir.join(name).exists())
.collect())
}
fn index(&self) -> PathBuf {
self.dir.path().join("index")
}
fn work_tree(&self) -> PathBuf {
self.dir.path().join("work")
}
}
impl WorkareaGitContext for PreparingGitWorkArea {
fn cmd(&self) -> Command {
self.git()
}
}
impl GitWorkArea {
pub fn new(context: GitContext, rev: &CommitId) -> Result<Self> {
let intermediate = PreparingGitWorkArea::new(context, rev)?;
let workarea = GitWorkArea {
submodule_config: intermediate.query_submodules()?,
context: intermediate.context,
dir: intermediate.dir,
};
debug!(target: "git.workarea",
"creating prepared workarea with submodules under {}",
workarea.dir.path().display());
workarea.prepare_submodules()?;
debug!(target: "git.workarea",
"created prepared workarea with submodules under {}",
file_name(workarea.dir.path()));
Ok(workarea)
}
fn prepare_submodules(&self) -> Result<()> {
if self.submodule_config.is_empty() {
return Ok(());
}
debug!(target: "git.workarea",
"preparing submodules for {}",
file_name(self.dir.path()));
for (name, config) in &self.submodule_config {
let gitdir = self.context.gitdir().join("modules").join(name);
if !gitdir.exists() {
error!(target: "git.workarea",
"{}: submodule configuration for {} does not exist: {}",
file_name(self.dir.path()),
name,
gitdir.display());
continue;
}
let path = config.get("path")
.expect("the 'path` configuration for submodules is required.");
let gitfiledir = self.work_tree().join(path);
fs::create_dir_all(&gitfiledir).chain_err(|| {
format!("failed to create the {} submodule directory for the workarea",
name)
})?;
let mut gitfile = File::create(gitfiledir.join(".git"))
.chain_err(|| format!("failed to create the .git file for the {} module", name))?;
write!(gitfile, "gitdir: {}\n", gitdir.display())
.chain_err(|| format!("failed to write the .git file for the {} module", name))?;
}
Ok(())
}
pub fn git(&self) -> Command {
let mut git = self.context.git();
git.env("GIT_WORK_TREE", self.work_tree())
.env("GIT_INDEX_FILE", self.index());
git
}
fn submodule_conflict<P>(&self, path: P, ours: &CommitId, theirs: &CommitId) -> Result<Conflict>
where P: AsRef<Path>,
{
let path = path.as_ref().to_path_buf();
debug!(target: "git.workarea",
"{} checking for a submodule conflict for {}",
file_name(self.dir.path()),
path.display());
let branch_info = self.submodule_config
.iter()
.find(|&(_, config)| {
config.get("path")
.map_or(false,
|submod_path| submod_path.as_str() == path.to_string_lossy())
})
.map(|(name, config)| {
(name, config.get("branch").map_or("master", String::as_str))
});
let (name, branch) = if let Some((name, branch_name)) = branch_info {
if branch_name == "." {
debug!(target: "git.workarea",
"the `.` branch specifier for submodules is not supported for conflict \
resolution");
return Ok(Conflict::Path(path));
}
(name, branch_name)
} else {
debug!(target: "git.workarea",
"no submodule configured for {}; cannot attempt smarter resolution",
path.display());
return Ok(Conflict::Path(path));
};
let submodule_ctx = GitContext::new(self.gitdir().join("modules").join(name));
let refs = submodule_ctx.git()
.arg("rev-list")
.arg("--first-parent") .arg("--reverse") .arg(branch)
.arg(format!("^{}", ours))
.arg(format!("^{}", theirs))
.output()
.chain_err(|| {
"failed to construct rev-list command for submodule conflict resolution"
})?;
if !refs.status.success() {
return Ok(Conflict::SubmoduleNotPresent(path));
}
let refs = String::from_utf8_lossy(&refs.stdout);
for hash in refs.lines() {
let ours_ancestor = submodule_ctx.git()
.arg("merge-base")
.arg("--is-ancestor")
.arg(ours.as_str())
.arg(hash)
.status()
.chain_err(|| {
"failed to construct merge-base command for submodule conflict resolution"
})?;
let theirs_ancestor = submodule_ctx.git()
.arg("merge-base")
.arg("--is-ancestor")
.arg(theirs.as_str())
.arg(hash)
.status()
.chain_err(|| {
"failed to construct merge-base command for submodule conflict resolution"
})?;
if ours_ancestor.success() && theirs_ancestor.success() {
return Ok(Conflict::SubmoduleWithFix(path, CommitId::new(hash)));
}
}
Ok(Conflict::SubmoduleNotMerged(path))
}
fn conflict_information(&self) -> Result<Vec<Conflict>> {
let ls_files = self.git()
.arg("ls-files")
.arg("-u")
.output()
.chain_err(|| "failed to construct ls-files command for conflict resolution")?;
if !ls_files.status.success() {
bail!(ErrorKind::Git(format!("listing unmerged files: {}",
String::from_utf8_lossy(&ls_files.stderr))));
}
let conflicts = String::from_utf8_lossy(&ls_files.stdout);
let mut conflict_info = Vec::new();
let mut ours = CommitId::new(String::new());
for conflict in conflicts.lines() {
let info = conflict.split_whitespace()
.collect::<Vec<_>>();
assert!(info.len() == 4,
"expected 4 entries for a conflict, received {}",
info.len());
let permissions = info[0];
let hash = info[1];
let stage = info[2];
let path = info[3];
if permissions.starts_with("160000") {
if stage == "1" {
} else if stage == "2" {
ours = CommitId::new(hash);
} else if stage == "3" {
conflict_info.push(self.submodule_conflict(path, &ours, &CommitId::new(hash))?);
}
} else {
conflict_info.push(Conflict::Path(Path::new(path).to_path_buf()));
}
}
Ok(conflict_info)
}
pub fn checkout<I, P>(&self, paths: I) -> Result<()>
where I: IntoIterator<Item = P>,
P: AsRef<OsStr>,
{
checkout(self, paths)
}
pub fn setup_merge<'a>(&'a self, bases: &[CommitId], base: &CommitId, topic: &CommitId)
-> Result<MergeResult<'a>> {
let merge_recursive = self.git()
.arg("merge-recursive")
.args(&bases.iter()
.map(CommitId::as_str)
.collect::<Vec<_>>())
.arg("--")
.arg(base.as_str())
.arg(topic.as_str())
.output()
.chain_err(|| "failed to construct merge command")?;
if !merge_recursive.status.success() {
return Ok(MergeResult::Conflict(self.conflict_information()?));
}
self.setup_merge_impl(base, topic)
}
pub fn setup_update_merge<'a>(&'a self, base: &CommitId, topic: &CommitId)
-> Result<MergeResult<'a>> {
self.setup_merge_impl(base, topic)
}
fn setup_merge_impl<'a>(&'a self, base: &CommitId, topic: &CommitId)
-> Result<MergeResult<'a>> {
debug!(target: "git.workarea",
"merging {} into {}",
topic,
base);
let write_tree = self.git()
.arg("write-tree")
.output()
.chain_err(|| "failed to construct write-tree command")?;
if !write_tree.status.success() {
bail!(ErrorKind::Git(format!("writing the tree object: {}",
String::from_utf8_lossy(&write_tree.stderr))));
}
let merged_tree = String::from_utf8_lossy(&write_tree.stdout);
let merged_tree = merged_tree.trim();
let mut commit_tree = self.git();
commit_tree.arg("commit-tree")
.arg(merged_tree)
.arg("-p")
.arg(base.as_str())
.arg("-p")
.arg(topic.as_str())
.stdin(Stdio::piped())
.stdout(Stdio::piped());
Ok(MergeResult::Ready(MergeCommand {
command: commit_tree,
}))
}
fn index(&self) -> PathBuf {
self.dir.path().join("index")
}
fn work_tree(&self) -> PathBuf {
self.dir.path().join("work")
}
pub fn cd_to_work_tree<'a>(&self, cmd: &'a mut Command) -> &'a mut Command {
cmd.current_dir(self.work_tree())
}
pub fn gitdir(&self) -> &Path {
self.context.gitdir()
}
pub fn submodule_config(&self) -> &SubmoduleConfig {
&self.submodule_config
}
#[cfg(test)]
pub fn __work_tree(&self) -> PathBuf {
self.work_tree()
}
}
impl WorkareaGitContext for GitWorkArea {
fn cmd(&self) -> Command {
self.git()
}
}