delta_lib/subcommands/
diff.rs

1use 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
10/// Run `git diff` on the files provided on the command line and display the output.
11pub 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    // When called as `delta <(echo foo) <(echo bar)`, then git as of version 2.34 just prints the
20    // diff of the filenames which were created by the process substitution and does not read their
21    // content, so fall back to plain `diff` which simply opens the given input as files.
22    // This fallback ignores git settings, but is better than nothing.
23    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    // Return the exit code from the diff process, so that the exit code
68    // contract of `delta file_A file_B` is the same as that of `diff file_A
69    // file_B` (i.e. 0 if same, 1 if different, 2 if error).
70    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] // https://github.com/dandavison/delta/pull/546
92    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}