#![cfg(unix)]
use std::os::unix::fs::symlink;
use std::process::Command;
use tempfile::TempDir;
fn run_show(repo: &std::path::Path, sha: &str) -> serde_json::Value {
let bin = env!("CARGO_BIN_EXE_git-prism");
let tmp = TempDir::new().unwrap();
let shim_dir = tmp.path().join("bin");
std::fs::create_dir_all(&shim_dir).unwrap();
let git_link = shim_dir.join("git");
symlink(bin, &git_link).unwrap();
let real_path = std::env::var("PATH").unwrap_or_default();
let path = format!("{}:{}", shim_dir.display(), real_path);
let out = Command::new(&git_link)
.env("AI_AGENT", "1")
.env("GIT_PRISM_REPO", repo)
.env_remove("GIT_PRISM_INSIDE_SHIM")
.env_remove("CI")
.env("PATH", &path)
.args(["show", sha])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
serde_json::from_str(&stdout).unwrap_or_else(|e| {
panic!(
"expected structured JSON from intercepted `git show`, parse error: {e}\nstdout: {stdout}\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
)
})
}
fn git(repo: &std::path::Path, args: &[&str]) {
Command::new("git")
.args(args)
.current_dir(repo)
.output()
.unwrap();
}
fn head_sha(repo: &std::path::Path) -> String {
let out = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(repo)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
fn init_repo() -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
git(&path, &["init", "-b", "main"]);
git(&path, &["config", "user.email", "t@t.com"]);
git(&path, &["config", "user.name", "T"]);
(dir, path)
}
#[test]
fn show_returns_nonempty_files_for_a_commit_that_changed_files() {
let (_dir, path) = init_repo();
std::fs::write(path.join("only.txt"), "alpha\nbeta\n").unwrap();
git(&path, &["add", "only.txt"]);
git(&path, &["commit", "-m", "first"]);
std::fs::write(path.join("only.txt"), "alpha\nbeta\ngamma\n").unwrap();
git(&path, &["add", "only.txt"]);
git(&path, &["commit", "-m", "append a line"]);
let sha = head_sha(&path);
let json = run_show(&path, &sha);
let files = json["files"].as_array().unwrap();
assert!(
!files.is_empty(),
"git show enrichment must return the changed file(s), got empty files array\n{json}"
);
let files_changed = json["diffstat"]["files_changed"].as_u64().unwrap();
assert_eq!(
files_changed, 1,
"diffstat.files_changed must be 1 (only.txt), got {files_changed}\n{json}"
);
}
#[test]
fn show_diffstat_matches_real_git_for_one_line_modification() {
let (_dir, path) = init_repo();
std::fs::write(path.join("f.txt"), "line1\nline2\nline3\n").unwrap();
git(&path, &["add", "f.txt"]);
git(&path, &["commit", "-m", "first"]);
std::fs::write(path.join("f.txt"), "line1\nCHANGED\nline3\n").unwrap();
git(&path, &["add", "f.txt"]);
git(&path, &["commit", "-m", "modify one line"]);
let sha = head_sha(&path);
let json = run_show(&path, &sha);
let insertions = json["diffstat"]["insertions"].as_u64().unwrap();
let deletions = json["diffstat"]["deletions"].as_u64().unwrap();
assert_eq!(
insertions, 1,
"diffstat.insertions must match real git `1 insertion(+)`, got {insertions}\n{json}"
);
assert_eq!(
deletions, 1,
"diffstat.deletions must match real git `1 deletion(-)`, got {deletions}\n{json}"
);
}
#[test]
fn show_returns_files_for_root_commit() {
let (_dir, path) = init_repo();
std::fs::write(path.join("first.txt"), "hello\nworld\n").unwrap();
git(&path, &["add", "first.txt"]);
git(&path, &["commit", "-m", "root commit"]);
let sha = head_sha(&path);
let json = run_show(&path, &sha);
let parents = json["commit"]["parents"].as_array().unwrap();
assert!(
parents.is_empty(),
"root commit must have 0 parents, got: {json}"
);
let files = json["files"].as_array().unwrap();
assert!(
!files.is_empty(),
"root commit must list added files, got: {json}"
);
assert_eq!(
json["diffstat"]["files_changed"].as_u64().unwrap(),
1,
"diffstat.files_changed must be 1 for root commit, got: {json}"
);
assert!(
json["diffstat"]["insertions"].as_u64().unwrap() > 0,
"root commit insertions must be > 0, got: {json}"
);
}
#[test]
fn show_files_carry_per_file_change_type_and_line_counts() {
let (_dir, path) = init_repo();
std::fs::write(path.join("keep.txt"), "a\nb\nc\n").unwrap();
std::fs::write(path.join("drop.txt"), "x\ny\n").unwrap();
git(&path, &["add", "keep.txt", "drop.txt"]);
git(&path, &["commit", "-m", "base"]);
std::fs::write(path.join("keep.txt"), "a\nCHANGED\nc\n").unwrap();
std::fs::write(path.join("new.txt"), "p\nq\n").unwrap();
std::fs::remove_file(path.join("drop.txt")).unwrap();
git(&path, &["add", "keep.txt", "new.txt"]);
git(&path, &["rm", "drop.txt"]);
git(
&path,
&["commit", "-m", "modify keep, add new, delete drop"],
);
let sha = head_sha(&path);
let json = run_show(&path, &sha);
let files = json["files"].as_array().unwrap();
assert_eq!(files.len(), 3, "expected 3 file entries, got: {json}");
let find = |name: &str| {
files
.iter()
.find(|f| f["path"].as_str() == Some(name))
.unwrap_or_else(|| panic!("missing file entry for {name} in: {json}"))
};
let keep = find("keep.txt");
assert_eq!(keep["change_type"].as_str().unwrap(), "modified");
assert_eq!(
keep["additions"].as_u64().unwrap(),
1,
"keep.txt: 1 line added"
);
assert_eq!(
keep["deletions"].as_u64().unwrap(),
1,
"keep.txt: 1 line removed"
);
assert!(
!keep["is_binary"].as_bool().unwrap(),
"keep.txt must not be binary"
);
let new_f = find("new.txt");
assert_eq!(new_f["change_type"].as_str().unwrap(), "added");
assert_eq!(
new_f["additions"].as_u64().unwrap(),
2,
"new.txt: 2 lines added"
);
assert_eq!(
new_f["deletions"].as_u64().unwrap(),
0,
"new.txt: 0 deletions"
);
let drop = find("drop.txt");
assert_eq!(drop["change_type"].as_str().unwrap(), "deleted");
assert_eq!(
drop["additions"].as_u64().unwrap(),
0,
"drop.txt: 0 additions"
);
assert_eq!(
drop["deletions"].as_u64().unwrap(),
2,
"drop.txt: 2 lines deleted"
);
}
#[test]
fn show_merge_commit_has_two_parents() {
let (_dir, path) = init_repo();
std::fs::write(path.join("base.txt"), "base\n").unwrap();
git(&path, &["add", "base.txt"]);
git(&path, &["commit", "-m", "base"]);
git(&path, &["checkout", "-b", "feature"]);
std::fs::write(path.join("feature.txt"), "feature\n").unwrap();
git(&path, &["add", "feature.txt"]);
git(&path, &["commit", "-m", "add feature"]);
git(&path, &["checkout", "main"]);
git(
&path,
&["merge", "--no-ff", "feature", "-m", "merge feature"],
);
let sha = head_sha(&path);
let json = run_show(&path, &sha);
let parents = json["commit"]["parents"].as_array().unwrap();
assert_eq!(
parents.len(),
2,
"merge commit must have 2 parents, got: {json}"
);
assert_eq!(
json["diffstat"]["files_changed"].as_u64().unwrap(),
1,
"merge first-parent diff must show 1 file changed (feature.txt)\n{json}"
);
assert_eq!(
json["diffstat"]["insertions"].as_u64().unwrap(),
1,
"merge first-parent diff must show 1 insertion (feature.txt: 1 line)\n{json}"
);
assert_eq!(
json["diffstat"]["deletions"].as_u64().unwrap(),
0,
"merge first-parent diff must show 0 deletions\n{json}"
);
let files = json["files"].as_array().unwrap();
assert!(
files.iter().any(|f| f["path"] == "feature.txt"),
"feature.txt must appear in merge first-parent diff\n{json}"
);
}
#[test]
fn show_diffstat_matches_real_git_for_mixed_multi_file_commit() {
let (_dir, path) = init_repo();
std::fs::write(path.join("a.txt"), "1\n2\n3\n4\n5\n").unwrap();
git(&path, &["add", "a.txt"]);
git(&path, &["commit", "-m", "base"]);
std::fs::write(path.join("a.txt"), "A\nB\nC\nD\nE\n").unwrap();
std::fs::write(path.join("b.txt"), "x\ny\nz\n").unwrap();
git(&path, &["add", "a.txt", "b.txt"]);
git(&path, &["commit", "-m", "modify a, add b"]);
let sha = head_sha(&path);
let json = run_show(&path, &sha);
let insertions = json["diffstat"]["insertions"].as_u64().unwrap();
let deletions = json["diffstat"]["deletions"].as_u64().unwrap();
assert_eq!(
insertions, 8,
"diffstat.insertions must be 8 (5 modified in a.txt + 3 added in b.txt), got {insertions}\n{json}"
);
assert_eq!(
deletions, 5,
"diffstat.deletions must be 5 (5 lines replaced in a.txt), got {deletions}\n{json}"
);
}