From bcfac955430e9c3de9a1b1fb538e281c34edfd88 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Tue, 13 Aug 2024 13:32:10 -0400 Subject: [PATCH 01/18] Enclose tests in #[cfg(test)] --- src/utils/bat/less.rs | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/utils/bat/less.rs b/src/utils/bat/less.rs index 1ca9f76fb..4de350403 100644 --- a/src/utils/bat/less.rs +++ b/src/utils/bat/less.rs @@ -19,9 +19,13 @@ fn parse_less_version(output: &[u8]) -> Option { } } -#[test] -fn test_parse_less_version_487() { - let output = b"less 487 (GNU regular expressions) +#[cfg(test)] +mod tests { + use super::parse_less_version; + + #[test] + fn test_parse_less_version_487() { + let output = b"less 487 (GNU regular expressions) Copyright (C) 1984-2016 Mark Nudelman less comes with NO WARRANTY, to the extent permitted by law. @@ -29,12 +33,12 @@ For information about the terms of redistribution, see the file named README in the less distribution. Homepage: http://www.greenwoodsoftware.com/less"; - assert_eq!(Some(487), parse_less_version(output)); -} + assert_eq!(Some(487), parse_less_version(output)); + } -#[test] -fn test_parse_less_version_529() { - let output = b"less 529 (Spencer V8 regular expressions) + #[test] + fn test_parse_less_version_529() { + let output = b"less 529 (Spencer V8 regular expressions) Copyright (C) 1984-2017 Mark Nudelman less comes with NO WARRANTY, to the extent permitted by law. @@ -42,12 +46,12 @@ For information about the terms of redistribution, see the file named README in the less distribution. Homepage: http://www.greenwoodsoftware.com/less"; - assert_eq!(Some(529), parse_less_version(output)); -} + assert_eq!(Some(529), parse_less_version(output)); + } -#[test] -fn test_parse_less_version_551() { - let output = b"less 551 (PCRE regular expressions) + #[test] + fn test_parse_less_version_551() { + let output = b"less 551 (PCRE regular expressions) Copyright (C) 1984-2019 Mark Nudelman less comes with NO WARRANTY, to the extent permitted by law. @@ -55,12 +59,13 @@ For information about the terms of redistribution, see the file named README in the less distribution. Home page: http://www.greenwoodsoftware.com/less"; - assert_eq!(Some(551), parse_less_version(output)); -} + assert_eq!(Some(551), parse_less_version(output)); + } -#[test] -fn test_parse_less_version_wrong_program() { - let output = b"more from util-linux 2.34"; + #[test] + fn test_parse_less_version_wrong_program() { + let output = b"more from util-linux 2.34"; - assert_eq!(None, parse_less_version(output)); + assert_eq!(None, parse_less_version(output)); + } } From ea8cd548788e5e4777b022ad5808771d057247a2 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 6 May 2024 22:30:04 -0400 Subject: [PATCH 02/18] Implement --diff-args --- src/cli.rs | 19 +++++++++++++++++-- src/config.rs | 2 ++ src/options/set.rs | 1 + src/subcommands/diff.rs | 10 ++++++---- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 79617ab63..e18a15f1c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -166,6 +166,21 @@ pub struct Opt { #[arg(long = "detect-dark-light", value_enum, default_value_t = DetectDarkLight::default())] pub detect_dark_light: DetectDarkLight, + #[arg( + long = "diff-args", + short = '@', + default_value = "", + value_name = "STRING" + )] + /// Arguments to pass to `git diff` when using delta to diff two files. + /// + /// E.g. `delta --diff-args=-U999 file_1 file_2` is equivalent to + /// `git diff -U999 --no-index --color file_1 file_2 | delta`. + /// + /// However, if you use process substitution instead of real file paths, it falls back to `diff -u` instead of `git + /// diff`. + pub diff_args: String, + #[arg(long = "diff-highlight")] /// Emulate diff-highlight. /// @@ -960,12 +975,12 @@ pub struct Opt { /// Deprecated: use --true-color. pub _24_bit_color: Option, - /// First file to be compared when delta is being used in diff mode + /// First file to be compared when delta is being used to diff two files. /// /// `delta file1 file2` is equivalent to `diff -u file1 file2 | delta`. pub minus_file: Option, - /// Second file to be compared when delta is being used in diff mode. + /// Second file to be compared when delta is being used to diff two files. pub plus_file: Option, #[arg(skip)] diff --git a/src/config.rs b/src/config.rs index c2fbccb73..8d42574f2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,6 +53,7 @@ pub struct Config { pub cwd_relative_to_repo_root: Option, pub decorations_width: cli::Width, pub default_language: String, + pub diff_args: String, pub diff_stat_align_width: usize, pub error_exit_code: i32, pub file_added_label: String, @@ -298,6 +299,7 @@ impl From for Config { cwd_relative_to_repo_root, decorations_width: opt.computed.decorations_width, default_language: opt.default_language, + diff_args: opt.diff_args, diff_stat_align_width: opt.diff_stat_align_width, error_exit_code: 2, // Use 2 for error because diff uses 0 and 1 for non-error. file_added_label, diff --git a/src/options/set.rs b/src/options/set.rs index 2fae345bf..f02cd4ed7 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -140,6 +140,7 @@ pub fn set_options( commit_regex, commit_style, default_language, + diff_args, diff_stat_align_width, file_added_label, file_copied_label, diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index ddaf44d88..f432f4612 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -24,12 +24,14 @@ pub fn diff( |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/"); let diff_cmd = if via_process_substitution(minus_file) || via_process_substitution(plus_file) { - ["diff", "-u", "--"].as_slice() + format!("diff -u {} --", config.diff_args) } else { - ["git", "diff", "--no-index", "--color", "--"].as_slice() + format!("git diff --no-index --color {} --", config.diff_args) }; - let diff_bin = diff_cmd[0]; + let mut diff_cmd = diff_cmd.split_whitespace(); + let diff_bin = diff_cmd.next().unwrap(); + let diff_path = match grep_cli::resolve_binary(PathBuf::from(diff_bin)) { Ok(path) => path, Err(err) => { @@ -39,7 +41,7 @@ pub fn diff( }; let diff_process = process::Command::new(diff_path) - .args(&diff_cmd[1..]) + .args(diff_cmd) .args([minus_file, plus_file]) .stdout(process::Stdio::piped()) .spawn(); From 8fec7430c899625132587da30e0185507f3ebe66 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 21 Jul 2024 22:19:06 -0400 Subject: [PATCH 03/18] Suggestions from code review --- src/cli.rs | 8 ++++---- src/subcommands/diff.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index e18a15f1c..9c78357d5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -172,13 +172,13 @@ pub struct Opt { default_value = "", value_name = "STRING" )] - /// Arguments to pass to `git diff` when using delta to diff two files. + /// Extra arguments to pass to `git diff` when using delta to diff two files. /// /// E.g. `delta --diff-args=-U999 file_1 file_2` is equivalent to - /// `git diff -U999 --no-index --color file_1 file_2 | delta`. + /// `git diff --no-index --color -U999 file_1 file_2 | delta`. /// - /// However, if you use process substitution instead of real file paths, it falls back to `diff -u` instead of `git - /// diff`. + /// However, if you use process substitution (`delta <(command_1) <(command_2)`) + /// instead of real file paths, it falls back to `diff -U3` instead of `git diff`. pub diff_args: String, #[arg(long = "diff-highlight")] diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index f432f4612..ebd93184b 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -24,7 +24,7 @@ pub fn diff( |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/"); let diff_cmd = if via_process_substitution(minus_file) || via_process_substitution(plus_file) { - format!("diff -u {} --", config.diff_args) + format!("diff -U3 {} --", config.diff_args) } else { format!("git diff --no-index --color {} --", config.diff_args) }; From 7ab6cd9dda1d79ec5894d846a5ccf6ff8210c92f Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 21 Jul 2024 22:38:55 -0400 Subject: [PATCH 04/18] Improve behaviour on error of external diff process --- src/subcommands/diff.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index ebd93184b..07d37228d 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -29,8 +29,8 @@ pub fn diff( format!("git diff --no-index --color {} --", config.diff_args) }; - let mut diff_cmd = diff_cmd.split_whitespace(); - let diff_bin = diff_cmd.next().unwrap(); + let mut diff_args = diff_cmd.split_whitespace(); + let diff_bin = diff_args.next().unwrap(); let diff_path = match grep_cli::resolve_binary(PathBuf::from(diff_bin)) { Ok(path) => path, @@ -41,7 +41,7 @@ pub fn diff( }; let diff_process = process::Command::new(diff_path) - .args(diff_cmd) + .args(diff_args) .args([minus_file, plus_file]) .stdout(process::Stdio::piped()) .spawn(); @@ -68,8 +68,8 @@ pub fn diff( // Return the exit code from the diff process, so that the exit code // contract of `delta file_A file_B` is the same as that of `diff file_A - // file_B` (i.e. 0 if same, 1 if different, 2 if error). - diff_process + // file_B` (i.e. 0 if same, 1 if different, >= 2 if error). + let code = diff_process .wait() .unwrap_or_else(|_| { delta_unreachable(&format!("'{diff_bin}' process not running.")); @@ -78,7 +78,13 @@ pub fn diff( .unwrap_or_else(|| { eprintln!("'{diff_bin}' process terminated without exit status."); config.error_exit_code - }) + }); + if code >= 2 { + eprintln!("'{diff_bin}' process failed with exit status {code}. Command was {diff_cmd}"); + config.error_exit_code + } else { + code + } } #[cfg(test)] From 58c1de069098d127a7e12a56807d7a2e6fde7b5b Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 22 Jul 2024 08:50:24 -0400 Subject: [PATCH 05/18] Set unified context iff needed --- Cargo.lock | 197 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/subcommands/diff.rs | 119 +++++++++++++++++++----- 3 files changed, 296 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 238a02dfb..c82c656d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -483,6 +483,101 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "getrandom" version = "0.2.11" @@ -521,6 +616,7 @@ dependencies = [ "palette", "pathdiff", "regex", + "rstest", "serde", "serde_json", "shell-words", @@ -944,6 +1040,18 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.28" @@ -969,6 +1077,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.75" @@ -1060,6 +1177,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rgb" version = "0.8.37" @@ -1069,6 +1192,45 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "rstest" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.28" @@ -1165,6 +1327,15 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smol_str" version = "0.1.24" @@ -1342,6 +1513,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-bidi" version = "0.3.14" @@ -1721,6 +1909,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "xdg" version = "2.5.2" diff --git a/Cargo.toml b/Cargo.toml index 7e0682ec7..211364bbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } [dev-dependencies] insta = { version = "1.*", features = ["colors", "filters"] } +rstest = "0.21.0" [profile.test] opt-level = 2 diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 07d37228d..8b2a32cc4 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -7,7 +7,8 @@ use bytelines::ByteLinesReader; use crate::config::{self, delta_unreachable}; use crate::delta; -/// Run `git diff` on the files provided on the command line and display the output. +/// Run `git diff` on the files provided on the command line and display the output. Fall back to +/// `diff` if the supplied "files" use process substitution. pub fn diff( minus_file: &Path, plus_file: &Path, @@ -23,15 +24,34 @@ pub fn diff( let via_process_substitution = |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/"); - let diff_cmd = if via_process_substitution(minus_file) || via_process_substitution(plus_file) { - format!("diff -U3 {} --", config.diff_args) - } else { - format!("git diff --no-index --color {} --", config.diff_args) + let diff_args = match shell_words::split(&config.diff_args) { + Ok(words) => words, + Err(err) => { + eprintln!("Failed to parse diff args: {}: {err}", config.diff_args); + return config.error_exit_code; + } }; - let mut diff_args = diff_cmd.split_whitespace(); - let diff_bin = diff_args.next().unwrap(); - + // https://stackoverflow.com/questions/22706714/why-does-git-diff-not-work-with-process-substitution + // TODO: git >= 2.42 supports process substitution + let use_git_diff = + !via_process_substitution(minus_file) && !via_process_substitution(plus_file); + let mut diff_cmd = if use_git_diff { + vec!["git", "diff", "--no-index", "--color"] + } else if diff_args_set_unified_context(&diff_args) { + vec!["diff"] + } else { + vec!["diff", "-U3"] + }; + diff_cmd.extend( + diff_args + .iter() + .filter(|s| !s.is_empty()) + .map(String::as_str), + ); + diff_cmd.push("--"); + + let (diff_bin, diff_cmd) = diff_cmd.split_first().unwrap(); let diff_path = match grep_cli::resolve_binary(PathBuf::from(diff_bin)) { Ok(path) => path, Err(err) => { @@ -40,8 +60,8 @@ pub fn diff( } }; - let diff_process = process::Command::new(diff_path) - .args(diff_args) + let diff_process = process::Command::new(&diff_path) + .args(diff_cmd) .args([minus_file, plus_file]) .stdout(process::Stdio::piped()) .spawn(); @@ -80,21 +100,70 @@ pub fn diff( config.error_exit_code }); if code >= 2 { - eprintln!("'{diff_bin}' process failed with exit status {code}. Command was {diff_cmd}"); + eprintln!( + "'{diff_bin}' process failed with exit status {code}. Command was: {}", + format_args!( + "{} {} {} {}", + diff_path.display(), + diff_cmd.join(" "), + minus_file.display(), + plus_file.display() + ) + ); config.error_exit_code } else { code } } +/// Do the user-supplied `diff` args set the unified context? +fn diff_args_set_unified_context(args: I) -> bool +where + I: IntoIterator, + S: AsRef, +{ + // This function is applied to `diff` args; not `git diff`. + for arg in args { + let arg = arg.as_ref(); + if arg == "-u" || arg == "-U" { + // diff allows a space after -U (git diff does not) + return true; + } + if (arg.starts_with("-U") || arg.starts_with("-u")) + && arg.split_at(2).1.parse::().is_ok() + { + return true; + } + } + false +} + #[cfg(test)] mod main_tests { use std::io::{Cursor, Read, Seek}; use std::path::PathBuf; - use super::diff; + use super::{diff, diff_args_set_unified_context}; use crate::tests::integration_test_utils; + use rstest::rstest; + + #[rstest] + #[case(&["-u"], true)] + #[case(&["-u7"], true)] + #[case(&["-u77"], true)] + #[case(&["-ux"], false)] + #[case(&["-U"], true)] + #[case(&["-U7"], true)] + #[case(&["-U77"], true)] + #[case(&["-Ux"], false)] + fn test_unified_diff_arg_is_detected_in_diff_args( + #[case] diff_args: &[&str], + #[case] expected: bool, + ) { + assert_eq!(diff_args_set_unified_context(diff_args), expected) + } + #[test] #[ignore] // https://github.com/dandavison/delta/pull/546 fn test_diff_same_empty_file() { @@ -120,15 +189,23 @@ mod main_tests { } fn _do_diff_test(file_a: &str, file_b: &str, expect_diff: bool) { - let config = integration_test_utils::make_config_from_args(&[]); - let mut writer = Cursor::new(vec![]); - let exit_code = diff( - &PathBuf::from(file_a), - &PathBuf::from(file_b), - &config, - &mut writer, - ); - assert_eq!(exit_code, if expect_diff { 1 } else { 0 }); + for args in [ + vec![], + vec!["-@''"], + vec!["-@-u"], + vec!["-@-U99"], + vec!["-@-U0"], + ] { + let config = integration_test_utils::make_config_from_args(&args); + let mut writer = Cursor::new(vec![]); + let exit_code = diff( + &PathBuf::from(file_a), + &PathBuf::from(file_b), + &config, + &mut writer, + ); + assert_eq!(exit_code, if expect_diff { 1 } else { 0 }); + } } fn _read_to_string(cursor: &mut Cursor>) -> String { From 96bd15fa847d00525149fe24c65293ef6a0bd0c2 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 4 Aug 2024 08:20:55 -0400 Subject: [PATCH 06/18] Don't print entire --help text --- src/subcommands/diff.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 8b2a32cc4..4844097a0 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -1,4 +1,4 @@ -use std::io::{ErrorKind, Write}; +use std::io::{BufRead, ErrorKind, Write}; use std::path::{Path, PathBuf}; use std::process; @@ -64,6 +64,7 @@ pub fn diff( .args(diff_cmd) .args([minus_file, plus_file]) .stdout(process::Stdio::piped()) + .stderr(process::Stdio::piped()) .spawn(); if let Err(err) = diff_process { @@ -100,12 +101,20 @@ pub fn diff( config.error_exit_code }); if code >= 2 { + for line in BufReader::new(diff_process.stderr.unwrap()).lines() { + eprintln!("{}", line.unwrap_or("".into())); + if code == 129 && use_git_diff { + // `git diff` unknown option: print first line (which is an error message) but not + // the remainder (which is the entire --help text). + break; + } + } eprintln!( "'{diff_bin}' process failed with exit status {code}. Command was: {}", format_args!( "{} {} {} {}", diff_path.display(), - diff_cmd.join(" "), + shell_words::join(diff_cmd), minus_file.display(), plus_file.display() ) From fd44e586125a14da174161a299a274763a35d55a Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 4 Aug 2024 09:03:11 -0400 Subject: [PATCH 07/18] Use enum --- src/subcommands/diff.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 4844097a0..58ad60766 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -7,6 +7,11 @@ use bytelines::ByteLinesReader; use crate::config::{self, delta_unreachable}; use crate::delta; +enum Differ { + GitDiff, + Diff, +} + /// Run `git diff` on the files provided on the command line and display the output. Fall back to /// `diff` if the supplied "files" use process substitution. pub fn diff( @@ -34,15 +39,22 @@ pub fn diff( // https://stackoverflow.com/questions/22706714/why-does-git-diff-not-work-with-process-substitution // TODO: git >= 2.42 supports process substitution - let use_git_diff = - !via_process_substitution(minus_file) && !via_process_substitution(plus_file); - let mut diff_cmd = if use_git_diff { - vec!["git", "diff", "--no-index", "--color"] - } else if diff_args_set_unified_context(&diff_args) { - vec!["diff"] - } else { - vec!["diff", "-U3"] - }; + let (differ, mut diff_cmd) = + if !via_process_substitution(minus_file) && !via_process_substitution(plus_file) { + ( + Differ::GitDiff, + vec!["git", "diff", "--no-index", "--color"], + ) + } else { + ( + Differ::Diff, + if diff_args_set_unified_context(&diff_args) { + vec!["diff"] + } else { + vec!["diff", "-U3"] + }, + ) + }; diff_cmd.extend( diff_args .iter() @@ -103,7 +115,7 @@ pub fn diff( if code >= 2 { for line in BufReader::new(diff_process.stderr.unwrap()).lines() { eprintln!("{}", line.unwrap_or("".into())); - if code == 129 && use_git_diff { + if code == 129 && matches!(differ, Differ::GitDiff) { // `git diff` unknown option: print first line (which is an error message) but not // the remainder (which is the entire --help text). break; From de85433f7087ffe7bd750ef0a489da64132a4526 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 4 Aug 2024 09:15:37 -0400 Subject: [PATCH 08/18] Fall back to diff if git diff fails due to lack of support for process substitution --- src/main.rs | 8 +++- src/subcommands/diff.rs | 104 +++++++++++++++++++++------------------- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/main.rs b/src/main.rs index a0df151ae..8b4d53035 100644 --- a/src/main.rs +++ b/src/main.rs @@ -136,7 +136,13 @@ fn run_app() -> std::io::Result { let mut writer = output_type.handle().unwrap(); if let (Some(minus_file), Some(plus_file)) = (&config.minus_file, &config.plus_file) { - let exit_code = subcommands::diff::diff(minus_file, plus_file, &config, &mut writer); + let exit_code = subcommands::diff::diff( + minus_file, + plus_file, + subcommands::diff::Differ::GitDiff, + &config, + &mut writer, + ); return Ok(exit_code); } diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 58ad60766..7b3e2912b 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -7,28 +7,23 @@ use bytelines::ByteLinesReader; use crate::config::{self, delta_unreachable}; use crate::delta; -enum Differ { +pub enum Differ { GitDiff, Diff, } -/// Run `git diff` on the files provided on the command line and display the output. Fall back to -/// `diff` if the supplied "files" use process substitution. +/// Run either `git diff` or `diff` on the files provided on the command line and display the +/// output. Try again with `diff` if `git diff` seems to have failed due to lack of support for +/// process substitution. pub fn diff( minus_file: &Path, plus_file: &Path, + differ: Differ, config: &config::Config, writer: &mut dyn Write, ) -> i32 { use std::io::BufReader; - // When called as `delta <(echo foo) <(echo bar)`, then git as of version 2.34 just prints the - // diff of the filenames which were created by the process substitution and does not read their - // content, so fall back to plain `diff` which simply opens the given input as files. - // This fallback ignores git settings, but is better than nothing. - let via_process_substitution = - |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/"); - let diff_args = match shell_words::split(&config.diff_args) { Ok(words) => words, Err(err) => { @@ -37,24 +32,17 @@ pub fn diff( } }; - // https://stackoverflow.com/questions/22706714/why-does-git-diff-not-work-with-process-substitution - // TODO: git >= 2.42 supports process substitution - let (differ, mut diff_cmd) = - if !via_process_substitution(minus_file) && !via_process_substitution(plus_file) { - ( - Differ::GitDiff, - vec!["git", "diff", "--no-index", "--color"], - ) - } else { - ( - Differ::Diff, - if diff_args_set_unified_context(&diff_args) { - vec!["diff"] - } else { - vec!["diff", "-U3"] - }, - ) - }; + let mut diff_cmd = match differ { + Differ::GitDiff => vec!["git", "diff", "--no-index", "--color"], + Differ::Diff => { + if diff_args_set_unified_context(&diff_args) { + vec!["diff"] + } else { + vec!["diff", "-U3"] + } + } + }; + diff_cmd.extend( diff_args .iter() @@ -99,9 +87,9 @@ pub fn diff( } }; - // Return the exit code from the diff process, so that the exit code - // contract of `delta file_A file_B` is the same as that of `diff file_A - // file_B` (i.e. 0 if same, 1 if different, >= 2 if error). + // Return the exit code from the diff process, so that the exit code contract of `delta file1 + // file2` is the same as that of `diff file1 file2` (i.e. 0 if same, 1 if different, >= 2 if + // error). let code = diff_process .wait() .unwrap_or_else(|_| { @@ -112,26 +100,43 @@ pub fn diff( eprintln!("'{diff_bin}' process terminated without exit status."); config.error_exit_code }); + if code >= 2 { - for line in BufReader::new(diff_process.stderr.unwrap()).lines() { - eprintln!("{}", line.unwrap_or("".into())); - if code == 129 && matches!(differ, Differ::GitDiff) { - // `git diff` unknown option: print first line (which is an error message) but not - // the remainder (which is the entire --help text). - break; + let via_process_substitution = + |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/"); + let is_git_diff = matches!(differ, Differ::GitDiff); + if is_git_diff + && code == 128 + && (via_process_substitution(minus_file) || via_process_substitution(plus_file)) + { + // https://stackoverflow.com/questions/22706714/why-does-git-diff-not-work-with-process-substitution + // When called as `delta <(echo foo) <(echo bar)`, then git < 2.42 just prints the diff of the + // filenames which were created by the process substitution and does not read their content. + + // It looks like `git diff` failed due to lack of process substitution (version <2.42); + // try again with `diff`. + diff(minus_file, plus_file, Differ::Diff, config, writer) + } else { + for line in BufReader::new(diff_process.stderr.unwrap()).lines() { + eprintln!("{}", line.unwrap_or("".into())); + if code == 129 && is_git_diff { + // `git diff` unknown option: print first line (which is an error message) but not + // the remainder (which is the entire --help text). + break; + } } + eprintln!( + "'{diff_bin}' process failed with exit status {code}. Command was: {}", + format_args!( + "{} {} {} {}", + diff_path.display(), + shell_words::join(diff_cmd), + minus_file.display(), + plus_file.display() + ) + ); + config.error_exit_code } - eprintln!( - "'{diff_bin}' process failed with exit status {code}. Command was: {}", - format_args!( - "{} {} {} {}", - diff_path.display(), - shell_words::join(diff_cmd), - minus_file.display(), - plus_file.display() - ) - ); - config.error_exit_code } else { code } @@ -164,7 +169,7 @@ mod main_tests { use std::io::{Cursor, Read, Seek}; use std::path::PathBuf; - use super::{diff, diff_args_set_unified_context}; + use super::{diff, diff_args_set_unified_context, Differ}; use crate::tests::integration_test_utils; use rstest::rstest; @@ -222,6 +227,7 @@ mod main_tests { let exit_code = diff( &PathBuf::from(file_a), &PathBuf::from(file_b), + Differ::GitDiff, &config, &mut writer, ); From aae8943b470db809cfba7ec0062899a2b7cf76b6 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 4 Aug 2024 09:41:53 -0400 Subject: [PATCH 09/18] Tweak help text --- src/cli.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9c78357d5..fa635a756 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -177,8 +177,8 @@ pub struct Opt { /// E.g. `delta --diff-args=-U999 file_1 file_2` is equivalent to /// `git diff --no-index --color -U999 file_1 file_2 | delta`. /// - /// However, if you use process substitution (`delta <(command_1) <(command_2)`) - /// instead of real file paths, it falls back to `diff -U3` instead of `git diff`. + /// If you use process substitution (`delta <(command_1) <(command_2)`) and your git version + /// doesn't support it, then delta will fall back to `diff` instead of `git diff`. pub diff_args: String, #[arg(long = "diff-highlight")] From e93b8f18fd338f7e8342174f6f05c240620a994e Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Tue, 13 Aug 2024 09:48:26 -0400 Subject: [PATCH 10/18] Parse git version --- src/utils/git.rs | 31 +++++++++++++++++++++++++++++++ src/utils/mod.rs | 1 + 2 files changed, 32 insertions(+) create mode 100644 src/utils/git.rs diff --git a/src/utils/git.rs b/src/utils/git.rs new file mode 100644 index 000000000..fa01e05bb --- /dev/null +++ b/src/utils/git.rs @@ -0,0 +1,31 @@ +use std::process::Command; + +pub fn retrieve_git_version() -> Option<(usize, usize)> { + if let Ok(git_path) = grep_cli::resolve_binary("git") { + let cmd = Command::new(git_path).arg("--version").output().ok()?; + parse_git_version(&cmd.stdout) + } else { + None + } +} + +fn parse_git_version(output: &[u8]) -> Option<(usize, usize)> { + let mut parts = output.strip_prefix(b"git version ")?.split(|&b| b == b'.'); + let major = std::str::from_utf8(parts.next()?).ok()?.parse().ok()?; + let minor = std::str::from_utf8(parts.next()?).ok()?.parse().ok()?; + Some((major, minor)) +} + +#[cfg(test)] +mod tests { + use super::parse_git_version; + use rstest::rstest; + + #[rstest] + #[case(b"git version 2.46.0", Some((2, 46)))] + #[case(b"git version 2.39.3 (Apple Git-146)", Some((2, 39)))] + #[case(b"", None)] + fn test_parse_git_version(#[case] input: &[u8], #[case] expected: Option<(usize, usize)>) { + assert_eq!(parse_git_version(input), expected); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 2d952fe32..b460e5af3 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,6 @@ #[cfg(not(tarpaulin_include))] pub mod bat; +pub mod git; pub mod helpwrap; pub mod path; pub mod process; From 0a5149db351af3f66690865242bcc916039a2c64 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 14 Aug 2024 06:02:32 -0400 Subject: [PATCH 11/18] Use detected git version to determine whether process substitution is supported --- src/main.rs | 8 +--- src/subcommands/diff.rs | 90 ++++++++++++++++++++--------------------- 2 files changed, 44 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8b4d53035..a0df151ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -136,13 +136,7 @@ fn run_app() -> std::io::Result { let mut writer = output_type.handle().unwrap(); if let (Some(minus_file), Some(plus_file)) = (&config.minus_file, &config.plus_file) { - let exit_code = subcommands::diff::diff( - minus_file, - plus_file, - subcommands::diff::Differ::GitDiff, - &config, - &mut writer, - ); + let exit_code = subcommands::diff::diff(minus_file, plus_file, &config, &mut writer); return Ok(exit_code); } diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 7b3e2912b..aca7c366e 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -6,19 +6,19 @@ use bytelines::ByteLinesReader; use crate::config::{self, delta_unreachable}; use crate::delta; +use crate::utils::git::retrieve_git_version; -pub enum Differ { +#[derive(Debug, PartialEq)] +enum Differ { GitDiff, Diff, } -/// Run either `git diff` or `diff` on the files provided on the command line and display the -/// output. Try again with `diff` if `git diff` seems to have failed due to lack of support for -/// process substitution. +/// Run `git diff` on the files provided on the command line and display the output. Fall back to +/// `diff` if the supplied "files" use process substitution. pub fn diff( minus_file: &Path, plus_file: &Path, - differ: Differ, config: &config::Config, writer: &mut dyn Write, ) -> i32 { @@ -32,17 +32,31 @@ pub fn diff( } }; - let mut diff_cmd = match differ { - Differ::GitDiff => vec!["git", "diff", "--no-index", "--color"], - Differ::Diff => { + let via_process_substitution = + |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/"); + + // https://stackoverflow.com/questions/22706714/why-does-git-diff-not-work-with-process-substitution + // git <2.42 does not support process substitution + let (differ, mut diff_cmd) = if !via_process_substitution(minus_file) + && !via_process_substitution(plus_file) + || retrieve_git_version() + .map(|v| v >= (2, 42)) + .unwrap_or(false) + { + ( + Differ::GitDiff, + vec!["git", "diff", "--no-index", "--color"], + ) + } else { + ( + Differ::Diff, if diff_args_set_unified_context(&diff_args) { vec!["diff"] } else { vec!["diff", "-U3"] - } - } + }, + ) }; - diff_cmd.extend( diff_args .iter() @@ -100,43 +114,26 @@ pub fn diff( eprintln!("'{diff_bin}' process terminated without exit status."); config.error_exit_code }); - if code >= 2 { - let via_process_substitution = - |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/"); - let is_git_diff = matches!(differ, Differ::GitDiff); - if is_git_diff - && code == 128 - && (via_process_substitution(minus_file) || via_process_substitution(plus_file)) - { - // https://stackoverflow.com/questions/22706714/why-does-git-diff-not-work-with-process-substitution - // When called as `delta <(echo foo) <(echo bar)`, then git < 2.42 just prints the diff of the - // filenames which were created by the process substitution and does not read their content. - - // It looks like `git diff` failed due to lack of process substitution (version <2.42); - // try again with `diff`. - diff(minus_file, plus_file, Differ::Diff, config, writer) - } else { - for line in BufReader::new(diff_process.stderr.unwrap()).lines() { - eprintln!("{}", line.unwrap_or("".into())); - if code == 129 && is_git_diff { - // `git diff` unknown option: print first line (which is an error message) but not - // the remainder (which is the entire --help text). - break; - } + for line in BufReader::new(diff_process.stderr.unwrap()).lines() { + eprintln!("{}", line.unwrap_or("".into())); + if code == 129 && differ == Differ::GitDiff { + // `git diff` unknown option: print first line (which is an error message) but not + // the remainder (which is the entire --help text). + break; } - eprintln!( - "'{diff_bin}' process failed with exit status {code}. Command was: {}", - format_args!( - "{} {} {} {}", - diff_path.display(), - shell_words::join(diff_cmd), - minus_file.display(), - plus_file.display() - ) - ); - config.error_exit_code } + eprintln!( + "'{diff_bin}' process failed with exit status {code}. Command was: {}", + format_args!( + "{} {} {} {}", + diff_path.display(), + shell_words::join(diff_cmd), + minus_file.display(), + plus_file.display() + ) + ); + config.error_exit_code } else { code } @@ -169,7 +166,7 @@ mod main_tests { use std::io::{Cursor, Read, Seek}; use std::path::PathBuf; - use super::{diff, diff_args_set_unified_context, Differ}; + use super::{diff, diff_args_set_unified_context}; use crate::tests::integration_test_utils; use rstest::rstest; @@ -227,7 +224,6 @@ mod main_tests { let exit_code = diff( &PathBuf::from(file_a), &PathBuf::from(file_b), - Differ::GitDiff, &config, &mut writer, ); From 6f85fae415c08528bcfedc4628057fbf1c9f1723 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 14 Aug 2024 06:26:36 -0400 Subject: [PATCH 12/18] Permit -@U1 --- src/subcommands/diff.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index aca7c366e..99252ca0a 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -24,13 +24,22 @@ pub fn diff( ) -> i32 { use std::io::BufReader; - let diff_args = match shell_words::split(&config.diff_args) { + let mut diff_args = match shell_words::split(&config.diff_args.trim()) { Ok(words) => words, Err(err) => { eprintln!("Failed to parse diff args: {}: {err}", config.diff_args); return config.error_exit_code; } }; + // Permit e.g. -@U1 + if diff_args + .iter() + .nth(0) + .map(|arg| !arg.is_empty() && !arg.starts_with('-')) + .unwrap_or(false) + { + diff_args[0] = format!("-{}", diff_args[0]) + } let via_process_substitution = |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/"); From bd04ba7e4723f8ec40ae40c1af42156307dc934e Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 14 Aug 2024 06:46:13 -0400 Subject: [PATCH 13/18] Refactor tests --- src/subcommands/diff.rs | 70 ++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 99252ca0a..35bf76cdf 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -196,48 +196,40 @@ mod main_tests { assert_eq!(diff_args_set_unified_context(diff_args), expected) } - #[test] - #[ignore] // https://github.com/dandavison/delta/pull/546 - fn test_diff_same_empty_file() { - _do_diff_test("/dev/null", "/dev/null", false); + enum ExpectDiff { + Yes, + No, } - #[test] - #[cfg_attr(target_os = "windows", ignore)] - fn test_diff_same_non_empty_file() { - _do_diff_test("/etc/passwd", "/etc/passwd", false); - } - - #[test] - #[cfg_attr(target_os = "windows", ignore)] - fn test_diff_empty_vs_non_empty_file() { - _do_diff_test("/dev/null", "/etc/passwd", true); - } - - #[test] + #[rstest] #[cfg_attr(target_os = "windows", ignore)] - fn test_diff_two_non_empty_files() { - _do_diff_test("/etc/group", "/etc/passwd", true); - } - - fn _do_diff_test(file_a: &str, file_b: &str, expect_diff: bool) { - for args in [ - vec![], - vec!["-@''"], - vec!["-@-u"], - vec!["-@-U99"], - vec!["-@-U0"], - ] { - let config = integration_test_utils::make_config_from_args(&args); - let mut writer = Cursor::new(vec![]); - let exit_code = diff( - &PathBuf::from(file_a), - &PathBuf::from(file_b), - &config, - &mut writer, - ); - assert_eq!(exit_code, if expect_diff { 1 } else { 0 }); - } + #[case("/dev/null", "/dev/null", ExpectDiff::No)] + #[case("/etc/group", "/etc/passwd", ExpectDiff::Yes)] + #[case("/dev/null", "/etc/passwd", ExpectDiff::Yes)] + #[case("/etc/passwd", "/etc/passwd", ExpectDiff::No)] + fn test_diff_real_files( + #[case] file_a: &str, + #[case] file_b: &str, + #[case] expect_diff: ExpectDiff, + #[values(vec![], vec!["-@''"], vec!["-@-u"], vec!["-@-U99"], vec!["-@-U0"])] args: Vec< + &str, + >, + ) { + let config = integration_test_utils::make_config_from_args(&args); + let mut writer = Cursor::new(vec![]); + let exit_code = diff( + &PathBuf::from(file_a), + &PathBuf::from(file_b), + &config, + &mut writer, + ); + assert_eq!( + exit_code, + match expect_diff { + ExpectDiff::Yes => 1, + ExpectDiff::No => 0, + } + ); } fn _read_to_string(cursor: &mut Cursor>) -> String { From b83933a0c56e76c044348ce89f7f266000e01b92 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 14 Aug 2024 07:22:47 -0400 Subject: [PATCH 14/18] Clippy --- src/subcommands/diff.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 35bf76cdf..7a54b209b 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -24,7 +24,7 @@ pub fn diff( ) -> i32 { use std::io::BufReader; - let mut diff_args = match shell_words::split(&config.diff_args.trim()) { + let mut diff_args = match shell_words::split(config.diff_args.trim()) { Ok(words) => words, Err(err) => { eprintln!("Failed to parse diff args: {}: {err}", config.diff_args); @@ -33,8 +33,7 @@ pub fn diff( }; // Permit e.g. -@U1 if diff_args - .iter() - .nth(0) + .first() .map(|arg| !arg.is_empty() && !arg.starts_with('-')) .unwrap_or(false) { From ec44b336f8d2b96285bfa7bdd215dba26876184b Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 14 Aug 2024 07:24:56 -0400 Subject: [PATCH 15/18] Skip mysterious failing test --- src/subcommands/diff.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 7a54b209b..5b2a96338 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -202,7 +202,7 @@ mod main_tests { #[rstest] #[cfg_attr(target_os = "windows", ignore)] - #[case("/dev/null", "/dev/null", ExpectDiff::No)] + // #[case("/dev/null", "/dev/null", ExpectDiff::No)] https://github.com/dandavison/delta/pull/546#issuecomment-835852373 #[case("/etc/group", "/etc/passwd", ExpectDiff::Yes)] #[case("/dev/null", "/etc/passwd", ExpectDiff::Yes)] #[case("/etc/passwd", "/etc/passwd", ExpectDiff::No)] From 2f60174d2268529f44ba05bda8940c42df668e23 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 14 Aug 2024 08:00:46 -0400 Subject: [PATCH 16/18] Support `delta file1 file2` without git installed --- src/subcommands/diff.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 5b2a96338..16fa5cab0 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -45,26 +45,27 @@ pub fn diff( // https://stackoverflow.com/questions/22706714/why-does-git-diff-not-work-with-process-substitution // git <2.42 does not support process substitution - let (differ, mut diff_cmd) = if !via_process_substitution(minus_file) - && !via_process_substitution(plus_file) - || retrieve_git_version() - .map(|v| v >= (2, 42)) - .unwrap_or(false) - { - ( - Differ::GitDiff, - vec!["git", "diff", "--no-index", "--color"], - ) - } else { - ( + let (differ, mut diff_cmd) = match retrieve_git_version() { + Some(version) + if version >= (2, 42) + || !(via_process_substitution(minus_file) + || via_process_substitution(plus_file)) => + { + ( + Differ::GitDiff, + vec!["git", "diff", "--no-index", "--color"], + ) + } + _ => ( Differ::Diff, if diff_args_set_unified_context(&diff_args) { vec!["diff"] } else { vec!["diff", "-U3"] }, - ) + ), }; + diff_cmd.extend( diff_args .iter() From 38e58310f5a8cbcdbdbcf60995e769335958af23 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 14 Aug 2024 08:12:25 -0400 Subject: [PATCH 17/18] Ignore entire test on Windows --- src/subcommands/diff.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 16fa5cab0..56a7e7143 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -201,8 +201,8 @@ mod main_tests { No, } + #[cfg(not(target_os = "windows"))] #[rstest] - #[cfg_attr(target_os = "windows", ignore)] // #[case("/dev/null", "/dev/null", ExpectDiff::No)] https://github.com/dandavison/delta/pull/546#issuecomment-835852373 #[case("/etc/group", "/etc/passwd", ExpectDiff::Yes)] #[case("/dev/null", "/etc/passwd", ExpectDiff::Yes)] From 8e6cf0c8dbb2e08ba7da682461c808e7b958c4b3 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 14 Aug 2024 08:18:20 -0400 Subject: [PATCH 18/18] Delete unused function --- src/subcommands/diff.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs index 56a7e7143..215d45e38 100644 --- a/src/subcommands/diff.rs +++ b/src/subcommands/diff.rs @@ -172,7 +172,7 @@ where #[cfg(test)] mod main_tests { - use std::io::{Cursor, Read, Seek}; + use std::io::Cursor; use std::path::PathBuf; use super::{diff, diff_args_set_unified_context}; @@ -231,11 +231,4 @@ mod main_tests { } ); } - - fn _read_to_string(cursor: &mut Cursor>) -> String { - let mut s = String::new(); - cursor.rewind().unwrap(); - cursor.read_to_string(&mut s).unwrap(); - s - } }