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::{self, Read, Write};
use std::iter;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use crates::chrono::{DateTime, Utc};
use crates::regex::Regex;
use crates::tempdir::TempDir;
use crates::thiserror::Error;
use git::{CommitId, GitContext, GitError, GitResult, Identity};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubmoduleIntent {
CreateDirectory,
CreateGitFile,
WriteGitFile,
#[doc(hidden)]
_NonExhaustive,
}
impl fmt::Display for SubmoduleIntent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let intent = match self {
SubmoduleIntent::CreateDirectory => "create the directory structure",
SubmoduleIntent::CreateGitFile => "create the .git file",
SubmoduleIntent::WriteGitFile => "write the .git file",
_ => unreachable!(),
};
write!(f, "{}", intent)
}
}
#[derive(Debug, Error)]
pub enum WorkAreaError {
#[error("failed to create workarea's temporary directory")]
CreateTempDirectory {
#[source]
source: io::Error,
},
#[error("failed to create workarea's work tree directory")]
CreateWorkTree {
#[source]
source: io::Error,
},
#[error("failed to {} for the {} submodule", intent, submodule)]
SubmoduleSetup {
intent: SubmoduleIntent,
submodule: String,
#[source]
source: io::Error,
},
#[error("git error: {}", source)]
Git {
#[from]
source: GitError,
},
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
impl WorkAreaError {
pub(crate) fn temp_directory(source: io::Error) -> Self {
WorkAreaError::CreateTempDirectory {
source,
}
}
pub(crate) fn work_tree(source: io::Error) -> Self {
WorkAreaError::CreateWorkTree {
source,
}
}
pub(crate) fn submodule<S>(intent: SubmoduleIntent, submodule: S, source: io::Error) -> Self
where
S: Into<String>,
{
WorkAreaError::SubmoduleSetup {
intent,
submodule: submodule.into(),
source,
}
}
}
pub(crate) type WorkAreaResult<T> = Result<T, WorkAreaError>;
#[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<'a> {
command: Command,
_phantom: PhantomData<&'a str>,
}
impl<'a> MergeCommand<'a> {
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) -> GitResult<CommitId>
where
M: AsRef<str>,
{
self.commit_impl(message.as_ref())
}
fn commit_impl(mut self, message: &str) -> GitResult<CommitId> {
let mut commit_tree = self
.command
.spawn()
.map_err(|err| GitError::subcommand("commit-tree", err))?;
{
let commit_tree_stdin = commit_tree
.stdin
.as_mut()
.expect("expected commit-tree to have a stdin");
commit_tree_stdin
.write_all(message.as_bytes())
.map_err(|err| {
GitError::git_with_source(
"failed to write the commit message to commit-tree",
err,
)
})?;
}
let commit_tree = commit_tree
.wait_with_output()
.map_err(|err| GitError::subcommand("commit-tree", err))?;
if !commit_tree.status.success() {
return Err(GitError::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<'a> Debug for MergeCommand<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("MergeCommand").finish()
}
}
#[derive(Debug)]
pub enum MergeResult<'a> {
Conflict(Vec<Conflict>),
Ready(MergeCommand<'a>),
}
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: &dyn WorkAreaGitContext, paths: I) -> GitResult<()>
where
I: IntoIterator<Item = P>,
P: AsRef<OsStr>,
{
let ls_files = ctx
.cmd()
.arg("ls-files")
.arg("--")
.args(paths.into_iter())
.output()
.map_err(|err| GitError::subcommand("ls-files", err))?;
if !ls_files.status.success() {
return Err(GitError::git(format!(
"listing paths in the index: {}",
String::from_utf8_lossy(&ls_files.stderr),
)));
}
checkout_files(ctx, &ls_files.stdout)
}
fn checkout_files(ctx: &dyn WorkAreaGitContext, files: &[u8]) -> GitResult<()> {
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()
.map_err(|err| GitError::subcommand("checkout-index", err))?;
checkout_index
.stdin
.as_mut()
.expect("expected checkout-index to have a stdin")
.write_all(files)
.map_err(|err| GitError::git_with_source("writing to checkout-index", err))?;
let res = checkout_index
.wait()
.expect("expected checkout-index to execute successfully");
if !res.success() {
let mut stderr = Vec::new();
checkout_index
.stderr
.as_mut()
.expect("expected checkout-index to have a stderr")
.read_to_end(&mut stderr)
.map_err(|err| GitError::git_with_source("failed to read from checkout-index", err))?;
return Err(GitError::git(format!(
"running checkout-index: {}",
String::from_utf8_lossy(&stderr),
)));
}
let mut update_index = ctx
.cmd()
.arg("update-index")
.arg("--stdin")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| GitError::subcommand("update-index", err))?;
update_index
.stdin
.as_mut()
.expect("expected update-index to have a stdin")
.write_all(files)
.map_err(|err| GitError::git_with_source("writing to update-index", err))?;
let res = update_index
.wait()
.expect("expected update-index to execute successfully");
if !res.success() {
let mut stderr = Vec::new();
update_index
.stderr
.as_mut()
.expect("expected update-index to have a stderr")
.read_to_end(&mut stderr)
.map_err(|err| GitError::git_with_source("failed to read from update-index", err))?;
return Err(GitError::git(format!(
"running update-index: {}",
String::from_utf8_lossy(&stderr),
)));
}
Ok(())
}
impl PreparingGitWorkArea {
fn new(context: GitContext, rev: &CommitId) -> Result<Self, WorkAreaError> {
let tempdir = TempDir::new_in(context.gitdir(), "git-work-area")
.map_err(WorkAreaError::temp_directory)?;
let workarea = Self {
context,
dir: tempdir,
};
debug!(
target: "git.workarea",
"creating prepared workarea under {}",
workarea.dir.path().display(),
);
fs::create_dir_all(workarea.work_tree()).map_err(WorkAreaError::work_tree)?;
workarea.prepare(rev)?;
debug!(
target: "git.workarea",
"created prepared workarea under {}",
workarea.dir.path().display(),
);
Ok(workarea)
}
fn prepare(&self, rev: &CommitId) -> GitResult<()> {
let res = self
.git()
.arg("read-tree")
.arg("-i")
.arg("-m")
.arg(rev.as_str())
.output()
.map_err(|err| GitError::subcommand("read-tree", err))?;
if !res.status.success() {
return Err(GitError::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()
.map_err(|err| GitError::subcommand("update-index", err))?;
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) -> GitResult<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()
.map_err(|err| GitError::subcommand("config -f .gitmodules", err))?;
if !config.status.success() {
return Err(GitError::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) -> WorkAreaResult<Self> {
let intermediate = PreparingGitWorkArea::new(context, rev)?;
let workarea = Self {
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 {}",
workarea.dir.path().display(),
);
Ok(workarea)
}
fn prepare_submodules(&self) -> WorkAreaResult<()> {
if self.submodule_config.is_empty() {
return Ok(());
}
debug!(
target: "git.workarea",
"preparing submodules for {}",
self.dir.path().display(),
);
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: {}",
self.dir.path().display(),
name,
gitdir.display(),
);
continue;
}
let path = match config.get("path") {
Some(path) => path,
None => {
error!(
target: "git.workarea",
"{}: submodule configuration for {}.path does not exist (skipping): {}",
self.dir.path().display(),
name,
gitdir.display(),
);
continue;
},
};
let gitfiledir = self.work_tree().join(path);
fs::create_dir_all(&gitfiledir).map_err(|err| {
WorkAreaError::submodule(SubmoduleIntent::CreateDirectory, name as &str, err)
})?;
let mut gitfile = File::create(gitfiledir.join(".git")).map_err(|err| {
WorkAreaError::submodule(SubmoduleIntent::CreateGitFile, name as &str, err)
})?;
writeln!(gitfile, "gitdir: {}", gitdir.display()).map_err(|err| {
WorkAreaError::submodule(SubmoduleIntent::WriteGitFile, name as &str, err)
})?;
}
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,
) -> GitResult<Conflict>
where
P: AsRef<Path>,
{
let path = path.as_ref().to_path_buf();
debug!(
target: "git.workarea",
"{} checking for a submodule conflict for {}",
self.dir.path().display(),
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(String::as_str)));
let (submodule_ctx, branch) = if let Some((name, branch_name)) = branch_info {
let submodule_ctx = GitContext::new(self.gitdir().join("modules").join(name));
let branch_name = if let Some(branch_name) = branch_name {
Cow::Borrowed(branch_name)
} else {
submodule_ctx
.default_branch()?
.map_or(Cow::Borrowed("master"), Into::into)
};
if branch_name == "." {
debug!(
target: "git.workarea",
"the `.` branch specifier for submodules is not supported for conflict \
resolution",
);
return Ok(Conflict::Path(path));
}
(submodule_ctx, branch_name)
} else {
debug!(
target: "git.workarea",
"no submodule configured for {}; cannot attempt smarter resolution",
path.display(),
);
return Ok(Conflict::Path(path));
};
let refs = submodule_ctx
.git()
.arg("rev-list")
.arg("--first-parent")
.arg("--reverse")
.arg(branch.as_ref())
.arg(format!("^{}", ours))
.arg(format!("^{}", theirs))
.output()
.map_err(|err| GitError::subcommand("rev-list new-submodule ^old-submodule", err))?;
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()
.map_err(|err| GitError::subcommand("merge-base --is-ancestor ours", err))?;
let theirs_ancestor = submodule_ctx
.git()
.arg("merge-base")
.arg("--is-ancestor")
.arg(theirs.as_str())
.arg(hash)
.status()
.map_err(|err| GitError::subcommand("merge-base --is-ancestor theirs", err))?;
if ours_ancestor.success() && theirs_ancestor.success() {
return Ok(Conflict::SubmoduleWithFix(path, CommitId::new(hash)));
}
}
Ok(Conflict::SubmoduleNotMerged(path))
}
fn conflict_information(&self) -> GitResult<Vec<Conflict>> {
let ls_files = self
.git()
.arg("ls-files")
.arg("-u")
.output()
.map_err(|err| GitError::subcommand("ls-files -u", err))?;
if !ls_files.status.success() {
return Err(GitError::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>(&mut self, paths: I) -> GitResult<()>
where
I: IntoIterator<Item = P>,
P: AsRef<OsStr>,
{
checkout(self, paths)
}
pub fn setup_merge<'a>(
&'a self,
bases: &[CommitId],
base: &CommitId,
topic: &CommitId,
) -> GitResult<MergeResult<'a>> {
let merge_recursive = self
.git()
.arg("merge-recursive")
.args(bases.iter().map(CommitId::as_str))
.arg("--")
.arg(base.as_str())
.arg(topic.as_str())
.output()
.map_err(|err| GitError::subcommand("merge-recursive", err))?;
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,
) -> GitResult<MergeResult<'a>> {
self.setup_merge_impl(base, topic)
}
fn setup_merge_impl<'a>(
&'a self,
base: &CommitId,
topic: &CommitId,
) -> GitResult<MergeResult<'a>> {
debug!(
target: "git.workarea",
"merging {} into {}",
topic,
base,
);
let write_tree = self
.git()
.arg("write-tree")
.output()
.map_err(|err| GitError::subcommand("write-tree", err))?;
if !write_tree.status.success() {
return Err(GitError::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,
_phantom: PhantomData,
}))
}
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()
}
}