From c54daa3917fc22eae09634733ea2190a7bf1f248 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:35:34 +0530 Subject: [PATCH 1/3] diff: add -L/--label --- src/context_diff.rs | 12 +++---- src/diff.rs | 24 ++++++++++--- src/params.rs | 69 ++++++++++++++++++++++++++++++++++++++ src/unified_diff.rs | 12 +++---- src/utils.rs | 13 +++++++ tests/by-util/test_diff.rs | 43 ++++++++++++++++++++++++ 6 files changed, 153 insertions(+), 20 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index a37e7765..055f5e0f 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -8,7 +8,7 @@ use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; -use crate::utils::get_modification_time; +use crate::utils::header_name; #[derive(Debug, PartialEq)] pub enum DiffLine { @@ -315,14 +315,10 @@ fn make_diff( #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { - let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); - let to_modified_time = get_modification_time(¶ms.to.to_string_lossy()); let mut output = format!( - "*** {0}\t{1}\n--- {2}\t{3}\n", - params.from.to_string_lossy(), - from_modified_time, - params.to.to_string_lossy(), - to_modified_time + "*** {}\n--- {}\n", + header_name(¶ms.from, params.from_label.as_ref()), + header_name(¶ms.to, params.to_label.as_ref()) ) .into_bytes(); let diff_results = make_diff(expected, actual, params.context_count, params.brief); diff --git a/src/diff.rs b/src/diff.rs index f4c0614c..ddc53145 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -28,8 +28,16 @@ pub fn main(opts: Peekable) -> ExitCode { if params.report_identical_files { println!( "Files {} and {} are identical", - params.from.to_string_lossy(), - params.to.to_string_lossy(), + params + .from_label + .as_ref() + .unwrap_or(¶ms.from) + .to_string_lossy(), + params + .to_label + .as_ref() + .unwrap_or(¶ms.to) + .to_string_lossy(), ); } }; @@ -87,8 +95,16 @@ pub fn main(opts: Peekable) -> ExitCode { if params.brief && !result.is_empty() { println!( "Files {} and {} differ", - params.from.to_string_lossy(), - params.to.to_string_lossy() + params + .from_label + .as_ref() + .unwrap_or(¶ms.from) + .to_string_lossy(), + params + .to_label + .as_ref() + .unwrap_or(¶ms.to) + .to_string_lossy() ); } else { io::stdout().write_all(&result).unwrap(); diff --git a/src/params.rs b/src/params.rs index 239ebca4..619e3ecd 100644 --- a/src/params.rs +++ b/src/params.rs @@ -26,6 +26,8 @@ pub struct Params { pub expand_tabs: bool, pub tabsize: usize, pub width: usize, + pub from_label: Option, + pub to_label: Option, } impl Default for Params { @@ -41,6 +43,8 @@ impl Default for Params { expand_tabs: false, tabsize: 8, width: 130, + from_label: None, + to_label: None, } } } @@ -62,6 +66,28 @@ pub fn parse_params>(mut opts: Peekable) -> Resu let tabsize_re = Regex::new(r"^--tabsize=(?\d+)$").unwrap(); let width_re = Regex::new(r"^--width=(?P\d+)$").unwrap(); while let Some(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. + let label = match param.to_str() { + Some("-L" | "--label") => Some(opts.next().ok_or_else(|| { + format!("option '{}' requires an argument", param.to_string_lossy()) + })?), + Some(p) => p + .strip_prefix("--label=") + .or_else(|| p.strip_prefix("-L")) + .map(OsString::from), + None => None, + }; + 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(); if param == "--" { break; @@ -831,6 +857,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 { diff --git a/src/unified_diff.rs b/src/unified_diff.rs index bd83afe3..0d866d1f 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -8,7 +8,7 @@ use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; -use crate::utils::get_modification_time; +use crate::utils::header_name; #[derive(Debug, PartialEq)] pub enum DiffLine { @@ -242,14 +242,10 @@ fn make_diff( #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { - let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); - let to_modified_time = get_modification_time(¶ms.to.to_string_lossy()); let mut output = format!( - "--- {0}\t{1}\n+++ {2}\t{3}\n", - params.from.to_string_lossy(), - from_modified_time, - params.to.to_string_lossy(), - to_modified_time + "--- {}\n+++ {}\n", + header_name(¶ms.from, params.from_label.as_ref()), + header_name(¶ms.to, params.to_label.as_ref()) ) .into_bytes(); let diff_results = make_diff(expected, actual, params.context_count, params.brief); diff --git a/src/utils.rs b/src/utils.rs index d9415afd..43e1e915 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -71,6 +71,19 @@ pub fn get_modification_time(file_path: &str) -> String { modification_time } +/// The file name and modification time of a context or unified diff header, +/// or just the label when one was given. +pub fn header_name(path: &OsString, label: Option<&OsString>) -> String { + match label { + Some(label) => label.to_string_lossy().into_owned(), + None => format!( + "{}\t{}", + path.to_string_lossy(), + get_modification_time(&path.to_string_lossy()) + ), + } +} + pub fn format_failure_to_read_input_file( executable: &OsString, filepath: &OsString, diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index fbdd4940..08c611b0 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -118,6 +118,49 @@ fn differences_brief() -> Result<(), Box> { Ok(()) } +#[test] +fn labels_replace_file_names() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar\n".as_bytes())?; + for (args, expected) in [ + ( + vec!["-u", "--label", "a/x", "--label=b/x"], + "--- a/x\n+++ b/x\n@@ -1 +1 @@\n-foo\n+bar\n", + ), + ( + vec!["-c", "-L", "old", "-Lnew"], + "*** old\n--- new\n***************\n*** 1 ****\n! foo\n--- 1 ----\n! bar\n", + ), + ( + vec!["-q", "-L", "old", "-L", "new"], + "Files old and new differ\n", + ), + ] { + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff") + .args(args) + .arg(file1.path()) + .arg(file2.path()); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::eq(expected)); + } + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff") + .args(["-L", "a", "-L", "b", "-L", "c"]) + .arg(file1.path()) + .arg(file2.path()); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::contains("too many file label options")); + Ok(()) +} + #[test] fn missing_newline() -> Result<(), Box> { let mut file1 = NamedTempFile::new()?; From 8c613e46e02ec25f386f84948f74e7e9caa268c7 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:31:48 +0530 Subject: [PATCH 2/3] diff: read -L labels as bytes and add Params::names -L