use std::io::{ErrorKind, Write}; use std::path::{Path, PathBuf}; use std::process; 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. pub fn diff( minus_file: &Path, plus_file: &Path, 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_cmd = if via_process_substitution(minus_file) || via_process_substitution(plus_file) { ["diff", "-u", "--"].as_slice() } else { ["git", "diff", "--no-index", "--color", "--"].as_slice() }; let diff_bin = diff_cmd[0]; let diff_path = match grep_cli::resolve_binary(PathBuf::from(diff_bin)) { Ok(path) => path, Err(err) => { eprintln!("Failed to resolve command '{diff_bin}': {err}"); return config.error_exit_code; } }; let diff_process = process::Command::new(diff_path) .args(&diff_cmd[1..]) .args([minus_file, plus_file]) .stdout(process::Stdio::piped()) .spawn(); if let Err(err) = diff_process { eprintln!("Failed to execute the command '{diff_bin}': {err}"); return config.error_exit_code; } let mut diff_process = diff_process.unwrap(); if let Err(error) = delta::delta( BufReader::new(diff_process.stdout.take().unwrap()).byte_lines(), writer, config, ) { match error.kind() { ErrorKind::BrokenPipe => return 0, _ => { eprintln!("{error}"); return config.error_exit_code; } } }; // 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 .wait() .unwrap_or_else(|_| { delta_unreachable(&format!("'{diff_bin}' process not running.")); }) .code() .unwrap_or_else(|| { eprintln!("'{diff_bin}' process terminated without exit status."); config.error_exit_code }) } #[cfg(test)] mod main_tests { use std::io::{Cursor, Read, Seek}; use std::path::PathBuf; use super::diff; use crate::tests::integration_test_utils; #[test] #[ignore] // https://github.com/dandavison/delta/pull/546 fn test_diff_same_empty_file() { _do_diff_test("/dev/null", "/dev/null", false); } #[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] #[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) { 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 }); } fn _read_to_string(cursor: &mut Cursor>) -> String { let mut s = String::new(); cursor.rewind().unwrap(); cursor.read_to_string(&mut s).unwrap(); s } }