From cd10de9241f80b998d6bc6f2880d019f6d680c96 Mon Sep 17 00:00:00 2001 From: WGH Date: Sat, 30 Aug 2025 00:51:31 +0300 Subject: [PATCH] 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) --- src/git.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 9 ++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/git.rs diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 000000000..07a61888b --- /dev/null +++ b/src/git.rs @@ -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 { + 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 +} diff --git a/src/main.rs b/src/main.rs index 8a7d3b7f4..d6119b098 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 {