From a0b52f6a67e12c2b4985facf06237830ba94c7fb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 30 Apr 2026 09:48:52 +0800 Subject: [PATCH 1/3] to-tree implementation TBD --- gix-index/Cargo.toml | 5 + gix-index/benches/to_tree.rs | 89 +++++++++++ gix-index/src/init.rs | 268 ++++++++++++++++++++++++++++++++++ gix-index/tests/index/init.rs | 174 +++++++++++++++++++++- 4 files changed, 534 insertions(+), 2 deletions(-) create mode 100644 gix-index/benches/to_tree.rs diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index 53f759af17c..85995ab5af2 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -21,6 +21,11 @@ name = "from-tree" harness = false path = "./benches/from_tree.rs" +[[bench]] +name = "to-tree" +harness = false +path = "./benches/to_tree.rs" + [features] ## Enable support for the SHA-1 hash by enabling the respective feature in the `gix-hash` crate. sha1 = ["gix-hash/sha1"] diff --git a/gix-index/benches/to_tree.rs b/gix-index/benches/to_tree.rs new file mode 100644 index 00000000000..5006772cd83 --- /dev/null +++ b/gix-index/benches/to_tree.rs @@ -0,0 +1,89 @@ +use std::hint::black_box; + +use bstr::ByteSlice; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use gix_index::{ + State, + entry::{Flags, Mode}, +}; + +fn to_tree(c: &mut Criterion) { + let objects = memory_db(); + let options = missing_ok(); + let mut group = c.benchmark_group("to_tree"); + + let mut flat = State::new(gix_hash::Kind::Sha1); + for idx in 0..10_000 { + flat.dangerously_push_entry( + Default::default(), + repeated_id(b'a'), + Flags::empty(), + Mode::FILE, + format!("file-{idx:05}").as_bytes().as_bstr(), + ); + } + group.throughput(Throughput::Elements(flat.entries().len() as u64)); + group.bench_function("flat 10k files", |b| { + b.iter(|| { + let id = flat.to_tree(&objects, options).expect("tree can be written"); + black_box(id); + }); + }); + + let mut wide_deep = State::new(gix_hash::Kind::Sha1); + for dir_idx in 0..100 { + for file_idx in 0..100 { + wide_deep.dangerously_push_entry( + Default::default(), + repeated_id(b'a'), + Flags::empty(), + Mode::FILE, + format!("dir-{dir_idx:03}/file-{file_idx:03}").as_bytes().as_bstr(), + ); + } + } + group.throughput(Throughput::Elements(wide_deep.entries().len() as u64)); + group.bench_function("wide 100 x 100 files", |b| { + b.iter(|| { + let id = wide_deep.to_tree(&objects, options).expect("tree can be written"); + black_box(id); + }); + }); + + let mut sparse = State::new(gix_hash::Kind::Sha1); + for idx in 0..10_000 { + sparse.dangerously_push_entry( + Default::default(), + repeated_id(b't'), + Flags::empty(), + Mode::DIR, + format!("sparse-{idx:05}/").as_bytes().as_bstr(), + ); + } + group.throughput(Throughput::Elements(sparse.entries().len() as u64)); + group.bench_function("sparse 10k directories", |b| { + b.iter(|| { + let id = sparse.to_tree(&objects, options).expect("tree can be written"); + black_box(id); + }); + }); +} + +criterion_group!(benches, to_tree); +criterion_main!(benches); + +type MemoryDb = gix_odb::memory::Proxy; + +fn memory_db() -> MemoryDb { + gix_odb::memory::Proxy::new(gix_object::find::Never, gix_hash::Kind::Sha1) +} + +fn missing_ok() -> gix_index::init::to_tree::Options { + let mut options = gix_index::init::to_tree::Options::default(); + options.missing_ok = true; + options +} + +fn repeated_id(byte: u8) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_hex(&vec![byte; gix_hash::Kind::Sha1.len_in_hex()]).expect("valid hex") +} diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index b1d6519cd18..7097b3e0c65 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -232,3 +232,271 @@ pub mod from_tree { } } } + +/// Initialize tree objects from an index state. +pub mod to_tree { + use std::io::Write; + + use bstr::{BStr, BString, ByteSlice}; + use gix_object::{tree, tree::EntryMode}; + + use crate::{ + Entry, State, + entry::{self, Stage}, + extension, + }; + + /// The error returned by [State::to_tree()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Entry '{path}' is unmerged at stage {stage}")] + Unmerged { path: BString, stage: u32 }, + #[error("Entry '{path}' is invalid as both a file and directory would exist in the tree")] + FileDirectoryConflict { path: BString }, + #[error("The path \"{path}\" is invalid")] + InvalidComponent { + path: BString, + source: gix_validate::path::component::Error, + }, + #[error("Entry '{path}' has an invalid index mode {mode:?}")] + InvalidMode { path: BString, mode: entry::Mode }, + #[error("The object {id} at '{path}' does not exist")] + MissingObject { path: BString, id: gix_hash::ObjectId }, + #[error(transparent)] + Write(#[from] gix_object::write::Error), + #[error(transparent)] + Entries(#[from] crate::verify::entries::Error), + #[error("More than 4 billion entries would be represented by the tree at '{path}'")] + EntriesOverflow { path: BString }, + } + + /// Options for use with [State::to_tree()]. + #[derive(Default, Debug, Clone, Copy)] + pub struct Options { + /// Path component validation options. + pub validate: gix_validate::path::component::Options, + /// If true, don't fail if referenced objects are missing from `objects`. + /// + /// Commit entries, representing submodules, are never checked for existence. + pub missing_ok: bool, + } + + /// Tree creation. + impl State { + /// Write this index state as Git tree objects into `objects` and return the root tree id. + /// + /// If this state has a TREE extension, it is refreshed from the written trees on success. + /// If no TREE extension is present, none is created. + pub fn to_tree(&mut self, objects: Db, options: Options) -> Result + where + Db: gix_object::Write + gix_object::Exists, + { + let _span = gix_features::trace::coarse!("gix_index::State::to_tree()"); + self.verify_entries()?; + let update_tree_cache = self.tree.is_some(); + + let mut builder = Builder { + state: self, + objects: &objects, + options, + update_tree_cache, + }; + let (next_index, written) = builder.write_tree_at(0, BStr::new(b""), BStr::new(b""), true)?; + debug_assert_eq!(next_index, builder.state.entries.len()); + let written = written.expect("the root tree is always written"); + if update_tree_cache { + self.tree = written.cache_tree; + } + Ok(written.id) + } + } + + struct Builder<'a, Db> { + state: &'a State, + objects: &'a Db, + options: Options, + update_tree_cache: bool, + } + + struct WrittenTree { + id: gix_hash::ObjectId, + num_entries: u32, + cache_tree: Option, + } + + impl Builder<'_, Db> + where + Db: gix_object::Write + gix_object::Exists, + { + fn write_tree_at( + &mut self, + mut index: usize, + prefix: &BStr, + name: &BStr, + write_empty: bool, + ) -> Result<(usize, Option), Error> { + let mut tree_data = Vec::new(); + let mut num_entries = 0u32; + let mut children = Vec::new(); + let mut leaf_names = Vec::<&BStr>::new(); + + while let Some(entry) = self.state.entries.get(index) { + let path = entry.path(self.state); + if !path.starts_with(prefix) { + break; + } + + self.assure_unmerged(entry, path)?; + + if entry + .flags + .intersects(entry::Flags::REMOVE | entry::Flags::INTENT_TO_ADD) + { + index += 1; + continue; + } + + let rela_path = BStr::new(&path[prefix.len()..]); + if rela_path.is_empty() { + return Err(Error::InvalidComponent { + path: path.into(), + source: gix_validate::path::component(BStr::new(b""), None, self.options.validate) + .expect_err("empty component is invalid"), + }); + } + + let slash_pos = rela_path.find_byte(b'/'); + let is_sparse_leaf = entry.mode.is_sparse() && slash_pos == Some(rela_path.len() - 1); + if let Some(slash_pos) = slash_pos.filter(|_| !is_sparse_leaf) { + let component = BStr::new(&rela_path[..slash_pos]); + let child_path = BStr::new(&path[..prefix.len() + slash_pos]); + self.validate_component(child_path, component, None)?; + if leaf_names.binary_search_by(|name| (*name).cmp(component)).is_ok() { + return Err(Error::FileDirectoryConflict { + path: child_path.into(), + }); + } + + let child_prefix = BStr::new(&path[..prefix.len() + slash_pos + 1]); + let (next_index, child) = self.write_tree_at(index, child_prefix, component, false)?; + index = next_index; + + if let Some(child) = child { + encode_entry(&mut tree_data, EntryKind::Tree.into(), component, &child.id)?; + num_entries = num_entries + .checked_add(child.num_entries) + .ok_or_else(|| Error::EntriesOverflow { path: prefix.into() })?; + if let Some(child_cache_tree) = child.cache_tree { + children.push(child_cache_tree); + } + } + } else { + let filename = if is_sparse_leaf { + BStr::new(&rela_path[..rela_path.len() - 1]) + } else { + rela_path + }; + let insertion_pos = match leaf_names.binary_search_by(|name| (*name).cmp(filename)) { + Ok(_) => return Err(Error::FileDirectoryConflict { path: path.into() }), + Err(pos) => pos, + }; + self.write_entry(&mut tree_data, path, filename, entry)?; + leaf_names.insert(insertion_pos, filename); + num_entries = num_entries + .checked_add(1) + .ok_or_else(|| Error::EntriesOverflow { path: prefix.into() })?; + index += 1; + } + } + + if num_entries == 0 && !write_empty { + return Ok((index, None)); + } + + let id = self.objects.write_buf(gix_object::Kind::Tree, &tree_data)?; + let cache_tree = self.update_tree_cache.then(|| extension::Tree { + name: name.iter().copied().collect(), + id, + num_entries: Some(num_entries), + children, + }); + Ok(( + index, + Some(WrittenTree { + id, + num_entries, + cache_tree, + }), + )) + } + + fn assure_unmerged(&self, entry: &Entry, path: &BStr) -> Result<(), Error> { + let stage = entry.stage(); + if stage != Stage::Unconflicted { + return Err(Error::Unmerged { + path: path.into(), + stage: stage as u32, + }); + } + Ok(()) + } + + fn write_entry( + &self, + tree_data: &mut Vec, + path: &BStr, + filename: &BStr, + entry: &Entry, + ) -> Result<(), Error> { + let mode = entry.mode.to_tree_entry_mode().ok_or_else(|| Error::InvalidMode { + path: path.into(), + mode: entry.mode, + })?; + self.validate_component( + path, + filename, + mode.is_link().then_some(gix_validate::path::component::Mode::Symlink), + )?; + if !self.options.missing_ok && !mode.is_commit() && !self.objects.exists(&entry.id) { + return Err(Error::MissingObject { + path: path.into(), + id: entry.id, + }); + } + + encode_entry(tree_data, mode, filename, &entry.id)?; + Ok(()) + } + + fn validate_component( + &self, + path: &BStr, + component: &BStr, + mode: Option, + ) -> Result<(), Error> { + gix_validate::path::component(component, mode, self.options.validate) + .map(|_| ()) + .map_err(|source| Error::InvalidComponent { + path: path.into(), + source, + }) + } + } + + fn encode_entry( + out: &mut Vec, + mode: EntryMode, + filename: &BStr, + id: &gix_hash::oid, + ) -> Result<(), gix_object::write::Error> { + out.write_all(mode.as_bytes(&mut Default::default()))?; + out.write_all(b" ")?; + out.write_all(filename)?; + out.write_all(b"\0")?; + out.write_all(id.as_bytes())?; + Ok(()) + } + + use tree::EntryKind; +} diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index 0f46a8693c4..328bd52038d 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -1,6 +1,7 @@ use std::{error::Error, path::Path}; use crate::{odb_at, scripted_fixture_read_only}; +use bstr::ByteSlice; use gix_index::State; #[test] @@ -80,6 +81,124 @@ fn from_tree_returns_file_directory_conflicts_until_fixed() -> crate::Result { Ok(()) } +#[test] +fn to_tree_roundtrips_to_fixture_tree() -> crate::Result { + let fixtures = [ + "make_index/v2.sh", + "make_index/v2_deeper_tree.sh", + "make_index/v2_all_file_kinds.sh", + "make_index/v3_added_files.sh", + "make_index/v3_sparse_index.sh", + "make_index/v4_more_files_IEOT.sh", + ]; + + for fixture in fixtures { + let worktree_dir = scripted_fixture_read_only(fixture)?; + let expected_tree_id = tree_id(&worktree_dir); + let git_dir = worktree_dir.join(".git"); + let mut index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default())?; + let objects = memory_db(); + + let actual_tree_id = index.to_tree(&objects, missing_ok())?; + assert_eq!(actual_tree_id, expected_tree_id, "tree mismatch in {fixture:?}"); + } + Ok(()) +} + +#[test] +fn to_tree_empty_index_is_empty_tree() -> crate::Result { + let mut state = State::new(gix_hash::Kind::Sha1); + let objects = memory_db(); + + let actual = state.to_tree(&objects, Default::default())?; + + assert_eq!(actual, gix_hash::ObjectId::empty_tree(gix_hash::Kind::Sha1)); + assert!(state.tree().is_none(), "TREE extension isn't created if absent"); + Ok(()) +} + +#[test] +fn to_tree_rejects_unmerged_entries() { + let mut index = super::Fixture::Loose("conflicting-file").open(); + let objects = memory_db(); + + let err = index.to_tree(&objects, Default::default()).unwrap_err(); + + assert!(matches!(err, gix_index::init::to_tree::Error::Unmerged { .. })); +} + +#[test] +fn to_tree_rejects_file_directory_conflicts() { + let mut state = state_with_entries(["a", "a.b", "a/b"]); + let objects = memory_db(); + + let err = state.to_tree(&objects, missing_ok()).unwrap_err(); + + assert!(matches!( + err, + gix_index::init::to_tree::Error::FileDirectoryConflict { .. } + )); +} + +#[test] +fn to_tree_rejects_invalid_components() { + let mut state = state_with_entries(["a//b"]); + let objects = memory_db(); + + let err = state.to_tree(&objects, Default::default()).unwrap_err(); + + assert!(matches!(err, gix_index::init::to_tree::Error::InvalidComponent { .. })); +} + +#[test] +fn to_tree_rejects_missing_objects_unless_allowed() -> crate::Result { + let mut state = state_with_entries(["file"]); + let objects = memory_db(); + + let err = state.to_tree(&objects, Default::default()).unwrap_err(); + assert!(matches!(err, gix_index::init::to_tree::Error::MissingObject { .. })); + + let mut options = gix_index::init::to_tree::Options::default(); + options.missing_ok = true; + let actual = state.to_tree(&objects, options)?; + assert_ne!(actual, gix_hash::Kind::Sha1.null()); + Ok(()) +} + +#[test] +fn to_tree_refreshes_existing_tree_extension() -> crate::Result { + let mut index = super::Fixture::Generated("v2").open(); + let original_cached_tree = index.tree().expect("fixture has TREE extension").id; + index.entries_mut()[0].id = repeated_id(b'b'); + let objects = memory_db(); + + let actual = index.to_tree(&objects, missing_ok())?; + + assert_ne!(actual, original_cached_tree); + let tree = index.tree().expect("TREE extension is preserved and refreshed"); + assert_eq!(tree.id, actual); + assert_eq!( + tree.num_entries, + Some(index.entries().len().try_into().expect("small fixture")) + ); + tree.verify(false, gix_object::find::Never)?; + Ok(()) +} + +#[test] +fn to_tree_does_not_create_missing_tree_extension() -> crate::Result { + let worktree_dir = scripted_fixture_read_only("make_index/v2.sh")?; + let odb = gix_odb::at(worktree_dir.join(".git").join("objects"))?; + let mut state = State::from_tree(&tree_id(&worktree_dir), &odb, Default::default())?; + assert!(state.tree().is_none()); + let objects = memory_db(); + + state.to_tree(&objects, missing_ok())?; + + assert!(state.tree().is_none()); + Ok(()) +} + #[test] fn new() { let state = State::new(gix_hash::Kind::Sha1); @@ -107,7 +226,58 @@ fn compare_states(actual: &State, expected: &State, fixture: &str) { } fn tree_id(root: &Path) -> gix_hash::ObjectId { - let hex_hash = - std::fs::read_to_string(root.join("head.tree")).expect("head.tree was created by git rev-parse @^{tree}"); + let hex_hash = std::fs::read_to_string(root.join("head.tree")).unwrap_or_else(|_| { + let mut out = std::process::Command::new("git") + .arg("-C") + .arg(root) + .args(["rev-parse", "@^{tree}"]) + .output() + .expect("git can determine the tree id"); + if !out.status.success() { + out = std::process::Command::new("git") + .arg("-C") + .arg(root) + .arg("write-tree") + .output() + .expect("git can write the tree id"); + } + assert!( + out.status.success(), + "git couldn't determine a tree: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).expect("hex tree id is utf8") + }); hex_hash.trim().parse().expect("valid hash") } + +fn state_with_entries(paths: [&str; N]) -> State { + let mut state = State::new(gix_hash::Kind::Sha1); + for path in paths { + state.dangerously_push_entry( + Default::default(), + repeated_id(b'a'), + gix_index::entry::Flags::empty(), + gix_index::entry::Mode::FILE, + path.as_bytes().as_bstr(), + ); + } + state.sort_entries(); + state +} + +fn repeated_id(byte: u8) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_hex(&vec![byte; gix_hash::Kind::Sha1.len_in_hex()]).expect("valid hex") +} + +type MemoryDb = gix_odb::memory::Proxy; + +fn memory_db() -> MemoryDb { + gix_odb::memory::Proxy::new(gix_object::find::Never, gix_hash::Kind::Sha1) +} + +fn missing_ok() -> gix_index::init::to_tree::Options { + let mut options = gix_index::init::to_tree::Options::default(); + options.missing_ok = true; + options +} From ed0aa83e4b6f343c70c32769ff3baa8b87348f29 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 30 Apr 2026 10:05:08 +0800 Subject: [PATCH 2/3] invalidate tree cache TBD --- gitoxide-core/src/index/checkout.rs | 2 +- gix-dir/tests/walk_utils/mod.rs | 2 +- gix-index/src/access/mod.rs | 40 +++++++++++++++-- gix-index/src/extension/mod.rs | 17 +++++++ gix-index/src/init.rs | 3 ++ gix-index/tests/index/access.rs | 47 ++++++++++++++++++++ gix-index/tests/index/file/write.rs | 4 +- gix-index/tests/index/init.rs | 17 +++++++ gix-status/tests/status/index_as_worktree.rs | 9 +++- gix/src/status/iter/types.rs | 2 +- 10 files changed, 134 insertions(+), 9 deletions(-) diff --git a/gitoxide-core/src/index/checkout.rs b/gitoxide-core/src/index/checkout.rs index 8c71380e184..3927cb90070 100644 --- a/gitoxide-core/src/index/checkout.rs +++ b/gitoxide-core/src/index/checkout.rs @@ -44,7 +44,7 @@ pub fn checkout_exclusive( } else { gix::index::entry::Mode::SYMLINK }; - for entry in index.entries_mut().iter_mut().filter(|e| { + for entry in index.entries_mut_keep_tree_cache().iter_mut().filter(|e| { e.mode .contains(maybe_symlink_mode | gix::index::entry::Mode::DIR | gix::index::entry::Mode::COMMIT) }) { diff --git a/gix-dir/tests/walk_utils/mod.rs b/gix-dir/tests/walk_utils/mod.rs index 93196477e65..471eb9085b0 100644 --- a/gix-dir/tests/walk_utils/mod.rs +++ b/gix-dir/tests/walk_utils/mod.rs @@ -301,7 +301,7 @@ pub fn try_collect_filtered_opts( ); if fresh_index { index - .entries_mut() + .entries_mut_keep_tree_cache() .iter_mut() .filter(|e| { // relevant for partial checkouts, all related entries will have skip-worktree set, diff --git a/gix-index/src/access/mod.rs b/gix-index/src/access/mod.rs index a55402b969d..5d019692055 100644 --- a/gix-index/src/access/mod.rs +++ b/gix-index/src/access/mod.rs @@ -63,6 +63,7 @@ impl State { &'state mut self, backing: &'backing PathStorageRef, ) -> impl Iterator { + self.invalidate_tree_cache(); self.entries.iter_mut().map(move |e| { let path = backing[e.path.clone()].as_bstr(); (e, path) @@ -489,21 +490,43 @@ impl State { self.path_backing.is_empty(), "BUG: return path backing only after taking it, once" ); + self.invalidate_tree_cache(); self.path_backing = backing; } - /// Return mutable entries in a slice. + /// Return mutable entries in a slice and invalidate the TREE extension, if present. + /// + /// Prefer [`entries_mut_keep_tree_cache()`][Self::entries_mut_keep_tree_cache()] if only tree-neutral fields + /// are changed. pub fn entries_mut(&mut self) -> &mut [Entry] { + self.invalidate_tree_cache(); + &mut self.entries + } + + /// Return mutable entries in a slice without invalidating the TREE extension. + /// + /// Use this only for mutations that cannot change the tree produced by [`State::to_tree()`][crate::State::to_tree()]. + /// This includes `stat` updates, storage metadata like [`EXTENDED`][entry::Flags::EXTENDED], and flags that only + /// describe worktree/cache status, like [`ASSUME_VALID`][entry::Flags::ASSUME_VALID], + /// [`UPTODATE`][entry::Flags::UPTODATE], [`FSMONITOR_VALID`][entry::Flags::FSMONITOR_VALID], or + /// [`SKIP_WORKTREE`][entry::Flags::SKIP_WORKTREE]. + /// + /// Do not use this method to change object ids, modes, stage bits, paths, entry ordering, or flags that affect + /// tree construction like [`REMOVE`][entry::Flags::REMOVE] or [`INTENT_TO_ADD`][entry::Flags::INTENT_TO_ADD]. + /// Use [`entries_mut()`][Self::entries_mut()] instead for those changes. + pub fn entries_mut_keep_tree_cache(&mut self) -> &mut [Entry] { &mut self.entries } /// Return a writable slice to entries and read-access to their path storage at the same time. pub fn entries_mut_and_pathbacking(&mut self) -> (&mut [Entry], &PathStorageRef) { + self.invalidate_tree_cache(); (&mut self.entries, &self.path_backing) } /// Return mutable entries along with their paths in an iterator. pub fn entries_mut_with_paths(&mut self) -> impl Iterator { + self.invalidate_tree_cache(); let paths = &self.path_backing; self.entries.iter_mut().map(move |e| { let path = paths[e.path.clone()].as_bstr(); @@ -526,6 +549,7 @@ impl State { self.path_backing.is_empty(), "BUG: cannot take out backing multiple times" ); + self.invalidate_tree_cache(); std::mem::take(&mut self.path_backing) } @@ -534,8 +558,9 @@ impl State { /// /// The `path` must use the repository-relative, slash-separated [`State`] path format. pub fn entry_mut_by_path_and_stage(&mut self, path: &BStr, stage: entry::Stage) -> Option<&mut Entry> { - self.entry_index_by_path_and_stage(path, stage) - .map(|idx| &mut self.entries[idx]) + let idx = self.entry_index_by_path_and_stage(path, stage)?; + self.invalidate_tree_cache(); + Some(&mut self.entries[idx]) } /// Push a new entry containing `stat`, `id`, `flags` and `mode` and `path` to the end of our storage, without performing @@ -558,6 +583,7 @@ impl State { mode: entry::Mode, path: &BStr, ) { + self.invalidate_tree_cache(); let path = { let path_start = self.path_backing.len(); self.path_backing.push_str(path); @@ -603,6 +629,7 @@ impl State { /// To implement this operation typically, one would rather add [entry::Flags::REMOVE] to each entry to remove /// them when [writing the index](Self::write_to()). pub fn remove_entries(&mut self, mut should_remove: impl FnMut(usize, &BStr, &mut Entry) -> bool) { + self.invalidate_tree_cache(); let mut index = 0; let paths = &self.path_backing; self.entries.retain_mut(|e| { @@ -620,8 +647,15 @@ impl State { /// Note that the memory used for the removed entries paths is not freed, as it's append-only, and /// that some extensions might refer to paths which are now deleted. pub fn remove_entry_at_index(&mut self, index: usize) -> Entry { + self.invalidate_tree_cache(); self.entries.remove(index) } + + fn invalidate_tree_cache(&mut self) { + if let Some(tree) = self.tree.as_mut() { + tree.invalidate_recursively(); + } + } } /// Extensions diff --git a/gix-index/src/extension/mod.rs b/gix-index/src/extension/mod.rs index f16c3b69c88..38ff9abf0b0 100644 --- a/gix-index/src/extension/mod.rs +++ b/gix-index/src/extension/mod.rs @@ -34,6 +34,23 @@ pub struct Tree { pub children: Vec, } +impl Tree { + /// Return true if this tree and all child trees are valid and their tree objects exist in `objects`. + pub fn is_fully_valid(&self, objects: &impl gix_object::Exists) -> bool { + self.num_entries.is_some() + && objects.exists(&self.id) + && self.children.iter().all(|child| child.is_fully_valid(objects)) + } + + /// Invalidate this tree and all child trees. + pub(crate) fn invalidate_recursively(&mut self) { + self.num_entries = None; + for child in &mut self.children { + child.invalidate_recursively(); + } + } +} + /// The link extension to track a shared index. #[derive(Clone)] pub struct Link { diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index 7097b3e0c65..b7c330db9e7 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -294,6 +294,9 @@ pub mod to_tree { { let _span = gix_features::trace::coarse!("gix_index::State::to_tree()"); self.verify_entries()?; + if let Some(tree) = self.tree.as_ref().filter(|tree| tree.is_fully_valid(&objects)) { + return Ok(tree.id); + } let update_tree_cache = self.tree.is_some(); let mut builder = Builder { diff --git a/gix-index/tests/index/access.rs b/gix-index/tests/index/access.rs index 3ec58a8344d..3894f2cabd6 100644 --- a/gix-index/tests/index/access.rs +++ b/gix-index/tests/index/access.rs @@ -283,6 +283,46 @@ fn remove_entries() { file.remove_entries(|_, _, _| unreachable!("should not be called")); } +#[test] +fn entries_mut_invalidates_tree_cache() { + let mut file = Fixture::Generated("v2_more_files").open(); + assert!(file + .tree() + .expect("TREE extension is present") + .children + .iter() + .all(|tree| tree.num_entries.is_some())); + + file.entries_mut()[0].stat.size = 42; + + let tree = file.tree().expect("TREE extension remains present"); + assert_tree_cache_is_invalid(tree); +} + +#[test] +fn entries_mut_keep_tree_cache_preserves_tree_cache() { + let mut file = Fixture::Generated("v2_more_files").open(); + let tree_before = file.tree().expect("TREE extension is present").clone(); + + file.entries_mut_keep_tree_cache()[0].stat.size = 42; + + assert_eq!( + file.tree(), + Some(&tree_before), + "tree-neutral mutations may preserve the TREE extension" + ); +} + +#[test] +fn take_path_backing_invalidates_tree_cache() { + let mut file = Fixture::Generated("v2_more_files").open(); + + let _backing = file.take_path_backing(); + + let tree = file.tree().expect("TREE extension remains present"); + assert_tree_cache_is_invalid(tree); +} + #[test] fn remove_entry_at_index() { let mut file = Fixture::Loose("conflicting-file").open(); @@ -295,6 +335,13 @@ fn remove_entry_at_index() { assert_eq!(file.entries().len(), 0); } +fn assert_tree_cache_is_invalid(tree: &gix_index::extension::Tree) { + assert_eq!(tree.num_entries, None); + for child in &tree.children { + assert_tree_cache_is_invalid(child); + } +} + #[test] fn sort_entries() { let mut file = Fixture::Generated("v4_more_files_IEOT").open(); diff --git a/gix-index/tests/index/file/write.rs b/gix-index/tests/index/file/write.rs index d612b82b2b5..65dd621c781 100644 --- a/gix-index/tests/index/file/write.rs +++ b/gix-index/tests/index/file/write.rs @@ -192,7 +192,9 @@ fn state_comparisons_with_various_extension_configurations() { fn extended_flags_automatically_upgrade_the_version_to_avoid_data_loss() -> crate::Result { let mut expected = Generated("v2").open(); assert_eq!(expected.version(), Version::V2); - expected.entries_mut()[0].flags.insert(entry::Flags::EXTENDED); + expected.entries_mut_keep_tree_cache()[0] + .flags + .insert(entry::Flags::EXTENDED); let mut buf = Vec::new(); let (actual_version, _digest) = expected.write_to(&mut buf, Default::default())?; diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index 328bd52038d..1766643dba7 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -185,6 +185,23 @@ fn to_tree_refreshes_existing_tree_extension() -> crate::Result { Ok(()) } +#[test] +fn to_tree_reuses_fully_valid_tree_extension() -> crate::Result { + let mut index = super::Fixture::Generated("v2").open(); + let original_cached_tree = index.tree().expect("fixture has TREE extension").id; + index.entries_mut_keep_tree_cache()[0].stat.size = 42; + let objects = MemoryDb::exists_all(gix_hash::Kind::Sha1); + + let actual = index.to_tree(&objects, Default::default())?; + + assert_eq!(actual, original_cached_tree); + assert!( + objects.written.borrow().is_empty(), + "a fully-valid TREE cache can be reused without writing objects" + ); + Ok(()) +} + #[test] fn to_tree_does_not_create_missing_tree_extension() -> crate::Result { let worktree_dir = scripted_fixture_read_only("make_index/v2.sh")?; diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index ba0efbfdce0..a4cba34a5ca 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -833,7 +833,12 @@ fn refresh() { assert_eq!( fixture_with_index( "status_changed", - |index| { index.entries_mut().iter_mut().for_each(|e| e.stat = Default::default()) }, + |index| { + index + .entries_mut_keep_tree_cache() + .iter_mut() + .for_each(|e| e.stat = Default::default()) + }, #[cfg(not(windows))] &[ ( @@ -1033,7 +1038,7 @@ fn racy_git() { // change. // This case doesn't happen in the realworld (except for file corruption) but // makes sure we are actually hitting the right codepath. - index.entries_mut()[0].stat.mtime.secs = timestamp; + index.entries_mut_keep_tree_cache()[0].stat.mtime.secs = timestamp; set_file_mtime( worktree.join("content"), FileTime::from_unix_time(i64::from(timestamp), 0), diff --git a/gix/src/status/iter/types.rs b/gix/src/status/iter/types.rs index 13585127839..c4013d745d0 100644 --- a/gix/src/status/iter/types.rs +++ b/gix/src/status/iter/types.rs @@ -92,7 +92,7 @@ impl Outcome { IndexPersistedOrInMemory::InMemory(index) => index.clone(), }; - let entries = index.entries_mut(); + let entries = index.entries_mut_keep_tree_cache(); for (entry_index, change) in changes { let entry = &mut entries[entry_index]; match change { From 40c23234440c45857ac9725a1eb753c75aeb1de7 Mon Sep 17 00:00:00 2001 From: Codex GPT-5 Date: Thu, 30 Apr 2026 10:30:22 +0800 Subject: [PATCH 3/3] Fix PR CI fallout The PR CI lint job failed with clippy::semicolon-if-nothing-returned in gix-status/tests/status/index_as_worktree.rs after the keep-tree-cache call was expanded into a block closure. Add the requested semicolon so the closure has an explicit unit statement. The fast-test matrix also failed after tests regenerated gix-index/tests/fixtures/generated-archives/v2_deeper_tree.tar and git diff reported the archive had changed. The archive still carried an older fixture identity, so regenerate it from the current v2_deeper_tree fixture script identity to prevent CI from rewriting it. --- gix-index/benches/to_tree.rs | 7 ++- .../generated-archives/v2_deeper_tree.tar | Bin 78336 -> 81408 bytes .../v2_deeper_tree_sha256.tar | Bin 0 -> 81408 bytes gix-index/tests/index/access.rs | 13 +++-- gix-index/tests/index/init.rs | 55 ++++++++++-------- gix-status/tests/status/index_as_worktree.rs | 2 +- 6 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 gix-index/tests/fixtures/generated-archives/v2_deeper_tree_sha256.tar diff --git a/gix-index/benches/to_tree.rs b/gix-index/benches/to_tree.rs index 5006772cd83..c0b74a16066 100644 --- a/gix-index/benches/to_tree.rs +++ b/gix-index/benches/to_tree.rs @@ -79,9 +79,10 @@ fn memory_db() -> MemoryDb { } fn missing_ok() -> gix_index::init::to_tree::Options { - let mut options = gix_index::init::to_tree::Options::default(); - options.missing_ok = true; - options + gix_index::init::to_tree::Options { + missing_ok: true, + ..Default::default() + } } fn repeated_id(byte: u8) -> gix_hash::ObjectId { diff --git a/gix-index/tests/fixtures/generated-archives/v2_deeper_tree.tar b/gix-index/tests/fixtures/generated-archives/v2_deeper_tree.tar index c5ec346f6f07407cc1e48138ff9d2ddcc88c1ba3..815d09e29987be3c6892d6dc594aa2df154c0f82 100644 GIT binary patch delta 2755 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 RzwBe6kAXf0zDqIi{{bZ3LI?l= literal 0 HcmV?d00001 diff --git a/gix-index/tests/index/access.rs b/gix-index/tests/index/access.rs index 3894f2cabd6..9613eb159ff 100644 --- a/gix-index/tests/index/access.rs +++ b/gix-index/tests/index/access.rs @@ -286,12 +286,13 @@ fn remove_entries() { #[test] fn entries_mut_invalidates_tree_cache() { let mut file = Fixture::Generated("v2_more_files").open(); - assert!(file - .tree() - .expect("TREE extension is present") - .children - .iter() - .all(|tree| tree.num_entries.is_some())); + assert!( + file.tree() + .expect("TREE extension is present") + .children + .iter() + .all(|tree| tree.num_entries.is_some()) + ); file.entries_mut()[0].stat.size = 42; diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index 1766643dba7..7aa2bf75f85 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -96,8 +96,9 @@ fn to_tree_roundtrips_to_fixture_tree() -> crate::Result { let worktree_dir = scripted_fixture_read_only(fixture)?; let expected_tree_id = tree_id(&worktree_dir); let git_dir = worktree_dir.join(".git"); - let mut index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default())?; - let objects = memory_db(); + let object_hash = gix_testtools::object_hash(); + let mut index = gix_index::File::at(git_dir.join("index"), object_hash, false, Default::default())?; + let objects = memory_db(object_hash); let actual_tree_id = index.to_tree(&objects, missing_ok())?; assert_eq!(actual_tree_id, expected_tree_id, "tree mismatch in {fixture:?}"); @@ -108,7 +109,7 @@ fn to_tree_roundtrips_to_fixture_tree() -> crate::Result { #[test] fn to_tree_empty_index_is_empty_tree() -> crate::Result { let mut state = State::new(gix_hash::Kind::Sha1); - let objects = memory_db(); + let objects = memory_db(gix_hash::Kind::Sha1); let actual = state.to_tree(&objects, Default::default())?; @@ -120,7 +121,7 @@ fn to_tree_empty_index_is_empty_tree() -> crate::Result { #[test] fn to_tree_rejects_unmerged_entries() { let mut index = super::Fixture::Loose("conflicting-file").open(); - let objects = memory_db(); + let objects = memory_db(gix_hash::Kind::Sha1); let err = index.to_tree(&objects, Default::default()).unwrap_err(); @@ -130,7 +131,7 @@ fn to_tree_rejects_unmerged_entries() { #[test] fn to_tree_rejects_file_directory_conflicts() { let mut state = state_with_entries(["a", "a.b", "a/b"]); - let objects = memory_db(); + let objects = memory_db(gix_hash::Kind::Sha1); let err = state.to_tree(&objects, missing_ok()).unwrap_err(); @@ -143,7 +144,7 @@ fn to_tree_rejects_file_directory_conflicts() { #[test] fn to_tree_rejects_invalid_components() { let mut state = state_with_entries(["a//b"]); - let objects = memory_db(); + let objects = memory_db(gix_hash::Kind::Sha1); let err = state.to_tree(&objects, Default::default()).unwrap_err(); @@ -153,13 +154,15 @@ fn to_tree_rejects_invalid_components() { #[test] fn to_tree_rejects_missing_objects_unless_allowed() -> crate::Result { let mut state = state_with_entries(["file"]); - let objects = memory_db(); + let objects = memory_db(gix_hash::Kind::Sha1); let err = state.to_tree(&objects, Default::default()).unwrap_err(); assert!(matches!(err, gix_index::init::to_tree::Error::MissingObject { .. })); - let mut options = gix_index::init::to_tree::Options::default(); - options.missing_ok = true; + let options = gix_index::init::to_tree::Options { + missing_ok: true, + ..Default::default() + }; let actual = state.to_tree(&objects, options)?; assert_ne!(actual, gix_hash::Kind::Sha1.null()); Ok(()) @@ -169,8 +172,8 @@ fn to_tree_rejects_missing_objects_unless_allowed() -> crate::Result { fn to_tree_refreshes_existing_tree_extension() -> crate::Result { let mut index = super::Fixture::Generated("v2").open(); let original_cached_tree = index.tree().expect("fixture has TREE extension").id; - index.entries_mut()[0].id = repeated_id(b'b'); - let objects = memory_db(); + index.entries_mut()[0].id = repeated_id(b'b', gix_testtools::object_hash()); + let objects = memory_db(gix_testtools::object_hash()); let actual = index.to_tree(&objects, missing_ok())?; @@ -187,16 +190,19 @@ fn to_tree_refreshes_existing_tree_extension() -> crate::Result { #[test] fn to_tree_reuses_fully_valid_tree_extension() -> crate::Result { - let mut index = super::Fixture::Generated("v2").open(); + let worktree_dir = scripted_fixture_read_only("make_index/v2.sh")?; + let git_dir = worktree_dir.join(".git"); + let object_hash = gix_testtools::object_hash(); + let mut index = gix_index::File::at(git_dir.join("index"), object_hash, false, Default::default())?; let original_cached_tree = index.tree().expect("fixture has TREE extension").id; index.entries_mut_keep_tree_cache()[0].stat.size = 42; - let objects = MemoryDb::exists_all(gix_hash::Kind::Sha1); + let objects = gix_odb::memory::Proxy::new(odb_at(git_dir.join("objects"))?, object_hash); let actual = index.to_tree(&objects, Default::default())?; assert_eq!(actual, original_cached_tree); assert!( - objects.written.borrow().is_empty(), + objects.num_objects_in_memory() == 0, "a fully-valid TREE cache can be reused without writing objects" ); Ok(()) @@ -205,10 +211,10 @@ fn to_tree_reuses_fully_valid_tree_extension() -> crate::Result { #[test] fn to_tree_does_not_create_missing_tree_extension() -> crate::Result { let worktree_dir = scripted_fixture_read_only("make_index/v2.sh")?; - let odb = gix_odb::at(worktree_dir.join(".git").join("objects"))?; + let odb = odb_at(worktree_dir.join(".git").join("objects"))?; let mut state = State::from_tree(&tree_id(&worktree_dir), &odb, Default::default())?; assert!(state.tree().is_none()); - let objects = memory_db(); + let objects = memory_db(gix_testtools::object_hash()); state.to_tree(&objects, missing_ok())?; @@ -273,7 +279,7 @@ fn state_with_entries(paths: [&str; N]) -> State { for path in paths { state.dangerously_push_entry( Default::default(), - repeated_id(b'a'), + repeated_id(b'a', gix_hash::Kind::Sha1), gix_index::entry::Flags::empty(), gix_index::entry::Mode::FILE, path.as_bytes().as_bstr(), @@ -283,18 +289,19 @@ fn state_with_entries(paths: [&str; N]) -> State { state } -fn repeated_id(byte: u8) -> gix_hash::ObjectId { - gix_hash::ObjectId::from_hex(&vec![byte; gix_hash::Kind::Sha1.len_in_hex()]).expect("valid hex") +fn repeated_id(byte: u8, object_hash: gix_hash::Kind) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_hex(&vec![byte; object_hash.len_in_hex()]).expect("valid hex") } type MemoryDb = gix_odb::memory::Proxy; -fn memory_db() -> MemoryDb { - gix_odb::memory::Proxy::new(gix_object::find::Never, gix_hash::Kind::Sha1) +fn memory_db(object_hash: gix_hash::Kind) -> MemoryDb { + gix_odb::memory::Proxy::new(gix_object::find::Never, object_hash) } fn missing_ok() -> gix_index::init::to_tree::Options { - let mut options = gix_index::init::to_tree::Options::default(); - options.missing_ok = true; - options + gix_index::init::to_tree::Options { + missing_ok: true, + ..Default::default() + } } diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index a4cba34a5ca..80b5574b2f0 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -837,7 +837,7 @@ fn refresh() { index .entries_mut_keep_tree_cache() .iter_mut() - .for_each(|e| e.stat = Default::default()) + .for_each(|e| e.stat = Default::default()); }, #[cfg(not(windows))] &[