use gunnar_store::gix_hash::{Kind as HashKind, ObjectId};
use gunnar_store::object::{self, EntryKind, Kind};
const SHA1: HashKind = HashKind::Sha1;
fn oid(byte: u8) -> ObjectId {
ObjectId::from_bytes_or_panic(&[byte; 20])
}
fn hex(id: &ObjectId) -> String {
id.to_string()
}
fn commit_bytes(parents: &[ObjectId], extra_headers: &str, message: &str) -> Vec<u8> {
let mut s = format!("tree {}\n", hex(&oid(0x11)));
for p in parents {
s.push_str(&format!("parent {}\n", hex(p)));
}
s.push_str("author A U Thor <author@example.com> 1700000000 +0200\n");
s.push_str("committer C O Mitter <committer@example.com> 1723200000 -0730\n");
s.push_str(extra_headers);
s.push('\n');
s.push_str(message);
s.into_bytes()
}
fn tree_bytes(entries: &[(&str, &str, ObjectId)]) -> Vec<u8> {
let mut out = Vec::new();
for (mode, name, id) in entries {
out.extend_from_slice(mode.as_bytes());
out.push(b' ');
out.extend_from_slice(name.as_bytes());
out.push(0);
out.extend_from_slice(id.as_bytes());
}
out
}
fn tag_bytes(target: &ObjectId, ty: &str) -> Vec<u8> {
format!(
"object {}\ntype {ty}\ntag v1.0\ntagger T Agger <tag@example.com> 1700000000 +0000\n\nrelease\n",
hex(target)
)
.into_bytes()
}
#[test]
fn commit_links_agree_with_gix_for_every_parent_count() {
for n in [0usize, 1, 2, 7] {
let parents: Vec<ObjectId> = (0..n).map(|i| oid(0x20 + i as u8)).collect();
let bytes = commit_bytes(&parents, "", "a message\n");
let mut mine = Vec::new();
let tree = object::commit_links(&bytes, SHA1, &mut mine).expect("gunnar decodes");
let mut iter = gix_object::CommitRefIter::from_bytes(&bytes, SHA1);
let gix_tree = iter.tree_id().expect("gix decodes");
let gix_parents: Vec<ObjectId> = gix_object::CommitRefIter::from_bytes(&bytes, SHA1)
.parent_ids()
.collect();
assert_eq!(tree, gix_tree, "{n} parents: tree");
assert_eq!(mine, gix_parents, "{n} parents: parents");
assert_eq!(mine, parents, "{n} parents: and the fixture agrees too");
}
}
#[test]
fn commit_facts_agree_with_gix_on_the_committer_time() {
let bytes = commit_bytes(&[oid(0x33)], "", "msg\n");
let mut mine = Vec::new();
let (tree, when) = object::commit_facts(&bytes, SHA1, &mut mine).expect("gunnar decodes");
let gix = gix_object::CommitRef::from_bytes(&bytes, SHA1).expect("gix decodes");
let gix_committer = gix.committer().expect("gix decodes the committer");
assert_eq!(tree, gix.tree(), "tree");
assert_eq!(
when,
gix_committer.time().unwrap().seconds,
"committer seconds"
);
assert_eq!(when, 1723200000);
let full = object::commit(&bytes, SHA1).expect("gunnar decodes the whole commit");
assert_eq!(
full.committer.offset_seconds,
gix_committer.time().unwrap().offset,
"-0730 is minus seven and a half hours, not minus 730 minutes"
);
assert_eq!(full.committer.offset_seconds, -(7 * 3600 + 30 * 60));
}
#[test]
fn a_folded_gpgsig_header_does_not_derail_the_scan() {
let gpgsig = "gpgsig -----BEGIN PGP SIGNATURE-----\n \n iHUEABYKAB0WIQ\n parent deadbeef\n -----END PGP SIGNATURE-----\n";
let parents = [oid(0x41), oid(0x42)];
let bytes = commit_bytes(&parents, gpgsig, "signed\n");
let mut mine = Vec::new();
let tree = object::commit_links(&bytes, SHA1, &mut mine).unwrap();
let gix_parents: Vec<ObjectId> = gix_object::CommitRefIter::from_bytes(&bytes, SHA1)
.parent_ids()
.collect();
assert_eq!(mine, gix_parents);
assert_eq!(
mine, parents,
"the ` parent deadbeef` line inside the signature is not a parent"
);
let (facts_tree, when) = object::commit_facts(&bytes, SHA1, &mut mine).unwrap();
assert_eq!(facts_tree, tree);
assert_eq!(
when, 1723200000,
"the committer is still found past the fold"
);
let full = object::commit(&bytes, SHA1).unwrap();
let gix = gix_object::CommitRef::from_bytes(&bytes, SHA1).unwrap();
assert_eq!(full.parents, parents.to_vec());
assert_eq!(full.message, b"signed\n");
assert_eq!(full.message, gix.message.to_vec(), "message");
assert_eq!(full.tree, gix.tree(), "tree");
}
#[test]
fn a_name_with_angle_brackets_splits_where_gix_splits_it() {
let mut s = format!("tree {}\n", hex(&oid(0x11)));
s.push_str("author A <weird> Thor <author@example.com> 1700000000 +0200\n");
s.push_str("committer A <weird> Thor <author@example.com> 1700000000 +0200\n");
s.push_str("\nmsg\n");
let bytes = s.into_bytes();
let mine = object::commit(&bytes, SHA1).unwrap();
let gix = gix_object::CommitRef::from_bytes(&bytes, SHA1).unwrap();
let gix_author = gix.author().unwrap();
assert_eq!(mine.author.name, gix_author.name.to_vec(), "name");
assert_eq!(mine.author.email, gix_author.email.to_vec(), "email");
assert_eq!(mine.author.seconds, gix_author.time().unwrap().seconds);
}
#[test]
fn a_headers_only_commit_is_where_gunnar_is_deliberately_more_tolerant_than_gix() {
let mut s = format!("tree {}\n", hex(&oid(0x11)));
s.push_str("author A <a@e> 1 +0000\ncommitter C <c@e> 2 +0000\n");
let bytes = s.into_bytes();
let mine = object::commit(&bytes, SHA1).expect("gunnar reads it");
assert_eq!(mine.tree, oid(0x11));
assert!(mine.message.is_empty());
assert!(
gix_object::CommitRef::from_bytes(&bytes, SHA1).is_err(),
"gix refuses a commit with no blank line; if this ever starts passing, \
the divergence documented in src/object.rs is stale"
);
let mut parents = Vec::new();
assert_eq!(
object::commit_links(&bytes, SHA1, &mut parents).unwrap(),
oid(0x11)
);
assert_eq!(
object::commit_facts(&bytes, SHA1, &mut parents).unwrap(),
(oid(0x11), 2)
);
}
#[test]
fn an_unreadable_committer_time_is_zero_and_not_an_error() {
let mut s = format!("tree {}\n", hex(&oid(0x11)));
s.push_str("parent ");
s.push_str(&hex(&oid(0x55)));
s.push_str("\ncommitter C O Mitter <c@e>\n\nmsg\n");
let bytes = s.into_bytes();
let mut parents = Vec::new();
let (tree, when) = object::commit_facts(&bytes, SHA1, &mut parents).unwrap();
assert_eq!(tree, oid(0x11));
assert_eq!(parents, vec![oid(0x55)]);
assert_eq!(when, 0);
}
#[test]
fn a_commit_with_no_tree_is_an_error_in_all_three_readers() {
let bytes = b"parent 1111111111111111111111111111111111111111\n\nmsg\n";
let mut parents = Vec::new();
assert!(object::commit_links(bytes, SHA1, &mut parents).is_err());
assert!(object::commit_facts(bytes, SHA1, &mut parents).is_err());
assert!(object::commit(bytes, SHA1).is_err());
assert!(gix_object::CommitRef::from_bytes(bytes, SHA1).is_err());
}
#[test]
fn a_short_oid_in_a_header_is_an_error() {
let bytes = b"tree 1111\n\nmsg\n";
let mut parents = Vec::new();
assert!(object::commit_links(bytes, SHA1, &mut parents).is_err());
}
#[test]
fn the_parent_buffer_is_cleared_between_commits() {
let mut parents = vec![oid(0xEE); 4];
let bytes = commit_bytes(&[oid(0x60)], "", "m\n");
object::commit_links(&bytes, SHA1, &mut parents).unwrap();
assert_eq!(parents, vec![oid(0x60)]);
}
#[test]
fn tree_entries_agree_with_gix_on_oid_and_kind() {
let entries = [
("100644", "readme", oid(0x01)),
("100755", "run.sh", oid(0x02)),
("120000", "link", oid(0x03)),
("40000", "dir", oid(0x04)),
("040000", "dir2", oid(0x05)),
("160000", "submodule", oid(0x06)),
];
let bytes = tree_bytes(&entries);
let mine: Vec<_> = object::TreeIter::from_bytes(&bytes, SHA1)
.collect::<Result<Vec<_>, _>>()
.expect("gunnar decodes");
let gix: Vec<_> = gix_object::TreeRefIter::from_bytes(&bytes, SHA1)
.collect::<Result<Vec<_>, _>>()
.expect("gix decodes");
assert_eq!(mine.len(), gix.len());
assert_eq!(mine.len(), entries.len());
for (m, g) in mine.iter().zip(gix.iter()) {
assert_eq!(m.oid, g.oid.to_owned(), "oid");
let same_kind = match g.mode.kind() {
gix_object::tree::EntryKind::Tree => m.kind == EntryKind::Tree,
gix_object::tree::EntryKind::Blob => m.kind == EntryKind::Blob,
gix_object::tree::EntryKind::BlobExecutable => m.kind == EntryKind::BlobExecutable,
gix_object::tree::EntryKind::Link => m.kind == EntryKind::Link,
gix_object::tree::EntryKind::Commit => m.kind == EntryKind::Commit,
};
assert!(
same_kind,
"kind: gunnar {:?} vs gix {:?}",
m.kind,
g.mode.kind()
);
}
assert_eq!(mine[3].kind, EntryKind::Tree, "`40000` is a tree");
assert_eq!(mine[4].kind, EntryKind::Tree, "`040000` is the same tree");
assert_eq!(
mine[5].kind,
EntryKind::Commit,
"a gitlink is not ours to send"
);
}
#[test]
fn an_empty_tree_yields_no_entries() {
assert_eq!(object::TreeIter::from_bytes(&[], SHA1).count(), 0);
}
#[test]
fn a_tree_whose_last_oid_is_truncated_is_an_error() {
let mut bytes = tree_bytes(&[("100644", "a", oid(0x01))]);
bytes.truncate(bytes.len() - 3);
let got: Result<Vec<_>, _> = object::TreeIter::from_bytes(&bytes, SHA1).collect();
assert!(got.is_err());
assert!(gix_object::TreeRefIter::from_bytes(&bytes, SHA1)
.collect::<Result<Vec<_>, _>>()
.is_err());
}
#[test]
fn a_failed_tree_iterator_does_not_keep_yielding() {
let mut bytes = tree_bytes(&[("100644", "a", oid(0x01))]);
bytes.extend_from_slice(b"99xx bad\0short");
let mut it = object::TreeIter::from_bytes(&bytes, SHA1);
assert!(it.next().unwrap().is_ok());
assert!(it.next().unwrap().is_err());
assert!(it.next().is_none());
}
#[test]
fn tag_targets_agree_with_gix() {
for ty in ["commit", "tree", "blob", "tag"] {
let target = oid(0x77);
let bytes = tag_bytes(&target, ty);
let mine = object::tag(&bytes, SHA1).expect("gunnar decodes");
let gix = gix_object::TagRef::from_bytes(&bytes, SHA1).expect("gix decodes");
assert_eq!(mine.target, gix.target(), "{ty}: target");
assert_eq!(mine.target, target);
assert_eq!(
mine.target_kind,
Kind::from_bytes(ty.as_bytes()),
"{ty}: type"
);
assert_eq!(object::tag_target(&bytes, SHA1).unwrap(), gix.target());
}
}
#[test]
fn a_tag_with_no_object_header_is_an_error() {
let bytes = b"type commit\ntag v1\n\nmsg\n";
assert!(object::tag(bytes, SHA1).is_err());
}
#[test]
fn the_delta_codes_do_not_resolve_to_a_kind() {
use gunnar_store::git_storage_trait::ObjType;
assert_eq!(Kind::resolved(ObjType::Commit), Some(Kind::Commit));
assert_eq!(Kind::resolved(ObjType::Tree), Some(Kind::Tree));
assert_eq!(Kind::resolved(ObjType::Blob), Some(Kind::Blob));
assert_eq!(Kind::resolved(ObjType::Tag), Some(Kind::Tag));
assert_eq!(Kind::resolved(ObjType::OfsDelta), None);
assert_eq!(Kind::resolved(ObjType::RefDelta), None);
}
#[test]
fn the_kind_names_are_gits_own() {
for (mine, gix) in [
(Kind::Commit, gix_object::Kind::Commit),
(Kind::Tree, gix_object::Kind::Tree),
(Kind::Blob, gix_object::Kind::Blob),
(Kind::Tag, gix_object::Kind::Tag),
] {
assert_eq!(mine.to_string(), gix.to_string());
assert_eq!(Kind::from_bytes(mine.as_str().as_bytes()), Some(mine));
}
}
#[test]
fn a_named_lookup_finds_its_entry_and_only_its_entry() {
let entries = [
("100644", "readme", oid(0x01)),
("40000", "src", oid(0x02)),
("100644", "readme2", oid(0x03)),
];
let bytes = tree_bytes(&entries);
let readme = object::tree_entry_named(&bytes, SHA1, b"readme")
.expect("decodes")
.expect("present");
assert_eq!(readme.oid, oid(0x01));
assert_eq!(readme.kind, EntryKind::Blob);
let src = object::tree_entry_named(&bytes, SHA1, b"src")
.expect("decodes")
.expect("present");
assert_eq!(src.oid, oid(0x02));
assert_eq!(src.kind, EntryKind::Tree);
assert!(object::tree_entry_named(&bytes, SHA1, b"read")
.expect("decodes")
.is_none());
assert!(object::tree_entry_named(&bytes, SHA1, b"absent")
.expect("decodes")
.is_none());
let mut torn = tree_bytes(&entries);
torn.truncate(torn.len() - 3);
assert!(object::tree_entry_named(&torn, SHA1, b"absent").is_err());
}