use std::path::Path;
use anyhow::{Context, Result};
use super::nav::is_markdown_path;
const EXIT_INTERRUPTED: i32 = 130;
pub fn read_side(path: &Path) -> Result<String> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {} for review", path.display()))?;
Ok(crate::document::buffer::normalize_newlines(raw))
}
pub fn is_markdown_pair(old: &Path, new: &Path) -> bool {
is_markdown_path(old) || is_markdown_path(new)
}
pub fn diff_label(old: &Path, new: &Path) -> String {
let name = |p: &Path| {
p.file_name()
.map(|n| n.to_string_lossy().into_owned())
.filter(|n| n != "null")
};
match (name(old), name(new)) {
(Some(o), Some(n)) if o != n => format!("{o} → {n}"),
(_, Some(n)) => n,
(Some(o), None) => o,
(None, None) => "[diff]".to_owned(),
}
}
pub fn under_git_difftool() -> bool {
is_difftool_env(
std::env::var_os("GIT_DIFFTOOL_EXTCMD").is_some(),
std::env::var_os("GIT_DIFF_PATH_TOTAL").is_some(),
)
}
fn is_difftool_env(extcmd: bool, path_total: bool) -> bool {
extcmd || path_total
}
pub fn stop_walk() -> ! {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGINT, libc::SIG_DFL);
libc::kill(0, libc::SIGINT);
}
std::process::exit(EXIT_INTERRUPTED);
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn label(old: &str, new: &str) -> String {
diff_label(&PathBuf::from(old), &PathBuf::from(new))
}
#[test]
fn a_modified_file_is_labelled_by_its_name() {
assert_eq!(
label("/tmp/git-difftool.a/left/docs/x.md", "docs/x.md"),
"x.md"
);
}
#[test]
fn an_add_or_delete_is_labelled_by_the_side_that_exists() {
assert_eq!(label("/dev/null", "docs/new.md"), "new.md");
assert_eq!(label("docs/gone.md", "/dev/null"), "gone.md");
}
#[test]
fn a_rename_names_both_sides() {
assert_eq!(label("old/a.md", "new/b.md"), "a.md → b.md");
}
#[test]
fn a_nameless_pair_falls_back_to_a_placeholder() {
assert_eq!(label("/dev/null", "/dev/null"), "[diff]");
}
#[test]
fn is_markdown_pair_accepts_either_side() {
let md = PathBuf::from("notes.md");
let devnull = PathBuf::from("/dev/null");
assert!(is_markdown_pair(&devnull, &md));
assert!(is_markdown_pair(&md, &devnull));
assert!(is_markdown_pair(
&PathBuf::from("a.markdown"),
&PathBuf::from("b.MD")
));
}
#[test]
fn is_markdown_pair_refuses_a_non_markdown_pair() {
assert!(!is_markdown_pair(
&PathBuf::from("src/main.rs"),
&PathBuf::from("src/main.rs")
));
assert!(!is_markdown_pair(
&PathBuf::from("/dev/null"),
&PathBuf::from("deploy.sh")
));
}
#[test]
fn a_difftool_walk_is_recognised_from_either_variable() {
assert!(is_difftool_env(true, false), "only -x sets EXTCMD");
assert!(
is_difftool_env(false, true),
"-t and a gitattributes driver set PATH_TOTAL alone",
);
assert!(is_difftool_env(true, true));
}
#[test]
fn a_hand_typed_invocation_is_not_a_difftool_walk() {
assert!(!is_difftool_env(false, false));
}
#[test]
#[cfg(unix)]
fn read_side_reads_dev_null_as_an_empty_side() {
assert_eq!(read_side(Path::new("/dev/null")).unwrap(), "");
}
#[test]
fn read_side_reads_an_empty_file_as_an_empty_side() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.md");
std::fs::write(&path, b"").unwrap();
assert_eq!(read_side(&path).unwrap(), "");
}
#[test]
fn read_side_names_the_path_it_could_not_read() {
let err = read_side(Path::new("/nonexistent/edamame-difftool-fixture.md"))
.expect_err("missing path must fail");
assert!(
format!("{err}").contains("edamame-difftool-fixture.md"),
"the error must name the file: {err}",
);
}
}