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(())Lines 1–500 of 1044. Load more
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(())