delta_lib/subcommands/
diff.rs1use std::io::{ErrorKind, Write};
2use std::path::{Path, PathBuf};
3use std::process;
4
5use bytelines::ByteLinesReader;
6
7use crate::config::{self, delta_unreachable};
8use crate::delta;
9
10pub fn diff(
12 minus_file: &Path,
13 plus_file: &Path,
14 config: &config::Config,
15 writer: &mut dyn Write,
16) -> i32 {
17 use std::io::BufReader;
18
19 let via_process_substitution =
24 |f: &Path| f.starts_with("/proc/self/fd/") || f.starts_with("/dev/fd/");
25
26 let diff_cmd = if via_process_substitution(minus_file) || via_process_substitution(plus_file) {
27 ["diff", "-u", "--"].as_slice()
28 } else {
29 ["git", "diff", "--no-index", "--color", "--"].as_slice()
30 };
31
32 let diff_bin = diff_cmd[0];
33 let diff_path = match grep_cli::resolve_binary(PathBuf::from(diff_bin)) {
34 Ok(path) => path,
35 Err(err) => {
36 eprintln!("Failed to resolve command '{}': {}", diff_bin, err);
37 return config.error_exit_code;
38 }
39 };
40
41 let diff_process = process::Command::new(diff_path)
42 .args(&diff_cmd[1..])
43 .args(&[minus_file, plus_file])
44 .stdout(process::Stdio::piped())
45 .spawn();
46
47 if let Err(err) = diff_process {
48 eprintln!("Failed to execute the command '{}': {}", diff_bin, err);
49 return config.error_exit_code;
50 }
51 let mut diff_process = diff_process.unwrap();
52
53 if let Err(error) = delta::delta(
54 BufReader::new(diff_process.stdout.take().unwrap()).byte_lines(),
55 writer,
56 config,
57 ) {
58 match error.kind() {
59 ErrorKind::BrokenPipe => return 0,
60 _ => {
61 eprintln!("{}", error);
62 return config.error_exit_code;
63 }
64 }
65 };
66
67 diff_process
71 .wait()
72 .unwrap_or_else(|_| {
73 delta_unreachable(&format!("'{}' process not running.", diff_bin));
74 })
75 .code()
76 .unwrap_or_else(|| {
77 eprintln!("'{}' process terminated without exit status.", diff_bin);
78 config.error_exit_code
79 })
80}
81
82#[cfg(test)]
83mod main_tests {
84 use std::io::{Cursor, Read, Seek, SeekFrom};
85 use std::path::PathBuf;
86
87 use super::diff;
88 use crate::tests::integration_test_utils;
89
90 #[test]
91 #[ignore] fn test_diff_same_empty_file() {
93 _do_diff_test("/dev/null", "/dev/null", false);
94 }
95
96 #[test]
97 #[cfg_attr(target_os = "windows", ignore)]
98 fn test_diff_same_non_empty_file() {
99 _do_diff_test("/etc/passwd", "/etc/passwd", false);
100 }
101
102 #[test]
103 #[cfg_attr(target_os = "windows", ignore)]
104 fn test_diff_empty_vs_non_empty_file() {
105 _do_diff_test("/dev/null", "/etc/passwd", true);
106 }
107
108 #[test]
109 #[cfg_attr(target_os = "windows", ignore)]
110 fn test_diff_two_non_empty_files() {
111 _do_diff_test("/etc/group", "/etc/passwd", true);
112 }
113
114 fn _do_diff_test(file_a: &str, file_b: &str, expect_diff: bool) {
115 let config = integration_test_utils::make_config_from_args(&[]);
116 let mut writer = Cursor::new(vec![]);
117 let exit_code = diff(
118 &PathBuf::from(file_a),
119 &PathBuf::from(file_b),
120 &config,
121 &mut writer,
122 );
123 assert_eq!(exit_code, if expect_diff { 1 } else { 0 });
124 }
125
126 fn _read_to_string(cursor: &mut Cursor<Vec<u8>>) -> String {
127 let mut s = String::new();
128 cursor.seek(SeekFrom::Start(0)).unwrap();
129 cursor.read_to_string(&mut s).unwrap();
130 s
131 }
132}