960 lines · 33.9 KB · Rust
RawDownload
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.