Add support for gitattributes

Runs git check-attr to get the files git attributes, and treats the file
as binary if the attribute value is "unset", matching the built-in git diff.

Fixes #466

Helped-by: @anuramat (https://github.com/Wilfred/difftastic/issues/466#issuecomment-3229267882)
pull/874/head
WGH 2025-08-30 00:51:31 +07:00
parent b5a4df1efc
commit cd10de9241
2 changed files with 70 additions and 2 deletions

@ -0,0 +1,63 @@
use std::path::Path;
use std::process::Command;
/// Corresponds to the diff attribute. See man gitattribute.
pub(crate) enum DiffAttribute {
Set,
Unset,
Unspecified,
Other,
}
impl From<&str> for DiffAttribute {
fn from(s: &str) -> Self {
match s {
"set" => Self::Set,
"unset" => Self::Unset,
"unspecified" => Self::Unspecified,
_ => Self::Other,
}
}
}
/// Runs `git check-attr diff` to get the diff attribute of the path. Returns
/// [`Option::None`] when either `git` is not available, file is not inside git
/// directory, or something else went wrong.
pub(crate) fn check_attr(path: &Path) -> Option<DiffAttribute> {
let res = Command::new("git")
.args(["check-attr", "diff", "-z", "--"])
.arg(path)
.output();
match res {
Ok(output) => {
// Either git is not available, or file is outside git directory.
if !output.status.success() {
debug!("git check-attr exited with status {}", output.status);
return None;
}
// The output format is "path" "attribute name" "value". We
// specified both path and attribute name explicitly,
// so we only need value here.
let stdout = &output.stdout;
let value = stdout.split(|&b| b == b'\0').nth(2);
match value {
None => {
warn!("malformed git check-attr output {stdout:#?}");
}
Some(value) => match std::str::from_utf8(value) {
Ok(s) => return Some(s.into()),
Err(err) => {
warn!("invalid diff attribute value: {err}");
}
},
}
}
Err(err) => {
warn!("failed to execute git: {err}");
}
}
None
}

@ -38,6 +38,7 @@ mod diff;
mod display;
mod exit_codes;
mod files;
mod git;
mod hash;
mod line_parser;
mod lines;
@ -68,6 +69,7 @@ use crate::files::{
guess_content, read_file_or_die, read_files_or_die, read_or_die, relative_paths_in_either,
ProbableFileKind,
};
use crate::git::{check_attr, DiffAttribute};
use crate::parse::guess_language::language_globs;
use crate::parse::guess_language::{guess, language_name, Language, LanguageOverride};
use crate::parse::syntax;
@ -405,8 +407,11 @@ fn diff_file(
let (mut lhs_src, mut rhs_src) = match (
guess_content(&lhs_bytes, lhs_path, binary_overrides),
guess_content(&rhs_bytes, rhs_path, binary_overrides),
check_attr(Path::new(display_path)),
) {
(ProbableFileKind::Binary, _) | (_, ProbableFileKind::Binary) => {
(ProbableFileKind::Binary, _, _)
| (_, ProbableFileKind::Binary, _)
| (_, _, Some(DiffAttribute::Unset)) => {
let has_byte_changes = if lhs_bytes == rhs_bytes {
None
} else {
@ -425,7 +430,7 @@ fn diff_file(
has_syntactic_changes: false,
};
}
(ProbableFileKind::Text(lhs_src), ProbableFileKind::Text(rhs_src)) => (lhs_src, rhs_src),
(ProbableFileKind::Text(lhs_src), ProbableFileKind::Text(rhs_src), _) => (lhs_src, rhs_src),
};
if diff_options.strip_cr {