use super::suspended;
use super::widgets::Modal;
use crate::git::git_capture;
use ratatui::DefaultTerminal;
use std::io::{self, Read};
use std::process::{Command, ExitStatus, Stdio};
const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
const MAX_ERR_LINES: usize = 40;
pub enum DiffTool {
Quiet,
Note(String),
Failed(Modal),
}
fn has_difftool(dir: &str) -> bool {
["diff.tool", "merge.tool"]
.iter()
.any(|k| git_capture(dir, &["config", k]).is_some_and(|v| !v.is_empty()))
}
pub fn difftool_commit(
terminal: &mut DefaultTerminal,
enhanced: bool,
dir: &str,
hash: &str,
path: &str,
) -> DiffTool {
let base = if commit_has_parent(dir, hash) {
format!("{hash}^")
} else {
EMPTY_TREE.to_string()
};
run_difftool(terminal, enhanced, dir, &[&base, hash, "--", path])
}
fn commit_has_parent(dir: &str, hash: &str) -> bool {
git_capture(dir, &["rev-list", "--parents", "-n", "1", hash])
.map(|s| s.split_whitespace().count() > 1)
.unwrap_or(false)
}
pub fn run_difftool(
terminal: &mut DefaultTerminal,
enhanced: bool,
dir: &str,
args: &[&str],
) -> DiffTool {
if !has_difftool(dir) {
return DiffTool::Failed(Modal::new(
" no difftool configured ",
"Enter hands the file to `git difftool`, but neither diff.tool nor \
merge.tool is set, so there is nothing to open it with.\n\n\
Set one and try again, for example:\n\n \
git config --global diff.tool vimdiff\n\n\
`git difftool --tool-help` lists the tools your git can drive.",
));
}
let outcome = suspended(terminal, enhanced, || handoff(dir, args));
let _ = terminal.clear();
match outcome {
Err(e) => DiffTool::Failed(Modal::new(
" difftool would not start ",
format!("Could not run git difftool: {e}"),
)),
Ok((status, said)) if !status.success() => {
let code = match status.code() {
Some(c) => c.to_string(),
None => "a signal".to_string(),
};
if said.is_empty() {
DiffTool::Note(format!("difftool exited with status {code}"))
} else {
DiffTool::Failed(Modal::new(
" difftool failed ",
format!("git difftool exited with status {code}.\n\n{said}"),
))
}
}
Ok(_) => DiffTool::Quiet,
}
}
fn handoff(dir: &str, args: &[&str]) -> io::Result<(ExitStatus, String)> {
let mut child = Command::new("git")
.arg("-C")
.arg(dir)
.arg("difftool")
.arg("-y")
.args(args)
.stderr(Stdio::piped())
.spawn()?;
let mut pipe = child.stderr.take().expect("stderr is piped");
let drain = std::thread::spawn(move || {
let mut buf = Vec::new();
let _ = pipe.read_to_end(&mut buf);
buf
});
let status = child.wait()?;
let said = drain.join().unwrap_or_default();
Ok((status, tail(&String::from_utf8_lossy(&said))))
}
fn tail(said: &str) -> String {
let mut lines: Vec<&str> = said
.lines()
.map(str::trim_end)
.filter(|l| !l.is_empty())
.collect();
lines.dedup();
let start = lines.len().saturating_sub(MAX_ERR_LINES);
lines[start..].join("\n")
}