#![allow(clippy::needless_return)]
#![deny(warnings)]
#[macro_use]
extern crate lazy_static;
use backtrace::Backtrace;
use git_version::git_version;
use line_collector::LineCollector;
use std::io::{self};
use std::panic;
use std::path;
use std::process::exit;
use std::process::{Command, Stdio};
use std::str;
use std::{env, fs::File};
mod commit_line;
mod constants;
mod line_collector;
mod refiner;
mod token_collector;
mod tokenizer;
const HELP_TEXT: &str = r#"
Usage:
diff ... | riff
riff [-b] [--no-pager] <file1> <file2>
riff [-b] [--no-pager] <directory1> <directory2>
Colors diff output, highlighting the changed parts of every line.
Git integration:
git config --global pager.diff riff
git config --global pager.show riff
git config --global pager.log riff
git config --global interactive.diffFilter riff
Options:
-b: Ignore changes in amount of whitespace
--no-pager: Don't page the result
--help: Print this text
--version: Print version number
"#;
const HELP_TEXT_FOOTER: &str = r#"
Report issues at <https://github.com/walles/riff>.
"#;
const CRASH_FOOTER: &str = r#"
Please copy the above crash report and report it at one of:
* <https://github.com/walles/riff/issues> (preferred)
* <johan.walles@gmail.com>
"#;
const PAGER_FORKBOMB_STOP: &str = "_RIFF_IGNORE_PAGER";
const GIT_VERSION: &str = git_version!(cargo_prefix = "");
fn highlight_diff<W: io::Write + Send + 'static>(input: &mut dyn io::Read, output: W) {
let mut line_collector = LineCollector::new(output);
let mut line_bytes: Vec<u8> = Vec::new();
let mut buf: [u8; 1] = [0];
loop {
let result = input.read(&mut buf);
if result.is_err() {
panic!("Error reading input stream: {:?}", result.err().unwrap());
}
if result.unwrap() == 0 {
if !line_bytes.is_empty() {
line_collector.consume_line(String::from_utf8_lossy(&line_bytes).to_string());
}
break;
}
let single_byte = buf[0];
if single_byte as char == '\r' {
continue;
}
if single_byte as char != '\n' {
line_bytes.push(single_byte);
continue;
}
line_collector.consume_line(String::from_utf8_lossy(&line_bytes).to_string());
line_bytes.clear();
continue;
}
}
#[must_use]
fn try_pager(input: &mut dyn io::Read, pager_name: &str) -> bool {
let mut command = Command::new(pager_name);
if env::var(PAGER_FORKBOMB_STOP).is_ok() {
return false;
}
command.env(PAGER_FORKBOMB_STOP, "1");
if env::var("LESS").is_err() {
command.env("LESS", "FRX");
}
if env::var("LV").is_err() {
command.env("LV", "-c");
}
command.stdin(Stdio::piped());
match command.spawn() {
Ok(mut pager) => {
let pager_stdin = pager.stdin.unwrap();
pager.stdin = None;
highlight_diff(input, pager_stdin);
pager.wait().expect("Waiting for pager failed");
return true;
}
Err(_) => {
return false;
}
}
}
#[must_use]
fn consume(option: &str, argv: &mut Vec<String>) -> bool {
if !argv.contains(&option.to_string()) {
return false;
}
argv.retain(|x| x != option);
return true;
}
fn print_help(output: &mut dyn io::Write) {
output.write_all(HELP_TEXT.trim().as_bytes()).unwrap();
output.write_all(b"\n").unwrap();
output.write_all(b"\n").unwrap();
output
.write_all(b"Installing riff in the $PATH:\n")
.unwrap();
output
.write_all(b" sudo cp riff /usr/local/bin\n")
.unwrap();
output.write_all(b"\n").unwrap();
output
.write_all(HELP_TEXT_FOOTER.trim().as_bytes())
.unwrap();
output.write_all(b"\n").unwrap();
}
fn panic_handler(panic_info: &panic::PanicInfo) {
eprintln!("\n\n-v-v-v----------- RIFF CRASHED ---------------v-v-v-\n",);
eprintln!("Panic message: <{:#?}>", panic_info);
eprintln!();
eprintln!("{:?}", Backtrace::new());
eprintln!("Riff version: {}", GIT_VERSION);
eprintln!();
eprintln!("Command line arguments: {:?}", env::args());
eprintln!("\n-^-^-^------- END OF RIFF CRASH REPORT -------^-^-^-\n",);
eprintln!("{}", CRASH_FOOTER);
}
fn highlight_stream(input: &mut dyn io::Read, no_pager: bool) {
if !atty::is(atty::Stream::Stdout) {
highlight_diff(input, io::stdout());
return;
}
if no_pager {
highlight_diff(input, io::stdout());
return;
}
if let Ok(pager_value) = env::var("PAGER") {
if try_pager(input, &pager_value) {
return;
}
}
if try_pager(input, "moar") {
return;
}
if try_pager(input, "less") {
return;
}
highlight_diff(input, io::stdout());
}
pub fn type_string(path: &path::Path) -> &str {
if !path.exists() {
return "Not found";
}
if path.is_file() {
return "File";
}
if path.is_dir() {
return "Directory";
}
return "Not file not dir";
}
fn ensure_readable(path: &path::Path) {
if let Err(why) = File::open(path) {
eprintln!("ERROR: {}: {}", why, path.to_string_lossy());
exit(1);
};
}
fn ensure_listable(path: &path::Path) {
if let Err(why) = std::fs::read_dir(path) {
eprintln!("ERROR: {}: {}", why, path.to_string_lossy());
exit(1);
}
}
fn exec_diff_highlight(path1: &str, path2: &str, ignore_space_change: bool, no_pager: bool) {
let path1 = path::Path::new(path1);
let path2 = path::Path::new(path2);
let both_paths_are_non_dirs = !path1.is_dir() && !path2.is_dir();
let both_paths_are_dirs = path1.is_dir() && path2.is_dir();
if !(both_paths_are_non_dirs || both_paths_are_dirs) {
eprintln!("Can only compare directory to directory or not-directory to not-directory, not like this:",);
eprintln!(" {:<9}: {}", type_string(path1), path1.to_string_lossy());
eprintln!(" {:<9}: {}", type_string(path2), path2.to_string_lossy());
exit(1);
}
if both_paths_are_non_dirs {
ensure_readable(path1);
ensure_readable(path2);
} else {
ensure_listable(path1);
ensure_listable(path2);
}
let mut command: &mut Command = &mut Command::new("diff");
if ignore_space_change {
command = command.arg("-b");
}
let command = command
.arg("-ur") .arg("--show-c-function")
.arg("--new-file")
.arg(path1)
.arg(path2)
.stdout(Stdio::piped());
let pretty_command = format!("{:#?}", command);
let mut diff_subprocess: std::process::Child;
match command.spawn() {
Ok(subprocess) => diff_subprocess = subprocess,
Err(err) => {
eprintln!(
"ERROR: Spawning diff failed:\n {}\n {}\n",
pretty_command, err
);
exit(1);
}
}
let diff_stdout = diff_subprocess.stdout.as_mut().unwrap();
highlight_stream(diff_stdout, no_pager);
let diff_result = diff_subprocess.wait().unwrap();
let diff_exit_code = diff_result.code().unwrap_or(2);
if diff_exit_code != 0 && diff_exit_code != 1 {
eprintln!("Exit code {}: {}", diff_exit_code, pretty_command);
exit(diff_exit_code);
}
}
fn main() {
panic::set_hook(Box::new(|panic_info: &panic::PanicInfo| {
panic_handler(panic_info);
}));
let mut args: Vec<String> = env::args().collect();
if consume("--help", &mut args) || consume("-h", &mut args) {
print_help(&mut io::stdout());
return;
}
if consume("--version", &mut args) {
println!("riff {}", GIT_VERSION);
println!();
println!("Source code available at <https://github.com/walles/riff>.");
return;
}
let ignore_space_change = consume("-b", &mut args);
if consume("--please-panic", &mut args) {
panic!("Panicking on purpose");
}
let no_pager = consume("--no-pager", &mut args);
if args.len() == 3 {
exec_diff_highlight(
args.get(1).unwrap(),
args.get(2).unwrap(),
ignore_space_change,
no_pager,
);
return;
}
if ignore_space_change {
eprintln!(
"ERROR: -b is only supported when diffing two named paths (\"riff -b a.txt b.txt\")"
);
eprintln!();
print_help(&mut io::stderr());
exit(1);
}
if args.len() != 1 {
eprintln!("ERROR: Unknown command line: {:?}", args);
eprintln!();
print_help(&mut io::stderr());
exit(1);
}
if atty::is(atty::Stream::Stdin) {
eprintln!("ERROR: Expected input from a pipe");
eprintln!();
print_help(&mut io::stderr());
exit(1);
}
highlight_stream(&mut io::stdin().lock(), no_pager);
}
#[cfg(test)]
mod tests {
use crate::constants::*;
use super::*;
use std::fs;
#[cfg(test)]
use pretty_assertions::assert_eq;
fn new(text: &str) -> String {
return format!("{}{}{}", NEW, text, NORMAL);
}
#[test]
fn test_trailing_newline_context() {
let mut input = "+bepa\n apa\n\\ No newline at end of file\n".as_bytes();
let expected = format!(
"{}\n{}\n{}\\ No newline at end of file{}\n",
new("+bepa"),
" apa",
NO_EOF_NEWLINE_COLOR,
NORMAL
);
let file = tempfile::NamedTempFile::new().unwrap();
highlight_diff(&mut input, file.reopen().unwrap());
let actual = fs::read_to_string(file.path()).unwrap();
assert_eq!(
actual.lines().collect::<Vec<_>>(),
expected.lines().collect::<Vec<_>>()
);
}
#[test]
fn test_testdata_examples() {
let exe_path = std::env::current_exe().unwrap();
let mut project_path = exe_path
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap();
let mut testdata_path = project_path.join("testdata");
if !testdata_path.is_dir() {
project_path = project_path.parent().unwrap();
testdata_path = project_path.join("testdata");
}
assert!(testdata_path.is_dir());
let mut failing_example: Option<String> = None;
let mut failing_example_expected = vec![];
let mut failing_example_actual = vec![];
for diff in fs::read_dir(&testdata_path).unwrap() {
let diff = diff.unwrap();
let diff = diff.path();
let diff = diff.as_path();
if !diff.is_file() {
continue;
}
if diff.extension().unwrap() != "diff" {
continue;
}
println!("Evaluating example file <{}>...", diff.to_str().unwrap());
let file = tempfile::NamedTempFile::new().unwrap();
highlight_diff(&mut fs::File::open(diff).unwrap(), file.reopen().unwrap());
let actual_result = fs::read_to_string(file.path()).unwrap();
let basename = diff.file_stem().unwrap().to_str().unwrap();
let expected_path = format!(
"{}/{}.riff-output",
testdata_path.to_str().unwrap(),
basename
);
let expected_result = fs::read_to_string(expected_path).unwrap();
let actual_lines: Vec<String> = actual_result.split('\n').map(str::to_string).collect();
let expected_lines: Vec<String> =
expected_result.split('\n').map(str::to_string).collect();
if actual_lines != expected_lines {
if failing_example.is_none() {
failing_example = Some(diff.to_str().unwrap().to_string());
failing_example_actual = actual_lines;
failing_example_expected = expected_lines;
}
println!(" FAILED: Output mismatches!");
}
}
if failing_example.is_some() {
println!();
println!("Example: {}", failing_example.unwrap());
assert_eq!(failing_example_actual, failing_example_expected);
}
}
}