use std::{ cell::Cell, collections::BTreeSet, io::{self, Read},}; use gix_hash::{Kind, ObjectId, oid};use gix_object::{ FindExt, Tree, Write, bstr::{BString, ByteSlice}, tree::{Entry, EntryKind},}; type ObjectDb = gix_odb::memory::Proxy<gix_object::find::Never>; mod one_shot { use super::*; pub fn get( root_tree_id: ObjectId, annotated_object_id: &oid, objects: &impl gix_object::Find, ) -> Result<Option<ObjectId>, gix_note::Error> { let mut state = gix_note::State::new(root_tree_id, objects)?; state.get(annotated_object_id, objects) } pub fn replace( root_tree_id: ObjectId, annotated_object_id: ObjectId, note_blob_id: ObjectId, objects: &(impl gix_object::Find + Write), ) -> Result<gix_note::Edit, gix_note::Error> { let mut state = gix_note::State::new(root_tree_id, objects)?; state.replace(annotated_object_id, note_blob_id, objects) } pub fn remove( root_tree_id: ObjectId, annotated_object_id: ObjectId, objects: &(impl gix_object::Find + Write), ) -> Result<gix_note::Edit, gix_note::Error> { let mut state = gix_note::State::new(root_tree_id, objects)?; state.remove(annotated_object_id, objects) }} struct CountingObjectDb { inner: ObjectDb, reads: Cell<usize>, writes: Cell<usize>, fail_next_read: Cell<bool>, fail_next_write: Cell<bool>,} impl CountingObjectDb { fn new(kind: Kind) -> Self { Self { inner: ObjectDb::new(gix_object::find::Never, kind), reads: Cell::new(0), writes: Cell::new(0), fail_next_read: Cell::new(false), fail_next_write: Cell::new(false), } } fn maybe_fail_write(&self) -> Result<(), gix_object::write::Error> { if self.fail_next_write.replace(false) { return Err(io::Error::other("injected write failure").into()); } Ok(()) }} impl gix_object::Find for CountingObjectDb { fn try_find<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec<u8>, ) -> Result<Option<gix_object::Data<'a>>, gix_object::find::Error> { self.reads.set(self.reads.get() + 1); if self.fail_next_read.replace(false) { return Err(io::Error::other("injected read failure").into()); } self.inner.try_find(id, buffer) }} impl gix_object::Write for CountingObjectDb { fn write_buf_with_known_id( &self, kind: gix_object::Kind, from: &[u8], id: ObjectId, ) -> Result<ObjectId, gix_object::write::Error> { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_buf_with_known_id(kind, from, id) } fn write_stream( &self, kind: gix_object::Kind, size: u64, from: &mut dyn Read, ) -> Result<ObjectId, gix_object::write::Error> { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_stream(kind, size, from) } fn write_stream_with_known_id( &self, kind: gix_object::Kind, size: u64, from: &mut dyn Read, id: ObjectId, ) -> Result<ObjectId, gix_object::write::Error> { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_stream_with_known_id(kind, size, from, id) }} #[test]fn reads_notes_without_fanout() -> gix_testtools::Result { assert_note_at_fanout(0)} #[test]fn reads_notes_with_one_fanout_level() -> gix_testtools::Result { assert_note_at_fanout(1)} #[test]fn reads_notes_with_two_fanout_levels() -> gix_testtools::Result { assert_note_at_fanout(2)} #[test]fn reads_notes_with_three_fanout_levels() -> gix_testtools::Result { assert_note_at_fanout(3)} #[test]fn replacing_a_note_does_not_read_untouched_fanout_subtrees() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let replacement = objects.write_buf(gix_object::Kind::Blob, b"replacement")?; let mut annotated = Vec::new(); let mut root_entries = Vec::new(); for high_nibble in b"0123456789abcdef" { for low_nibble in b"01" { let object = object_id_with_nibbles(kind, &[(0, *high_nibble), (1, *low_nibble)])?; let hex = object.to_hex().to_string(); let subtree = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Blob.into(), filename: hex[2..].into(), oid: note, }], })?; annotated.push(object); root_entries.push(Entry { mode: EntryKind::Tree.into(), filename: hex[..2].into(), oid: subtree, }); } } let root = objects.write(&Tree { entries: root_entries.clone(), })?; let target_hex = annotated[0].to_hex().to_string(); let replacement_subtree = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Blob.into(), filename: target_hex[2..].into(), oid: replacement, }], })?; root_entries[0].oid = replacement_subtree; let expected_root = objects.write(&Tree { entries: root_entries })?; objects.writes.set(0); let outcome = one_shot::replace(root, annotated[0], replacement, &objects)?; assert_eq!(outcome.previous, Some(note), "replacement returns the previous note"); assert_eq!( outcome.tree, expected_root, "the rewritten tree matches a path-local edit" ); assert_eq!( objects.reads.get(), 2, "only the root and the changed fanout subtree are read" ); assert_eq!( objects.writes.get(), 2, "only the changed fanout subtree and root are written" ); assert_eq!( one_shot::get(outcome.tree, &annotated[0], &objects)?, Some(replacement), "the replacement is visible" ); Ok(())} #[test]fn state_reuses_materialized_trees_across_operations() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let replacement = objects.write_buf(gix_object::Kind::Blob, b"replacement")?; let root = notes_tree(&objects, &annotated, note, 2)?; objects.reads.set(0); let mut state = gix_note::State::new(root, &objects)?; assert_eq!(objects.reads.get(), 1, "initialization reads only the root tree"); assert_eq!(state.get(&annotated, &objects)?, Some(note), "the note is found"); let reads_after_first_lookup = objects.reads.get(); assert_eq!(reads_after_first_lookup, 3, "the first lookup reads both fanout trees"); assert_eq!(state.get(&annotated, &objects)?, Some(note), "the cached note is found"); assert_eq!( objects.reads.get(), reads_after_first_lookup, "a repeated lookup reuses the materialized trees" ); let edit = state.replace(annotated, replacement, &objects)?; assert_eq!(edit.previous, Some(note), "replacement returns the cached note"); assert_eq!( objects.reads.get(), reads_after_first_lookup, "replacement reuses the materialized trees" ); assert_eq!( state.get(&annotated, &objects)?, Some(replacement), "the state contains the replacement" ); assert_eq!( objects.reads.get(), reads_after_first_lookup, "looking up the replacement needs no tree reads" ); let edit = state.replace(annotated, note, &objects)?; assert_eq!( edit.previous, Some(replacement), "a second edit sees the cached replacement" ); assert_eq!( objects.reads.get(), reads_after_first_lookup, "writing the state retains its materialized trees" ); let edit = state.remove(annotated, &objects)?; assert_eq!(edit.previous, Some(note), "removal sees the cached note"); assert_eq!(state.get(&annotated, &objects)?, None, "the cached note is removed"); assert_eq!( objects.reads.get(), reads_after_first_lookup, "removal and its subsequent lookup need no tree reads" ); Ok(())} #[test]fn state_recovers_after_failed_operations() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let replacement = objects.write_buf(gix_object::Kind::Blob, b"replacement")?; let root = notes_tree(&objects, &annotated, note, 2)?; let mut state = gix_note::State::new(root, &objects)?; objects.fail_next_read.set(true); state .get(&annotated, &objects) .expect_err("the injected subtree read fails"); assert_eq!(state.root_tree_id(), root, "a failed lookup retains the current root"); assert_eq!( state.get(&annotated, &objects)?, Some(note), "the state reloads from its root after a failed lookup" ); objects.fail_next_write.set(true); state .replace(annotated, replacement, &objects) .expect_err("the injected tree write fails"); assert_eq!(state.root_tree_id(), root, "a failed edit retains the current root"); assert_eq!( state.get(&annotated, &objects)?, Some(note), "the state reloads the pre-edit note after a failed edit" ); let edit = state.replace(annotated, replacement, &objects)?; assert_eq!(edit.previous, Some(note), "the recovered state can be edited"); assert_eq!( state.root_tree_id(), edit.tree, "a successful edit advances the recovered state" ); Ok(())} fn assert_note_at_fanout(fanout: usize) -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let root = notes_tree(&objects, &annotated, note, fanout)?; assert_eq!( one_shot::get(root, &annotated, &objects)?, Some(note), "the note is found with {fanout} fanout levels" ); Ok(())} fn notes_tree(objects: &impl Write, annotated: &oid, note: ObjectId, fanout: usize) -> gix_testtools::Result<ObjectId> { let hex = annotated.to_hex().to_string(); let mut tree = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Blob.into(), filename: BString::from(&hex[fanout * 2..]), oid: note, }], })?; for level in (0..fanout).rev() { tree = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Tree.into(), filename: BString::from(&hex[level * 2..][..2]), oid: tree, }], })?; } Ok(tree)} #[test]fn ignores_entries_that_are_not_notes() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = gix_odb::memory::Proxy::new(gix_object::find::Never, kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; let hex = annotated.to_hex().to_string(); let root = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Tree.into(), filename: BString::from(hex), oid: kind.empty_tree(), }], })?; assert_eq!( one_shot::get(root, &annotated, &objects)?, None, "the canonical empty-tree ID in a tree-mode entry at the full object-ID path is not a blob note" ); Ok(())} #[test]fn mutations_rebalance_when_each_nibble_bucket_has_multiple_notes_and_preserve_non_notes() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let unrelated = objects.write_buf(gix_object::Kind::Blob, b"keep")?; let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let mut root = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Blob.into(), filename: "README".into(), oid: unrelated, }], })?; let mut annotated = Vec::new(); for nibble in b"0123456789abcdef" { let object = object_id_with_nibble(kind, 0, *nibble)?; annotated.push(object); let outcome = one_shot::replace(root, object, note, &objects)?; assert_eq!(outcome.previous, None, "each distinct object receives a new note"); root = outcome.tree; } let mut buf = Vec::new(); let tree = objects.find_tree(&root, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_tree()).count(), 0, "one note in every leading-nibble bucket remains flat, like Git" ); assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_blob()).count(), 17, "the flat tree contains sixteen notes and README" ); for nibble in b"0123456789abcde" { let object = object_id_with_nibbles(kind, &[(0, *nibble), (2, b'1')])?; annotated.push(object); let outcome = one_shot::replace(root, object, note, &objects)?; assert_eq!(outcome.previous, None, "each bucket receives a second note"); root = outcome.tree; } let mut buf = Vec::new(); let tree = objects.find_tree(&root, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_tree()).count(), 0, "a bucket with only one note keeps all thirty-one notes flat" ); let last = object_id_with_nibbles(kind, &[(0, b'f'), (2, b'1')])?; annotated.push(last); let outcome = one_shot::replace(root, last, note, &objects)?; assert_eq!(outcome.previous, None, "the final bucket receives its second note"); root = outcome.tree; let mut buf = Vec::new(); let tree = objects.find_tree(&root, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_tree()).count(), 16, "two notes in every first-nibble bucket cause Git's first fanout level" ); assert!( tree.entries .iter() .any(|entry| entry.filename == "README" && entry.oid == unrelated), "non-note entries survive rebalancing" ); for object in &annotated { assert_eq!( one_shot::get(root, object, &objects)?, Some(note), "every note remains readable after fanout" ); } let replacement = objects.write_buf(gix_object::Kind::Blob, b"replacement")?; let outcome = one_shot::replace(root, annotated[0], replacement, &objects)?; assert_eq!(outcome.previous, Some(note), "replacement returns the previous note"); assert_eq!( one_shot::get(outcome.tree, &annotated[0], &objects)?, Some(replacement), "replacement is visible through lookup" ); let outcome = one_shot::remove(outcome.tree, annotated[0], &objects)?; assert_eq!(outcome.previous, Some(replacement), "removal returns the removed note"); assert_eq!( one_shot::get(outcome.tree, &annotated[0], &objects)?, None, "the removed mapping is no longer visible" ); let mut buf = Vec::new(); let tree = objects.find_tree(&outcome.tree, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_tree()).count(), 0, "Git collapses fanout when the edited bucket materializes as a single note" ); let outcome = one_shot::remove(outcome.tree, annotated[16], &objects)?; assert_eq!(outcome.previous, Some(note), "the second removal empties the bucket"); let mut buf = Vec::new(); let tree = objects.find_tree(&outcome.tree, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_blob()).count(), 31, "emptying one leading-nibble bucket collapses the thirty remaining notes beside README" ); Ok(())} #[test]fn mutation_fanout_matches_git_at_bucket_count_boundaries() -> gix_testtools::Result { let fixture = gix_testtools::scripted_fixture_read_only("make_notes_fanout_repo.sh")?; let baseline = std::fs::read_to_string(fixture.join("fanout.baseline"))?; let mut lines = baseline.lines(); let mut note_fields = lines .next() .expect("the baseline starts with the shared note ID") .split(' '); assert_eq!( note_fields.next(), Some("note"), "the first baseline record identifies the note" );Lines 1–500 of 960. Load more
use std::{ cell::Cell, collections::BTreeSet, io::{self, Read}, }; use gix_hash::{Kind, ObjectId, oid}; use gix_object::{ FindExt, Tree, Write, bstr::{BString, ByteSlice}, tree::{Entry, EntryKind}, }; type ObjectDb = gix_odb::memory::Proxy<gix_object::find::Never>; mod one_shot { use super::*; pub fn get( root_tree_id: ObjectId, annotated_object_id: &oid, objects: &impl gix_object::Find, ) -> Result<Option<ObjectId>, gix_note::Error> { let mut state = gix_note::State::new(root_tree_id, objects)?; state.get(annotated_object_id, objects) } pub fn replace( root_tree_id: ObjectId, annotated_object_id: ObjectId, note_blob_id: ObjectId, objects: &(impl gix_object::Find + Write), ) -> Result<gix_note::Edit, gix_note::Error> { let mut state = gix_note::State::new(root_tree_id, objects)?; state.replace(annotated_object_id, note_blob_id, objects) } pub fn remove( root_tree_id: ObjectId, annotated_object_id: ObjectId, objects: &(impl gix_object::Find + Write), ) -> Result<gix_note::Edit, gix_note::Error> { let mut state = gix_note::State::new(root_tree_id, objects)?; state.remove(annotated_object_id, objects) } } struct CountingObjectDb { inner: ObjectDb, reads: Cell<usize>, writes: Cell<usize>, fail_next_read: Cell<bool>, fail_next_write: Cell<bool>, } impl CountingObjectDb { fn new(kind: Kind) -> Self { Self { inner: ObjectDb::new(gix_object::find::Never, kind), reads: Cell::new(0), writes: Cell::new(0), fail_next_read: Cell::new(false), fail_next_write: Cell::new(false), } } fn maybe_fail_write(&self) -> Result<(), gix_object::write::Error> { if self.fail_next_write.replace(false) { return Err(io::Error::other("injected write failure").into()); } Ok(()) } } impl gix_object::Find for CountingObjectDb { fn try_find<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec<u8>, ) -> Result<Option<gix_object::Data<'a>>, gix_object::find::Error> { self.reads.set(self.reads.get() + 1); if self.fail_next_read.replace(false) { return Err(io::Error::other("injected read failure").into()); } self.inner.try_find(id, buffer) } } impl gix_object::Write for CountingObjectDb { fn write_buf_with_known_id( &self, kind: gix_object::Kind, from: &[u8], id: ObjectId, ) -> Result<ObjectId, gix_object::write::Error> { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_buf_with_known_id(kind, from, id) } fn write_stream( &self, kind: gix_object::Kind, size: u64, from: &mut dyn Read, ) -> Result<ObjectId, gix_object::write::Error> { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_stream(kind, size, from) } fn write_stream_with_known_id( &self, kind: gix_object::Kind, size: u64, from: &mut dyn Read, id: ObjectId, ) -> Result<ObjectId, gix_object::write::Error> { self.writes.set(self.writes.get() + 1); self.maybe_fail_write()?; self.inner.write_stream_with_known_id(kind, size, from, id) } } #[test] fn reads_notes_without_fanout() -> gix_testtools::Result { assert_note_at_fanout(0) } #[test] fn reads_notes_with_one_fanout_level() -> gix_testtools::Result { assert_note_at_fanout(1) } #[test] fn reads_notes_with_two_fanout_levels() -> gix_testtools::Result { assert_note_at_fanout(2) } #[test] fn reads_notes_with_three_fanout_levels() -> gix_testtools::Result { assert_note_at_fanout(3) } #[test] fn replacing_a_note_does_not_read_untouched_fanout_subtrees() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let replacement = objects.write_buf(gix_object::Kind::Blob, b"replacement")?; let mut annotated = Vec::new(); let mut root_entries = Vec::new(); for high_nibble in b"0123456789abcdef" { for low_nibble in b"01" { let object = object_id_with_nibbles(kind, &[(0, *high_nibble), (1, *low_nibble)])?; let hex = object.to_hex().to_string(); let subtree = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Blob.into(), filename: hex[2..].into(), oid: note, }], })?; annotated.push(object); root_entries.push(Entry { mode: EntryKind::Tree.into(), filename: hex[..2].into(), oid: subtree, }); } } let root = objects.write(&Tree { entries: root_entries.clone(), })?; let target_hex = annotated[0].to_hex().to_string(); let replacement_subtree = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Blob.into(), filename: target_hex[2..].into(), oid: replacement, }], })?; root_entries[0].oid = replacement_subtree; let expected_root = objects.write(&Tree { entries: root_entries })?; objects.writes.set(0); let outcome = one_shot::replace(root, annotated[0], replacement, &objects)?; assert_eq!(outcome.previous, Some(note), "replacement returns the previous note"); assert_eq!( outcome.tree, expected_root, "the rewritten tree matches a path-local edit" ); assert_eq!( objects.reads.get(), 2, "only the root and the changed fanout subtree are read" ); assert_eq!( objects.writes.get(), 2, "only the changed fanout subtree and root are written" ); assert_eq!( one_shot::get(outcome.tree, &annotated[0], &objects)?, Some(replacement), "the replacement is visible" ); Ok(()) } #[test] fn state_reuses_materialized_trees_across_operations() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let replacement = objects.write_buf(gix_object::Kind::Blob, b"replacement")?; let root = notes_tree(&objects, &annotated, note, 2)?; objects.reads.set(0); let mut state = gix_note::State::new(root, &objects)?; assert_eq!(objects.reads.get(), 1, "initialization reads only the root tree"); assert_eq!(state.get(&annotated, &objects)?, Some(note), "the note is found"); let reads_after_first_lookup = objects.reads.get(); assert_eq!(reads_after_first_lookup, 3, "the first lookup reads both fanout trees"); assert_eq!(state.get(&annotated, &objects)?, Some(note), "the cached note is found"); assert_eq!( objects.reads.get(), reads_after_first_lookup, "a repeated lookup reuses the materialized trees" ); let edit = state.replace(annotated, replacement, &objects)?; assert_eq!(edit.previous, Some(note), "replacement returns the cached note"); assert_eq!( objects.reads.get(), reads_after_first_lookup, "replacement reuses the materialized trees" ); assert_eq!( state.get(&annotated, &objects)?, Some(replacement), "the state contains the replacement" ); assert_eq!( objects.reads.get(), reads_after_first_lookup, "looking up the replacement needs no tree reads" ); let edit = state.replace(annotated, note, &objects)?; assert_eq!( edit.previous, Some(replacement), "a second edit sees the cached replacement" ); assert_eq!( objects.reads.get(), reads_after_first_lookup, "writing the state retains its materialized trees" ); let edit = state.remove(annotated, &objects)?; assert_eq!(edit.previous, Some(note), "removal sees the cached note"); assert_eq!(state.get(&annotated, &objects)?, None, "the cached note is removed"); assert_eq!( objects.reads.get(), reads_after_first_lookup, "removal and its subsequent lookup need no tree reads" ); Ok(()) } #[test] fn state_recovers_after_failed_operations() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let replacement = objects.write_buf(gix_object::Kind::Blob, b"replacement")?; let root = notes_tree(&objects, &annotated, note, 2)?; let mut state = gix_note::State::new(root, &objects)?; objects.fail_next_read.set(true); state .get(&annotated, &objects) .expect_err("the injected subtree read fails"); assert_eq!(state.root_tree_id(), root, "a failed lookup retains the current root"); assert_eq!( state.get(&annotated, &objects)?, Some(note), "the state reloads from its root after a failed lookup" ); objects.fail_next_write.set(true); state .replace(annotated, replacement, &objects) .expect_err("the injected tree write fails"); assert_eq!(state.root_tree_id(), root, "a failed edit retains the current root"); assert_eq!( state.get(&annotated, &objects)?, Some(note), "the state reloads the pre-edit note after a failed edit" ); let edit = state.replace(annotated, replacement, &objects)?; assert_eq!(edit.previous, Some(note), "the recovered state can be edited"); assert_eq!( state.root_tree_id(), edit.tree, "a successful edit advances the recovered state" ); Ok(()) } fn assert_note_at_fanout(fanout: usize) -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let root = notes_tree(&objects, &annotated, note, fanout)?; assert_eq!( one_shot::get(root, &annotated, &objects)?, Some(note), "the note is found with {fanout} fanout levels" ); Ok(()) } fn notes_tree(objects: &impl Write, annotated: &oid, note: ObjectId, fanout: usize) -> gix_testtools::Result<ObjectId> { let hex = annotated.to_hex().to_string(); let mut tree = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Blob.into(), filename: BString::from(&hex[fanout * 2..]), oid: note, }], })?; for level in (0..fanout).rev() { tree = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Tree.into(), filename: BString::from(&hex[level * 2..][..2]), oid: tree, }], })?; } Ok(tree) } #[test] fn ignores_entries_that_are_not_notes() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = gix_odb::memory::Proxy::new(gix_object::find::Never, kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; let hex = annotated.to_hex().to_string(); let root = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Tree.into(), filename: BString::from(hex), oid: kind.empty_tree(), }], })?; assert_eq!( one_shot::get(root, &annotated, &objects)?, None, "the canonical empty-tree ID in a tree-mode entry at the full object-ID path is not a blob note" ); Ok(()) } #[test] fn mutations_rebalance_when_each_nibble_bucket_has_multiple_notes_and_preserve_non_notes() -> gix_testtools::Result { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let unrelated = objects.write_buf(gix_object::Kind::Blob, b"keep")?; let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; let mut root = objects.write(&Tree { entries: vec![Entry { mode: EntryKind::Blob.into(), filename: "README".into(), oid: unrelated, }], })?; let mut annotated = Vec::new(); for nibble in b"0123456789abcdef" { let object = object_id_with_nibble(kind, 0, *nibble)?; annotated.push(object); let outcome = one_shot::replace(root, object, note, &objects)?; assert_eq!(outcome.previous, None, "each distinct object receives a new note"); root = outcome.tree; } let mut buf = Vec::new(); let tree = objects.find_tree(&root, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_tree()).count(), 0, "one note in every leading-nibble bucket remains flat, like Git" ); assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_blob()).count(), 17, "the flat tree contains sixteen notes and README" ); for nibble in b"0123456789abcde" { let object = object_id_with_nibbles(kind, &[(0, *nibble), (2, b'1')])?; annotated.push(object); let outcome = one_shot::replace(root, object, note, &objects)?; assert_eq!(outcome.previous, None, "each bucket receives a second note"); root = outcome.tree; } let mut buf = Vec::new(); let tree = objects.find_tree(&root, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_tree()).count(), 0, "a bucket with only one note keeps all thirty-one notes flat" ); let last = object_id_with_nibbles(kind, &[(0, b'f'), (2, b'1')])?; annotated.push(last); let outcome = one_shot::replace(root, last, note, &objects)?; assert_eq!(outcome.previous, None, "the final bucket receives its second note"); root = outcome.tree; let mut buf = Vec::new(); let tree = objects.find_tree(&root, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_tree()).count(), 16, "two notes in every first-nibble bucket cause Git's first fanout level" ); assert!( tree.entries .iter() .any(|entry| entry.filename == "README" && entry.oid == unrelated), "non-note entries survive rebalancing" ); for object in &annotated { assert_eq!( one_shot::get(root, object, &objects)?, Some(note), "every note remains readable after fanout" ); } let replacement = objects.write_buf(gix_object::Kind::Blob, b"replacement")?; let outcome = one_shot::replace(root, annotated[0], replacement, &objects)?; assert_eq!(outcome.previous, Some(note), "replacement returns the previous note"); assert_eq!( one_shot::get(outcome.tree, &annotated[0], &objects)?, Some(replacement), "replacement is visible through lookup" ); let outcome = one_shot::remove(outcome.tree, annotated[0], &objects)?; assert_eq!(outcome.previous, Some(replacement), "removal returns the removed note"); assert_eq!( one_shot::get(outcome.tree, &annotated[0], &objects)?, None, "the removed mapping is no longer visible" ); let mut buf = Vec::new(); let tree = objects.find_tree(&outcome.tree, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_tree()).count(), 0, "Git collapses fanout when the edited bucket materializes as a single note" ); let outcome = one_shot::remove(outcome.tree, annotated[16], &objects)?; assert_eq!(outcome.previous, Some(note), "the second removal empties the bucket"); let mut buf = Vec::new(); let tree = objects.find_tree(&outcome.tree, &mut buf)?; assert_eq!( tree.entries.iter().filter(|entry| entry.mode.is_blob()).count(), 31, "emptying one leading-nibble bucket collapses the thirty remaining notes beside README" ); Ok(()) } #[test] fn mutation_fanout_matches_git_at_bucket_count_boundaries() -> gix_testtools::Result { let fixture = gix_testtools::scripted_fixture_read_only("make_notes_fanout_repo.sh")?; let baseline = std::fs::read_to_string(fixture.join("fanout.baseline"))?; let mut lines = baseline.lines(); let mut note_fields = lines .next() .expect("the baseline starts with the shared note ID") .split(' '); assert_eq!( note_fields.next(), Some("note"), "the first baseline record identifies the note" );