use std::collections::BTreeSet;
use std::path::Path;
use std::process::Command;
use crate::repo_scope::{is_supported, Tree};
type R<T> = Result<T, Box<dyn std::error::Error>>;
fn git(dir: &Path, args: &[&str]) -> R<String> {
let out = Command::new("git").args(args).current_dir(dir).output()?;
if !out.status.success() {
return Err(format!(
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
)
.into());
}
Ok(String::from_utf8_lossy(&out.stdout).to_string())
}
pub(crate) fn rev_exists(dir: &Path, rev: &str) -> bool {
Command::new("git")
.args(["rev-parse", "--verify", "--quiet", rev])
.current_dir(dir)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub(crate) fn resolve_revs(
dir: &Path,
base: Option<&str>,
ours: Option<&str>,
theirs: Option<&str>,
) -> R<(String, String, String)> {
if !rev_exists(dir, "HEAD") {
return Err(
"not a git repository (or no commits yet) — use the directory mode instead".into(),
);
}
let ours = ours.unwrap_or("HEAD").to_string();
let theirs = match theirs {
Some(t) => t.to_string(),
None if rev_exists(dir, "MERGE_HEAD") => "MERGE_HEAD".to_string(),
None => {
return Err("no --theirs given and no merge in progress (MERGE_HEAD absent)".into())
}
};
let base = match base {
Some(b) => b.to_string(),
None => git(dir, &["merge-base", &ours, &theirs])?
.trim()
.to_string(),
};
Ok((base, ours, theirs))
}
pub fn trees(
dir: &Path,
base: Option<&str>,
ours: Option<&str>,
theirs: Option<&str>,
) -> R<(Tree, Tree, Tree)> {
let (base, ours, theirs) = resolve_revs(dir, base, ours, theirs)?;
Ok((
read_rev_tree(dir, &base)?,
read_rev_tree(dir, &ours)?,
read_rev_tree(dir, &theirs)?,
))
}
pub struct MergeScope {
pub base: Tree,
pub ours: Tree,
pub theirs: Tree,
pub work: Tree,
pub subjects: Vec<String>,
pub scope: String,
}
pub fn merge_scope(dir: &Path) -> R<Option<MergeScope>> {
if !rev_exists(dir, "HEAD") {
return Ok(None);
}
let (ours_rev, theirs_rev, moment) = if rev_exists(dir, "MERGE_HEAD") {
("HEAD".to_string(), "MERGE_HEAD".to_string(), "in progress")
} else if rev_exists(dir, "HEAD^2") {
("HEAD^1".to_string(), "HEAD^2".to_string(), "just committed")
} else {
return Ok(None);
};
let base_rev = git(dir, &["merge-base", &ours_rev, &theirs_rev])?
.trim()
.to_string();
let base = read_rev_tree(dir, &base_rev)?;
let ours = read_rev_tree(dir, &ours_rev)?;
let theirs = read_rev_tree(dir, &theirs_rev)?;
let work = read_worktree(dir)?;
let mut subjects: BTreeSet<String> = base
.keys()
.chain(ours.keys())
.chain(theirs.keys())
.filter(|p| ours.get(*p) != base.get(*p) || theirs.get(*p) != base.get(*p))
.cloned()
.collect();
if let Ok(list) = git(dir, &["diff", "--name-only", "--diff-filter=U", "-z"]) {
subjects.extend(
list.split('\0')
.filter(|p| !p.is_empty() && is_supported(p))
.map(str::to_string),
);
}
let subjects: Vec<String> = subjects.into_iter().collect();
let scope = format!(
"working tree vs the three merge stages of {ours_rev} × {theirs_rev} \
(base {}, merge {moment}) — {} file(s) either side changed",
&base_rev[..base_rev.len().min(8)],
subjects.len()
);
Ok(Some(MergeScope {
base,
ours,
theirs,
work,
subjects,
scope,
}))
}
pub(crate) fn read_worktree(dir: &Path) -> R<Tree> {
let listing = git(dir, &["ls-files", "-z"])?;
let mut tree = Tree::new();
for rel in listing.split('\0').filter(|p| !p.is_empty()) {
if !is_supported(rel) {
continue;
}
if let Ok(content) = std::fs::read_to_string(dir.join(rel)) {
tree.insert(rel.to_string(), content);
}
}
Ok(tree)
}
pub fn file_stages(dir: &Path, path: &str) -> R<(String, String, String)> {
let stage = |n: u8| -> Option<String> {
let out = Command::new("git")
.args(["show", &format!(":{n}:{path}")])
.current_dir(dir)
.output()
.ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).to_string())
};
if let (Some(b), Some(o), Some(t)) = (stage(1), stage(2), stage(3)) {
return Ok((b, o, t));
}
let scope = merge_scope(dir)?.ok_or(
"no merge in progress and HEAD is not a merge commit — there is no three-way \
context to explain this file against",
)?;
let get = |t: &Tree| t.get(path).cloned().unwrap_or_default();
if !scope.ours.contains_key(path) && !scope.theirs.contains_key(path) {
return Err(format!("`{path}` is in neither side of this merge").into());
}
Ok((get(&scope.base), get(&scope.ours), get(&scope.theirs)))
}
pub(crate) fn read_rev_tree(dir: &Path, rev: &str) -> R<Tree> {
let listing = git(dir, &["ls-tree", "-r", "--name-only", "-z", rev])?;
let mut tree = Tree::new();
for path in listing.split('\0').filter(|p| !p.is_empty()) {
if !is_supported(path) {
continue;
}
let out = Command::new("git")
.args(["show", &format!("{rev}:{path}")])
.current_dir(dir)
.output()?;
if !out.status.success() {
continue;
}
if let Ok(content) = String::from_utf8(out.stdout) {
tree.insert(path.to_string(), content);
}
}
Ok(tree)
}