From 7c9ca079d82e9624c491da6db55279a39b4c4b4f Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:23:20 +0530 Subject: [PATCH] diff: add -L/--label --- src/context_diff.rs | 16 +++++- src/diff.rs | 10 ++-- src/dir_diff.rs | 5 +- src/params.rs | 107 +++++++++++++++++++++++++++++++++++++ src/unified_diff.rs | 16 +++++- src/utils.rs | 21 ++++++-- tests/by-util/test_diff.rs | 62 +++++++++++++++++++++ 7 files changed, 222 insertions(+), 15 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index 86f12857..790d58fa 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -323,8 +323,20 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { let to_modified_time = get_modification_time(¶ms.to.to_string_lossy(), style, params.to_missing); let mut output = Vec::new(); - write_file_header(&mut output, "***", ¶ms.from, &from_modified_time); - write_file_header(&mut output, "---", ¶ms.to, &to_modified_time); + write_file_header( + &mut output, + "***", + ¶ms.from, + params.from_label.as_deref(), + &from_modified_time, + ); + write_file_header( + &mut output, + "---", + ¶ms.to, + params.to_label.as_deref(), + &to_modified_time, + ); let diff_results = make_diff(expected, actual, params.context_count, params.brief); if diff_results.is_empty() { return Vec::new(); diff --git a/src/diff.rs b/src/diff.rs index 7de5f55c..55fd30e5 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -72,10 +72,11 @@ pub fn main(opts: Peekable) -> ExitCode { pub fn compare_files(params: &Params, header: Option<&[u8]>) -> Outcome { let maybe_report_identical_files = || { if params.report_identical_files { + let (from, to) = params.names(); println!( "Files {} and {} are identical", - params.from.to_string_lossy(), - params.to.to_string_lossy(), + from.to_string_lossy(), + to.to_string_lossy(), ); } }; @@ -259,10 +260,11 @@ fn is_binary(content: &[u8]) -> bool { /// Print the one-line summary used instead of the differences, `kind` naming /// what was compared. fn print_files_differ(kind: &str, params: &Params) { + let (from, to) = params.names(); println!( "{kind} {} and {} differ", - params.from.to_string_lossy(), - params.to.to_string_lossy() + from.to_string_lossy(), + to.to_string_lossy() ); } diff --git a/src/dir_diff.rs b/src/dir_diff.rs index 30fe23af..5eca727c 100644 --- a/src/dir_diff.rs +++ b/src/dir_diff.rs @@ -261,9 +261,10 @@ impl DirectoryWalker<'_> { header.push(b' '); header.extend_from_slice("e_shell_argument(arg.as_encoded_bytes())); } - for path in [from, to] { + let (from_name, to_name) = params.names(); + for name in [from_name, to_name] { header.push(b' '); - header.extend_from_slice("e_file_name(path.as_encoded_bytes())); + header.extend_from_slice("e_file_name(name.as_encoded_bytes())); } header.push(b'\n'); compare_files(¶ms, Some(&header)) diff --git a/src/params.rs b/src/params.rs index 9e3e5a35..aeb126be 100644 --- a/src/params.rs +++ b/src/params.rs @@ -62,6 +62,19 @@ pub struct Params { pub from_missing: bool, /// Set when `to` did not exist and was compared as an empty file. pub to_missing: bool, + pub from_label: Option, + pub to_label: Option, +} + +impl Params { + /// The names to show for the from-file and the to-file: their labels if + /// given, else their paths. + pub fn names(&self) -> (&OsString, &OsString) { + ( + self.from_label.as_ref().unwrap_or(&self.from), + self.to_label.as_ref().unwrap_or(&self.to), + ) + } } /// The pattern of `-p`, matching the first line of C functions. @@ -92,6 +105,8 @@ impl Default for Params { no_dereference: false, from_missing: false, to_missing: false, + from_label: None, + to_label: None, } } } @@ -129,6 +144,32 @@ pub fn parse_params>( let tabsize_re = Regex::new(r"^--tabsize=(?\d+)$").unwrap(); let width_re = Regex::new(r"^--width=(?P\d+)$").unwrap(); while let Some((position, param)) = opts.next() { + // -L LABEL, -LLABEL, --label LABEL or --label=LABEL: the first names + // the from-file in headers and messages, the second the to-file. + // The label may be any bytes, so match on those, not on a str. + let bytes = param.as_encoded_bytes(); + let label = if bytes == b"-L" || bytes == b"--label" { + Some(opts.next().map(|(_, label)| label).ok_or_else(|| { + format!("option '{}' requires an argument", param.to_string_lossy()) + })?) + } else { + bytes + .strip_prefix(b"--label=") + .or_else(|| bytes.strip_prefix(b"-L")) + // SAFETY: the bytes come from an OsStr and are split right + // after an ASCII prefix. + .map(|rest| unsafe { OsStr::from_encoded_bytes_unchecked(rest) }.to_os_string()) + }; + if let Some(label) = label { + if params.from_label.is_none() { + params.from_label = Some(label); + } else if params.to_label.is_none() { + params.to_label = Some(label); + } else { + return Err("too many file label options".to_string()); + } + continue; + } let next_param = opts.peek().map(|(_, next_param)| next_param); if param == "--" { break; @@ -1080,6 +1121,49 @@ mod tests { ); } #[test] + fn labels() { + assert_eq!( + Ok(Params { + executable: os("diff"), + from: os("foo"), + to: os("bar"), + from_label: Some(os("old")), + to_label: Some(os("new")), + ..Default::default() + }), + parse_params( + [ + os("diff"), + os("--label"), + os("old"), + os("-Lnew"), + os("foo"), + os("bar") + ] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Err("too many file label options".to_string()), + parse_params( + [ + os("diff"), + os("--label=a"), + os("-L"), + os("b"), + os("-Lc"), + os("foo"), + os("bar") + ] + .iter() + .cloned() + .peekable() + ) + ); + } + #[test] fn width_suffix_is_an_operand() { assert_eq!( Ok(Params { @@ -1111,6 +1195,29 @@ mod tests { .unwrap(); assert_eq!(params.from, bad); } + #[cfg(unix)] + #[test] + fn labels_non_utf8() { + use std::os::unix::ffi::OsStringExt; + let bytes = |b: &[u8]| OsString::from_vec(b.to_vec()); + let params = parse_params( + [ + os("diff"), + bytes(b"-L\xffold"), + bytes(b"--label=\xfenew"), + os("foo"), + os("bar"), + ] + .iter() + .cloned() + .peekable(), + ) + .unwrap(); + assert_eq!(params.from_label, Some(bytes(b"\xffold"))); + assert_eq!(params.to_label, Some(bytes(b"\xfenew"))); + assert_eq!(params.from, os("foo")); + assert_eq!(params.to, os("bar")); + } #[test] fn double_dash() { assert_eq!( diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 14f4384b..32f227de 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -248,8 +248,20 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { params.to_missing, ); let mut output = Vec::new(); - write_file_header(&mut output, "---", ¶ms.from, &from_modified_time); - write_file_header(&mut output, "+++", ¶ms.to, &to_modified_time); + write_file_header( + &mut output, + "---", + ¶ms.from, + params.from_label.as_deref(), + &from_modified_time, + ); + write_file_header( + &mut output, + "+++", + ¶ms.to, + params.to_label.as_deref(), + &to_modified_time, + ); let diff_results = make_diff(expected, actual, params.context_count, params.brief); if diff_results.is_empty() { return Vec::new(); diff --git a/src/utils.rs b/src/utils.rs index 29ab93df..b223de19 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -104,13 +104,24 @@ pub fn quote_file_name(name: &[u8]) -> Cow<'_, [u8]> { } /// Appends a file header line of the context or unified formats, such as -/// `--- name\ttimestamp`, to `output`. -pub fn write_file_header(output: &mut Vec, marker: &str, name: &OsStr, timestamp: &str) { +/// `--- name\ttimestamp`, to `output`. A label replaces both the name and the +/// timestamp, and is written as it is, without quoting. +pub fn write_file_header( + output: &mut Vec, + marker: &str, + name: &OsStr, + label: Option<&OsStr>, + timestamp: &str, +) { output.extend_from_slice(marker.as_bytes()); output.push(b' '); - output.extend_from_slice("e_file_name(name.as_encoded_bytes())); - output.push(b'\t'); - output.extend_from_slice(timestamp.as_bytes()); + if let Some(label) = label { + output.extend_from_slice(label.as_encoded_bytes()); + } else { + output.extend_from_slice("e_file_name(name.as_encoded_bytes())); + output.push(b'\t'); + output.extend_from_slice(timestamp.as_bytes()); + } output.push(b'\n'); } diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index c6d41404..ea2f2bf3 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -942,3 +942,65 @@ fn trailing_context_without_final_newlines(#[case] format: &str, #[case] hunk: & .fails_with_code(1) .stdout_str_check(|s| s.ends_with(hunk)); } + +#[rstest] +#[case::unified( + &["-u", "--label", "a/x", "--label=b/x"], + "--- a/x\n+++ b/x\n@@ -1 +1 @@\n-foo\n+bar\n" +)] +#[case::context( + &["-c", "-L", "old", "-Lnew"], + "*** old\n--- new\n***************\n*** 1 ****\n! foo\n--- 1 ----\n! bar\n" +)] +#[case::brief(&["-q", "-L", "old", "-L", "new"], "Files old and new differ\n")] +#[case::space( + &["-u", "-L", "old name", "-L", "new"], + "--- old name\n+++ new\n@@ -1 +1 @@\n-foo\n+bar\n" +)] +fn labels_replace_file_names(#[case] options: &[&str], #[case] expected: &str) { + let (at, ts) = at_and_ts!(); + at.write("file1", "foo\n"); + at.write("file2", "bar\n"); + ts.ucmd() + .args(options) + .args(&["file1", "file2"]) + .fails_with_code(1) + .stdout_is(expected); +} + +#[test] +fn labels_replace_identical_file_names() { + let (at, ts) = at_and_ts!(); + at.write("file1", "foo\n"); + at.write("file2", "foo\n"); + ts.ucmd() + .args(&["-s", "-L", "old", "-L", "new", "file1", "file2"]) + .succeeds() + .stdout_is("Files old and new are identical\n"); +} + +#[test] +fn labels_replace_file_names_in_directories() { + let (at, ts) = at_and_ts!(); + at.mkdir("a"); + at.mkdir("b"); + at.write("a/f", "foo\n"); + at.write("b/f", "bar\n"); + ts.ucmd() + .args(&["-r", "-u", "-L", "old", "-L", "new", "a", "b"]) + .fails_with_code(1) + .stdout_is("diff -r -u -L old -L new old new\n--- old\n+++ new\n@@ -1 +1 @@\n-foo\n+bar\n"); +} + +#[rstest] +#[case::too_many(&["-L", "a", "-L", "b", "-L", "c", "file1", "file2"], "too many file label options")] +#[case::missing_argument(&["file1", "file2", "-L"], "option '-L' requires an argument")] +fn label_errors(#[case] args: &[&str], #[case] error: &str) { + let (at, ts) = at_and_ts!(); + at.write("file1", "foo\n"); + at.write("file2", "bar\n"); + ts.ucmd() + .args(args) + .fails_with_code(2) + .stderr_contains(error); +}