use gix_object::{ Tree, tree::{Entry, EntryKind}, }; fn hash_kind() -> gix_hash::Kind { crate::fixture_hash_kind() } fn null_id() -> gix_hash::ObjectId { hash_kind().null() } #[test] fn from_empty_cursor() -> crate::Result { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); edit.upsert(Some("root-file"), EntryKind::Blob, any_blob())?.upsert( ["nested", "from", "root"], EntryKind::BlobExecutable, any_blob(), )?; let cursor_path = ["some", "deeply", "nested", "path"]; let mut cursor = edit.cursor_at(cursor_path)?; let actual = cursor .upsert(Some("file"), EntryKind::Blob, any_blob())? .upsert(Some("empty-dir-via-cursor"), EntryKind::Tree, empty_tree())? .upsert(["with-subdir", "dir", "file"], EntryKind::Blob, any_blob())? .upsert(["with-subdir2", "dir", "file"], EntryKind::Blob, any_blob())? .remove(Some("file"))? .remove(["with-subdir", "dir", "file"])? .remove(Some("with-subdir2"))? .remove(Some("with-subdir2"))? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "only one item is left in the tree, which also keeps it alive", @r#" Oid(1) └── empty-dir-via-cursor (empty) "# ); assert_eq!(num_writes_and_clear(), 1, "root tree"); assert_eq!( cursor.get(None::<&str>), None, "the 'root' can't be obtained, no entry exists for it, ever" ); assert_eq!( cursor.get(Some("empty-dir-via-cursor")), Some(&Entry { mode: EntryKind::Tree.into(), filename: "empty-dir-via-cursor".into(), oid: empty_tree(), }), ); let actual = edit.write(&mut write)?; insta::assert_snapshot!(crate::normalize_tree_snapshot(&display_tree(actual, &storage)), @r#" Oid(1) ├── nested │ └── from │ └── root Oid(2).100755 ├── root-file Oid(2).100644 └── some └── deeply └── nested └── path └── empty-dir-via-cursor (empty) "#); let mut cursor = edit.cursor_at(cursor_path)?; let actual = cursor.remove(Some("empty-dir-via-cursor"))?.write(&mut write)?; assert_eq!(actual, empty_tree(), "it keeps the empty tree like the editor would"); assert_eq!( edit.get(["some", "deeply", "nested", "path"]), Some(&Entry { mode: EntryKind::Tree.into(), filename: "path".into(), oid: null_id(), }), "the directory leading to the removed one is still present" ); assert_eq!( edit.get(["some", "deeply", "nested", "path", "empty-dir-via-cursor"]), None, "but the removed entry is indee removed" ); let actual = edit.write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "now the editor naturally prunes all empty trees thus far, removing the cursor root", @r#" Oid(1) ├── nested │ └── from │ └── root Oid(2).100755 └── root-file Oid(2).100644 "# ); let mut cursor = edit.cursor_at(cursor_path)?; let actual = cursor .upsert(Some("root-file"), EntryKind::BlobExecutable, any_blob())? .upsert(["nested", "from"], EntryKind::BlobExecutable, any_blob())? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "it is able to write the sub-tree, even though names from the top-level tree are re-used", @r#" Oid(1) ├── nested │ └── from Oid(2).100755 └── root-file Oid(2).100755 "# ); let actual = edit.write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "it places the subtree exactly where it's expected", @r#" Oid(1) ├── nested │ └── from │ └── root Oid(2).100755 ├── root-file Oid(2).100644 └── some └── deeply └── nested └── path ├── nested │ └── from Oid(2).100755 └── root-file Oid(2).100755 "# ); Ok(()) } #[test] fn from_existing_cursor() -> crate::Result { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new_with_odb(storage.clone(), tree_odb()?); let root_tree_id = crate::generated_tree_root_id()?; let root_tree = find_tree(&odb, root_tree_id)?; odb.access_count_and_clear(); let mut edit = gix_object::tree::Editor::new(root_tree.clone(), &odb, hash_kind()); let mut cursor = edit.to_cursor(); let actual = cursor .remove(Some("bin"))? .remove(Some("bin.d"))? .remove(Some("file.to"))? .remove(Some("file.toml"))? .remove(Some("file.toml.bin"))? .upsert(["some", "nested", "file"], EntryKind::Blob, any_blob())? .write(&mut write)?; assert_eq!( num_writes_and_clear(), 1 + 2, "just the altered root tree, and two of trees towards `some/tested/file`" ); insta::assert_snapshot!( crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), "a cursor at '' is equivalent to 'as_cursor()', or the editor itself", @r#" Oid(1) ├── file │ └── a Oid(2).100644 ├── file0 Oid(2).100644 └── some └── nested └── file Oid(3).100644 "# ); let mut cursor = edit.cursor_at(["some", "nested"])?; let actual = cursor .upsert(Some("hello-from-cursor"), EntryKind::Blob, any_blob())? .remove(Some("file"))? .write(&mut write)?; insta::assert_snapshot!(crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), @r#" Oid(1) └── hello-from-cursor Oid(2).100644 "#); let mut cursor = edit.set_root(root_tree).to_cursor(); let actual = cursor .remove(Some("bin"))? .remove(Some("bin.d"))? .remove(Some("file.to"))? .remove(Some("file.toml"))? .remove(Some("file.toml.bin"))? .upsert(["some", "nested", "file"], EntryKind::Blob, any_blob())? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), "this cursor is the same as the editor", @r#" Oid(1) ├── file │ └── a Oid(2).100644 ├── file0 Oid(2).100644 └── some └── nested └── file Oid(3).100644 "# ); let actual = cursor.remove(["some", "nested", "file"])?.write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), "it's possible to delete a deeply nested file", @r#" Oid(1) ├── file │ └── a Oid(2).100644 └── file0 Oid(2).100644 "# ); Ok(()) } #[test] fn from_empty_removal() -> crate::Result { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); let actual = edit .remove(Some("non-existing"))? .remove(["still", "does", "not", "exist"])? .write(&mut write)?; assert_eq!(actual, empty_tree(), "nothing was actually done"); assert_eq!(num_writes_and_clear(), 1, "it has to write the empty tree though"); assert_eq!(storage.borrow().len(), 1, "the empty tree ends up in storage, too"); assert_eq!(odb.access_count_and_clear(), 0); let actual = edit .upsert(Some("file"), EntryKind::Blob, any_blob())? .upsert(Some("empty-dir"), EntryKind::Tree, empty_tree())? .upsert(["with-subdir", "dir", "file"], EntryKind::Blob, any_blob())? .upsert(["with-subdir2", "dir", "file"], EntryKind::Blob, any_blob())? .remove(Some("file"))? .remove(Some("empty-dir"))? .remove(Some("with-subdir"))? .remove(["with-subdir2", "dir"])? .remove(Some("with-subdir2"))? .write(&mut write)?; assert_eq!(actual, empty_tree(), "nothing was actually done"); assert_eq!(num_writes_and_clear(), 1, "still nothing to write but the empty tree"); assert_eq!(odb.access_count_and_clear(), 0); let actual = edit .upsert(Some("file"), EntryKind::Blob, any_blob())? .upsert(Some("empty-dir"), EntryKind::Tree, empty_tree())? .upsert(["with-subdir", "dir", "file"], EntryKind::Blob, any_blob())? .upsert(["with-subdir2", "dir", "file"], EntryKind::Blob, any_blob())? .write(&mut write)?; insta::assert_snapshot!(crate::normalize_tree_snapshot(&display_tree(actual, &storage)), @r#" Oid(1) ├── empty-dir (empty) ├── file Oid(2).100644 ├── with-subdir │ └── dir │ └── file Oid(2).100644 └── with-subdir2 └── dir └── file Oid(2).100644 "#); assert_eq!(num_writes_and_clear(), 5); assert_eq!(odb.access_count_and_clear(), 0); let actual = edit .remove(Some("file"))? .remove(Some("empty-dir"))? .remove(Some("with-subdir"))? .remove(["with-subdir2", "dir"])? .remove(Some("with-subdir2"))? .write(&mut write)?; assert_eq!(actual, empty_tree(), "everything was removed, leaving nothing"); assert_eq!(num_writes_and_clear(), 1, "only the empty tree to write"); assert_eq!( odb.access_count_and_clear(), 1, "has to get `with-subdir2` to remove child-entry" ); let actual = edit .upsert(["with-subdir", "file"], EntryKind::Blob, any_blob())? .upsert(["with-subdir", "dir", "file"], EntryKind::Blob, any_blob())? .remove(["with-subdir", "dir"])? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "only one file remains, empty dirs are removed automatically", @r#" Oid(1) └── with-subdir └── file Oid(2).100644 "# ); assert_eq!(num_writes_and_clear(), 1 + 1, "root and one subtree"); assert_eq!(storage.borrow().len(), 1 + 4, "empty tree and 4 unique trees"); assert_eq!(odb.access_count_and_clear(), 0); Ok(()) } #[test] fn from_empty_remove_accepts_empty_segments_after_unreachable_paths() -> crate::Result { let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); let actual = edit .upsert(Some("file"), EntryKind::Blob, any_blob())? .upsert(["dir", "file"], EntryKind::Blob, any_blob())? .remove(["missing", ""])? .remove(["file", ""])? .remove(["dir", "missing", ""])? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "empty path components after an unreachable removal target are not reached or validated", @r#" Oid(1) ├── dir │ └── file Oid(2).100644 └── file Oid(2).100644 "# ); Ok(()) } #[test] fn from_empty_remove_leaf_rejects_tree_entries() -> crate::Result { let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); edit.upsert(["A", "one"], EntryKind::Blob, any_blob())?; let err = edit.remove_leaf(Some("A")).unwrap_err(); assert_eq!( err.to_string(), "Cannot remove 'A' as leaf entry because it is a tree", "leaf-only removal must reject non-leaf entries" ); edit.remove_leaf(["A", "one"])?; let actual = edit.write(&mut write)?; assert_eq!(actual, empty_tree(), "the leaf itself can still be removed"); let actual = edit .upsert(Some("empty-dir"), EntryKind::Tree, empty_tree())? .remove_leaf(["empty-dir", "missing"])? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "an absent leaf below an empty tree is a no-op and must not prune the tree", @r#" Oid(1) └── empty-dir (empty) "# ); edit.remove_leaf(["empty-dir", ""])?; edit.remove_leaf(["empty-dir", "missing", ""])?; edit.remove_leaf(["missing", ""])?; edit.upsert(["non-empty", "file"], EntryKind::Blob, any_blob())? .remove_leaf(["non-empty", "missing", ""])? .remove_leaf(["non-empty", "file", ""])?; let actual = edit .set_root(Tree::default()) .upsert(Some("empty-dir"), EntryKind::Tree, empty_tree())? .upsert(["empty-dir", "file"], EntryKind::Blob, any_blob())? .remove_leaf(["empty-dir", "file"])? .write(&mut write)?; assert_eq!( actual, empty_tree(), "leaf-only removal still descends into edited subtrees backed by an empty tree" ); Ok(()) } #[test] fn from_existing_remove() -> crate::Result { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new_with_odb(storage.clone(), tree_odb()?); let root_tree_id = crate::generated_tree_root_id()?; let root_tree = find_tree(&odb, root_tree_id)?; odb.access_count_and_clear(); let mut edit = gix_object::tree::Editor::new(root_tree.clone(), &odb, hash_kind()); let actual = edit .remove(["file"])? .remove(Some("does not exist"))? .remove(["also", "does", "not", "exist"])? .remove(Some("bin.d"))? .remove(Some("file.toml.bin"))? .remove(Some("file.0"))? .write(&mut write)?; insta::assert_snapshot!(crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), @r#" Oid(1) ├── bin Oid(2).100644 ├── file.to Oid(2).100644 ├── file.toml Oid(2).100644 └── file0 Oid(2).100644 "#); assert_eq!(num_writes_and_clear(), 1, "only the root tree is written"); assert_eq!( odb.access_count_and_clear(), 0, "no sub-tree has to be queried for removal" ); let actual = edit .remove(Some("bin"))? .remove(Some("file.to"))? .remove(Some("file.toml"))? .remove(Some("file0"))? .write(&mut write)?; assert_eq!(actual, empty_tree(), "nothing is left"); assert_eq!(num_writes_and_clear(), 1, "only the empty tree is written"); assert_eq!(odb.access_count_and_clear(), 0); let actual = edit.set_root(root_tree).remove(["file", "a"])?.write(&mut write)?; assert_eq!(num_writes_and_clear(), 1, "it writes the changed root-tree"); assert_eq!( odb.access_count_and_clear(), 1, "lookup `file` to delete its (only) content" ); insta::assert_snapshot!( crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), "`file` is removed as it remains empty", @r#" Oid(1) ├── bin Oid(2).100644 ├── bin.d Oid(2).100644 ├── file.to Oid(2).100644 ├── file.toml Oid(2).100644 ├── file.toml.bin Oid(2).100644 └── file0 Oid(2).100644 "# ); Ok(()) } #[test] fn from_empty_invalid_write() -> crate::Result { let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); let actual = edit .upsert(["a", "\n"], EntryKind::Blob, any_blob())? .write(&mut write) .expect("no validation is performed"); insta::assert_snapshot!(crate::normalize_tree_snapshot(&display_tree(actual, &storage)), @r#" Oid(1) └── a └── Oid(2).100644 "#); let err = edit .upsert(Some("with\0null"), EntryKind::Blob, any_blob())? .write(&mut write) .unwrap_err(); assert_eq!( err.to_string(), r#"Nullbytes are invalid in file paths as they are separators: "with\0null""# ); let err = edit.upsert(Some(""), EntryKind::Blob, any_blob()).unwrap_err(); let expected_msg = "Empty path components are not allowed"; assert_eq!(err.to_string(), expected_msg); let err = edit .upsert(["fine", "", "previous is not fine"], EntryKind::Blob, any_blob()) .unwrap_err(); assert_eq!(err.to_string(), expected_msg); let actual = edit .remove(Some("a"))? .remove(Some("with\0null"))? .upsert(Some("works"), EntryKind::Blob, any_blob())? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "after removing invalid entries, it can write again", @r#" Oid(1) └── works Oid(2).100644 "# ); Ok(()) } #[test] fn from_empty_add() -> crate::Result { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); let actual = edit.write(&mut write).expect("no changes are fine"); assert_eq!(actual, empty_tree(), "empty stays empty"); assert_eq!(num_writes_and_clear(), 1, "the empty tree was written"); insta::assert_snapshot!(crate::normalize_tree_snapshot(&display_tree(actual, &storage)), @"Oid(1)"); assert_eq!(odb.access_count_and_clear(), 0); assert_eq!( edit.get(None::<&str>), None, "the 'root' can't be obtained, no entry exists for it, ever" ); let actual = edit .upsert(Some("hi"), EntryKind::Blob, null_id())? .write(&mut write) .expect("effectively no changes are fine"); assert_eq!( actual, empty_tree(), "null-ids are dropped automatically, they act as placeholders" ); assert_eq!(num_writes_and_clear(), 1, "the empty tree was written, nothing new"); assert_eq!(odb.access_count_and_clear(), 0); let actual = edit .upsert(["a", "b", "c"], EntryKind::Blob, null_id())? .upsert(["a", "b", "d", "e"], EntryKind::Blob, null_id())? .write(&mut write) .expect("effectively no changes are fine"); assert_eq!( actual, empty_tree(), "null-ids are dropped automatically, recursively, they act as placeholders" ); assert_eq!( num_writes_and_clear(), 1, "the empty tree was written as root, nothing new" ); assert_eq!(storage.borrow().len(), 1, "still nothing but empty trees"); assert_eq!(odb.access_count_and_clear(), 0); edit.upsert(["a", "b"], EntryKind::Tree, empty_tree())? .upsert(["a", "b", "c"], EntryKind::Tree, empty_tree())? .upsert(["a", "b", "d", "e"], EntryKind::Tree, empty_tree())?; assert_eq!( edit.get(["a", "b"]), Some(&Entry { mode: EntryKind::Tree.into(), filename: "b".into(), oid: empty_tree(), }), "before writing, entries are still present, just like they were written" ); assert_eq!( edit.get(["a", "b", "c"]), Some(&Entry { mode: EntryKind::Tree.into(), filename: "c".into(), oid: empty_tree(), }), ); let actual = edit.write(&mut write).expect("it's OK to write empty trees"); insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "one can write through trees, and empty trees are also fine", @r#" Oid(1) └── a └── b ├── c (empty) └── d └── e (empty) "# ); assert_eq!(num_writes_and_clear(), 4, "it wrote the trees it needed to write"); assert_eq!(odb.access_count_and_clear(), 0); assert_eq!(edit.get(["a", "b"]), None, "nothing exists here"); let oid = gix_hash::ObjectId::from_hex(match hash_kind() { gix_hash::Kind::Sha1 => &b"850bf83c26003cb0541318718bc9217c4a5bde6d"[..], gix_hash::Kind::Sha256 => &b"76be2e1aa5ce87f85b81d707c3a5f91c37d09fd064e28e13442b657d419e15f4"[..], _ => unreachable!("tests only support sha1 and sha256 fixtures"), }) .expect("valid object id"); assert_eq!( edit.get(Some("a")), Some(&Entry { mode: EntryKind::Tree.into(), filename: "a".into(), oid, }), "but the top-level tree is still available and can yield its entries, as written with proper ids" ); let actual = edit .upsert(["a"], EntryKind::Blob, any_blob())? .upsert(["a", "b"], EntryKind::Blob, any_blob())? .upsert(["a", "b", "c"], EntryKind::BlobExecutable, any_blob())? .upsert(["x", "z"], EntryKind::Blob, any_blob())? .write(&mut write) .expect("writing made-up blobs is fine"); insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "it's possible to write through previously added blobs", @r#" Oid(1) ├── a │ └── b │ └── c Oid(2).100755 └── x └── z Oid(2).100644 "# ); assert_eq!(num_writes_and_clear(), 4); assert_eq!(odb.access_count_and_clear(), 0); let actual = edit.upsert(["x"], EntryKind::Blob, any_blob())?.write(&mut write)?; insta::assert_snapshot!(crate::normalize_tree_snapshot(&display_tree(actual, &storage)), @r#" Oid(1) ├── a │ └── b │ └── c Oid(2).100755 └── x Oid(2).100644 "#); assert_eq!(num_writes_and_clear(), 1, "just the root tree changed"); assert_eq!(odb.access_count_and_clear(), 0); let prev_tree = actual; let actual = edit .upsert(["a", "b", "c"], EntryKind::BlobExecutable, any_blob())? .write(&mut write)?; assert_eq!(actual, prev_tree, "writing the same path again is a no-op"); assert_eq!( num_writes_and_clear(), 3, "it still rewrites all paths that (potentially) changed. \ There is no actual change tracking as no-changes aren't the default case for an editor" ); assert_eq!(odb.access_count_and_clear(), 2, "`a` and `a/b`"); let actual = edit .upsert(["a", "b", "c"], EntryKind::Blob, any_blob())? .upsert(["a"], EntryKind::Blob, any_blob())? .write(&mut write) .expect("we can turn overwrite a newly added tree (at 'a/') with a blob"); insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "now a tree was once again changed into a blob", @r#" Oid(1) ├── a Oid(2).100644 └── x Oid(2).100644 "# ); assert_eq!(num_writes_and_clear(), 1, "only the root-tree changes effectively"); assert_eq!(odb.access_count_and_clear(), 2, "`a` and `a/b`"); let actual = edit .set_root(Tree::default()) .upsert(["a", "b", "c"], EntryKind::Blob, any_blob())? .upsert(["a"], EntryKind::BlobExecutable, any_blob())? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "now the root is back to a well-known state, so edits are more intuitive", @r#" Oid(1) └── a Oid(2).100755 "# ); assert_eq!( num_writes_and_clear(), 1, "still, only the root-tree changes effectively" ); assert_eq!(odb.access_count_and_clear(), 0); let actual = edit .upsert(["a", "b"], EntryKind::Tree, empty_tree())? .upsert(["a", "b", "c"], EntryKind::BlobExecutable, any_blob())? // .upsert(["a", "b", "d"], EntryKind::Blob, any_blob())? .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "the intermediate tree is rewritten to be suitable to hold the blob", @r#" Oid(1) └── a └── b └── c Oid(2).100755 "# ); assert_eq!(num_writes_and_clear(), 3, "root, and two child-trees"); assert_eq!(odb.access_count_and_clear(), 0); Ok(()) } #[test] fn from_existing_add() -> crate::Result { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new_with_odb(storage.clone(), tree_odb()?); let root_tree_id = crate::generated_tree_root_id()?; let root_tree = find_tree(&odb, root_tree_id)?; odb.access_count_and_clear(); let mut edit = gix_object::tree::Editor::new(root_tree.clone(), &odb, hash_kind()); assert!(edit.get(["bin"]).is_some(), "the root is immediately available"); let actual = edit.write(&mut write).expect("no changes are fine"); assert_eq!(actual, root_tree_id, "it rewrites the same tree"); assert_eq!(odb.access_count_and_clear(), 0); insta::assert_snapshot!(crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), @r#" Oid(1) ├── bin Oid(2).100644 ├── bin.d Oid(2).100644 ├── file.to Oid(2).100644 ├── file.toml Oid(2).100644 ├── file.toml.bin Oid(2).100644 ├── file │ └── a Oid(2).100644 └── file0 Oid(2).100644 "#); assert_eq!( num_writes_and_clear(), 1, "only the root is written - there is no change tracking" ); let actual = edit .upsert(["file", "hi"], EntryKind::Blob, null_id())? .write(&mut write) .expect("effectively no changes are fine"); assert_eq!( actual, root_tree_id, "null-ids are dropped automatically, they act as placeholders, ultimately the tree is not changed" ); assert_eq!( storage.borrow().len(), 2, "it writes two trees, even though none is new" ); assert_eq!(num_writes_and_clear(), 2, "the write-count reflects that"); odb.access_count_and_clear(); let actual = edit .upsert(["a", "b", "c"], EntryKind::Blob, null_id())? .upsert(["a", "b", "d", "e"], EntryKind::Blob, null_id())? .write(&mut write) .expect("effectively no changes are fine"); assert_eq!( actual, root_tree_id, "null-ids are dropped automatically, recursively, and empty intermediate trees are removed as well" ); assert_eq!(storage.borrow().len(), 2, "still the same amount of trees"); assert_eq!( num_writes_and_clear(), 1, "but only the root-tree is written (with nulls pruned)" ); assert_eq!(odb.access_count_and_clear(), 0); odb.access_count_and_clear(); let actual = edit .upsert(["bin", "b"], EntryKind::Tree, empty_tree())? .upsert(["bin", "b", "c"], EntryKind::Tree, empty_tree())? .upsert(["a", "b", "d", "e"], EntryKind::Tree, empty_tree())? .write(&mut write) .expect("it's OK to write empty leaf-trees"); assert_eq!( odb.access_count_and_clear(), 0, "we write through blobs, and thus create trees on the fly" ); insta::assert_snapshot!( crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), "one can write through trees and blobs, and empty leaf trees are also fine", @r#" Oid(1) ├── a │ └── b │ └── d │ └── e (empty) ├── bin.d Oid(2).100644 ├── bin │ └── b │ └── c (empty) ├── file.to Oid(2).100644 ├── file.toml Oid(2).100644 ├── file.toml.bin Oid(2).100644 ├── file │ └── a Oid(2).100644 └── file0 Oid(2).100644 "# ); assert_eq!( num_writes_and_clear(), 1 + 2 + 3, "each changed tree is written: root, the two subtrees" ); odb.access_count_and_clear(); let actual = edit .upsert(["a", "b", "c"], EntryKind::Blob, any_blob())? .upsert(["a", "b"], EntryKind::Blob, any_blob())? .upsert(["file"], EntryKind::BlobExecutable, any_blob())? .write(&mut write)?; assert_eq!(odb.access_count_and_clear(), 2, "`a` and `a/b`"); insta::assert_snapshot!( crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), "it properly sorts entries after type-changes", @r#" Oid(1) ├── a │ └── b Oid(2).100644 ├── bin.d Oid(3).100644 ├── bin │ └── b │ └── c (empty) ├── file Oid(2).100755 ├── file.to Oid(3).100644 ├── file.toml Oid(3).100644 ├── file.toml.bin Oid(3).100644 └── file0 Oid(3).100644 "# ); assert_eq!(num_writes_and_clear(), 1 + 1, "the root and one subtree"); odb.access_count_and_clear(); let actual = edit .set_root(root_tree) .upsert(["file", "subdir", "exe"], EntryKind::BlobExecutable, any_blob())? .write(&mut write)?; assert_eq!( odb.access_count_and_clear(), 1, "`file` only, everything else is inserted" ); insta::assert_snapshot!( crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), "now the root is back to a well-known state", @r#" Oid(1) ├── bin Oid(2).100644 ├── bin.d Oid(2).100644 ├── file.to Oid(2).100644 ├── file.toml Oid(2).100644 ├── file.toml.bin Oid(2).100644 ├── file │ ├── a Oid(2).100644 │ └── subdir │ └── exe Oid(3).100755 └── file0 Oid(2).100644 "# ); assert_eq!(num_writes_and_clear(), 1 + 2, "the root and one subtree with directory"); Ok(()) } mod utils { use std::{ cell::{Cell, RefCell}, rc::Rc, }; use bstr::{BStr, ByteSlice}; use gix_hash::ObjectId; use gix_object::{Tree, WriteTo}; type TreeStore = Rc>>; pub(super) struct StorageOdb(TreeStore, Option, Cell); pub(super) fn tree_odb() -> gix_testtools::Result { let root = gix_testtools::scripted_fixture_read_only("make_trees.sh")?; Ok(gix_odb::at_opts( root.join(".git/objects"), Vec::new(), gix_odb::store::init::Options { object_hash: crate::fixture_hash_kind(), ..Default::default() }, )?) } pub(super) fn find_tree(odb: &impl gix_object::FindExt, id: ObjectId) -> gix_testtools::Result { let mut buf = Vec::new(); Ok(odb.find_tree(&id, &mut buf)?.into()) } pub(super) fn new_inmemory_writes() -> ( TreeStore, impl FnMut(&Tree) -> Result, impl Fn() -> usize, ) { let store = TreeStore::default(); let num_writes = Rc::new(Cell::new(0_usize)); let write_tree = { let store = store.clone(); let num_writes = num_writes.clone(); let mut buf = Vec::with_capacity(512); move |tree: &Tree| { buf.clear(); tree.write_to(&mut buf)?; let id = gix_object::compute_hash(crate::fixture_hash_kind(), gix_object::Kind::Tree, &buf)?; store.borrow_mut().insert(id, tree.clone()); let old = num_writes.get(); num_writes.set(old + 1); Ok(id) } }; let obtain_num_writes = { let c = num_writes.clone(); move || { let res = c.get(); c.set(0); res } }; (store, write_tree, obtain_num_writes) } impl StorageOdb { pub fn new(storage: TreeStore) -> Self { Self(storage, None, Cell::new(0)) } pub fn new_with_odb(storage: TreeStore, odb: gix_odb::Handle) -> Self { Self(storage, Some(odb), Cell::new(0)) } pub fn access_count_and_clear(&self) -> usize { let res = self.2.get(); self.2.set(0); res } } impl gix_object::Find for StorageOdb { fn try_find<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result>, gix_object::find::Error> { let borrow = self.0.borrow(); let old = self.2.get(); self.2.set(old + 1); match borrow.get(id) { None => self.1.as_ref().map_or(Ok(None), |odb| odb.try_find(id, buffer)), Some(tree) => { buffer.clear(); tree.write_to(buffer).expect("valid trees can always be serialized"); Ok(Some(gix_object::Data { kind: gix_object::Kind::Tree, object_hash: id.kind(), data: &*buffer, })) } } } } fn display_tree_recursive( tree_id: ObjectId, storage: &TreeStore, odb: Option<&dyn gix_object::FindExt>, name: Option<&BStr>, buf: &mut Vec, ) -> termtree::Tree { let mut tree_storage = None; let borrow = storage.borrow(); let tree = borrow .get(&tree_id) .or_else(|| { if tree_id.is_empty_tree() { tree_storage = Some(Tree::default()); tree_storage.as_ref() } else { odb.and_then(|odb| { tree_storage = odb.find_tree(&tree_id, buf).map(Into::into).ok(); tree_storage.as_ref() }) } }) .unwrap_or_else(|| panic!("tree {tree_id} is always present")); let mut termtree = termtree::Tree::new(if let Some(name) = name { if tree.entries.is_empty() { format!("{name} (empty)") } else { name.to_string() } } else { tree_id.to_string() }); for entry in &tree.entries { if entry.mode.is_tree() { termtree.push(display_tree_recursive( entry.oid, storage, odb, Some(entry.filename.as_bstr()), buf, )); } else { termtree.push(format!( "{} {}.{}", entry.filename, entry.oid, entry.mode.kind().as_octal_str() )); } } termtree } pub(super) fn display_tree(tree_id: ObjectId, storage: &TreeStore) -> String { let mut buf = Vec::new(); display_tree_recursive(tree_id, storage, None, None, &mut buf).to_string() } pub(super) fn display_tree_with_odb( tree_id: ObjectId, storage: &TreeStore, odb: &impl gix_object::FindExt, ) -> String { let mut buf = Vec::new(); display_tree_recursive(tree_id, storage, Some(odb), None, &mut buf).to_string() } pub(super) fn empty_tree() -> ObjectId { ObjectId::empty_tree(crate::fixture_hash_kind()) } pub(super) fn any_blob() -> ObjectId { ObjectId::from_hex(&vec![b'b'; crate::fixture_hash_kind().len_in_hex()]).expect("valid repeated hex") } } use utils::{ StorageOdb, any_blob, display_tree, display_tree_with_odb, empty_tree, find_tree, new_inmemory_writes, tree_odb, };