mod common;
use assert_cmd::Command;
use predicates::prelude::*;
use std::process::Command as Sys;
#[test]
fn rename_is_preserved() {
let dir = common::repo_with(&[("old.txt", "line1\nline2\nline3\nline4\nline5\n")]);
let new_path = dir.path().join("new.txt");
std::fs::rename(dir.path().join("old.txt"), &new_path).unwrap();
std::fs::write(&new_path, "line1\nline2\nline3\nline4\nline5_changed\n").unwrap();
common::sys(&dir, &["add", "-A"]);
let out = Sys::new("git")
.args(["diff", "--staged", "-M"])
.current_dir(dir.path())
.output()
.unwrap();
let diff = String::from_utf8(out.stdout).unwrap();
assert!(!diff.is_empty(), "staged diff must be non-empty");
if diff.contains("rename from") {
let stdout = Command::cargo_bin("hunkpick")
.unwrap()
.args(["select", "1"])
.write_stdin(diff.clone())
.assert()
.success()
.get_output()
.stdout
.clone();
let text = std::str::from_utf8(&stdout).unwrap();
assert!(
text.contains("rename from") || text.contains("diff --git"),
"rename headers must appear in output: {text}"
);
assert!(!text.is_empty());
Command::cargo_bin("hunkpick")
.unwrap()
.args(["list", "--json"])
.write_stdin(diff)
.assert()
.success();
} else {
Command::cargo_bin("hunkpick")
.unwrap()
.args(["list", "--json"])
.write_stdin(diff.clone())
.assert()
.success();
Command::cargo_bin("hunkpick")
.unwrap()
.args(["select", "1"])
.write_stdin(diff)
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
}
#[cfg(unix)]
#[test]
fn mode_change_passthrough() {
let dir = common::repo_with(&[("f.sh", "line1\nline2\n")]);
let path = dir.path().join("f.sh");
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
std::fs::write(&path, "line1\nline2_changed\n").unwrap();
let diff = common::diff_after(&dir, &[]);
assert!(
diff.contains("old mode") && diff.contains("new mode"),
"mode-change diff must contain old/new mode headers: {diff}"
);
let stdout = Command::cargo_bin("hunkpick")
.unwrap()
.args(["select", "1"])
.write_stdin(diff)
.assert()
.success()
.get_output()
.stdout
.clone();
let text = std::str::from_utf8(&stdout).unwrap();
assert!(text.contains("old mode"), "output must contain 'old mode'");
assert!(text.contains("new mode"), "output must contain 'new mode'");
}
#[test]
fn new_file() {
let dir = common::repo_with(&[]);
std::fs::write(dir.path().join("newf.txt"), "line1\nline2\n").unwrap();
common::sys(&dir, &["add", "newf.txt"]);
let diff = common::diff_staged(&dir);
assert!(!diff.is_empty(), "staged diff must be non-empty");
assert!(
diff.contains("new file mode"),
"diff must contain 'new file mode': {diff}"
);
Command::cargo_bin("hunkpick")
.unwrap()
.arg("list")
.write_stdin(diff.clone())
.assert()
.success();
let stdout = Command::cargo_bin("hunkpick")
.unwrap()
.args(["select", "1"])
.write_stdin(diff)
.assert()
.success()
.get_output()
.stdout
.clone();
let text = std::str::from_utf8(&stdout).unwrap();
assert!(
text.contains("new file mode"),
"output must contain 'new file mode'"
);
assert!(text.contains("+line1"), "output must contain added lines");
}
#[test]
fn deleted_file() {
let dir = common::repo_with(&[("f.txt", "line1\nline2\n")]);
std::fs::remove_file(dir.path().join("f.txt")).unwrap();
let diff = common::diff_after(&dir, &[]);
assert!(!diff.is_empty(), "diff must be non-empty after deletion");
assert!(
diff.contains("deleted file mode"),
"diff must contain 'deleted file mode': {diff}"
);
Command::cargo_bin("hunkpick")
.unwrap()
.arg("list")
.write_stdin(diff.clone())
.assert()
.success();
let stdout = Command::cargo_bin("hunkpick")
.unwrap()
.args(["select", "1"])
.write_stdin(diff)
.assert()
.success()
.get_output()
.stdout
.clone();
let text = std::str::from_utf8(&stdout).unwrap();
assert!(
text.contains("deleted file mode"),
"output must contain 'deleted file mode'"
);
}
#[test]
fn binary_file() {
let dir = common::repo_with(&[]);
std::fs::write(dir.path().join("f.bin"), b"hello\x00world").unwrap();
common::sys(&dir, &["add", "f.bin"]);
common::sys(&dir, &["commit", "-q", "-m", "add binary"]);
std::fs::write(dir.path().join("f.bin"), b"bye\x00world").unwrap();
let diff = common::diff_after(&dir, &[]);
assert!(!diff.is_empty(), "binary diff must be non-empty");
assert!(
diff.contains("Binary files"),
"diff must contain 'Binary files': {diff}"
);
let json_output = Command::cargo_bin("hunkpick")
.unwrap()
.args(["list", "--json"])
.write_stdin(diff.clone())
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value =
serde_json::from_slice(&json_output).expect("list --json must produce valid JSON");
let files = json.as_array().expect("top-level must be array");
assert_eq!(files.len(), 1);
assert_eq!(
files[0]["binary"], true,
"binary file must be marked binary: true"
);
let hunks = files[0]["hunks"].as_array().expect("hunks must be array");
assert_eq!(hunks.len(), 0, "binary file must have zero sub-hunks");
Command::cargo_bin("hunkpick")
.unwrap()
.args(["select", "1"])
.write_stdin(diff)
.assert()
.success()
.stdout(predicate::str::contains("Binary files"));
}
#[test]
fn crlf_preserved() {
let diff: Vec<u8> = concat!(
"diff --git a/f b/f\r\n",
"--- a/f\r\n",
"+++ b/f\r\n",
"@@ -1,3 +1,3 @@\r\n",
" a\r\n",
"-b\r\n",
"+B\r\n",
" c\r\n",
)
.bytes()
.collect();
let stdout = Command::cargo_bin("hunkpick")
.unwrap()
.arg("select")
.arg("1")
.write_stdin(diff)
.assert()
.success()
.get_output()
.stdout
.clone();
assert!(
stdout.contains(&b'\r'),
"output must preserve \\r bytes from CRLF input"
);
}
#[test]
fn plain_non_git_diff() {
let diff = "\
--- old/f
+++ new/f
@@ -1,3 +1,3 @@
a
-b
+B
c
";
Command::cargo_bin("hunkpick")
.unwrap()
.args(["select", "1"])
.write_stdin(diff)
.assert()
.success()
.stdout(predicate::str::starts_with("--- "));
}