Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/context_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,20 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
let to_modified_time =
get_modification_time(&params.to.to_string_lossy(), style, params.to_missing);
let mut output = Vec::new();
write_file_header(&mut output, "***", &params.from, &from_modified_time);
write_file_header(&mut output, "---", &params.to, &to_modified_time);
write_file_header(
&mut output,
"***",
&params.from,
params.from_label.as_deref(),
&from_modified_time,
);
write_file_header(
&mut output,
"---",
&params.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();
Expand Down
10 changes: 6 additions & 4 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ pub fn main(opts: Peekable<ArgsOs>) -> 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(),
);
}
};
Expand Down Expand Up @@ -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()
);
}

Expand Down
5 changes: 3 additions & 2 deletions src/dir_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,10 @@ impl DirectoryWalker<'_> {
header.push(b' ');
header.extend_from_slice(&quote_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(&quote_file_name(path.as_encoded_bytes()));
header.extend_from_slice(&quote_file_name(name.as_encoded_bytes()));
}
header.push(b'\n');
compare_files(&params, Some(&header))
Expand Down
107 changes: 107 additions & 0 deletions src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OsString>,
pub to_label: Option<OsString>,
}

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.
Expand Down Expand Up @@ -92,6 +105,8 @@ impl Default for Params {
no_dereference: false,
from_missing: false,
to_missing: false,
from_label: None,
to_label: None,
}
}
}
Expand Down Expand Up @@ -129,6 +144,32 @@ pub fn parse_params<I: Iterator<Item = OsString>>(
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
let width_re = Regex::new(r"^--width=(?P<long>\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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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!(
Expand Down
16 changes: 14 additions & 2 deletions src/unified_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,20 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
params.to_missing,
);
let mut output = Vec::new();
write_file_header(&mut output, "---", &params.from, &from_modified_time);
write_file_header(&mut output, "+++", &params.to, &to_modified_time);
write_file_header(
&mut output,
"---",
&params.from,
params.from_label.as_deref(),
&from_modified_time,
);
write_file_header(
&mut output,
"+++",
&params.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();
Expand Down
21 changes: 16 additions & 5 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>, 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<u8>,
marker: &str,
name: &OsStr,
label: Option<&OsStr>,
timestamp: &str,
) {
output.extend_from_slice(marker.as_bytes());
output.push(b' ');
output.extend_from_slice(&quote_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(&quote_file_name(name.as_encoded_bytes()));
output.push(b'\t');
output.extend_from_slice(timestamp.as_bytes());
}
output.push(b'\n');
}

Expand Down
62 changes: 62 additions & 0 deletions tests/by-util/test_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading