#![allow(clippy::unwrap_used)]
mod common;
use std::process::Output;
use common::Repo;
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn has_hunk_for(diff_out: &str, path: &str) -> bool {
let marker = format!("diff --git a/{path} b/{path}");
let Some(start) = diff_out.find(&marker) else {
panic!("no `diff --git` header for {path} in:\n{diff_out}");
};
let rest = &diff_out[start + marker.len()..];
let end = rest.find("diff --git").unwrap_or(rest.len());
rest[..end].contains("@@")
}
fn setup_whitespace_repo() -> Repo {
let repo = Repo::new();
repo.commit_file("nospace.txt", b"head\nfoo(a, b)\ntail\n", "base");
repo.write("nospace.txt", b"head\nfoo(a,b)\ntail\n");
repo.commit_file("amount.txt", b"head\nfoo(a, b)\ntail\n", "base2");
repo.write("amount.txt", b"head\nfoo(a, b)\ntail\n");
repo.ok(&["add", "nospace.txt", "amount.txt"]);
repo
}
#[test]
fn default_shows_hunks_for_any_whitespace_change() {
let repo = setup_whitespace_repo();
let out = stdout(&repo.ok(&["diff"]));
assert!(has_hunk_for(&out, "nospace.txt"));
assert!(has_hunk_for(&out, "amount.txt"));
}
#[test]
fn dash_w_ignores_all_whitespace() {
let repo = setup_whitespace_repo();
let out = stdout(&repo.ok(&["diff", "-w"]));
assert!(!has_hunk_for(&out, "nospace.txt"));
assert!(!has_hunk_for(&out, "amount.txt"));
let out2 = stdout(&repo.ok(&["diff", "--ignore-all-space"]));
assert!(!has_hunk_for(&out2, "nospace.txt"));
}
#[test]
fn dash_b_ignores_only_amount_changes() {
let repo = setup_whitespace_repo();
let out = stdout(&repo.ok(&["diff", "-b"]));
assert!(has_hunk_for(&out, "nospace.txt"));
assert!(!has_hunk_for(&out, "amount.txt"));
let out2 = stdout(&repo.ok(&["diff", "--ignore-space-change"]));
assert!(has_hunk_for(&out2, "nospace.txt"));
assert!(!has_hunk_for(&out2, "amount.txt"));
}
#[test]
fn dash_w_wins_when_both_given() {
let repo = setup_whitespace_repo();
let out = stdout(&repo.ok(&["diff", "-w", "-b"]));
assert!(!has_hunk_for(&out, "nospace.txt"));
assert!(!has_hunk_for(&out, "amount.txt"));
}
fn context_line_count(diff_out: &str, path: &str) -> usize {
let marker = format!("diff --git a/{path} b/{path}");
let start = diff_out
.find(&marker)
.unwrap_or_else(|| panic!("no header for {path}"));
let rest = &diff_out[start + marker.len()..];
let end = rest.find("diff --git").unwrap_or(rest.len());
rest[..end]
.lines()
.skip_while(|l| !l.starts_with("@@"))
.skip(1) .filter(|l| l.starts_with(' '))
.count()
}
fn setup_context_repo() -> Repo {
let repo = Repo::new();
let lines: Vec<String> = (1..=10).map(|n| format!("l{n}")).collect();
let mut before = lines.join("\n");
before.push('\n');
repo.commit_file("ctx.txt", before.as_bytes(), "base");
let mut after_lines = lines;
after_lines[4] = "l5-changed".to_string(); let mut after = after_lines.join("\n");
after.push('\n');
repo.write("ctx.txt", after.as_bytes());
repo.ok(&["add", "ctx.txt"]);
repo
}
#[test]
fn default_context_is_three_lines_each_side() {
let repo = setup_context_repo();
let out = stdout(&repo.ok(&["diff"]));
assert_eq!(context_line_count(&out, "ctx.txt"), 6);
}
#[test]
fn dash_u_one_shows_one_line_of_context_each_side() {
let repo = setup_context_repo();
let out = stdout(&repo.ok(&["diff", "-U1"]));
assert_eq!(context_line_count(&out, "ctx.txt"), 2);
let out2 = stdout(&repo.ok(&["diff", "--unified=1"]));
assert_eq!(context_line_count(&out2, "ctx.txt"), 2);
}
#[test]
fn dash_u_zero_shows_no_context() {
let repo = setup_context_repo();
let out = stdout(&repo.ok(&["diff", "-U0"]));
assert_eq!(context_line_count(&out, "ctx.txt"), 0);
assert!(out.contains("-l5\n"));
assert!(out.contains("+l5-changed\n"));
}