diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f256e74f..37d1bdbae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ directory. Fixed a race condition where diffing directories would lead to interleaved output from different files. +### Command Line Interface + +Difftastic now sets the exit code if it finds changes. See [usage in +the manual](https://difftastic.wilfred.me.uk/usage.html) for the full +list of exit codes used. + ## 0.38 (released 14th November 2022) ### Parsing diff --git a/manual/src/usage.md b/manual/src/usage.md index 5e88da4ea..2c2c562c3 100644 --- a/manual/src/usage.md +++ b/manual/src/usage.md @@ -53,3 +53,14 @@ are also visible in `--help`. For example, `DFT_BACKGROUND=light` is equivalent to `--background=light`. This is useful when using VCS tools like git, where you are not invoking the `difft` binary directly. + +## Exit Codes + +0: Difftastic found no syntactic changes (in text files) or no byte +changes (in binary files). + +1: Difftastic found syntactic changes (in text files) or byte changes +(in binary files). + +2: Difftastic was given invalid arguments, such as file paths that it +couldn't read, or the wrong number of arguments. diff --git a/src/exit_codes.rs b/src/exit_codes.rs new file mode 100644 index 000000000..d1e646904 --- /dev/null +++ b/src/exit_codes.rs @@ -0,0 +1,12 @@ +/// Successfully ran a diff, found no syntactic changes in text files +/// or byte changes in binary files. +pub const EXIT_SUCCESS: i32 = 0; + +/// Successfully ran a diff, found syntactic changes in text files or +/// byte changes in binary files. +pub const EXIT_FOUND_CHANGES: i32 = 1; + +/// Invalid arguments given to difftastic. This could be usage errors +/// (e.g. invalid numbers of arguments) or invalid paths (e.g. files +/// we don't have permission to read). +pub const EXIT_BAD_ARGUMENTS: i32 = 2; diff --git a/src/files.rs b/src/files.rs index 55e153b82..1eb8c5692 100644 --- a/src/files.rs +++ b/src/files.rs @@ -10,6 +10,7 @@ use std::{ use rustc_hash::FxHashSet; use walkdir::WalkDir; +use crate::exit_codes::EXIT_BAD_ARGUMENTS; use crate::options::FileArgument; pub fn read_files_or_die( @@ -39,7 +40,7 @@ pub fn read_files_or_die( if let Err(e) = rhs_res { eprint_read_error(rhs_path, &e); } - std::process::exit(1); + std::process::exit(EXIT_BAD_ARGUMENTS); } } } @@ -96,7 +97,7 @@ pub fn read_or_die(path: &Path) -> Vec { Ok(src) => src, Err(e) => { eprint_read_error(&FileArgument::NamedPath(path.to_path_buf()), &e); - std::process::exit(1); + std::process::exit(EXIT_BAD_ARGUMENTS); } } } diff --git a/src/main.rs b/src/main.rs index 5c50e677d..81563138a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod constants; mod diff; mod display; +mod exit_codes; mod files; mod line_parser; mod lines; @@ -40,6 +41,7 @@ use crate::parse::syntax; use diff::changes::ChangeMap; use diff::dijkstra::ExceededGraphLimit; use display::context::opposite_positions; +use exit_codes::{EXIT_FOUND_CHANGES, EXIT_SUCCESS}; use files::{ guess_content, read_files_or_die, read_or_die, relative_paths_in_either, ProbableFileKind, }; @@ -58,6 +60,8 @@ use diff::sliders::fix_all_sliders; use options::{DisplayMode, DisplayOptions, FileArgument, Mode, DEFAULT_TAB_WIDTH}; use owo_colors::OwoColorize; use rayon::prelude::*; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::{env, path::Path}; use summary::{DiffResult, FileContent}; use syntax::init_next_prev; @@ -175,6 +179,7 @@ fn main() { ); } + let encountered_changes = Arc::new(AtomicBool::new(false)); match (&lhs_path, &rhs_path) { ( options::FileArgument::NamedPath(lhs_path), @@ -186,10 +191,16 @@ fn main() { // https://github.com/rayon-rs/rayon/issues/210#issuecomment-551319338 let (send, recv) = std::sync::mpsc::sync_channel(1); + let encountered_changes = encountered_changes.clone(); let print_options = display_options.clone(); + let printing_thread = std::thread::spawn(move || { for diff_result in recv.into_iter() { print_diff_result(&print_options, &diff_result); + + if diff_result.has_reportable_change() { + encountered_changes.store(true, Ordering::Relaxed); + } } }); @@ -221,8 +232,19 @@ fn main() { language_override, ); print_diff_result(&display_options, &diff_result); + + if diff_result.has_reportable_change() { + encountered_changes.store(true, Ordering::Relaxed); + } } } + + let exit_code = if encountered_changes.load(Ordering::Relaxed) { + EXIT_FOUND_CHANGES + } else { + EXIT_SUCCESS + }; + std::process::exit(exit_code); } }; } diff --git a/src/options.rs b/src/options.rs index bfe0ca4c1..bb40a76ae 100644 --- a/src/options.rs +++ b/src/options.rs @@ -6,7 +6,9 @@ use atty::Stream; use clap::{crate_authors, crate_description, crate_version, Arg, Command}; use const_format::formatcp; -use crate::{display::style::BackgroundColor, parse::guess_language}; +use crate::{ + display::style::BackgroundColor, exit_codes::EXIT_BAD_ARGUMENTS, parse::guess_language, +}; pub const DEFAULT_BYTE_LIMIT: usize = 1_000_000; // Chosen experimentally: this is sufficiently many for all the sample @@ -383,7 +385,7 @@ pub fn parse_args() -> Mode { } eprintln!("USAGE:\n\n {}\n", USAGE); eprintln!("For more information try --help"); - std::process::exit(1); + std::process::exit(EXIT_BAD_ARGUMENTS); } }; diff --git a/src/summary.rs b/src/summary.rs index 6a640459c..89b22aef9 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -23,3 +23,15 @@ pub struct DiffResult { pub rhs_positions: Vec, pub has_same_bytes: bool, } + +impl DiffResult { + pub fn has_reportable_change(&self) -> bool { + if matches!(self.lhs_src, FileContent::Binary) + || matches!(self.rhs_src, FileContent::Binary) + { + return !self.has_same_bytes; + } + + !self.hunks.is_empty() + } +}