#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
use git_core::{GitHash, GitRepo, ObjectKind};
struct TestRepo {
dir: TempDir,
}
impl TestRepo {
fn new_two_commit() -> Self {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
fn git(root: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(root)
.env("GIT_AUTHOR_NAME", "Forensic Tester")
.env("GIT_AUTHOR_EMAIL", "tester@forensic.example")
.env("GIT_COMMITTER_NAME", "Forensic Tester")
.env("GIT_COMMITTER_EMAIL", "tester@forensic.example")
.env("GIT_AUTHOR_DATE", "2024-01-15T08:00:00+0800")
.env("GIT_COMMITTER_DATE", "2024-01-15T08:00:00+0800")
.env("GIT_CONFIG_COUNT", "2")
.env("GIT_CONFIG_KEY_0", "commit.gpgsign")
.env("GIT_CONFIG_VALUE_0", "false")
.env("GIT_CONFIG_KEY_1", "tag.gpgsign")
.env("GIT_CONFIG_VALUE_1", "false")
.status()
.expect("git command failed");
assert!(status.success(), "git {args:?} failed");
}
git(root, &["init", "-b", "main"]);
git(root, &["config", "user.email", "tester@forensic.example"]);
git(root, &["config", "user.name", "Forensic Tester"]);
std::fs::write(root.join("hello.txt"), b"hello forensics\n").unwrap();
git(root, &["add", "hello.txt"]);
git(root, &["commit", "-m", "initial commit"]);
std::fs::write(root.join("hello.txt"), b"hello updated\n").unwrap();
std::fs::write(root.join("world.txt"), b"world\n").unwrap();
git(root, &["add", "hello.txt", "world.txt"]);
git(root, &["commit", "-m", "second commit"]);
Self { dir }
}
fn repo(&self) -> GitRepo {
GitRepo::open(self.dir.path()).expect("GitRepo::open")
}
}
fn git_out(root: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.args(args)
.current_dir(root)
.output()
.expect("git command failed");
assert!(out.status.success(), "git {args:?} failed");
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
#[test]
fn packed_object_is_read_from_the_packfile() {
let tr = TestRepo::new_two_commit();
let root = tr.dir.path();
let blob_hex = git_out(root, &["rev-parse", "HEAD:world.txt"]);
git_out(root, &["repack", "-a", "-d", "-q"]);
let repo = tr.repo();
let hash = GitHash::from_hex(&blob_hex).expect("valid hash");
let obj = repo.read_object(&hash).expect("packed object must be read");
assert_eq!(obj.data, b"world\n");
assert!(obj.verified, "packed object SHA1 must verify");
}
#[test]
fn open_worktree_root() {
let fix = TestRepo::new_two_commit();
let _ = fix.repo(); }
#[test]
fn open_bare_git_dir() {
let fix = TestRepo::new_two_commit();
let git_dir = fix.dir.path().join(".git");
let _ = GitRepo::open(&git_dir).expect("open bare .git dir");
}
#[test]
fn open_non_repo_returns_err() {
let dir = tempfile::tempdir().unwrap();
let result = GitRepo::open(dir.path());
assert!(result.is_err(), "opening non-repo must return Err");
}
#[test]
fn head_returns_a_hash() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD must resolve");
assert_eq!(head.to_hex().len(), 40, "HEAD hash must be 40 hex chars");
}
#[test]
fn resolve_ref_main_equals_head() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let main = repo
.resolve_ref("refs/heads/main")
.expect("refs/heads/main");
assert_eq!(
head, main,
"HEAD and refs/heads/main must point to same commit"
);
}
#[test]
fn read_head_commit() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let commit = repo.read_commit(&head).expect("read HEAD commit");
assert_eq!(commit.hash, head);
assert_eq!(commit.message.trim(), "second commit");
}
#[test]
fn commit_has_author_and_committer() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let commit = repo.read_commit(&head).expect("read commit");
assert_eq!(commit.author.name, "Forensic Tester");
assert_eq!(commit.author.email, "tester@forensic.example");
assert!(
commit.author.timestamp > 0,
"author timestamp must be positive"
);
assert_eq!(commit.committer.name, "Forensic Tester");
}
#[test]
fn commit_has_one_parent() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let commit = repo.read_commit(&head).expect("read commit");
assert_eq!(
commit.parents.len(),
1,
"second commit must have one parent"
);
}
#[test]
fn root_commit_has_no_parents() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let tip = repo.read_commit(&head).expect("tip");
let root = repo.read_commit(&tip.parents[0]).expect("root");
assert_eq!(root.parents.len(), 0, "initial commit must have no parents");
assert_eq!(root.message.trim(), "initial commit");
}
#[test]
fn read_tree_from_commit() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let commit = repo.read_commit(&head).expect("commit");
let tree = repo.read_tree(&commit.tree).expect("read tree");
assert!(!tree.entries.is_empty(), "tree must have entries");
let names: Vec<&str> = tree.entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"hello.txt"), "tree must contain hello.txt");
assert!(names.contains(&"world.txt"), "tree must contain world.txt");
}
#[test]
fn read_blob_content() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let commit = repo.read_commit(&head).expect("commit");
let tree = repo.read_tree(&commit.tree).expect("tree");
let hello_entry = tree
.entries
.iter()
.find(|e| e.name == "hello.txt")
.expect("hello.txt must be in tree");
let blob = repo.read_blob(&hello_entry.hash).expect("read blob");
assert_eq!(blob, b"hello updated\n");
}
#[test]
fn blob_at_root_commit_has_original_content() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let tip = repo.read_commit(&head).expect("tip");
let root = repo.read_commit(&tip.parents[0]).expect("root");
let root_tree = repo.read_tree(&root.tree).expect("root tree");
let hello_entry = root_tree
.entries
.iter()
.find(|e| e.name == "hello.txt")
.expect("hello.txt in root tree");
let blob = repo.read_blob(&hello_entry.hash).expect("root blob");
assert_eq!(blob, b"hello forensics\n");
}
#[test]
fn raw_object_is_verified() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let obj = repo.read_object(&head).expect("read object");
assert!(obj.verified, "SHA1 of object must verify against its hash");
assert_eq!(obj.kind, ObjectKind::Commit);
}
#[test]
fn read_object_not_found_returns_err() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let fake = GitHash::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").expect("valid hex");
let result = repo.read_object(&fake);
assert!(result.is_err(), "non-existent object must return Err");
}
#[test]
fn walk_commits_yields_newest_first() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let commits: Vec<_> = repo
.walk_commits(head)
.map(|r| r.expect("commit"))
.collect();
assert_eq!(commits.len(), 2, "must yield exactly 2 commits");
assert_eq!(commits[0].message.trim(), "second commit");
assert_eq!(commits[1].message.trim(), "initial commit");
}
#[test]
fn walk_commits_all_verified() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
for commit in repo.walk_commits(head) {
let c = commit.expect("commit");
let obj = repo.read_object(&c.hash).expect("read object");
assert!(obj.verified, "commit {} must verify", c.hash);
}
}
#[test]
fn hash_from_hex_roundtrip() {
let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
let h = GitHash::from_hex(hex).expect("valid hex");
assert_eq!(h.to_hex(), hex);
}
#[test]
fn hash_from_hex_bad_length_returns_err() {
assert!(GitHash::from_hex("deadbeef").is_err());
}
#[test]
fn hash_from_hex_bad_chars_returns_err() {
assert!(GitHash::from_hex("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz").is_err());
}
#[test]
fn read_commit_on_blob_hash_returns_err() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
let head = repo.head().expect("HEAD");
let commit = repo.read_commit(&head).expect("commit");
let tree = repo.read_tree(&commit.tree).expect("tree");
let blob_hash = tree.entries[0].hash;
assert!(
repo.read_commit(&blob_hash).is_err(),
"read_commit on a blob hash must return Err"
);
}
#[test]
fn resolve_nonexistent_ref_returns_err() {
let fix = TestRepo::new_two_commit();
let repo = fix.repo();
assert!(
repo.resolve_ref("refs/heads/no-such-branch").is_err(),
"non-existent ref must return Err"
);
}