From e3c445ea0969418d2b11204281704f5759c17672 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 10:51:37 +0100 Subject: [PATCH 01/17] fix: `gix tree entries` usees `find_header()` to show the object size. --- gitoxide-core/src/repository/tree.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gitoxide-core/src/repository/tree.rs b/gitoxide-core/src/repository/tree.rs index 04881317737..122891b2519 100644 --- a/gitoxide-core/src/repository/tree.rs +++ b/gitoxide-core/src/repository/tree.rs @@ -91,7 +91,7 @@ mod entries { fn visit_nontree(&mut self, entry: &EntryRef<'_>) -> Action { let size = self .repo - .and_then(|repo| repo.find_object(entry.oid).map(|o| o.data.len()).ok()); + .and_then(|repo| repo.find_header(entry.oid).map(|h| h.size()).ok()); if let Some(out) = &mut self.out { format_entry(out, entry, self.path.as_bstr(), size).ok(); } @@ -163,9 +163,7 @@ pub fn entries( &mut out, &entry.inner, entry.inner.filename, - extended - .then(|| entry.id().object().map(|o| o.data.len())) - .transpose()?, + extended.then(|| entry.id().header().map(|o| o.size())).transpose()?, )?; } } @@ -182,7 +180,7 @@ fn format_entry( mut out: impl io::Write, entry: &gix::objs::tree::EntryRef<'_>, filename: &gix::bstr::BStr, - size: Option, + size: Option, ) -> std::io::Result<()> { use gix::objs::tree::EntryKind::*; writeln!( From 8141765fcfaebe7eab28cb41be5cae7bb948d46a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 15:06:47 +0100 Subject: [PATCH 02/17] feat: add `TreeRefIter::offset_to_next_entry()`. Add a function to help resume the iterator without holding onto the data directly. --- gix-object/src/tree/ref_iter.rs | 18 ++++++++++++++++-- gix-object/tests/object/tree/iter.rs | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/gix-object/src/tree/ref_iter.rs b/gix-object/src/tree/ref_iter.rs index f13d9500383..ec86bc75579 100644 --- a/gix-object/src/tree/ref_iter.rs +++ b/gix-object/src/tree/ref_iter.rs @@ -1,8 +1,7 @@ +use crate::{tree, tree::EntryRef, TreeRef, TreeRefIter}; use bstr::BStr; use winnow::{error::ParserError, prelude::*}; -use crate::{tree, tree::EntryRef, TreeRef, TreeRefIter}; - impl<'a> TreeRefIter<'a> { /// Instantiate an iterator from the given tree data. pub fn from_bytes(data: &'a [u8]) -> TreeRefIter<'a> { @@ -126,6 +125,21 @@ impl<'a> TreeRefIter<'a> { pub fn entries(self) -> Result>, crate::decode::Error> { self.collect() } + + /// Return the offset in bytes that our data advanced from `buf`, the original buffer + /// to the beginning of the data of the tree. + /// + /// Then the tree-iteration can be resumed at the entry that would otherwise be returned next. + pub fn offset_to_next_entry(&self, buf: &[u8]) -> usize { + let before = (*buf).as_ptr(); + let after = (*self.data).as_ptr(); + + debug_assert!( + before <= after, + "`TreeRefIter::offset_to_next_entry(): {after:?} <= {before:?}) violated" + ); + (after as usize - before as usize) / std::mem::size_of::() + } } impl<'a> Iterator for TreeRefIter<'a> { diff --git a/gix-object/tests/object/tree/iter.rs b/gix-object/tests/object/tree/iter.rs index 1f597499238..b4717493a29 100644 --- a/gix-object/tests/object/tree/iter.rs +++ b/gix-object/tests/object/tree/iter.rs @@ -23,6 +23,24 @@ fn error_handling() { ); } +#[test] +fn offset_to_next_entry() { + let buf = fixture_name("tree", "everything.tree"); + let mut iter = TreeRefIter::from_bytes(&buf); + assert_eq!(iter.offset_to_next_entry(&buf), 0, "first entry is always at 0"); + iter.next(); + + let actual = iter.offset_to_next_entry(&buf); + assert_eq!(actual, 31, "now the offset increases"); + assert_eq!( + TreeRefIter::from_bytes(&buf[actual..]) + .next() + .map(|e| e.unwrap().filename), + iter.next().map(|e| e.unwrap().filename), + "One can now start the iteration at a certain entry" + ); +} + #[test] fn everything() -> crate::Result { assert_eq!( From 22f8939d862c74f2f55c74fded2f066a6d6536a1 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 10:57:37 +0100 Subject: [PATCH 03/17] feat!: `tree::depthfirst()` traversal A depth-first traversal yields the `.git/index` order. It's a breaking change as the `Visitor` trait gets another way to pop a tracked path, suitable for the stack used for depth first. --- Cargo.lock | 1 + gix-traverse/src/tree/breadthfirst.rs | 11 +- gix-traverse/src/tree/depthfirst.rs | 112 ++++++ gix-traverse/src/tree/mod.rs | 18 +- gix-traverse/src/tree/recorder.rs | 9 + gix-traverse/tests/Cargo.toml | 5 +- ...ke_traversal_repo_for_trees_depthfirst.tar | Bin 0 -> 71168 bytes ...ake_traversal_repo_for_trees_depthfirst.sh | 14 + .../tests/{ => traverse}/commit/mod.rs | 0 .../tests/{ => traverse}/commit/simple.rs | 0 .../tests/{ => traverse}/commit/topo.rs | 0 .../tests/{traverse.rs => traverse/main.rs} | 0 gix-traverse/tests/traverse/tree.rs | 378 ++++++++++++++++++ gix-traverse/tests/tree/mod.rs | 149 ------- 14 files changed, 540 insertions(+), 157 deletions(-) create mode 100644 gix-traverse/src/tree/depthfirst.rs create mode 100644 gix-traverse/tests/fixtures/generated-archives/make_traversal_repo_for_trees_depthfirst.tar create mode 100755 gix-traverse/tests/fixtures/make_traversal_repo_for_trees_depthfirst.sh rename gix-traverse/tests/{ => traverse}/commit/mod.rs (100%) rename gix-traverse/tests/{ => traverse}/commit/simple.rs (100%) rename gix-traverse/tests/{ => traverse}/commit/topo.rs (100%) rename gix-traverse/tests/{traverse.rs => traverse/main.rs} (100%) create mode 100644 gix-traverse/tests/traverse/tree.rs delete mode 100644 gix-traverse/tests/tree/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 365fb927fa5..fe6eaeae126 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2808,6 +2808,7 @@ dependencies = [ "gix-odb", "gix-testtools", "gix-traverse 0.43.1", + "insta", ] [[package]] diff --git a/gix-traverse/src/tree/breadthfirst.rs b/gix-traverse/src/tree/breadthfirst.rs index 441d4c50d38..5a0342c337d 100644 --- a/gix-traverse/src/tree/breadthfirst.rs +++ b/gix-traverse/src/tree/breadthfirst.rs @@ -2,7 +2,8 @@ use std::collections::VecDeque; use gix_hash::ObjectId; -/// The error is part of the item returned by the [`traverse()`][impl_::traverse()] function. +/// The error is part of the item returned by the [`breadthfirst()`](crate::tree::breadthfirst()) and +///[`depthfirst()`](crate::tree::depthfirst()) functions. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { @@ -28,7 +29,7 @@ impl State { } } -pub(crate) mod impl_ { +pub(super) mod function { use std::borrow::BorrowMut; use gix_object::{FindExt, TreeRefIter}; @@ -38,6 +39,8 @@ pub(crate) mod impl_ { /// Start a breadth-first iteration over the `root` trees entries. /// + /// Note that non-trees will be listed first, so the natural order of entries within a tree is lost. + /// /// * `root` /// * the tree to iterate in a nested fashion. /// * `state` - all state used for the iteration. If multiple iterations are performed, allocations can be minimized by reusing @@ -46,9 +49,9 @@ pub(crate) mod impl_ { /// an iterator over entries if the object is present and is a tree. Caching should be implemented within this function /// as needed. The return value is `Option` which degenerates all error information. Not finding a commit should also /// be considered an errors as all objects in the tree DAG should be present in the database. Hence [`Error::Find`] should - /// be escalated into a more specific error if its encountered by the caller. + /// be escalated into a more specific error if it's encountered by the caller. /// * `delegate` - A way to observe entries and control the iteration while allowing the optimizer to let you pay only for what you use. - pub fn traverse( + pub fn breadthfirst( root: TreeRefIter<'_>, mut state: StateMut, objects: Find, diff --git a/gix-traverse/src/tree/depthfirst.rs b/gix-traverse/src/tree/depthfirst.rs new file mode 100644 index 00000000000..5b0c9eab79c --- /dev/null +++ b/gix-traverse/src/tree/depthfirst.rs @@ -0,0 +1,112 @@ +pub use super::breadthfirst::Error; + +/// The state used and potentially shared by multiple tree traversals, reusing memory. +#[derive(Default, Clone)] +pub struct State { + freelist: Vec>, +} + +impl State { + /// Pop one empty buffer from the free-list. + pub fn pop_buf(&mut self) -> Vec { + match self.freelist.pop() { + None => Vec::new(), + Some(mut buf) => { + buf.clear(); + buf + } + } + } + + /// Make `buf` available for re-use with [`Self::pop_buf()`]. + pub fn push_buf(&mut self, buf: Vec) { + self.freelist.push(buf); + } +} + +pub(super) mod function { + use super::{Error, State}; + use crate::tree::visit::Action; + use crate::tree::Visit; + use gix_hash::ObjectId; + use gix_object::{FindExt, TreeRefIter}; + use std::borrow::BorrowMut; + + /// A depth-first traversal of the `root` tree, that preserves the natural order of a tree while immediately descending + /// into sub-trees. + /// + /// `state` can be passed to re-use memory during multiple invocations. + pub fn depthfirst( + root: ObjectId, + mut state: StateMut, + objects: Find, + delegate: &mut V, + ) -> Result<(), Error> + where + Find: gix_object::Find, + StateMut: BorrowMut, + V: Visit, + { + enum Machine { + GetTree(ObjectId), + Iterate { + tree_buf: Vec, + byte_offset_to_next_entry: usize, + }, + } + + let state = state.borrow_mut(); + let mut stack = vec![Machine::GetTree(root)]; + 'outer: while let Some(item) = stack.pop() { + match item { + Machine::GetTree(id) => { + let mut buf = state.pop_buf(); + objects.find_tree_iter(&id, &mut buf)?; + stack.push(Machine::Iterate { + tree_buf: buf, + byte_offset_to_next_entry: 0, + }); + } + Machine::Iterate { + tree_buf: buf, + byte_offset_to_next_entry, + } => { + let mut iter = TreeRefIter::from_bytes(&buf[byte_offset_to_next_entry..]); + delegate.pop_back_tracked_path_and_set_current(); + while let Some(entry) = iter.next() { + let entry = entry?; + if entry.mode.is_tree() { + delegate.push_path_component(entry.filename); + let res = delegate.visit_tree(&entry); + delegate.pop_path_component(); + match res { + Action::Continue => {} + Action::Cancel => break 'outer, + Action::Skip => continue, + } + + delegate.push_back_tracked_path_component("".into()); + delegate.push_back_tracked_path_component(entry.filename); + let recurse_tree = Machine::GetTree(entry.oid.to_owned()); + let continue_at_next_entry = Machine::Iterate { + byte_offset_to_next_entry: iter.offset_to_next_entry(&buf), + tree_buf: buf, + }; + stack.push(continue_at_next_entry); + stack.push(recurse_tree); + continue 'outer; + } else { + delegate.push_path_component(entry.filename); + if let Action::Cancel = delegate.visit_nontree(&entry) { + break 'outer; + } + delegate.pop_path_component(); + } + } + state.push_buf(buf); + } + } + } + Ok(()) + } +} diff --git a/gix-traverse/src/tree/mod.rs b/gix-traverse/src/tree/mod.rs index 6a74ada28f3..85258f4301f 100644 --- a/gix-traverse/src/tree/mod.rs +++ b/gix-traverse/src/tree/mod.rs @@ -5,9 +5,19 @@ use gix_object::bstr::{BStr, BString}; /// A trait to allow responding to a traversal designed to observe all entries in a tree, recursively while keeping track of /// paths if desired. pub trait Visit { - /// Sets the full path path in front of the queue so future calls to push and pop components affect it instead. + /// Sets the full path in the back of the queue so future calls to push and pop components affect it instead. + /// + /// Note that the first call is made without an accompanying call to [`Self::push_back_tracked_path_component()`] + /// + /// This is used by the depth-first traversal of trees. + fn pop_back_tracked_path_and_set_current(&mut self); + /// Sets the full path in front of the queue so future calls to push and pop components affect it instead. + /// + /// This is used by the breadth-first traversal of trees. fn pop_front_tracked_path_and_set_current(&mut self); /// Append a `component` to the end of a path, which may be empty. + /// + /// If `component` is empty, store the current path. fn push_back_tracked_path_component(&mut self, component: &BStr); /// Append a `component` to the end of a path, which may be empty. fn push_path_component(&mut self, component: &BStr); @@ -66,4 +76,8 @@ pub mod recorder; /// pub mod breadthfirst; -pub use breadthfirst::impl_::traverse as breadthfirst; +pub use breadthfirst::function::breadthfirst; + +/// +pub mod depthfirst; +pub use depthfirst::function::depthfirst; diff --git a/gix-traverse/src/tree/recorder.rs b/gix-traverse/src/tree/recorder.rs index 6447ffb7133..029751295b8 100644 --- a/gix-traverse/src/tree/recorder.rs +++ b/gix-traverse/src/tree/recorder.rs @@ -62,6 +62,9 @@ impl Recorder { } fn push_element(&mut self, name: &BStr) { + if name.is_empty() { + return; + } if !self.path.is_empty() { self.path.push(b'/'); } @@ -92,6 +95,12 @@ impl Recorder { } impl Visit for Recorder { + fn pop_back_tracked_path_and_set_current(&mut self) { + if let Some(Location::Path) = self.location { + self.path = self.path_deque.pop_back().unwrap_or_default(); + } + } + fn pop_front_tracked_path_and_set_current(&mut self) { if let Some(Location::Path) = self.location { self.path = self diff --git a/gix-traverse/tests/Cargo.toml b/gix-traverse/tests/Cargo.toml index 24c49ed843f..83c3860b992 100644 --- a/gix-traverse/tests/Cargo.toml +++ b/gix-traverse/tests/Cargo.toml @@ -11,10 +11,11 @@ edition = "2021" rust-version = "1.65" [[test]] -name = "test" -path = "traverse.rs" +name = "traverse" +path = "traverse/main.rs" [dev-dependencies] +insta = "1.40.0" gix-traverse = { path = ".." } gix-testtools = { path = "../../tests/tools" } gix-odb = { path = "../../gix-odb" } diff --git a/gix-traverse/tests/fixtures/generated-archives/make_traversal_repo_for_trees_depthfirst.tar b/gix-traverse/tests/fixtures/generated-archives/make_traversal_repo_for_trees_depthfirst.tar new file mode 100644 index 0000000000000000000000000000000000000000..4486330c739f42f312f889ba544b9d2f92a3c6d5 GIT binary patch literal 71168 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 CcdJSO literal 0 HcmV?d00001 diff --git a/gix-traverse/tests/fixtures/make_traversal_repo_for_trees_depthfirst.sh b/gix-traverse/tests/fixtures/make_traversal_repo_for_trees_depthfirst.sh new file mode 100755 index 00000000000..ae1a493eea3 --- /dev/null +++ b/gix-traverse/tests/fixtures/make_traversal_repo_for_trees_depthfirst.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q + +git checkout -q -b main +touch a b c +mkdir d e f +touch d/a e/b f/c f/z +mkdir f/ISSUE_TEMPLATE +touch f/ISSUE_TEMPLATE/x f/FUNDING.yml f/dependabot.yml + +git add . +git commit -q -m c1 diff --git a/gix-traverse/tests/commit/mod.rs b/gix-traverse/tests/traverse/commit/mod.rs similarity index 100% rename from gix-traverse/tests/commit/mod.rs rename to gix-traverse/tests/traverse/commit/mod.rs diff --git a/gix-traverse/tests/commit/simple.rs b/gix-traverse/tests/traverse/commit/simple.rs similarity index 100% rename from gix-traverse/tests/commit/simple.rs rename to gix-traverse/tests/traverse/commit/simple.rs diff --git a/gix-traverse/tests/commit/topo.rs b/gix-traverse/tests/traverse/commit/topo.rs similarity index 100% rename from gix-traverse/tests/commit/topo.rs rename to gix-traverse/tests/traverse/commit/topo.rs diff --git a/gix-traverse/tests/traverse.rs b/gix-traverse/tests/traverse/main.rs similarity index 100% rename from gix-traverse/tests/traverse.rs rename to gix-traverse/tests/traverse/main.rs diff --git a/gix-traverse/tests/traverse/tree.rs b/gix-traverse/tests/traverse/tree.rs new file mode 100644 index 00000000000..048ade4a205 --- /dev/null +++ b/gix-traverse/tests/traverse/tree.rs @@ -0,0 +1,378 @@ +fn db() -> crate::Result { + named_db("make_traversal_repo_for_trees.sh") +} + +fn named_db(name: &str) -> crate::Result { + let dir = gix_testtools::scripted_fixture_read_only_standalone(name)?; + let db = gix_odb::at(dir.join(".git").join("objects"))?; + Ok(db) +} + +mod depthfirst { + use crate::hex_to_id; + use crate::tree::{db, named_db}; + use gix_object::FindExt; + use gix_traverse::tree; + use gix_traverse::tree::recorder::Location; + + #[test] + fn full_path_and_filename() -> crate::Result { + let db = db()?; + let mut state = gix_traverse::tree::depthfirst::State::default(); + let mut buf = state.pop_buf(); + let mut recorder = tree::Recorder::default(); + let tree = db + .find_commit(&hex_to_id("85df34aa34848b8138b2b3dcff5fb5c2b734e0ce"), &mut buf)? + .tree(); + + gix_traverse::tree::depthfirst(tree, &mut state, &db, &mut recorder)?; + insta::assert_debug_snapshot!(recorder.records, @r#" + [ + Entry { + mode: EntryMode(0o100644), + filepath: "a", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "b", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "c", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o40000), + filepath: "d", + oid: Sha1(496d6428b9cf92981dc9495211e6e1120fb6f2ba), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "d/a", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o40000), + filepath: "e", + oid: Sha1(4277b6e69d25e5efa77c455340557b384a4c018a), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "e/b", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o40000), + filepath: "f", + oid: Sha1(70fb16fc77b03e16acb4a5b1a6caf79ba302919a), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "f/c", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o40000), + filepath: "f/d", + oid: Sha1(5805b676e247eb9a8046ad0c4d249cd2fb2513df), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "f/d/x", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "f/z", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + + recorder.records.clear(); + recorder = recorder.track_location(Some(Location::FileName)); + gix_traverse::tree::depthfirst(tree, state, &db, &mut recorder)?; + insta::assert_debug_snapshot!(recorder.records, @r#" + [ + Entry { + mode: EntryMode(0o100644), + filepath: "a", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "b", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "c", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o40000), + filepath: "d", + oid: Sha1(496d6428b9cf92981dc9495211e6e1120fb6f2ba), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "a", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o40000), + filepath: "e", + oid: Sha1(4277b6e69d25e5efa77c455340557b384a4c018a), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "b", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o40000), + filepath: "f", + oid: Sha1(70fb16fc77b03e16acb4a5b1a6caf79ba302919a), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "c", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o40000), + filepath: "d", + oid: Sha1(5805b676e247eb9a8046ad0c4d249cd2fb2513df), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "x", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "z", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + Ok(()) + } + + #[test] + fn more_difficult_fixture() -> crate::Result { + let db = named_db("make_traversal_repo_for_trees_depthfirst.sh")?; + let mut state = gix_traverse::tree::depthfirst::State::default(); + let mut buf = state.pop_buf(); + let mut recorder = tree::Recorder::default(); + let tree = db + .find_commit(&hex_to_id("fe63a8a9fb7c27c089835aae92cbda675523803a"), &mut buf)? + .tree(); + + gix_traverse::tree::depthfirst(tree, &mut state, &db, &mut recorder)?; + insta::assert_debug_snapshot!(recorder.records.into_iter().filter(|e| e.mode.is_no_tree()).collect::>(), @r#" + [ + Entry { + mode: EntryMode(0o100644), + filepath: "a", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "b", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "c", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "d/a", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "e/b", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "f/FUNDING.yml", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "f/ISSUE_TEMPLATE/x", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "f/c", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "f/dependabot.yml", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Entry { + mode: EntryMode(0o100644), + filepath: "f/z", + oid: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + Ok(()) + } +} + +mod breadthfirst { + use crate::hex_to_id; + use crate::tree::db; + use gix_object::bstr::BString; + use gix_odb::pack::FindExt; + use gix_traverse::tree; + use gix_traverse::tree::recorder::Location; + + #[test] + fn full_path() -> crate::Result { + let db = db()?; + let mut buf = Vec::new(); + let mut buf2 = Vec::new(); + let mut commit = db + .find_commit_iter(&hex_to_id("85df34aa34848b8138b2b3dcff5fb5c2b734e0ce"), &mut buf)? + .0; + // Full paths - that's the default. + let mut recorder = tree::Recorder::default(); + gix_traverse::tree::breadthfirst( + db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? + .0, + tree::breadthfirst::State::default(), + &db, + &mut recorder, + )?; + + use gix_object::tree::EntryKind::*; + use gix_traverse::tree::recorder::Entry; + assert_eq!( + recorder.records, + vec![ + Entry { + mode: Blob.into(), + filepath: "a".into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") + }, + Entry { + mode: Blob.into(), + filepath: "b".into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") + }, + Entry { + mode: Blob.into(), + filepath: "c".into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") + }, + Entry { + mode: Tree.into(), + filepath: "d".into(), + oid: hex_to_id("496d6428b9cf92981dc9495211e6e1120fb6f2ba") + }, + Entry { + mode: Tree.into(), + filepath: "e".into(), + oid: hex_to_id("4277b6e69d25e5efa77c455340557b384a4c018a") + }, + Entry { + mode: Tree.into(), + filepath: "f".into(), + oid: hex_to_id("70fb16fc77b03e16acb4a5b1a6caf79ba302919a") + }, + Entry { + mode: Blob.into(), + filepath: "d/a".into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") + }, + Entry { + mode: Blob.into(), + filepath: "e/b".into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") + }, + Entry { + mode: Blob.into(), + filepath: "f/c".into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") + }, + Entry { + mode: Tree.into(), + filepath: "f/d".into(), + oid: hex_to_id("5805b676e247eb9a8046ad0c4d249cd2fb2513df") + }, + Entry { + mode: Blob.into(), + filepath: "f/z".into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") + }, + Entry { + mode: Blob.into(), + filepath: "f/d/x".into(), + oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") + } + ] + ); + Ok(()) + } + + #[test] + fn filename_only() -> crate::Result<()> { + let db = db()?; + let mut buf = Vec::new(); + let mut buf2 = Vec::new(); + let mut commit = db + .find_commit_iter(&hex_to_id("85df34aa34848b8138b2b3dcff5fb5c2b734e0ce"), &mut buf)? + .0; + let mut recorder = tree::Recorder::default().track_location(Some(Location::FileName)); + gix_traverse::tree::breadthfirst( + db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? + .0, + tree::breadthfirst::State::default(), + &db, + &mut recorder, + )?; + + assert_eq!( + recorder.records.into_iter().map(|e| e.filepath).collect::>(), + ["a", "b", "c", "d", "e", "f", "a", "b", "c", "d", "z", "x"] + .into_iter() + .map(BString::from) + .collect::>() + ); + Ok(()) + } + + #[test] + fn no_location() -> crate::Result<()> { + let db = db()?; + let mut buf = Vec::new(); + let mut buf2 = Vec::new(); + let mut commit = db + .find_commit_iter(&hex_to_id("85df34aa34848b8138b2b3dcff5fb5c2b734e0ce"), &mut buf)? + .0; + let mut recorder = tree::Recorder::default().track_location(None); + gix_traverse::tree::breadthfirst( + db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? + .0, + tree::breadthfirst::State::default(), + &db, + &mut recorder, + )?; + + for path in recorder.records.into_iter().map(|e| e.filepath) { + assert_eq!(path, "", "path should be empty as it's not tracked at all"); + } + Ok(()) + } +} diff --git a/gix-traverse/tests/tree/mod.rs b/gix-traverse/tests/tree/mod.rs deleted file mode 100644 index 0200abf1690..00000000000 --- a/gix-traverse/tests/tree/mod.rs +++ /dev/null @@ -1,149 +0,0 @@ -use gix_object::bstr::BString; -use gix_odb::pack::FindExt; -use gix_traverse::{tree, tree::recorder::Location}; - -use crate::hex_to_id; - -fn db() -> crate::Result { - let dir = gix_testtools::scripted_fixture_read_only_standalone("make_traversal_repo_for_trees.sh")?; - let db = gix_odb::at(dir.join(".git").join("objects"))?; - Ok(db) -} - -#[test] -fn breadth_first_full_path() -> crate::Result<()> { - let db = db()?; - let mut buf = Vec::new(); - let mut buf2 = Vec::new(); - let mut commit = db - .find_commit_iter(&hex_to_id("85df34aa34848b8138b2b3dcff5fb5c2b734e0ce"), &mut buf)? - .0; - // Full paths - that's the default. - let mut recorder = tree::Recorder::default(); - gix_traverse::tree::breadthfirst( - db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? - .0, - tree::breadthfirst::State::default(), - &db, - &mut recorder, - )?; - - use gix_object::tree::EntryKind::*; - use gix_traverse::tree::recorder::Entry; - assert_eq!( - recorder.records, - vec![ - Entry { - mode: Blob.into(), - filepath: "a".into(), - oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") - }, - Entry { - mode: Blob.into(), - filepath: "b".into(), - oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") - }, - Entry { - mode: Blob.into(), - filepath: "c".into(), - oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") - }, - Entry { - mode: Tree.into(), - filepath: "d".into(), - oid: hex_to_id("496d6428b9cf92981dc9495211e6e1120fb6f2ba") - }, - Entry { - mode: Tree.into(), - filepath: "e".into(), - oid: hex_to_id("4277b6e69d25e5efa77c455340557b384a4c018a") - }, - Entry { - mode: Tree.into(), - filepath: "f".into(), - oid: hex_to_id("70fb16fc77b03e16acb4a5b1a6caf79ba302919a") - }, - Entry { - mode: Blob.into(), - filepath: "d/a".into(), - oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") - }, - Entry { - mode: Blob.into(), - filepath: "e/b".into(), - oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") - }, - Entry { - mode: Blob.into(), - filepath: "f/c".into(), - oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") - }, - Entry { - mode: Tree.into(), - filepath: "f/d".into(), - oid: hex_to_id("5805b676e247eb9a8046ad0c4d249cd2fb2513df") - }, - Entry { - mode: Blob.into(), - filepath: "f/z".into(), - oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") - }, - Entry { - mode: Blob.into(), - filepath: "f/d/x".into(), - oid: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") - } - ] - ); - Ok(()) -} - -#[test] -fn breadth_first_filename_only() -> crate::Result<()> { - let db = db()?; - let mut buf = Vec::new(); - let mut buf2 = Vec::new(); - let mut commit = db - .find_commit_iter(&hex_to_id("85df34aa34848b8138b2b3dcff5fb5c2b734e0ce"), &mut buf)? - .0; - let mut recorder = tree::Recorder::default().track_location(Some(Location::FileName)); - gix_traverse::tree::breadthfirst( - db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? - .0, - tree::breadthfirst::State::default(), - &db, - &mut recorder, - )?; - - assert_eq!( - recorder.records.into_iter().map(|e| e.filepath).collect::>(), - ["a", "b", "c", "d", "e", "f", "a", "b", "c", "d", "z", "x"] - .into_iter() - .map(BString::from) - .collect::>() - ); - Ok(()) -} - -#[test] -fn breadth_first_no_location() -> crate::Result<()> { - let db = db()?; - let mut buf = Vec::new(); - let mut buf2 = Vec::new(); - let mut commit = db - .find_commit_iter(&hex_to_id("85df34aa34848b8138b2b3dcff5fb5c2b734e0ce"), &mut buf)? - .0; - let mut recorder = tree::Recorder::default().track_location(None); - gix_traverse::tree::breadthfirst( - db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? - .0, - tree::breadthfirst::State::default(), - &db, - &mut recorder, - )?; - - for path in recorder.records.into_iter().map(|e| e.filepath) { - assert_eq!(path, "", "path should be empty as it's not tracked at all"); - } - Ok(()) -} From 1de4e70569cd7c3bfcc9094b7591699b5b419608 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 16:11:56 +0100 Subject: [PATCH 04/17] adapt to changes in `gix-traverse` --- gitoxide-core/src/repository/tree.rs | 9 ++++++++- gix-diff/src/tree/recorder.rs | 3 +++ gix-diff/src/tree_with_rewrites/function.rs | 4 ++++ gix-index/src/init.rs | 7 +++++++ gix-pack/src/data/output/count/objects/tree.rs | 2 ++ gix-worktree-stream/src/from_tree/traverse.rs | 7 +++++++ gix/examples/stats.rs | 3 ++- 7 files changed, 33 insertions(+), 2 deletions(-) diff --git a/gitoxide-core/src/repository/tree.rs b/gitoxide-core/src/repository/tree.rs index 122891b2519..4cf80b0ce09 100644 --- a/gitoxide-core/src/repository/tree.rs +++ b/gitoxide-core/src/repository/tree.rs @@ -58,6 +58,9 @@ mod entries { } fn push_element(&mut self, name: &BStr) { + if name.is_empty() { + return; + } if !self.path.is_empty() { self.path.push(b'/'); } @@ -66,6 +69,10 @@ mod entries { } impl gix::traverse::tree::Visit for Traverse<'_, '_> { + fn pop_back_tracked_path_and_set_current(&mut self) { + self.path = self.path_deque.pop_back().unwrap_or_default(); + } + fn pop_front_tracked_path_and_set_current(&mut self) { self.path = self.path_deque.pop_front().expect("every parent is set only once"); } @@ -96,7 +103,7 @@ mod entries { format_entry(out, entry, self.path.as_bstr(), size).ok(); } if let Some(size) = size { - self.stats.num_bytes += size as u64; + self.stats.num_bytes += size; } use gix::object::tree::EntryKind::*; diff --git a/gix-diff/src/tree/recorder.rs b/gix-diff/src/tree/recorder.rs index 69055ba16e7..9a3783447b9 100644 --- a/gix-diff/src/tree/recorder.rs +++ b/gix-diff/src/tree/recorder.rs @@ -89,6 +89,9 @@ impl Recorder { } fn push_element(&mut self, name: &BStr) { + if name.is_empty() { + return; + } if !self.path.is_empty() { self.path.push(b'/'); } diff --git a/gix-diff/src/tree_with_rewrites/function.rs b/gix-diff/src/tree_with_rewrites/function.rs index b37856a3379..df0b0be1a32 100644 --- a/gix-diff/src/tree_with_rewrites/function.rs +++ b/gix-diff/src/tree_with_rewrites/function.rs @@ -239,6 +239,10 @@ mod tree_to_changes { } impl gix_traverse::tree::Visit for Delegate<'_> { + fn pop_back_tracked_path_and_set_current(&mut self) { + self.recorder.pop_back_tracked_path_and_set_current(); + } + fn pop_front_tracked_path_and_set_current(&mut self) { self.recorder.pop_front_tracked_path_and_set_current(); } diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index a9dfd150a6e..3d27475c9ae 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -126,6 +126,9 @@ pub mod from_tree { } fn push_element(&mut self, name: &BStr) { + if name.is_empty() { + return; + } if !self.path.is_empty() { self.path.push(b'/'); } @@ -182,6 +185,10 @@ pub mod from_tree { } impl Visit for CollectEntries { + fn pop_back_tracked_path_and_set_current(&mut self) { + self.path = self.path_deque.pop_back().unwrap_or_default(); + } + fn pop_front_tracked_path_and_set_current(&mut self) { self.path = self .path_deque diff --git a/gix-pack/src/data/output/count/objects/tree.rs b/gix-pack/src/data/output/count/objects/tree.rs index 3f464217b8b..1187fa15baf 100644 --- a/gix-pack/src/data/output/count/objects/tree.rs +++ b/gix-pack/src/data/output/count/objects/tree.rs @@ -94,6 +94,8 @@ pub mod traverse { where H: InsertImmutable, { + fn pop_back_tracked_path_and_set_current(&mut self) {} + fn pop_front_tracked_path_and_set_current(&mut self) {} fn push_back_tracked_path_component(&mut self, _component: &BStr) {} diff --git a/gix-worktree-stream/src/from_tree/traverse.rs b/gix-worktree-stream/src/from_tree/traverse.rs index 8acb5bf6abd..3daa0d534ed 100644 --- a/gix-worktree-stream/src/from_tree/traverse.rs +++ b/gix-worktree-stream/src/from_tree/traverse.rs @@ -39,6 +39,9 @@ where } fn push_element(&mut self, name: &BStr) { + if name.is_empty() { + return; + } if !self.path.is_empty() { self.path.push(b'/'); } @@ -105,6 +108,10 @@ where AttributesFn: FnMut(&BStr, gix_object::tree::EntryMode, &mut gix_attributes::search::Outcome) -> Result<(), Error> + 'static, { + fn pop_back_tracked_path_and_set_current(&mut self) { + self.path = self.path_deque.pop_back().unwrap_or_default(); + } + fn pop_front_tracked_path_and_set_current(&mut self) { self.path = self .path_deque diff --git a/gix/examples/stats.rs b/gix/examples/stats.rs index 62bed720228..7c3680a5949 100644 --- a/gix/examples/stats.rs +++ b/gix/examples/stats.rs @@ -39,7 +39,6 @@ fn main() -> Result<(), Box> { let mut delegate = visit::Tree::new(repo.clone()); tree.traverse().breadthfirst(&mut delegate)?; - let _files = tree.traverse().breadthfirst.files()?; println!("num trees: {}", delegate.num_trees); println!("num blobs: {}", delegate.num_blobs); @@ -105,6 +104,8 @@ mod visit { } } impl gix_traverse::tree::Visit for Tree { + fn pop_back_tracked_path_and_set_current(&mut self) {} + fn pop_front_tracked_path_and_set_current(&mut self) {} fn push_back_tracked_path_component(&mut self, _component: &BStr) {} From 592e250f8f01788d37f9fb7b1938b67446042bf3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 16:20:22 +0100 Subject: [PATCH 05/17] feat: add `Tree::depthfirst()` with a delegate. This allows a depth-first traversal with a delegate. --- gix/src/object/tree/traverse.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/gix/src/object/tree/traverse.rs b/gix/src/object/tree/traverse.rs index 78159016abd..3c66ac56953 100644 --- a/gix/src/object/tree/traverse.rs +++ b/gix/src/object/tree/traverse.rs @@ -15,6 +15,8 @@ impl<'repo> Tree<'repo> { pub struct Platform<'a, 'repo> { root: &'a Tree<'repo>, /// Provides easy access to presets for common breadth-first traversal. + // TODO: remove this - it's a bit too much of a fixed function, or go all in once it's clear it's needed, + // but probably with depth-first. pub breadthfirst: BreadthFirstPresets<'a, 'repo>, } @@ -38,12 +40,12 @@ impl BreadthFirstPresets<'_, '_> { } impl Platform<'_, '_> { - /// Start a breadth-first, recursive traversal using `delegate`, for which a [`Recorder`][gix_traverse::tree::Recorder] can be used to get started. + /// Start a breadth-first, recursive traversal using `delegate`, for which a [`Recorder`](gix_traverse::tree::Recorder) can be used to get started. /// /// # Note /// - /// - Results are returned in sort order according to tree-entry sorting rules, one level at a time. - /// - for obtaining the direct children of the tree, use [.iter()][crate::Tree::iter()] instead. + /// - Results are returned in sort order as per tree-sorting rules, files first, then directories, one level at a time. + /// - for obtaining the direct children of the tree, use [Tree::iter()] instead. pub fn breadthfirst(&self, delegate: &mut V) -> Result<(), gix_traverse::tree::breadthfirst::Error> where V: gix_traverse::tree::Visit, @@ -52,4 +54,17 @@ impl Platform<'_, '_> { let state = gix_traverse::tree::breadthfirst::State::default(); gix_traverse::tree::breadthfirst(root, state, &self.root.repo.objects, delegate) } + + /// Start a depth-first, recursive traversal using `delegate`, for which a [`Recorder`](gix_traverse::tree::Recorder) can be used to get started. + /// + /// # Note + /// + /// For obtaining the direct children of the tree, use [Tree::iter()] instead. + pub fn depthfirst(&self, delegate: &mut V) -> Result<(), gix_traverse::tree::breadthfirst::Error> + where + V: gix_traverse::tree::Visit, + { + let state = gix_traverse::tree::depthfirst::State::default(); + gix_traverse::tree::depthfirst(self.root.id, state, &self.root.repo.objects, delegate) + } } From c17aee648c14035d094fe8e84373ee404f477d2a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 19:17:23 +0100 Subject: [PATCH 06/17] fix!: hex-display of any object ID is now faster as `oid::hex_to_buf()` now returns `&mut str`. This helps callers to avoid converting to UTF8 by hand. --- gix-hash/src/oid.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index 66fdf3f5568..4e67dadf9f1 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -44,9 +44,9 @@ pub struct HexDisplay<'a> { impl std::fmt::Display for HexDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut hex = Kind::hex_buf(); - let max_len = self.inner.hex_to_buf(hex.as_mut()); - let hex = std::str::from_utf8(&hex[..self.hex_len.min(max_len)]).expect("ascii only in hex"); - f.write_str(hex) + let hex = self.inner.hex_to_buf(hex.as_mut()); + let max_len = hex.len(); + f.write_str(&hex[..self.hex_len.min(max_len)]) } } @@ -152,22 +152,21 @@ impl oid { /// Sha1 specific methods impl oid { - /// Write ourselves to the `out` in hexadecimal notation, returning the amount of written bytes. + /// Write ourselves to the `out` in hexadecimal notation, returning the hex-string ready for display. /// /// **Panics** if the buffer isn't big enough to hold twice as many bytes as the current binary size. #[inline] #[must_use] - pub fn hex_to_buf(&self, buf: &mut [u8]) -> usize { + pub fn hex_to_buf<'a>(&self, buf: &'a mut [u8]) -> &'a mut str { let num_hex_bytes = self.bytes.len() * 2; - faster_hex::hex_encode(&self.bytes, &mut buf[..num_hex_bytes]).expect("to count correctly"); - num_hex_bytes + faster_hex::hex_encode(&self.bytes, &mut buf[..num_hex_bytes]).expect("to count correctly") } /// Write ourselves to `out` in hexadecimal notation. #[inline] pub fn write_hex_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { let mut hex = Kind::hex_buf(); - let hex_len = self.hex_to_buf(&mut hex); + let hex_len = self.hex_to_buf(&mut hex).len(); out.write_all(&hex[..hex_len]) } @@ -210,10 +209,8 @@ impl<'a> From<&'a [u8; SIZE_OF_SHA1_DIGEST]> for &'a oid { impl std::fmt::Display for &oid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for b in self.as_bytes() { - write!(f, "{b:02x}")?; - } - Ok(()) + let mut buf = Kind::hex_buf(); + f.write_str(self.hex_to_buf(&mut buf)) } } From 201e853b151983502377b1f04d821c07d159014f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 19:24:49 +0100 Subject: [PATCH 07/17] adapt to changes in `gix-hash` --- gix-odb/src/store_impls/loose/mod.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gix-odb/src/store_impls/loose/mod.rs b/gix-odb/src/store_impls/loose/mod.rs index 17e4a33d65a..9becb5cd4a4 100644 --- a/gix-odb/src/store_impls/loose/mod.rs +++ b/gix-odb/src/store_impls/loose/mod.rs @@ -42,10 +42,9 @@ impl Store { fn hash_path(id: &gix_hash::oid, mut root: PathBuf) -> PathBuf { let mut hex = gix_hash::Kind::hex_buf(); - let hex_len = id.hex_to_buf(hex.as_mut()); - let buf = std::str::from_utf8(&hex[..hex_len]).expect("ascii only in hex"); - root.push(&buf[..2]); - root.push(&buf[2..]); + let hex = id.hex_to_buf(hex.as_mut()); + root.push(&hex[..2]); + root.push(&hex[2..]); root } From 433b409ef6cc30569446c0ba98dafc76fe194860 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 16:36:54 +0100 Subject: [PATCH 08/17] fix: `gix tree entries` now uses a depth-first traversal. This makes the result similar to `git ls-tree` in terms of ordering. --- gitoxide-core/src/repository/tree.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/gitoxide-core/src/repository/tree.rs b/gitoxide-core/src/repository/tree.rs index 4cf80b0ce09..fcde83f8fb1 100644 --- a/gitoxide-core/src/repository/tree.rs +++ b/gitoxide-core/src/repository/tree.rs @@ -1,18 +1,17 @@ -use std::{borrow::Cow, io}; - use anyhow::bail; use gix::Tree; +use std::io::BufWriter; +use std::{borrow::Cow, io}; use crate::OutputFormat; mod entries { - use std::collections::VecDeque; - use gix::{ bstr::{BStr, BString, ByteSlice, ByteVec}, objs::tree::EntryRef, traverse::tree::visit::Action, }; + use std::collections::VecDeque; use crate::repository::tree::format_entry; @@ -161,8 +160,9 @@ pub fn entries( let tree = treeish_to_tree(treeish, &repo)?; if recursive { - let mut delegate = entries::Traverse::new(extended.then_some(&repo), Some(&mut out)); - tree.traverse().breadthfirst(&mut delegate)?; + let mut write = BufWriter::new(out); + let mut delegate = entries::Traverse::new(extended.then_some(&repo), Some(&mut write)); + tree.traverse().depthfirst(&mut delegate)?; } else { for entry in tree.iter() { let entry = entry?; @@ -190,9 +190,9 @@ fn format_entry( size: Option, ) -> std::io::Result<()> { use gix::objs::tree::EntryKind::*; - writeln!( + write!( out, - "{} {}{} {}", + "{} {}{} ", match entry.mode.kind() { Tree => "TREE", Blob => "BLOB", @@ -201,7 +201,8 @@ fn format_entry( Commit => "SUBM", }, entry.oid, - size.map_or_else(|| "".into(), |s| Cow::Owned(format!(" {s}"))), - filename - ) + size.map_or_else(|| "".into(), |s| Cow::Owned(format!(" {s}"))) + )?; + out.write_all(filename)?; + out.write_all(b"\n") } From 4989cda067c3c1e33e2525d9e30022aa2c61c275 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Dec 2024 20:02:51 +0100 Subject: [PATCH 09/17] fix: `State::from_tree()` is now faster. It uses depth-first traversal out of the box which allows it to save the sorting in the end. It's also a little bit faster. --- gix-index/src/init.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index 3d27475c9ae..968651806ce 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -3,8 +3,8 @@ pub mod from_tree { use std::collections::VecDeque; use bstr::{BStr, BString, ByteSlice, ByteVec}; - use gix_object::{tree, tree::EntryKind, FindExt}; - use gix_traverse::tree::{breadthfirst, visit::Action, Visit}; + use gix_object::{tree, tree::EntryKind}; + use gix_traverse::tree::{depthfirst, visit::Action, Visit}; use crate::{ entry::{Flags, Mode, Stat}, @@ -21,7 +21,7 @@ pub mod from_tree { source: gix_validate::path::component::Error, }, #[error(transparent)] - Traversal(#[from] gix_traverse::tree::breadthfirst::Error), + Traversal(#[from] gix_traverse::tree::depthfirst::Error), } /// Initialization @@ -58,12 +58,8 @@ pub mod from_tree { Find: gix_object::Find, { let _span = gix_features::trace::coarse!("gix_index::State::from_tree()"); - let mut buf = Vec::new(); - let root = objects - .find_tree_iter(tree, &mut buf) - .map_err(breadthfirst::Error::from)?; let mut delegate = CollectEntries::new(validate); - match breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate) { + match depthfirst(tree.to_owned(), depthfirst::State::default(), &objects, &mut delegate) { Ok(()) => {} Err(gix_traverse::tree::breadthfirst::Error::Cancelled) => { let (path, err) = delegate @@ -76,15 +72,17 @@ pub mod from_tree { } let CollectEntries { - mut entries, + entries, path_backing, path: _, path_deque: _, validate: _, - invalid_path: _, + invalid_path, } = delegate; - entries.sort_by(|a, b| Entry::cmp_filepaths(a.path_in(&path_backing), b.path_in(&path_backing))); + if let Some((path, err)) = invalid_path { + return Err(Error::InvalidComponent { path, source: err }); + } Ok(State { object_hash: tree.kind(), From 3b8c9711dac45d317781b571062ae7448b076e50 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 29 Dec 2024 08:36:39 +0100 Subject: [PATCH 10/17] feat: add `index()` to diff two indices It comes with pathspec support to allow for easier integration into the `status()` machinery. --- Cargo.lock | 5 + gix-diff/Cargo.toml | 11 +- gix-diff/src/index/change.rs | 197 +++ gix-diff/src/index/function.rs | 324 ++++ gix-diff/src/index/mod.rs | 141 ++ gix-diff/src/lib.rs | 6 + gix-diff/tests/Cargo.toml | 5 +- gix-diff/tests/diff/index.rs | 1367 +++++++++++++++++ gix-diff/tests/diff/main.rs | 1 + .../make_diff_for_rewrites_repo.tar | Bin 354816 -> 368128 bytes .../fixtures/make_diff_for_rewrites_repo.sh | 14 + 11 files changed, 2067 insertions(+), 4 deletions(-) create mode 100644 gix-diff/src/index/change.rs create mode 100644 gix-diff/src/index/function.rs create mode 100644 gix-diff/src/index/mod.rs create mode 100644 gix-diff/tests/diff/index.rs diff --git a/Cargo.lock b/Cargo.lock index fe6eaeae126..fbf3e71b887 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1715,12 +1715,15 @@ dependencies = [ "bstr", "document-features", "getrandom", + "gix-attributes 0.23.1", "gix-command", "gix-filter", "gix-fs 0.12.1", "gix-hash 0.15.1", + "gix-index 0.37.0", "gix-object 0.46.1", "gix-path 0.10.13", + "gix-pathspec", "gix-tempfile 15.0.0", "gix-trace 0.1.11", "gix-traverse 0.43.1", @@ -1738,8 +1741,10 @@ dependencies = [ "gix-filter", "gix-fs 0.12.1", "gix-hash 0.15.1", + "gix-index 0.37.0", "gix-object 0.46.1", "gix-odb", + "gix-pathspec", "gix-testtools", "gix-traverse 0.43.1", "gix-worktree 0.38.0", diff --git a/gix-diff/Cargo.toml b/gix-diff/Cargo.toml index 70a48195093..e3de39938c3 100644 --- a/gix-diff/Cargo.toml +++ b/gix-diff/Cargo.toml @@ -13,11 +13,13 @@ rust-version = "1.65" autotests = false [features] -default = ["blob"] -## Enable diffing of blobs using imara-diff, which also allows for a generic rewrite tracking implementation. +default = ["blob", "index"] +## Enable diffing of blobs using imara-diff. blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-traverse"] +## Enable diffing of two indices, which also allows for a generic rewrite tracking implementation. +index = ["dep:gix-index", "dep:gix-pathspec", "dep:gix-attributes"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"] +serde = ["dep:serde", "gix-hash/serde", "gix-object/serde", "gix-index?/serde"] ## Make it possible to compile to the `wasm32-unknown-unknown` target. wasm = ["dep:getrandom"] @@ -25,6 +27,9 @@ wasm = ["dep:getrandom"] doctest = false [dependencies] +gix-index = { version = "^0.37.0", path = "../gix-index", optional = true } +gix-pathspec = { version = "^0.8.1", path = "../gix-pathspec", optional = true } +gix-attributes = { version = "^0.23.1", path = "../gix-attributes", optional = true } gix-hash = { version = "^0.15.1", path = "../gix-hash" } gix-object = { version = "^0.46.1", path = "../gix-object" } gix-filter = { version = "^0.16.0", path = "../gix-filter", optional = true } diff --git a/gix-diff/src/index/change.rs b/gix-diff/src/index/change.rs new file mode 100644 index 00000000000..da5d98d1f4e --- /dev/null +++ b/gix-diff/src/index/change.rs @@ -0,0 +1,197 @@ +use crate::index::{Change, ChangeRef}; +use crate::rewrites; +use crate::rewrites::tracker::ChangeKind; +use crate::tree::visit::Relation; +use bstr::BStr; +use gix_object::tree; +use std::borrow::Cow; + +impl ChangeRef<'_, '_> { + /// Copy everything into an owned version of this instance. + pub fn into_owned(self) -> Change { + match self { + ChangeRef::Addition { + location, + index, + entry_mode, + id, + } => ChangeRef::Addition { + location: Cow::Owned(location.into_owned()), + index, + entry_mode, + id: Cow::Owned(id.into_owned()), + }, + ChangeRef::Deletion { + location, + index, + entry_mode, + id, + } => ChangeRef::Deletion { + location: Cow::Owned(location.into_owned()), + index, + entry_mode, + id: Cow::Owned(id.into_owned()), + }, + ChangeRef::Modification { + location, + previous_index, + previous_entry_mode, + previous_id, + index, + entry_mode, + id, + } => ChangeRef::Modification { + location: Cow::Owned(location.into_owned()), + previous_index, + previous_entry_mode, + previous_id: Cow::Owned(previous_id.into_owned()), + index, + entry_mode, + id: Cow::Owned(id.into_owned()), + }, + ChangeRef::Rewrite { + source_location, + source_index, + source_entry_mode, + source_id, + location, + index, + entry_mode, + id, + copy, + } => ChangeRef::Rewrite { + source_location: Cow::Owned(source_location.into_owned()), + source_index, + source_entry_mode, + source_id: Cow::Owned(source_id.into_owned()), + location: Cow::Owned(location.into_owned()), + index, + entry_mode, + id: Cow::Owned(id.into_owned()), + copy, + }, + ChangeRef::Unmerged { + location, + stage, + index, + entry_mode, + id, + } => ChangeRef::Unmerged { + location: Cow::Owned(location.into_owned()), + stage, + index, + entry_mode, + id: Cow::Owned(id.into_owned()), + }, + } + } +} + +impl ChangeRef<'_, '_> { + /// Return all shared fields among all variants: `(location, index, entry_mode, id)` + /// + /// In case of rewrites, the fields return to the current change. + pub fn fields(&self) -> (&BStr, usize, gix_index::entry::Mode, &gix_hash::oid) { + match self { + ChangeRef::Addition { + location, + index, + entry_mode, + id, + .. + } + | ChangeRef::Deletion { + location, + index, + entry_mode, + id, + .. + } + | ChangeRef::Modification { + location, + index, + entry_mode, + id, + .. + } + | ChangeRef::Rewrite { + location, + index, + entry_mode, + id, + .. + } + | ChangeRef::Unmerged { + location, + index, + entry_mode, + id, + .. + } => (location.as_ref(), *index, *entry_mode, id), + } + } +} + +impl rewrites::tracker::Change for ChangeRef<'_, '_> { + fn id(&self) -> &gix_hash::oid { + match self { + ChangeRef::Addition { id, .. } | ChangeRef::Deletion { id, .. } | ChangeRef::Modification { id, .. } => { + id.as_ref() + } + ChangeRef::Rewrite { .. } | ChangeRef::Unmerged { .. } => { + unreachable!("BUG") + } + } + } + + fn relation(&self) -> Option { + None + } + + fn kind(&self) -> ChangeKind { + match self { + ChangeRef::Addition { .. } => ChangeKind::Addition, + ChangeRef::Deletion { .. } => ChangeKind::Deletion, + ChangeRef::Modification { .. } => ChangeKind::Modification, + ChangeRef::Rewrite { .. } => { + unreachable!("BUG: rewrites can't be determined ahead of time") + } + ChangeRef::Unmerged { .. } => { + unreachable!("BUG: unmerged don't participate in rename tracking") + } + } + } + + fn entry_mode(&self) -> tree::EntryMode { + match self { + ChangeRef::Addition { entry_mode, .. } + | ChangeRef::Deletion { entry_mode, .. } + | ChangeRef::Modification { entry_mode, .. } + | ChangeRef::Rewrite { entry_mode, .. } + | ChangeRef::Unmerged { entry_mode, .. } => { + entry_mode + .to_tree_entry_mode() + // Default is for the impossible case - just don't let it participate in rename tracking. + .unwrap_or(tree::EntryKind::Tree.into()) + } + } + } + + fn id_and_entry_mode(&self) -> (&gix_hash::oid, tree::EntryMode) { + match self { + ChangeRef::Addition { id, entry_mode, .. } + | ChangeRef::Deletion { id, entry_mode, .. } + | ChangeRef::Modification { id, entry_mode, .. } + | ChangeRef::Rewrite { id, entry_mode, .. } + | ChangeRef::Unmerged { id, entry_mode, .. } => { + ( + id, + entry_mode + .to_tree_entry_mode() + // Default is for the impossible case - just don't let it participate in rename tracking. + .unwrap_or(tree::EntryKind::Tree.into()), + ) + } + } + } +} diff --git a/gix-diff/src/index/function.rs b/gix-diff/src/index/function.rs new file mode 100644 index 00000000000..bbf2a9a3140 --- /dev/null +++ b/gix-diff/src/index/function.rs @@ -0,0 +1,324 @@ +use super::{Action, ChangeRef, Error, RewriteOptions}; +use crate::rewrites; +use bstr::{BStr, BString, ByteSlice}; +use gix_filter::attributes::glob::pattern::Case; +use std::borrow::Cow; +use std::cell::RefCell; +use std::cmp::Ordering; + +/// Produce an entry-by-entry diff between `lhs` and `rhs`, sending changes to `cb(change) -> Action` for consumption, +/// which would turn `lhs` into `rhs` if applied. +/// Use `pathspec` to reduce the set of entries to look at, and `pathspec_attributes` may be used by pathspecs that perform +/// attribute lookups. +/// +/// If `cb` indicated that the operation should be cancelled, no error is triggered as this isn't supposed to +/// occur through user-interaction - this diff is typically too fast. +/// +/// Note that rewrites will be emitted at the end, so no ordering can be assumed. They will only be tracked if +/// `rewrite_options` is `Some`. Note that the set of entries participating in rename tracking is affected by `pathspec`. +/// +/// Return the outcome of the rewrite tracker if it was enabled. +/// +/// Note that only `rhs` may contain unmerged entries, as `rhs` is expected to be the index read from `.git/index`. +/// Unmerged entries are always provided as changes, one stage at a time, up to three stages for *base*, *ours* and *theirs*. +/// Conceptually, `rhs` is *ours*, and `lhs` is *theirs*. +/// The entries in `lhs` and `rhs` are both expected to be sorted like index entries are typically sorted. +/// +/// Note that sparse indices aren't supported, they must be "unsparsed" before. +pub fn diff<'rhs, 'lhs: 'rhs, E, Find>( + lhs: &'lhs gix_index::State, + rhs: &'rhs gix_index::State, + mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, + rewrite_options: Option>, + pathspec: &mut gix_pathspec::Search, + pathspec_attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool, +) -> Result, Error> +where + E: Into>, + Find: gix_object::FindObjectOrHeader, +{ + if lhs.is_sparse() || rhs.is_sparse() { + return Err(Error::IsSparse); + } + if lhs + .entries() + .iter() + .any(|e| e.stage() != gix_index::entry::Stage::Unconflicted) + { + return Err(Error::LhsHasUnmerged); + } + + let lhs_range = lhs + .prefixed_entries_range(pathspec.common_prefix()) + .unwrap_or_else(|| 0..lhs.entries().len()); + let rhs_range = rhs + .prefixed_entries_range(pathspec.common_prefix()) + .unwrap_or_else(|| 0..rhs.entries().len()); + + let pattern_matches = RefCell::new(|relative_path, entry: &gix_index::Entry| { + pathspec + .pattern_matching_relative_path(relative_path, Some(entry.mode.is_submodule()), pathspec_attributes) + .map_or(false, |m| !m.is_excluded()) + }); + + let (mut lhs_iter, mut rhs_iter) = ( + lhs.entries()[lhs_range.clone()] + .iter() + .enumerate() + .map(|(idx, e)| (idx + lhs_range.start, e.path(lhs), e)) + .filter(|(_, path, e)| pattern_matches.borrow_mut()(path, e)), + rhs.entries()[rhs_range.clone()] + .iter() + .enumerate() + .map(|(idx, e)| (idx + rhs_range.start, e.path(rhs), e)) + .filter(|(_, path, e)| pattern_matches.borrow_mut()(path, e)), + ); + + let mut conflicting_paths = Vec::::new(); + let mut cb = move |change: ChangeRef<'lhs, 'rhs>| { + let (location, ..) = change.fields(); + if let ChangeRef::Unmerged { .. } = &change { + if let Err(insert_idx) = conflicting_paths.binary_search_by(|p| p.as_bstr().cmp(location)) { + conflicting_paths.insert(insert_idx, location.to_owned()); + } + cb(change) + } else if conflicting_paths + .binary_search_by(|p| p.as_bstr().cmp(location)) + .is_err() + { + cb(change) + } else { + Ok(Action::Continue) + } + }; + let mut resource_cache_storage = None; + let mut tracker = rewrite_options.map( + |RewriteOptions { + resource_cache, + rewrites, + find, + }| { + resource_cache_storage = Some((resource_cache, find)); + rewrites::Tracker::>::new(rewrites) + }, + ); + + let (mut lhs_storage, mut rhs_storage) = (lhs_iter.next(), rhs_iter.next()); + loop { + match (lhs_storage, rhs_storage) { + (Some(lhs), Some(rhs)) => { + match emit_unmerged_ignore_intent_to_add(rhs, &mut cb)? { + None => {} + Some(Action::Cancel) => return Ok(None), + Some(Action::Continue) => { + rhs_storage = rhs_iter.next(); + continue; + } + }; + + let (lhs_idx, lhs_path, lhs_entry) = lhs; + let (rhs_idx, rhs_path, rhs_entry) = rhs; + match lhs_path.cmp(rhs_path) { + Ordering::Less => match emit_deletion(lhs, &mut cb, tracker.as_mut())? { + Action::Continue => { + lhs_storage = lhs_iter.next(); + } + Action::Cancel => return Ok(None), + }, + Ordering::Equal => { + if lhs_entry.id != rhs_entry.id || lhs_entry.mode != rhs_entry.mode { + let change = ChangeRef::Modification { + location: Cow::Borrowed(rhs_path), + previous_index: lhs_idx, + previous_entry_mode: lhs_entry.mode, + previous_id: Cow::Borrowed(lhs_entry.id.as_ref()), + index: rhs_idx, + entry_mode: rhs_entry.mode, + id: Cow::Borrowed(rhs_entry.id.as_ref()), + }; + + let change = match tracker.as_mut() { + None => Some(change), + Some(tracker) => tracker.try_push_change(change, rhs_path), + }; + if let Some(change) = change { + match cb(change).map_err(|err| Error::Callback(err.into()))? { + Action::Continue => {} + Action::Cancel => return Ok(None), + } + } + } + lhs_storage = lhs_iter.next(); + rhs_storage = rhs_iter.next(); + } + Ordering::Greater => match emit_addition(rhs, &mut cb, tracker.as_mut())? { + Action::Continue => { + rhs_storage = rhs_iter.next(); + } + Action::Cancel => return Ok(None), + }, + } + } + (Some(lhs), None) => match emit_deletion(lhs, &mut cb, tracker.as_mut())? { + Action::Cancel => return Ok(None), + Action::Continue => { + lhs_storage = lhs_iter.next(); + } + }, + (None, Some(rhs)) => match emit_addition(rhs, &mut cb, tracker.as_mut())? { + Action::Cancel => return Ok(None), + Action::Continue => { + rhs_storage = rhs_iter.next(); + } + }, + (None, None) => break, + } + } + + if let Some((mut tracker, (resource_cache, find))) = tracker.zip(resource_cache_storage) { + let mut cb_err = None; + let out = tracker.emit( + |dst, src| { + let change = if let Some(src) = src { + let (lhs_path, lhs_index, lhs_mode, lhs_id) = src.change.fields(); + let (rhs_path, rhs_index, rhs_mode, rhs_id) = dst.change.fields(); + ChangeRef::Rewrite { + source_location: Cow::Owned(lhs_path.into()), + source_index: lhs_index, + source_entry_mode: lhs_mode, + source_id: Cow::Owned(lhs_id.into()), + location: Cow::Owned(rhs_path.into()), + index: rhs_index, + entry_mode: rhs_mode, + id: Cow::Owned(rhs_id.into()), + copy: match src.kind { + rewrites::tracker::visit::SourceKind::Rename => false, + rewrites::tracker::visit::SourceKind::Copy => true, + }, + } + } else { + dst.change + }; + match cb(change) { + Ok(Action::Continue) => crate::tree::visit::Action::Continue, + Ok(Action::Cancel) => crate::tree::visit::Action::Cancel, + Err(err) => { + cb_err = Some(Error::Callback(err.into())); + crate::tree::visit::Action::Cancel + } + } + }, + resource_cache, + find, + |push| { + for (index, entry) in lhs.entries().iter().enumerate() { + let path = entry.path(rhs); + push( + ChangeRef::Modification { + location: Cow::Borrowed(path), + previous_index: 0, /* does not matter */ + previous_entry_mode: entry.mode, + previous_id: Cow::Owned(entry.id.kind().null()), + index, + entry_mode: entry.mode, + id: Cow::Borrowed(entry.id.as_ref()), + }, + path, + ); + } + Ok::<_, std::convert::Infallible>(()) + }, + )?; + + if let Some(err) = cb_err { + Err(err) + } else { + Ok(Some(out)) + } + } else { + Ok(None) + } +} + +fn emit_deletion<'rhs, 'lhs: 'rhs, E>( + (idx, path, entry): (usize, &'lhs BStr, &'lhs gix_index::Entry), + mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, + tracker: Option<&mut rewrites::Tracker>>, +) -> Result +where + E: Into>, +{ + let change = ChangeRef::Deletion { + location: Cow::Borrowed(path), + index: idx, + entry_mode: entry.mode, + id: Cow::Borrowed(entry.id.as_ref()), + }; + + let change = match tracker { + None => change, + Some(tracker) => match tracker.try_push_change(change, path) { + Some(change) => change, + None => return Ok(Action::Continue), + }, + }; + + cb(change).map_err(|err| Error::Callback(err.into())) +} + +fn emit_addition<'rhs, 'lhs: 'rhs, E>( + (idx, path, entry): (usize, &'rhs BStr, &'rhs gix_index::Entry), + mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, + tracker: Option<&mut rewrites::Tracker>>, +) -> Result +where + E: Into>, +{ + if let Some(action) = emit_unmerged_ignore_intent_to_add((idx, path, entry), &mut cb)? { + return Ok(action); + } + + let change = ChangeRef::Addition { + location: Cow::Borrowed(path), + index: idx, + entry_mode: entry.mode, + id: Cow::Borrowed(entry.id.as_ref()), + }; + + let change = match tracker { + None => change, + Some(tracker) => match tracker.try_push_change(change, path) { + Some(change) => change, + None => return Ok(Action::Continue), + }, + }; + + cb(change).map_err(|err| Error::Callback(err.into())) +} + +fn emit_unmerged_ignore_intent_to_add<'rhs, 'lhs: 'rhs, E>( + (idx, path, entry): (usize, &'rhs BStr, &'rhs gix_index::Entry), + cb: &mut impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result, +) -> Result, Error> +where + E: Into>, +{ + if entry.flags.contains(gix_index::entry::Flags::INTENT_TO_ADD) { + return Ok(Some(Action::Continue)); + } + let stage = entry.stage(); + if stage == gix_index::entry::Stage::Unconflicted { + return Ok(None); + } + + Ok(Some( + cb(ChangeRef::Unmerged { + location: Cow::Borrowed(path), + stage, + index: idx, + entry_mode: entry.mode, + id: Cow::Borrowed(entry.id.as_ref()), + }) + .map_err(|err| Error::Callback(err.into()))?, + )) +} diff --git a/gix-diff/src/index/mod.rs b/gix-diff/src/index/mod.rs new file mode 100644 index 00000000000..0a66b3ca1e7 --- /dev/null +++ b/gix-diff/src/index/mod.rs @@ -0,0 +1,141 @@ +use bstr::BStr; +use std::borrow::Cow; + +/// The error returned by [`index()`](crate::index()). +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Cannot diff indices that contain sparse entries")] + IsSparse, + #[error("Unmerged entries aren't allowed in the left-hand index, only in the right-hand index")] + LhsHasUnmerged, + #[error("The callback indicated failure")] + Callback(#[source] Box), + #[error("Failure during rename tracking")] + RenameTracking(#[from] crate::rewrites::tracker::emit::Error), +} + +/// What to do after a [ChangeRef] was passed ot the callback of [`index()`](crate::index()). +#[derive(Default, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] +pub enum Action { + /// Continue the operation. + #[default] + Continue, + /// Stop the operation immediately. + /// + /// This is useful if one just wants to determine if something changed or not. + Cancel, +} + +/// Options to configure how rewrites are tracked as part of the [`index()`](crate::index()) call. +pub struct RewriteOptions<'a, Find> +where + Find: gix_object::FindObjectOrHeader, +{ + /// The cache to be used when rename-tracking by similarity is enabled, typically the default. + /// Note that it's recommended to call [`clear_resource_cache()`](`crate::blob::Platform::clear_resource_cache()`) + /// between the calls to avoid runaway memory usage, as the cache isn't limited. + pub resource_cache: &'a mut crate::blob::Platform, + /// A way to lookup objects from the object database, for use in similarity checks. + pub find: &'a Find, + /// Configure how rewrites are tracked. + pub rewrites: crate::Rewrites, +} + +/// Identify a change that would have to be applied to `lhs` to obtain `rhs`, as provided in [`index()`](crate::index()). +/// +/// Note that all variants are unconflicted entries, unless it's the [`Self::Unmerged`] one. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ChangeRef<'lhs, 'rhs> { + /// An entry was added to `rhs`. + Addition { + /// The location of the newly added entry in `rhs`. + location: Cow<'rhs, BStr>, + /// The index into the entries array of `rhs` for full access. + index: usize, + /// The mode of the entry in `rhs`. + entry_mode: gix_index::entry::Mode, + /// The object id of the entry in `rhs`. + id: Cow<'rhs, gix_hash::oid>, + }, + /// An entry was removed from `rhs`. + Deletion { + /// The location the entry that doesn't exist in `rhs`. + location: Cow<'lhs, BStr>, + /// The index into the entries array of `lhs` for full access. + index: usize, + /// The mode of the entry in `lhs`. + entry_mode: gix_index::entry::Mode, + /// The object id of the entry in `lhs`. + id: Cow<'rhs, gix_hash::oid>, + }, + /// An entry was modified, i.e. has changed its content or its mode. + Modification { + /// The location of the modified entry both in `lhs` and `rhs`. + location: Cow<'rhs, BStr>, + /// The index into the entries array of `lhs` for full access. + previous_index: usize, + /// The previous mode of the entry, in `lhs`. + previous_entry_mode: gix_index::entry::Mode, + /// The previous object id of the entry, in `lhs`. + previous_id: Cow<'lhs, gix_hash::oid>, + /// The index into the entries array of `rhs` for full access. + index: usize, + /// The mode of the entry in `rhs`. + entry_mode: gix_index::entry::Mode, + /// The object id of the entry in `rhs`. + id: Cow<'rhs, gix_hash::oid>, + }, + /// An entry was renamed or copied from `lhs` to `rhs`. + /// + /// A rename is effectively fusing together the `Deletion` of the source and the `Addition` of the destination. + Rewrite { + /// The location of the source of the rename or copy operation, in `lhs`. + source_location: Cow<'lhs, BStr>, + /// The index of the entry before the rename, into the entries array of `rhs` for full access. + source_index: usize, + /// The mode of the entry before the rewrite, in `lhs`. + source_entry_mode: gix_index::entry::Mode, + /// The object id of the entry before the rewrite. + /// + /// Note that this is the same as `id` if we require the [similarity to be 100%](super::Rewrites::percentage), but may + /// be different otherwise. + source_id: Cow<'lhs, gix_hash::oid>, + + /// The current location of the entry in `rhs`. + location: Cow<'rhs, BStr>, + /// The index of the entry after the rename, into the entries array of `rhs` for full access. + index: usize, + /// The mode of the entry after the rename in `rhs`. + entry_mode: gix_index::entry::Mode, + /// The object id of the entry after the rename in `rhs`. + id: Cow<'rhs, gix_hash::oid>, + + /// If true, this rewrite is created by copy, and `source_id` is pointing to its source. Otherwise, it's a rename, + /// and `source_id` points to a deleted object, as renames are tracked as deletions and additions of the same + /// or similar content. + copy: bool, + }, + /// One of up to three unmerged entries that are provided in order, one for each stage, ordered + /// by `location` and `stage`. + /// + /// Unmerged entries also don't participate in rename tracking, and they are never present in `lhs`. + Unmerged { + /// The current location of the entry in `rhs`. + location: Cow<'rhs, BStr>, + /// The stage of the entry, either *base*, *ours*, or *theirs*. + stage: gix_index::entry::Stage, + /// The index into the entries array of `rhs` for full access. + index: usize, + /// The mode of the entry in `rhs`. + entry_mode: gix_index::entry::Mode, + /// The object id of the entry in `rhs`. + id: Cow<'rhs, gix_hash::oid>, + }, +} + +/// The fully-owned version of [`ChangeRef`]. +pub type Change = ChangeRef<'static, 'static>; + +mod change; +pub(super) mod function; diff --git a/gix-diff/src/lib.rs b/gix-diff/src/lib.rs index ce2451176f5..f438ee4c275 100644 --- a/gix-diff/src/lib.rs +++ b/gix-diff/src/lib.rs @@ -58,6 +58,12 @@ pub mod tree_with_rewrites; #[cfg(feature = "blob")] pub use tree_with_rewrites::function::diff as tree_with_rewrites; +/// +#[cfg(feature = "index")] +pub mod index; +#[cfg(feature = "index")] +pub use index::function::diff as index; + /// #[cfg(feature = "blob")] pub mod blob; diff --git a/gix-diff/tests/Cargo.toml b/gix-diff/tests/Cargo.toml index 9197a86be5b..35645e05d90 100644 --- a/gix-diff/tests/Cargo.toml +++ b/gix-diff/tests/Cargo.toml @@ -17,8 +17,9 @@ name = "diff" path = "diff/main.rs" [dev-dependencies] -insta = "1.40.0" gix-diff = { path = ".." } +gix-index = { version = "^0.37.0", path = "../../gix-index" } +gix-pathspec = { version = "^0.8.1", path = "../../gix-pathspec" } gix-hash = { path = "../../gix-hash" } gix-fs = { path = "../../gix-fs" } gix-worktree = { path = "../../gix-worktree" } @@ -27,5 +28,7 @@ gix-odb = { path = "../../gix-odb" } gix-filter = { path = "../../gix-filter" } gix-traverse = { path = "../../gix-traverse" } gix-testtools = { path = "../../tests/tools" } + +insta = "1.40.0" shell-words = "1" pretty_assertions = "1.4.0" diff --git a/gix-diff/tests/diff/index.rs b/gix-diff/tests/diff/index.rs new file mode 100644 index 00000000000..7e936a309cf --- /dev/null +++ b/gix-diff/tests/diff/index.rs @@ -0,0 +1,1367 @@ +use gix_diff::index::Change; +use gix_diff::rewrites::{Copies, CopySource}; +use gix_diff::Rewrites; +use gix_object::bstr::BStr; + +#[test] +fn empty_to_new_tree_without_rename_tracking() -> crate::Result { + let changes = collect_changes_no_renames(None, "c1 - initial").expect("really just an addition - nothing to track"); + insta::assert_debug_snapshot!(changes, @r#" + [ + Addition { + location: "a", + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "b", + index: 1, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "d", + index: 2, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "dir/c", + index: 3, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + + { + let (lhs, rhs, _cache, _odb, mut pathspec) = repo_with_indices(None, "c1 - initial", None)?; + let err = gix_diff::index( + &lhs, + &rhs, + |_change| Err(std::io::Error::new(std::io::ErrorKind::Other, "custom error")), + None::>, + &mut pathspec, + &mut |_, _, _, _| true, + ) + .unwrap_err(); + assert_eq!( + format!("{err:?}"), + r#"Callback(Custom { kind: Other, error: "custom error" })"#, + "custom errors made visible and not squelched" + ); + } + Ok(()) +} + +#[test] +fn changes_against_modified_tree_with_filename_tracking() -> crate::Result { + let changes = collect_changes_no_renames("c2", "c3-modification")?; + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "a", + previous_index: 0, + previous_entry_mode: Mode( + FILE, + ), + previous_id: Sha1(78981922613b2afb6025042ff6bd878ac1994e85), + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(b4f17b61de71d9b2e54ac9e62b1629ae2d97a6a7), + }, + Modification { + location: "dir/c", + previous_index: 3, + previous_entry_mode: Mode( + FILE, + ), + previous_id: Sha1(6695780ceb14b05e076a99bbd2babf34723b3464), + index: 3, + entry_mode: Mode( + FILE, + ), + id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), + }, + ] + "#); + Ok(()) +} + +#[test] +fn renames_by_identity() -> crate::Result { + for (from, to, expected, assert_msg, track_empty) in [ + ( + "c3-modification", + "r1-identity", + vec![BStr::new("a"), "dir/a-moved".into()], + "one rename and nothing else", + false, + ), + ( + "c4 - add identical files", + "r2-ambiguous", + vec![ + "s1".into(), + "b1".into(), + "s2".into(), + "b2".into(), + "s3".into(), + "z".into(), + ], + "multiple possible sources decide by ordering everything lexicographically", + true, + ), + ( + "c4 - add identical files", + "r2-ambiguous", + vec![], + "nothing is tracked with `track_empty = false`", + false, + ), + ( + "c5 - add links", + "r4-symlinks", + vec!["link-1".into(), "renamed-link-1".into()], + "symlinks are only tracked by identity", + false, + ), + ( + "r1-identity", + "c4 - add identical files", + vec![], + "not having any renames is OK as well", + false, + ), + ( + "tc1-identity", + "tc1-identity", + vec![], + "copy tracking is off by default", + false, + ), + ] { + for percentage in [None, Some(0.5)] { + let (changes, out) = collect_changes_opts( + from, + to, + Some(Rewrites { + percentage, + track_empty, + ..Default::default() + }), + )?; + let actual: Vec<_> = changes + .into_iter() + .flat_map(|c| match c { + Change::Rewrite { + source_location, + location, + copy, + .. + } => { + assert!(!copy); + vec![source_location, location] + } + _ => vec![], + }) + .collect(); + + assert_eq!(actual, expected, "{assert_msg}"); + #[cfg(not(windows))] + assert_eq!( + out.expect("present as rewrites are configured").num_similarity_checks, + 0, + "there are no fuzzy checks in if everything was resolved by identity only" + ); + } + } + Ok(()) +} + +#[test] +fn rename_by_similarity() -> crate::Result { + insta::allow_duplicates! { + for percentage in [ + None, + Some(0.76), /*cutoff point where git stops seeing it as equal */ + ] { + let (changes, out) = collect_changes_opts( + "r2-ambiguous", + "r3-simple", + Some(Rewrites { + percentage, + ..Default::default() + }), + ).expect("errors can only happen with IO or ODB access fails"); + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "b", + previous_index: 0, + previous_entry_mode: Mode( + FILE, + ), + previous_id: Sha1(61780798228d17af2d34fce4cfbdf35556832472), + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), + }, + Deletion { + location: "dir/c", + index: 5, + entry_mode: Mode( + FILE, + ), + id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), + }, + Addition { + location: "dir/c-moved", + index: 5, + entry_mode: Mode( + FILE, + ), + id: Sha1(f01e8ddf5adc56985b9a1cda6d7c7ef9e3abe034), + }, + ] + "#); + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, if percentage.is_some() { 1 } else { 0 }); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + } + } + + let (changes, out) = collect_changes_opts( + "r2-ambiguous", + "r3-simple", + Some(Rewrites { + percentage: Some(0.6), + limit: 1, // has no effect as it's just one item here. + ..Default::default() + }), + ) + .expect("it found all items at the cut-off point, similar to git"); + + insta::assert_debug_snapshot!(changes, @r#" + [ + Modification { + location: "b", + previous_index: 0, + previous_entry_mode: Mode( + FILE, + ), + previous_id: Sha1(61780798228d17af2d34fce4cfbdf35556832472), + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), + }, + Rewrite { + source_location: "dir/c", + source_index: 5, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(40006fcef15a8853a1b7ae186d93b7d680fd29cf), + location: "dir/c-moved", + index: 5, + entry_mode: Mode( + FILE, + ), + id: Sha1(f01e8ddf5adc56985b9a1cda6d7c7ef9e3abe034), + copy: false, + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 1); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + Ok(()) +} + +#[test] +fn renames_by_similarity_with_limit() -> crate::Result { + let (changes, out) = collect_changes_opts( + "c6", + "r5", + Some(Rewrites { + limit: 1, // prevent fuzzy tracking from happening + ..Default::default() + }), + )?; + assert_eq!( + changes.iter().filter(|c| matches!(c, Change::Rewrite { .. })).count(), + 0, + "fuzzy tracking is effectively disabled due to limit" + ); + let actual: Vec<_> = changes.iter().map(|c| c.fields().0).collect(); + assert_eq!(actual, ["f1", "f1-renamed", "f2", "f2-renamed"],); + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 0); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 4); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn copies_by_identity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "c7", + "tc1-identity", + Some(Rewrites { + copies: Some(Copies { + source: CopySource::FromSetOfModifiedFiles, + percentage: None, + }), + limit: 1, // the limit isn't actually used for identity based checks + ..Default::default() + }), + )?; + insta::assert_debug_snapshot!(changes, @r#" + [ + Rewrite { + source_location: "base", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(f00c965d8307308469e537302baa73048488f162), + location: "c1", + index: 4, + entry_mode: Mode( + FILE, + ), + id: Sha1(f00c965d8307308469e537302baa73048488f162), + copy: true, + }, + Rewrite { + source_location: "base", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(f00c965d8307308469e537302baa73048488f162), + location: "c2", + index: 5, + entry_mode: Mode( + FILE, + ), + id: Sha1(f00c965d8307308469e537302baa73048488f162), + copy: true, + }, + Rewrite { + source_location: "base", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(f00c965d8307308469e537302baa73048488f162), + location: "dir/c3", + index: 9, + entry_mode: Mode( + FILE, + ), + id: Sha1(f00c965d8307308469e537302baa73048488f162), + copy: true, + }, + ] + "#); + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 0); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn copies_by_similarity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "tc1-identity", + "tc2-similarity", + Some(Rewrites { + copies: Some(Copies::default()), + ..Default::default() + }), + )?; + insta::assert_debug_snapshot!(changes, @r#" + [ + Rewrite { + source_location: "base", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "c4", + index: 6, + entry_mode: Mode( + FILE, + ), + id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + copy: true, + }, + Rewrite { + source_location: "base", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "c5", + index: 7, + entry_mode: Mode( + FILE, + ), + id: Sha1(08fe19ca4d2f79624f35333157d610811efc1aed), + copy: true, + }, + Rewrite { + source_location: "base", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "dir/c6", + index: 12, + entry_mode: Mode( + FILE, + ), + id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + copy: true, + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 2, + "two are similar, the other one is identical" + ); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn copies_in_entire_tree_by_similarity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "tc2-similarity", + "tc3-find-harder", + Some(Rewrites { + copies: Some(Copies::default()), + ..Default::default() + }), + )?; + assert_eq!( + changes.iter().filter(|c| matches!(c, Change::Rewrite { .. })).count(), + 0, + "needs --find-copies-harder to detect rewrites here" + ); + let actual: Vec<_> = changes.iter().map(|c| c.fields().0).collect(); + assert_eq!(actual, ["b", "c6", "c7", "newly-added"]); + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 3, + "it does have some candidates, probably for rename tracking" + ); + assert_eq!( + out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, + "no limit configured" + ); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + let (changes, out) = collect_changes_opts( + "tc2-similarity", + "tc3-find-harder", + Some(Rewrites { + copies: Some(Copies { + source: CopySource::FromSetOfModifiedFilesAndAllSources, + ..Default::default() + }), + ..Default::default() + }), + )?; + + // As the full-tree traversal order is different, it sees candidates in different order. + // Let's keep this as expectations, as in future there might be a candidate-based search that considers filenames + // or similarity in names. + insta::assert_debug_snapshot!(changes, @r#" + [ + Rewrite { + source_location: "base", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "c6", + index: 8, + entry_mode: Mode( + FILE, + ), + id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + copy: true, + }, + Rewrite { + source_location: "r/c3di", + source_index: 12, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + location: "c7", + index: 9, + entry_mode: Mode( + FILE, + ), + id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + copy: true, + }, + Rewrite { + source_location: "c5", + source_index: 7, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(08fe19ca4d2f79624f35333157d610811efc1aed), + location: "newly-added", + index: 19, + entry_mode: Mode( + FILE, + ), + id: Sha1(97b3d1a5707f8a11fa5fa8bc6c3bd7b3965601fd), + copy: true, + }, + Modification { + location: "b", + previous_index: 0, + previous_entry_mode: Mode( + FILE, + ), + previous_id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(f198d0640214092732566fb00543163845c8252c), + }, + ] + "#); + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 4); + assert_eq!( + out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, + "no limit configured" + ); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { + let (changes, out) = collect_changes_opts( + "tc2-similarity", + "tc3-find-harder", + Some(Rewrites { + copies: Some(Copies { + source: CopySource::FromSetOfModifiedFilesAndAllSources, + ..Default::default() + }), + limit: 2, // similarity checks can't be made that way + track_empty: false, + ..Default::default() + }), + )?; + + // Again, it finds a different first match for the rewrite compared to tree-traversal, expected for now. + insta::assert_debug_snapshot!(changes, @r#" + [ + Rewrite { + source_location: "base", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + location: "c6", + index: 8, + entry_mode: Mode( + FILE, + ), + id: Sha1(3bb459b831ea471b9cd1cbb7c6d54a74251a711b), + copy: true, + }, + Rewrite { + source_location: "r/c3di", + source_index: 12, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + location: "c7", + index: 9, + entry_mode: Mode( + FILE, + ), + id: Sha1(cf7a729ca69bfabd0995fc9b083e86a18215bd91), + copy: true, + }, + Modification { + location: "b", + previous_index: 0, + previous_entry_mode: Mode( + FILE, + ), + previous_id: Sha1(54781fa52cf133fa9d0bf59cfe2ef2621b5ad29f), + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(f198d0640214092732566fb00543163845c8252c), + }, + Addition { + location: "newly-added", + index: 19, + entry_mode: Mode( + FILE, + ), + id: Sha1(97b3d1a5707f8a11fa5fa8bc6c3bd7b3965601fd), + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 0, "similarity checks can't run"); + assert_eq!( + out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, + "no limit configured" + ); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 21); + + Ok(()) +} + +#[test] +fn realistic_renames_by_identity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r1-base", + "r1-change", + Some(Rewrites { + copies: Some(Copies::default()), + limit: 1, + track_empty: true, + ..Default::default() + }), + )?; + + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Rewrite { + source_location: "git-index/src/file.rs", + source_index: 18, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "git-index/src/file/mod.rs", + index: 19, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + copy: false, + }, + Addition { + location: "git-index/tests/index/file/access.rs", + index: 45, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Modification { + location: "git-index/tests/index/file/mod.rs", + previous_index: 45, + previous_entry_mode: Mode( + FILE, + ), + previous_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + index: 46, + entry_mode: Mode( + FILE, + ), + id: Sha1(8ba3a16384aacc37d01564b28401755ce8053f51), + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, 1); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn realistic_renames_disabled() -> crate::Result { + let changes = collect_changes_no_renames("r1-base", "r1-change")?; + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Deletion { + location: "git-index/src/file.rs", + index: 18, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "git-index/src/file/mod.rs", + index: 19, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "git-index/tests/index/file/access.rs", + index: 45, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Modification { + location: "git-index/tests/index/file/mod.rs", + previous_index: 45, + previous_entry_mode: Mode( + FILE, + ), + previous_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + index: 46, + entry_mode: Mode( + FILE, + ), + id: Sha1(8ba3a16384aacc37d01564b28401755ce8053f51), + }, + ] + "#); + Ok(()) +} + +#[test] +fn realistic_renames_disabled_3() -> crate::Result { + let changes = collect_changes_no_renames("r3-base", "r3-change")?; + + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Addition { + location: "src/ein.rs", + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "src/gix.rs", + index: 1, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "src/plumbing-cli.rs", + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "src/porcelain-cli.rs", + index: 4, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + + Ok(()) +} + +#[test] +fn realistic_renames_by_identity_3() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r3-base", + "r3-change", + Some(Rewrites { + copies: Some(Copies::default()), + limit: 1, + track_empty: true, + ..Default::default() + }), + )?; + + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Rewrite { + source_location: "src/plumbing-cli.rs", + source_index: 0, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "src/ein.rs", + index: 0, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + copy: false, + }, + Rewrite { + source_location: "src/porcelain-cli.rs", + source_index: 4, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "src/gix.rs", + index: 1, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + copy: false, + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 0, + "similarity checks disabled, and not necessary" + ); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn realistic_renames_2() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r2-base", + "r2-change", + Some(Rewrites { + copies: Some(Copies::default()), + track_empty: false, + ..Default::default() + }), + )?; + + // We cannot capture renames if track-empty is disabled, as these are actually empty, + // and we can't take directory-shortcuts here (i.e. tracking knows no directories here + // as is the case with trees where we traverse breadth-first. + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Deletion { + location: "git-sec/CHANGELOG.md", + index: 3, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/Cargo.toml", + index: 4, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/src/identity.rs", + index: 5, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/src/lib.rs", + index: 6, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/src/permission.rs", + index: 7, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/src/trust.rs", + index: 8, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/tests/identity/mod.rs", + index: 9, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Deletion { + location: "git-sec/tests/sec.rs", + index: 10, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/CHANGELOG.md", + index: 231, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/Cargo.toml", + index: 232, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/src/identity.rs", + index: 233, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/src/lib.rs", + index: 234, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/src/permission.rs", + index: 235, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/src/trust.rs", + index: 236, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/tests/identity/mod.rs", + index: 237, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + Addition { + location: "gix-sec/tests/sec.rs", + index: 238, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 0, + "similarity checks disabled, and not necessary" + ); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + Ok(()) +} + +#[test] +fn realistic_renames_3_without_identity() -> crate::Result { + let (changes, out) = collect_changes_opts( + "r4-base", + "r4-dir-rename-non-identity", + Some(Rewrites { + copies: None, + percentage: None, + limit: 0, + track_empty: false, + }), + )?; + + // We don't actually track directory renames, only files show up. + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Rewrite { + source_location: "src/plumbing/options.rs", + source_index: 4, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(00750edc07d6415dcc07ae0351e9397b0222b7ba), + location: "src/plumbing-renamed/options/mod.rs", + index: 4, + entry_mode: Mode( + FILE, + ), + id: Sha1(00750edc07d6415dcc07ae0351e9397b0222b7ba), + copy: false, + }, + Rewrite { + source_location: "src/plumbing/mod.rs", + source_index: 3, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(0cfbf08886fca9a91cb753ec8734c84fcbe52c9f), + location: "src/plumbing-renamed/mod.rs", + index: 3, + entry_mode: Mode( + FILE, + ), + id: Sha1(0cfbf08886fca9a91cb753ec8734c84fcbe52c9f), + copy: false, + }, + Rewrite { + source_location: "src/plumbing/main.rs", + source_index: 2, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), + location: "src/plumbing-renamed/main.rs", + index: 2, + entry_mode: Mode( + FILE, + ), + id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), + copy: false, + }, + ] + "#); + + let out = out.expect("tracking enabled"); + assert_eq!( + out.num_similarity_checks, 0, + "similarity checks disabled, and not necessary" + ); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); + + let (changes, _out) = collect_changes_opts_with_pathspec( + "r4-base", + "r4-dir-rename-non-identity", + Some(Rewrites { + copies: None, + percentage: None, + limit: 0, + track_empty: false, + }), + Some("src/plumbing/m*"), + )?; + + // Pathspecs are applied in advance, which affects rename tracking. + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Deletion { + location: "src/plumbing/main.rs", + index: 2, + entry_mode: Mode( + FILE, + ), + id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), + }, + Deletion { + location: "src/plumbing/mod.rs", + index: 3, + entry_mode: Mode( + FILE, + ), + id: Sha1(0cfbf08886fca9a91cb753ec8734c84fcbe52c9f), + }, + ] + "#); + + let (changes, _out) = collect_changes_opts_with_pathspec( + "r4-base", + "r4-dir-rename-non-identity", + Some(Rewrites { + copies: None, + percentage: None, + limit: 0, + track_empty: false, + }), + Some("src/plumbing-renamed/m*"), + )?; + // One can also get the other side of the rename + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Addition { + location: "src/plumbing-renamed/main.rs", + index: 2, + entry_mode: Mode( + FILE, + ), + id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), + }, + Addition { + location: "src/plumbing-renamed/mod.rs", + index: 3, + entry_mode: Mode( + FILE, + ), + id: Sha1(0cfbf08886fca9a91cb753ec8734c84fcbe52c9f), + }, + ] + "#); + + Ok(()) +} + +#[test] +fn unmerged_entries_and_intent_to_add() -> crate::Result { + let (changes, _out) = collect_changes_opts( + "r4-dir-rename-non-identity", + ".git/index", + Some(Rewrites { + copies: None, + percentage: None, + limit: 0, + track_empty: false, + }), + )?; + + // each unmerged entry is emitted separately, and no entry is emitted for + // paths that are mentioned there. Intent-to-add is transparent. + // All that with rename tracking… + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Unmerged { + location: "src/plumbing-renamed/main.rs", + stage: Base, + index: 2, + entry_mode: Mode( + FILE, + ), + id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), + }, + Unmerged { + location: "src/plumbing-renamed/main.rs", + stage: Ours, + index: 3, + entry_mode: Mode( + FILE, + ), + id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), + }, + ] + "#); + + let changes = collect_changes_no_renames("r4-dir-rename-non-identity", ".git/index")?; + // …or without + insta::assert_debug_snapshot!(changes.into_iter().collect::>(), @r#" + [ + Unmerged { + location: "src/plumbing-renamed/main.rs", + stage: Base, + index: 2, + entry_mode: Mode( + FILE, + ), + id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), + }, + Unmerged { + location: "src/plumbing-renamed/main.rs", + stage: Ours, + index: 3, + entry_mode: Mode( + FILE, + ), + id: Sha1(d00491fd7e5bb6fa28c517a0bb32b8b506539d4d), + }, + ] + "#); + + let (index, _, _, _, _) = repo_with_indices(".git/index", ".git/index", None)?; + assert_eq!( + index.entry_by_path("will-add".into()).map(|e| e.id), + Some(hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")), + "the file is there, but we don't see it" + ); + + Ok(()) +} + +mod util { + use gix_diff::rewrites; + use std::convert::Infallible; + use std::path::{Path, PathBuf}; + + fn repo_workdir() -> crate::Result { + gix_testtools::scripted_fixture_read_only_standalone("make_diff_for_rewrites_repo.sh") + } + + pub fn repo_with_indices( + lhs: impl Into>, + rhs: impl Into>, + patterns: impl IntoIterator, + ) -> gix_testtools::Result<( + gix_index::State, + gix_index::State, + gix_diff::blob::Platform, + gix_odb::Handle, + gix_pathspec::Search, + )> { + let root = repo_workdir()?; + let odb = gix_odb::at(root.join(".git/objects"))?; + let lhs = read_index(&odb, &root, lhs.into())?; + let rhs = read_index(&odb, &root, rhs.into())?; + + let cache = gix_diff::blob::Platform::new( + Default::default(), + gix_diff::blob::Pipeline::new(Default::default(), Default::default(), Vec::new(), Default::default()), + Default::default(), + gix_worktree::Stack::new( + &root, + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::default()), + Default::default(), + Vec::new(), + Vec::new(), + ), + ); + let pathspecs = gix_pathspec::Search::from_specs( + patterns + .into_iter() + .map(|p| gix_pathspec::Pattern::from_bytes(p.as_bytes(), Default::default()).expect("valid pattern")), + None, + &root, + )?; + Ok((lhs, rhs, cache, odb, pathspecs)) + } + + pub fn collect_changes_no_renames( + lhs: impl Into>, + rhs: impl Into>, + ) -> gix_testtools::Result> { + Ok(collect_changes_opts(lhs, rhs, None)?.0) + } + + pub fn collect_changes_opts( + lhs: impl Into>, + rhs: impl Into>, + options: Option, + ) -> gix_testtools::Result<(Vec, Option)> { + collect_changes_opts_with_pathspec(lhs, rhs, options, None) + } + + pub fn collect_changes_opts_with_pathspec( + lhs: impl Into>, + rhs: impl Into>, + options: Option, + patterns: impl IntoIterator, + ) -> gix_testtools::Result<(Vec, Option)> { + let (from, to, mut cache, odb, mut pathspecs) = repo_with_indices(lhs, rhs, patterns)?; + let mut out = Vec::new(); + let rewrites_info = gix_diff::index( + &from, + &to, + |change| -> Result<_, Infallible> { + out.push(change.into_owned()); + Ok(gix_diff::index::Action::Continue) + }, + options.map(|rewrites| gix_diff::index::RewriteOptions { + rewrites, + resource_cache: &mut cache, + find: &odb, + }), + &mut pathspecs, + &mut |_, _, _, _| false, + )?; + Ok((out, rewrites_info)) + } + + fn read_index( + odb: impl gix_object::Find, + root: &Path, + tree: Option<&str>, + ) -> gix_testtools::Result { + let Some(tree) = tree else { + return Ok(gix_index::State::new(gix_hash::Kind::Sha1)); + }; + if tree == ".git/index" { + Ok(gix_index::File::at(root.join(tree), gix_hash::Kind::Sha1, false, Default::default())?.into()) + } else { + let tree_id_path = root.join(tree).with_extension("tree"); + let hex_id = std::fs::read_to_string(&tree_id_path).map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Could not read '{}': {}", tree_id_path.display(), err), + ) + })?; + let tree_id = gix_hash::ObjectId::from_hex(hex_id.trim().as_bytes())?; + Ok(gix_index::State::from_tree(&tree_id, odb, Default::default())?) + } + } +} +use crate::hex_to_id; +use util::{collect_changes_no_renames, collect_changes_opts, collect_changes_opts_with_pathspec, repo_with_indices}; diff --git a/gix-diff/tests/diff/main.rs b/gix-diff/tests/diff/main.rs index 2163b5d3b01..a6be530ffdb 100644 --- a/gix-diff/tests/diff/main.rs +++ b/gix-diff/tests/diff/main.rs @@ -5,6 +5,7 @@ fn hex_to_id(hex: &str) -> gix_hash::ObjectId { } mod blob; +mod index; mod rewrites; mod tree; mod tree_with_rewrites; diff --git a/gix-diff/tests/fixtures/generated-archives/make_diff_for_rewrites_repo.tar b/gix-diff/tests/fixtures/generated-archives/make_diff_for_rewrites_repo.tar index 878a4113ef799f5cc50be490602134493ac692e6..b36453ef90ea0a352495478bdb8b1366b24a9bfb 100644 GIT binary patch delta 2134 zcmbVOYfuwc6wXaHfdnxD5zs+reL@gr@9rj>Fcv{&c-Y98qEi&Lo81khJkklMm0HA6 z>kp?@vXuBh3O;b!QeQ!%)M~9`AL6Sm(<0Te^+yLC>tLM$i-Nsu5=7e{w)t~*^4)vS zch7$3J6qi`w)%AR5lrR9LN^ee%|C!h6$znHMu;$n-v=gvaG}(>TEI(LN7*b_xfewJ z3EWvqjU~gHnXxh}BYnQuG@zqeDkBw;89un4qw zsv<2sO?;}Hp?OvWkw}f5krs-});LN^X0kjVEKQy@BLh7b%4R|wYRX;Q&;zLv9qE)( z`b%t0$D<}^PRQ=H)Gk@~?IDa!ckUO6OoN{A{Hekt7wuT|F=+9YxZegZ4fnGVPVgmK3_T=iyKEH6+(hGLV_en z3MrC+29l=y2}J;&x^L*k!`;iq*<5THSCoR!a|vvPz~gq8vlLIOf*OSC6NHzPubAFF z`}r;rLDYBd4z;MhTREOF{*w))sS#|B-8dKS&PfmtbuZT!30$#rRYeMpnhurB&uFOk z@CqkB;tA@f`xYhLzp_z<(=JM`3lWj2D*5J8Vl4lM2`NCo{$K7mi>ILgFO0s$$G zF>|b7=Z$uPw9~Z7%yNXuPO!)nY&8+HB>#9FOI9&-P#8Y2RiOqR%~H%9eXf_Qeca_~ zUs#&@V9M`podsnJ?w7`R@|R*sn6q}#dZfNJppFOHE!q{pr(GcjuIZG~z?cY*F&K?J zqzE%dnn;>tZ3ZZiHo+_~CgeddnEN(>zMVX9g6YOL2q=?pzP*=MU8kZiXg6)Un%~EK zbZ6@~EsBAHzBLM2Z=!Z3%6Juc1}@7ZecEgC!QbQp9q>V4QIh-s&6R#7g1B?u>J7)=wWjnw3yVcs6ameE-|5OH@l+&iMnybzk+Yb^W-*{UAYn=xuCyr$LWJ z?ie*`T3=PcuEEZpUPXiTaN)g+*1nX-vDdw@vUthI#|zHJt)d4OoM#4)>ht@LebO{b z+nso@{m{*hu=Z`4%Y99)pajS{c6b2vC`jL~V~X;2ImHiGqZy!TP<*l-8d#cPdBMyY zIo3vTCcDYN@eDMYdBR4S37ZYQs9-f3r9yJ z^qUiJqSyEtsH?`b!*o|5y1wvg?4biqIhB9Pi8@nW4X%_#N?;Q`?-@aa3Q`b0bCekt z!Br^WenY|INAyL!Ml%JU7jVJy6wMU1J2DPUyi-GCRGn+h&J0e`B~k*Dl2il^_JV9o z5ty`=-1!*+l421?C~3mr&+Y{b1~lo5^Kw^8Y=;*mqD2(*jN;w|dGlRtZfMZ-(ZT}1 zV@`;L4dH8W%|m~5z&vYC;eg>f@0TZ4gwu`z?8 zfr*)+fsuicu>pgDfuWJ1p$SCUVn%bu$&4zJlMj4!*zBORjB%0|>tt5DIu=87Q-jUz zb`6ZOObiSRoaxdV+O%hBZG_Q`3=G0zFVj3H`#7mi_H+;t2gx7p)&t7}X{fvxSbnC1 zupLOkq=9)FjD{-kVPI%n!oa}z6=;bF5Hnq1nfSLZdfP9Jqv8v88|~Q27ChINL1vPG zk_1HO{XiHEHNkgsi=!vVdXvV!*-<-YcCTqJWh)64c z%3ad2hegx2V&=will-add +git add --intent-to-add will-add + +# a file with skip-worktree flag, which has no bearing on tree/index diffs. +git update-index --skip-worktree src/shared.rs +rm src/shared.rs + mv ../*.tree . \ No newline at end of file From c4e87454bfd4b92bcf2c8a47a031940cab6c2e18 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 15 Mar 2024 08:15:17 +0100 Subject: [PATCH 11/17] doc: inform about tree to index "status" --- gix-status/src/lib.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/gix-status/src/lib.rs b/gix-status/src/lib.rs index 672beafaf99..6e0ed72befe 100644 --- a/gix-status/src/lib.rs +++ b/gix-status/src/lib.rs @@ -2,10 +2,23 @@ //! of the repository state, like comparisons between… //! //! * index and working tree -//! * index and tree -//! * find untracked files +//! * *tree and index* //! -//! While also being able to check check if the working tree is dirty, quickly. +//! …while also being able to check if the working tree is dirty, quickly, by instructing the operation to stop once the first +//! change was found. +//! +//! ### Tree-Index Status +//! +//! This status is not actually implemented here as it's not implemented directly. Instead, one creates an Index from a tree +//! and then diffs two indices with `gix_diff::index(index_from_tree, usually_dot_git_index)`. This adds about 15% to the runtime +//! and comes at the cost of another index in memory. +//! Once there are generators implementing depth-first tree iteration should become trivial, but for now it's very hard if one +//! wants to return referenced state of the iterator (which is not possible). +//! +//! ### Difference to `gix-diff` +//! +//! Technically, `status` is just another form of diff between different kind of sides, i.e. an index and a working tree. +//! This is the difference to `gix-diff`, which compares only similar items. //! //! ### Feature Flags #![cfg_attr( From 83f3d93eaa1d7a96e0fa60840502f211c20edc3b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Dec 2024 17:22:36 +0100 Subject: [PATCH 12/17] feat: `Repository::tree_index_status()` to see the changes between a tree and an index. It also respects `status.rename` and `status.renameLimit` to configure rename tracking. --- gix/Cargo.toml | 4 +- gix/src/config/tree/sections/status.rs | 12 ++- gix/src/pathspec.rs | 2 +- gix/src/status/mod.rs | 3 + gix/src/status/tree_index.rs | 137 +++++++++++++++++++++++++ gix/src/worktree/mod.rs | 1 + 6 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 gix/src/status/tree_index.rs diff --git a/gix/Cargo.toml b/gix/Cargo.toml index c0578080ff9..61280a84054 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -86,7 +86,7 @@ comfort = [ command = ["dep:gix-command"] ## Obtain information similar to `git status`. -status = ["gix-status", "dirwalk", "index", "blob-diff"] +status = ["gix-status", "dirwalk", "index", "blob-diff", "gix-diff/index"] ## Utilities for interrupting computations and cleaning up tempfiles. interrupt = ["dep:signal-hook", "gix-tempfile/signals", "dep:parking_lot"] @@ -374,7 +374,7 @@ gix-command = { version = "^0.4.0", path = "../gix-command", optional = true } gix-worktree-stream = { version = "^0.18.0", path = "../gix-worktree-stream", optional = true } gix-archive = { version = "^0.18.0", path = "../gix-archive", default-features = false, optional = true } -gix-blame = { version= "^0.0.0", path ="../gix-blame", optional = true } +gix-blame = { version = "^0.0.0", path = "../gix-blame", optional = true } # For communication with remotes gix-protocol = { version = "^0.47.0", path = "../gix-protocol" } diff --git a/gix/src/config/tree/sections/status.rs b/gix/src/config/tree/sections/status.rs index f60600e214b..28038325785 100644 --- a/gix/src/config/tree/sections/status.rs +++ b/gix/src/config/tree/sections/status.rs @@ -9,6 +9,16 @@ impl Status { &config::Tree::STATUS, validate::ShowUntrackedFiles, ); + /// The `status.renameLimit` key. + pub const RENAME_LIMIT: keys::UnsignedInteger = keys::UnsignedInteger::new_unsigned_integer( + "renameLimit", + &config::Tree::MERGE, + ) + .with_note( + "The limit is actually squared, so 1000 stands for up to 1 million diffs if fuzzy rename tracking is enabled", + ); + /// The `status.renames` key. + pub const RENAMES: super::diff::Renames = super::diff::Renames::new_renames("renames", &config::Tree::MERGE); } /// The `status.showUntrackedFiles` key. @@ -41,7 +51,7 @@ impl Section for Status { } fn keys(&self) -> &[&dyn Key] { - &[&Self::SHOW_UNTRACKED_FILES] + &[&Self::SHOW_UNTRACKED_FILES, &Self::RENAMES, &Self::RENAME_LIMIT] } } diff --git a/gix/src/pathspec.rs b/gix/src/pathspec.rs index be69ad0321e..78316c1037b 100644 --- a/gix/src/pathspec.rs +++ b/gix/src/pathspec.rs @@ -202,7 +202,7 @@ impl PathspecDetached { } } -fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { +pub(crate) fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode { if is_dir { gix_index::entry::Mode::DIR } else { diff --git a/gix/src/status/mod.rs b/gix/src/status/mod.rs index 92656fcfd3e..6adc4ac04ce 100644 --- a/gix/src/status/mod.rs +++ b/gix/src/status/mod.rs @@ -176,3 +176,6 @@ mod platform; /// pub mod index_worktree; + +/// +pub mod tree_index; diff --git a/gix/src/status/tree_index.rs b/gix/src/status/tree_index.rs new file mode 100644 index 00000000000..c9beb25f41a --- /dev/null +++ b/gix/src/status/tree_index.rs @@ -0,0 +1,137 @@ +use crate::config::tree; +use crate::Repository; + +/// The error returned by [Repository::tree_index_status()]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + IndexFromMTree(#[from] crate::repository::index_from_tree::Error), + #[error(transparent)] + RewritesConfiguration(#[from] crate::diff::new_rewrites::Error), + #[error("Could not create diff-cache for similarity checks")] + DiffResourceCache(#[from] crate::repository::diff_resource_cache::Error), + #[error(transparent)] + TreeIndexDiff(#[from] gix_diff::index::Error), +} + +/// Specify how to perform rewrite tracking [Repository::tree_index_status()]. +#[derive(Debug, Copy, Clone)] +pub enum TrackRenames { + /// Check `status.renames` and then `diff.renames` if the former isn't set. Otherwise, default to performing rewrites if nothing + /// is set. + AsConfigured, + /// Track renames according ot the given configuration. + Given(gix_diff::Rewrites), + /// Do not track renames. + Disabled, +} + +/// The outcome of [Repository::tree_index_status()]. +#[derive(Clone)] +pub struct Outcome { + /// Additional information produced by the rename tracker. + /// + /// It may be `None` if rename tracking was disabled. + pub rewrite: Option, + /// The index produced from the input `tree` for the purpose of diffing. + /// + /// At some point this might go away once it's possible to diff an index from a tree directly. + pub tree_index: gix_index::State, +} + +impl Repository { + /// Produce the `git status` portion that shows the difference between `tree_id` (usually `HEAD^{tree}`) and the `worktree_index` + /// (typically the current `.git/index`), and pass all changes to `cb(change, tree_index, worktree_index)` with + /// full access to both indices that contributed to the change. + /// + /// *(It's notable that internally, the `tree_id` is converted into an index before diffing these)*. + /// Set `pathspec` to `Some(_)` to further reduce the set of files to check. + /// + /// ### Notes + /// + /// * This is a low-level method - prefer the [`Repository::status()`] platform instead for access to various iterators + /// over the same information. + pub fn tree_index_status<'repo, E>( + &'repo self, + tree_id: &gix_hash::oid, + worktree_index: &gix_index::State, + pathspec: Option<&mut crate::Pathspec<'repo>>, + renames: TrackRenames, + mut cb: impl FnMut( + gix_diff::index::ChangeRef<'_, '_>, + &gix_index::State, + &gix_index::State, + ) -> Result, + ) -> Result + where + E: Into>, + { + let tree_index: gix_index::State = self.index_from_tree(tree_id)?.into(); + let rewrites = match renames { + TrackRenames::AsConfigured => { + let (mut rewrites, mut is_configured) = crate::diff::utils::new_rewrites_inner( + &self.config.resolved, + self.config.lenient_config, + &tree::Status::RENAMES, + &tree::Status::RENAME_LIMIT, + )?; + if !is_configured { + (rewrites, is_configured) = + crate::diff::utils::new_rewrites(&self.config.resolved, self.config.lenient_config)?; + } + if !is_configured { + rewrites = Some(Default::default()); + } + rewrites + } + TrackRenames::Given(rewrites) => Some(rewrites), + TrackRenames::Disabled => None, + }; + let mut resource_cache = None; + if rewrites.is_some() { + resource_cache = Some(self.diff_resource_cache_for_tree_diff()?); + } + let mut pathspec_storage = None; + if pathspec.is_none() { + pathspec_storage = self + .pathspec( + true, + None::<&str>, + false, + &gix_index::State::new(self.object_hash()), + gix_worktree::stack::state::attributes::Source::IdMapping, + ) + .expect("Impossible for this to fail without patterns") + .into(); + } + + let pathspec = pathspec.unwrap_or(pathspec_storage.as_mut().expect("set if pathspec isn't set by user")); + let rewrite = gix_diff::index( + &tree_index, + worktree_index, + |change| cb(change, &tree_index, worktree_index), + rewrites + .zip(resource_cache.as_mut()) + .map(|(rewrites, resource_cache)| gix_diff::index::RewriteOptions { + resource_cache, + find: self, + rewrites, + }), + &mut pathspec.search, + &mut |relative_path, case, is_dir, out| { + let stack = pathspec.stack.as_mut().expect("initialized in advance"); + stack + .set_case(case) + .at_entry( + relative_path, + Some(crate::pathspec::is_dir_to_mode(is_dir)), + &pathspec.repo.objects, + ) + .map_or(false, |platform| platform.matching_attributes(out)) + }, + )?; + + Ok(Outcome { rewrite, tree_index }) + } +} diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index b56ccd3fbc1..39c2272b3d8 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -23,6 +23,7 @@ pub type Index = gix_fs::SharedFileSnapshot; /// A type to represent an index which either was loaded from disk as it was persisted there, or created on the fly in memory. #[cfg(feature = "index")] #[allow(clippy::large_enum_variant)] +#[derive(Clone)] pub enum IndexPersistedOrInMemory { /// The index as loaded from disk, and shared across clones of the owning `Repository`. Persisted(Index), From 8ae9e5729bd9e7d6308bd226f510b3415381de89 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Dec 2024 18:42:36 +0100 Subject: [PATCH 13/17] feat: `Repository::is_dirty()` now also checks for tree/index changes. This copmpletes the `is_dirty()` implementation. --- gix/src/config/cache/access.rs | 2 +- gix/src/status/mod.rs | 31 +++++++++++++++--- .../generated-archives/make_status_repos.tar | Bin 66048 -> 117248 bytes gix/tests/fixtures/make_status_repos.sh | 8 +++++ gix/tests/gix/status.rs | 17 +++++++--- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 46b842cdffb..4022b6430da 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -181,7 +181,7 @@ impl Cache { .user_agent .get_or_init(|| { self.resolved - .string(Gitoxide::USER_AGENT.logical_name().as_str()) + .string(&Gitoxide::USER_AGENT) .map_or_else(|| crate::env::agent().into(), |s| s.to_string()) }) .to_owned(); diff --git a/gix/src/status/mod.rs b/gix/src/status/mod.rs index 6adc4ac04ce..e165d3b5ee5 100644 --- a/gix/src/status/mod.rs +++ b/gix/src/status/mod.rs @@ -130,6 +130,7 @@ impl Repository { /// pub mod is_dirty { use crate::Repository; + use std::convert::Infallible; /// The error returned by [Repository::is_dirty()]. #[derive(Debug, thiserror::Error)] @@ -139,6 +140,12 @@ pub mod is_dirty { StatusPlatform(#[from] crate::status::Error), #[error(transparent)] CreateStatusIterator(#[from] crate::status::index_worktree::iter::Error), + #[error(transparent)] + TreeIndexStatus(#[from] crate::status::tree_index::Error), + #[error(transparent)] + HeadTreeId(#[from] crate::reference::head_tree_id::Error), + #[error(transparent)] + OpenWorktreeIndex(#[from] crate::worktree::open_index::Error), } impl Repository { @@ -150,12 +157,26 @@ pub mod is_dirty { /// * submodules are taken in consideration, along with their `ignore` and `isActive` configuration /// /// Note that *untracked files* do *not* affect this flag. - /// - /// ### Incomplete Implementation Warning - /// - /// Currently, this does not compute changes between the head and the index. - // TODO: use iterator which also tests for head->index changes. pub fn is_dirty(&self) -> Result { + { + let head_tree_id = self.head_tree_id()?; + let mut index_is_dirty = false; + + // Run this first as there is a high likelihood to find something, and it's very fast. + self.tree_index_status( + &head_tree_id, + &*self.index_or_empty()?, + None, + crate::status::tree_index::TrackRenames::Disabled, + |_, _, _| { + index_is_dirty = true; + Ok::<_, Infallible>(gix_diff::index::Action::Cancel) + }, + )?; + if index_is_dirty { + return Ok(true); + } + } let is_dirty = self .status(gix_features::progress::Discard)? .index_worktree_rewrites(None) diff --git a/gix/tests/fixtures/generated-archives/make_status_repos.tar b/gix/tests/fixtures/generated-archives/make_status_repos.tar index bfa898c90c1dd9d8bccccf5ce57fb2e680c4d1de..2666626e624b4aab1cbebeb6d39be555c35409ef 100644 GIT binary patch delta 2812 zcmbW3ZERCj7{~9q?QTom)@@w&Qnp!!;xuFTocr1`6~QquBqE?-WH@$j?_jVux?!eE z$Z!gVDAJV}NH#Jl!UvOKc9RhTXfy^#7=jUbNdQTlff%v~QJ90Io_4*bmvtWqA8v2j z{?2or|8t&~)A^CTbC08sX4coY&24-ye8=@}cfLKmf#w(}Ad!;rF98GGr-3c@ccy(+ z$m=u-gd{12C?P}%#|Q$FDL7$cc9iUrnMI(oqI_AV5ENwudB_Wpf)Me%g3M*vF0cu# z4_@1v$JKG>8&Fgh2siAZsBH%+3j9k^6pJ6*qGMg(%12*xd~&tt#LBnxD;6!nFNT_nywJ5}Yo(1T4(&d?Y|UV3SIw^Aw!WoCZTU`MMkDho@soyW zXo`4@5P*ScP~ua9QctaL!co74aN485HWSyk`{XjwKwin($+o;HB6{sj*(#!I%|Or z5$F&$r3f$)R|6cjxV4>7+p+Ho&csBr?ugon@6`mVj+w$|WKwIRETPU5>U>-wFPhXZ z7Poo`)l-x&36m3Fl10H>*86svQFGu+TgaOXV-;Cmh5eOrLMHrOZ1E&WpkNG2#;4UH z>N0^Y<1!T$vh5X%w=98T>CdB-x!75#;QF6v7kQ=N z@$-Lq{*h$S=2}hivRP3Ecyf~rF&XazsXCnmaN=-EB1w^HmT;yMD4k<)5lE)i3=1@e zK(~+`%9fB%#~qj}$Bmk6RyXQeTPuguQ1kcsvJ?`RKHJ9 zbwO4YL4%6V7ts8;J*tYVsj3gwX^N^sNipT57w7Sl>WzmIuEB)93I}l24GoRZRCa{^ zIiX}eQBwMd5?tL$=TemfkVOf?4I>GsDhVL&@!GtpL@dsmJTiGGBuA#?291|`i)MUr zYjs=Cv$KRB`_3&Z9o#W`=2*3_y8o+@%;{%$T(4ZWb53Knjr*{wqUiL#&rWx@S5({^ zT3G-6!5@s8O^Q9eSGVvcw*c_;X74JeydzrLwCsB5*Yy7wh-TX2i+n%089 zH#fF4x9VG(>b2&8`48Tx9b``2Rk*}h$L(TOJYP$S^HLfrQq1y?4ODtVePUG3AD~%# zge|cfY}OcfCU&owvDQ6mz7zM071sMj80M5sfb%0AaS*=~;2V#CojC!v*In}J-Ju?^ zWaYMbE8x@iUzHQUxZ`qyfvW|yQ84lVMA++$!B#qGV`h6a!|)8+bByBthYrIt1niNS zd+o-|;i%2<9I`u&;_VO9BA)78!*hPz7G`@kPcb_bAvwe?vtyvjgO>%z(<1C;td>o~ zyL%ouB5W?!&$tYBj*~VDUmi;{Sf7)L6!Q3ibfoYF#waWu17Y?;O97vHiXy_7okZn4 z(J2nxcGD5}ExX|!#UaSO`xiLyH$CuA4sGO&j{-MBe!n8}enp;(Q=mOjjtjf*eVmU+ vF--p>H6;4hcFr3+eG8g%bse6dB6_VZoso(zxQW$!` delta 2762 zcmb_eZ)_7~81LJ5qdRd3R3ZEU85>MwjF6z4qMg?zlhe z7BDvw{NNYEsBy&jL4{B?lBi8qV~9qL-<1!<7(XB}h7f~(B%tE+-d#&OKs2#Qdu{K1 zf6wpvJ-_FX@bR9*-7m7Pm{okCdUwyt1@@>5Erq6fm zjL#RiLf>Wf@}RSj{pq|y7t*V@@Knpf<;z0&>P04g?c5JX9(YCjwf~fCEx$kaaP`>6 z5L#P6-TqqlGP)V4gcnihboj?$E&Lh^`D=Y2pzEj>K7&H-mA-e8ztZ;>f^To2_FCTw zbiJ(_zP7<%>wOm8dZf~mMq6t=GP(i({@&hM=@~@rod!_^GS5kJVu6#T)kiUU{N#JV zm11vCCc|cYDK;~jV$*DTnDw!2I>n9*``9uU5Nc>M+KzU^9)U9n=CH<@d=bYuMHo_b z@0USzi5PKwh={6Am;p?qNlF1rCSH&@>;x_!Hg|SnPSbQ%(O8A#=%91Y!?dZePHwi zw4|w%d6m{y5g>y1Lno9N2*>FJTLy?^!)7YjlC#&*62Q3vCn<(G?i2z$g?Fl*^ztS- z7d+-^Iu4mj1xblRsCzHZLG;@@_GTw0(XrKBVH=uxPoY3T~3{_mJW??O7wmHbZ2CFdg-P>*TqIrgfw^BTBR>psNXARB@ycw2}HaCc3Wb<5+~p zBn&*574U4*=m4ZKZ93N@_=#<4fLH`!QnZ~dh0fJIQ~BA+sho2?Fq3;~HlNF88D41S zPc@*1DgY)<9LA9s1xXZX9RUl}qhrQ3QDY>zBf_~uWO)8D9gSNp)5=)^`FRl?wa~j^>>>THrvOvH)VzVIIarWR;-myj_Eq9PG z(_JjmSoW<&MN8bsoMNxmK z*8dRtV?(v)ui%zie;4{QQ0v9$R-o3i7u^V~hCf7FcWQY3(I=Hjjd-XvbPhd_s;^!s zp%t81(@{8B4gcKcuXbO#*>*boi$6q<1<(q{%I3Xrm?{^~ux%OOG~yndK1Q8wl}#TZ ze{Itmg45a7&(KwL_EY3vJCC;bSGRnHjIQ+X`m8czW9judBN3T8%<>FUH|UjT`r jv!5gXCOD&|M$?&8hD}(Cv{>DI@i$b(7caRVD|px6!`pyL diff --git a/gix/tests/fixtures/make_status_repos.sh b/gix/tests/fixtures/make_status_repos.sh index 63c1cfb1d48..3a4c3d535f7 100755 --- a/gix/tests/fixtures/make_status_repos.sh +++ b/gix/tests/fixtures/make_status_repos.sh @@ -13,3 +13,11 @@ git init -q untracked-only mkdir new touch new/untracked subdir/untracked ) + +git init git-mv +(cd git-mv + echo hi > file + git add file && git commit -m "init" + + git mv file renamed +) diff --git a/gix/tests/gix/status.rs b/gix/tests/gix/status.rs index dad2eb188e7..0e5b97bc60c 100644 --- a/gix/tests/gix/status.rs +++ b/gix/tests/gix/status.rs @@ -134,7 +134,7 @@ mod index_worktree { } mod is_dirty { - use crate::status::submodule_repo; + use crate::status::{repo, submodule_repo}; #[test] fn various_changes_positive() -> crate::Result { @@ -155,7 +155,7 @@ mod is_dirty { let repo = submodule_repo("module1")?; assert_eq!( repo.status(gix::progress::Discard)? - .into_index_worktree_iter(Vec::new())? + .into_index_worktree_iter(None)? .count(), 1, "there is one untracked file" @@ -168,9 +168,18 @@ mod is_dirty { } #[test] - fn no_changes() -> crate::Result { + fn index_changed() -> crate::Result { + let repo = repo("git-mv")?; + assert!( + repo.is_dirty()?, + "the only detectable change is in the index, in comparison to the HEAD^{{tree}}" + ); + let repo = submodule_repo("with-submodules")?; - assert!(!repo.is_dirty()?, "there are no changes"); + assert!( + repo.is_dirty()?, + "the index changed here as well, this time there is also a new file" + ); Ok(()) } } From a6f397f529953ac4177962059c9e6d9bcee2b657 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Dec 2024 20:18:40 +0100 Subject: [PATCH 14/17] fix!: all `config::Snapshot` access now uses the new `Key` trait. That way one can officially use "section.name" strings or `&Section::NAME`. --- gix/src/config/snapshot/access.rs | 26 +++++++++---------- .../remote/connection/fetch/receive_pack.rs | 4 +-- gix/src/remote/url/scheme_permission.rs | 4 +-- gix/src/repository/identity.rs | 2 +- gix/src/repository/mailmap.rs | 4 +-- gix/src/repository/shallow.rs | 10 ++----- 6 files changed, 22 insertions(+), 28 deletions(-) diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 218417bc23a..6b753ab050e 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -22,13 +22,13 @@ impl<'repo> Snapshot<'repo> { /// For a non-degenerating version, use [`try_boolean(…)`][Self::try_boolean()]. /// /// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust. - pub fn boolean<'a>(&self, key: impl Into<&'a BStr>) -> Option { + pub fn boolean(&self, key: impl gix_config::AsKey) -> Option { self.try_boolean(key).and_then(Result::ok) } /// Like [`boolean()`][Self::boolean()], but it will report an error if the value couldn't be interpreted as boolean. - pub fn try_boolean<'a>(&self, key: impl Into<&'a BStr>) -> Option> { - self.repo.config.resolved.boolean(key.into()) + pub fn try_boolean(&self, key: impl gix_config::AsKey) -> Option> { + self.repo.config.resolved.boolean(key) } /// Return the resolved integer at `key`, or `None` if there is no such value or if the value can't be interpreted as @@ -37,40 +37,40 @@ impl<'repo> Snapshot<'repo> { /// For a non-degenerating version, use [`try_integer(…)`][Self::try_integer()]. /// /// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust. - pub fn integer<'a>(&self, key: impl Into<&'a BStr>) -> Option { + pub fn integer(&self, key: impl gix_config::AsKey) -> Option { self.try_integer(key).and_then(Result::ok) } /// Like [`integer()`][Self::integer()], but it will report an error if the value couldn't be interpreted as boolean. - pub fn try_integer<'a>(&self, key: impl Into<&'a BStr>) -> Option> { - self.repo.config.resolved.integer(key.into()) + pub fn try_integer(&self, key: impl gix_config::AsKey) -> Option> { + self.repo.config.resolved.integer(key) } /// Return the string at `key`, or `None` if there is no such value. /// /// Note that this method takes the most recent value at `key` even if it is from a file with reduced trust. - pub fn string<'a>(&self, key: impl Into<&'a BStr>) -> Option> { - self.repo.config.resolved.string(key.into()) + pub fn string(&self, key: impl gix_config::AsKey) -> Option> { + self.repo.config.resolved.string(key) } /// Return the trusted and fully interpolated path at `key`, or `None` if there is no such value /// or if no value was found in a trusted file. /// An error occurs if the path could not be interpolated to its final value. - pub fn trusted_path<'a>( + pub fn trusted_path( &self, - key: impl Into<&'a BStr>, + key: impl gix_config::AsKey, ) -> Option, gix_config::path::interpolate::Error>> { - self.repo.config.trusted_file_path(key.into()) + self.repo.config.trusted_file_path(key) } /// Return the trusted string at `key` for launching using [command::prepare()](gix_command::prepare()), /// or `None` if there is no such value or if no value was found in a trusted file. - pub fn trusted_program<'a>(&self, key: impl Into<&'a BStr>) -> Option> { + pub fn trusted_program(&self, key: impl gix_config::AsKey) -> Option> { let value = self .repo .config .resolved - .string_filter(key.into(), &mut self.repo.config.filter_config_section.clone())?; + .string_filter(key, &mut self.repo.config.filter_config_section.clone())?; Some(match gix_path::from_bstr(value) { Cow::Borrowed(v) => Cow::Borrowed(v.as_os_str()), Cow::Owned(v) => Cow::Owned(v.into_os_string()), diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index ef328da2d38..e9e932d44a9 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -1,7 +1,7 @@ use crate::{ config::{ cache::util::ApplyLeniency, - tree::{Clone, Fetch, Key}, + tree::{Clone, Fetch}, }, remote, remote::{ @@ -117,7 +117,7 @@ where let negotiator = repo .config .resolved - .string(Fetch::NEGOTIATION_ALGORITHM.logical_name().as_str()) + .string(Fetch::NEGOTIATION_ALGORITHM) .map(|n| Fetch::NEGOTIATION_ALGORITHM.try_into_negotiation_algorithm(n)) .transpose() .with_leniency(repo.config.lenient_config)? diff --git a/gix/src/remote/url/scheme_permission.rs b/gix/src/remote/url/scheme_permission.rs index 7709537fed7..47fbd351b8a 100644 --- a/gix/src/remote/url/scheme_permission.rs +++ b/gix/src/remote/url/scheme_permission.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, collections::BTreeMap}; use crate::{ bstr::{BStr, BString, ByteSlice}, config, - config::tree::{gitoxide, Key, Protocol}, + config::tree::{gitoxide, Protocol}, }; /// All allowed values of the `protocol.allow` key. @@ -91,7 +91,7 @@ impl SchemePermission { let user_allowed = saw_user.then(|| { config - .string_filter(gitoxide::Allow::PROTOCOL_FROM_USER.logical_name().as_str(), &mut filter) + .string_filter(gitoxide::Allow::PROTOCOL_FROM_USER, &mut filter) .map_or(true, |val| val.as_ref() == "1") }); Ok(SchemePermission { diff --git a/gix/src/repository/identity.rs b/gix/src/repository/identity.rs index 5b833991de9..a9b3d7899b3 100644 --- a/gix/src/repository/identity.rs +++ b/gix/src/repository/identity.rs @@ -149,7 +149,7 @@ impl Personas { user_email = user_email.or_else(|| { config - .string(gitoxide::User::EMAIL_FALLBACK.logical_name().as_str()) + .string(gitoxide::User::EMAIL_FALLBACK) .map(std::borrow::Cow::into_owned) }); Personas { diff --git a/gix/src/repository/mailmap.rs b/gix/src/repository/mailmap.rs index 9b5b47d78a8..d19d567adeb 100644 --- a/gix/src/repository/mailmap.rs +++ b/gix/src/repository/mailmap.rs @@ -1,4 +1,4 @@ -use crate::config::tree::{Key, Mailmap}; +use crate::config::tree::Mailmap; use crate::Id; impl crate::Repository { @@ -68,7 +68,7 @@ impl crate::Repository { let configured_path = self .config_snapshot() - .trusted_path(Mailmap::FILE.logical_name().as_str()) + .trusted_path(&Mailmap::FILE) .and_then(|res| res.map_err(|e| err.get_or_insert(e.into())).ok()); if let Some(mut file) = diff --git a/gix/src/repository/shallow.rs b/gix/src/repository/shallow.rs index 322c0c315e8..90947a25d9c 100644 --- a/gix/src/repository/shallow.rs +++ b/gix/src/repository/shallow.rs @@ -1,9 +1,6 @@ use std::{borrow::Cow, path::PathBuf}; -use crate::{ - config::tree::{gitoxide, Key}, - Repository, -}; +use crate::{config::tree::gitoxide, Repository}; impl Repository { /// Return `true` if the repository is a shallow clone, i.e. contains history only up to a certain depth. @@ -36,10 +33,7 @@ impl Repository { let shallow_name = self .config .resolved - .string_filter( - gitoxide::Core::SHALLOW_FILE.logical_name().as_str(), - &mut self.filter_config_section(), - ) + .string_filter(gitoxide::Core::SHALLOW_FILE, &mut self.filter_config_section()) .unwrap_or_else(|| Cow::Borrowed("shallow".into())); self.common_dir().join(gix_path::from_bstr(shallow_name)) } From 801689b4aa860e1054dd9362a59d76077f31f248 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 2 Jan 2025 15:36:05 +0100 Subject: [PATCH 15/17] feat!: add `status::Platform::into_iter()` for obtaining a complete status. Note that it is still possible to disable the head-index status. Types moved around, effectivey removing the `iter::` module for most more general types, i.e. those that are quite genericlally useful in a status. --- gitoxide-core/src/repository/status.rs | 2 +- gix/src/dirwalk/iter.rs | 7 +- gix/src/status/index_worktree.rs | 525 +++++------------- gix/src/status/iter/mod.rs | 330 +++++++++++ gix/src/status/iter/types.rs | 139 +++++ gix/src/status/mod.rs | 44 +- gix/src/status/platform.rs | 18 +- gix/src/status/tree_index.rs | 7 +- gix/src/submodule/mod.rs | 13 +- gix/src/util.rs | 16 +- .../generated-archives/make_submodules.tar | Bin 1949184 -> 1997824 bytes gix/tests/fixtures/make_submodules.sh | 6 + gix/tests/gix/status.rs | 98 +++- 13 files changed, 799 insertions(+), 406 deletions(-) create mode 100644 gix/src/status/iter/mod.rs create mode 100644 gix/src/status/iter/types.rs diff --git a/gitoxide-core/src/repository/status.rs b/gitoxide-core/src/repository/status.rs index 99cbde7ed24..6264ddd5a7d 100644 --- a/gitoxide-core/src/repository/status.rs +++ b/gitoxide-core/src/repository/status.rs @@ -1,6 +1,6 @@ use anyhow::bail; use gix::bstr::{BStr, BString, ByteSlice}; -use gix::status::index_worktree::iter::Item; +use gix::status::index_worktree::Item; use gix_status::index_as_worktree::{Change, Conflict, EntryStatus}; use std::path::Path; diff --git a/gix/src/dirwalk/iter.rs b/gix/src/dirwalk/iter.rs index 6bfde0ef8f7..0f31dd30b72 100644 --- a/gix/src/dirwalk/iter.rs +++ b/gix/src/dirwalk/iter.rs @@ -160,7 +160,12 @@ impl Iterator for Iter { #[cfg(feature = "parallel")] impl Drop for Iter { fn drop(&mut self) { - crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt); + crate::util::parallel_iter_drop( + self.rx_and_join + .take() + .map(|(rx, handle)| (rx, handle, None::>)), + &self.should_interrupt, + ); } } diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index de20c5fd119..3dccb18ec28 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -110,16 +110,8 @@ impl Repository { crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, None, )?; - let pathspec = crate::Pathspec::new( - self, - options - .dirwalk_options - .as_ref() - .map_or(false, |opts| opts.empty_patterns_match_prefix), - patterns, - true, /* inherit ignore case */ - || Ok(attrs_and_excludes.clone()), - )?; + let pathspec = + self.index_worktree_status_pathspec::(patterns, index, options.dirwalk_options.as_ref())?; let cwd = self.current_dir(); let git_dir_realpath = crate::path::realpath_opts(self.git_dir(), cwd, crate::path::realpath::MAX_SYMLINKS)?; @@ -167,6 +159,31 @@ impl Repository { )?; Ok(out) } + + pub(super) fn index_worktree_status_pathspec( + &self, + patterns: impl IntoIterator>, + index: &gix_index::State, + options: Option<&crate::dirwalk::Options>, + ) -> Result, E> + where + E: From + From, + { + let empty_patterns_match_prefix = options.map_or(false, |opts| opts.empty_patterns_match_prefix); + let attrs_and_excludes = self.attributes( + index, + crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, + crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, + None, + )?; + Ok(crate::Pathspec::new( + self, + empty_patterns_match_prefix, + patterns, + true, /* inherit ignore case */ + move || Ok(attrs_and_excludes.inner), + )?) + } } /// An implementation of a trait to use with [`Repository::index_worktree_status()`] to compute the submodule status @@ -272,7 +289,7 @@ mod submodule_status { /// /// ### Index Changes /// -/// Changes to the index are collected and it's possible to write the index back using [iter::Outcome::write_changes()]. +/// Changes to the index are collected and it's possible to write the index back using [Outcome::write_changes()](crate::status::Outcome). /// Note that these changes are not observable, they will always be kept. /// /// ### Parallel Operation @@ -287,124 +304,110 @@ mod submodule_status { /// to interrupt unless [`status::Platform::should_interrupt_*()`](crate::status::Platform::should_interrupt_shared()) was /// configured. pub struct Iter { - #[cfg(feature = "parallel")] - #[allow(clippy::type_complexity)] - rx_and_join: Option<( - std::sync::mpsc::Receiver, - std::thread::JoinHandle>, - )>, - #[cfg(feature = "parallel")] - should_interrupt: crate::status::OwnedOrStaticAtomicBool, - /// Without parallelization, the iterator has to buffer all changes in advance. - #[cfg(not(feature = "parallel"))] - items: std::vec::IntoIter, - /// The outcome of the operation, only available once the operation has ended. - out: Option, - /// The set of `(entry_index, change)` we extracted in order to potentially write back the index with the changes applied. - changes: Vec<(usize, iter::ApplyChange)>, + inner: crate::status::Iter, +} + +/// The item produced by the iterator +#[derive(Clone, PartialEq, Debug)] +pub enum Item { + /// A tracked file was modified, and index-specific information is passed. + Modification { + /// The entry with modifications. + entry: gix_index::Entry, + /// The index of the `entry` for lookup in [`gix_index::State::entries()`] - useful to look at neighbors. + entry_index: usize, + /// The repository-relative path of the entry. + rela_path: BString, + /// The computed status of the entry. + status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>, + }, + /// An entry returned by the directory walk, without any relation to the index. + /// + /// This can happen if ignored files are returned as well, or if rename-tracking is disabled. + DirectoryContents { + /// The entry found during the disk traversal. + entry: gix_dir::Entry, + /// `collapsed_directory_status` is `Some(dir_status)` if this `entry` was part of a directory with the given + /// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was + /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). + /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). + collapsed_directory_status: Option, + }, + /// The rewrite tracking discovered a match between a deleted and added file, and considers them equal enough, + /// depending on the tracker settings. + /// + /// Note that the source of the rewrite is always the index as it detects the absence of entries, something that + /// can't be done during a directory walk. + Rewrite { + /// The source of the rewrite operation. + source: RewriteSource, + /// The untracked entry found during the disk traversal, the destination of the rewrite. + /// + /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the destination of the rewrite, and the current + /// location of the entry. + dirwalk_entry: gix_dir::Entry, + /// `collapsed_directory_status` is `Some(dir_status)` if this `dirwalk_entry` was part of a directory with the given + /// `dir_status` that wasn't the same as the one of `dirwalk_entry` and if [gix_dir::walk::Options::emit_collapsed] was + /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). + /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). + dirwalk_entry_collapsed_directory_status: Option, + /// The object id after the rename, specifically hashed in order to determine equality. + dirwalk_entry_id: gix_hash::ObjectId, + /// It's `None` if the 'source.id' is equal to `dirwalk_entry_id`, as identity made an actual diff computation unnecessary. + /// Otherwise, and if enabled, it's `Some(stats)` to indicate how similar both entries were. + diff: Option, + /// If true, this rewrite is created by copy, and 'source.id' is pointing to its source. + /// Otherwise, it's a rename, and 'source.id' points to a deleted object, + /// as renames are tracked as deletions and additions of the same or similar content. + copy: bool, + }, +} + +/// Either an index entry for renames or another directory entry in case of copies. +#[derive(Clone, PartialEq, Debug)] +pub enum RewriteSource { + /// The source originates in the index and is detected as missing in the working tree. + /// This can also happen for copies. + RewriteFromIndex { + /// The entry that is the source of the rewrite, which means it was removed on disk, + /// equivalent to [Change::Removed](gix_status::index_as_worktree::Change::Removed). + /// + /// Note that the [entry-id](gix_index::Entry::id) is the content-id of the source of the rewrite. + source_entry: gix_index::Entry, + /// The index of the `source_entry` for lookup in [`gix_index::State::entries()`] - useful to look at neighbors. + source_entry_index: usize, + /// The repository-relative path of the `source_entry`. + source_rela_path: BString, + /// The computed status of the `source_entry`. + source_status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>, + }, + /// This source originates in the directory tree and is always the source of copies. + CopyFromDirectoryEntry { + /// The source of the copy operation, which is also an entry of the directory walk. + /// + /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the source of the rewrite. + source_dirwalk_entry: gix_dir::Entry, + /// `collapsed_directory_status` is `Some(dir_status)` if this `source_dirwalk_entry` was part of a directory with the given + /// `dir_status` that wasn't the same as the one of `source_dirwalk_entry` and + /// if [gix_dir::walk::Options::emit_collapsed] was [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). + /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). + source_dirwalk_entry_collapsed_directory_status: Option, + /// The object id as it would appear if the entry was written to the object database. + /// It's the same as [`dirwalk_entry_id`](Item::Rewrite), or `diff` is `Some(_)` to indicate that the copy + /// was determined by similarity, not by content equality. + source_dirwalk_entry_id: gix_hash::ObjectId, + }, } /// pub mod iter { use crate::bstr::{BStr, BString}; - use crate::config::cache::util::ApplyLeniencyDefault; - use crate::status::index_worktree::{iter, BuiltinSubmoduleStatus}; use crate::status::{index_worktree, Platform}; - use crate::worktree::IndexPersistedOrInMemory; use gix_status::index_as_worktree::{Change, EntryStatus}; + use super::{Item, RewriteSource}; pub use gix_status::index_as_worktree_with_renames::Summary; - pub(super) enum ApplyChange { - SetSizeToZero, - NewStat(crate::index::entry::Stat), - } - - /// The data the thread sends over to the receiving iterator. - pub struct Outcome { - /// The outcome of the index-to-worktree comparison operation. - pub index_worktree: gix_status::index_as_worktree_with_renames::Outcome, - /// The index that was used for the operation. - pub index: crate::worktree::IndexPersistedOrInMemory, - skip_hash: bool, - changes: Option>, - } - - impl Outcome { - /// Returns `true` if the index has received currently unapplied changes that *should* be written back. - /// - /// If they are not written back, subsequent `status` operations will take longer to complete, whereas the - /// additional work can be prevented by writing the changes back to the index. - pub fn has_changes(&self) -> bool { - self.changes.as_ref().map_or(false, |changes| !changes.is_empty()) - } - - /// Write the changes if there are any back to the index file. - /// This can only be done once as the changes are consumed in the process, if there were any. - pub fn write_changes(&mut self) -> Option> { - let _span = gix_features::trace::coarse!("gix::status::index_worktree::iter::Outcome::write_changes()"); - let changes = self.changes.take()?; - let mut index = match &self.index { - IndexPersistedOrInMemory::Persisted(persisted) => (***persisted).clone(), - IndexPersistedOrInMemory::InMemory(index) => index.clone(), - }; - - let entries = index.entries_mut(); - for (entry_index, change) in changes { - let entry = &mut entries[entry_index]; - match change { - ApplyChange::SetSizeToZero => { - entry.stat.size = 0; - } - ApplyChange::NewStat(new_stat) => { - entry.stat = new_stat; - } - } - } - - Some(index.write(crate::index::write::Options { - extensions: Default::default(), - skip_hash: self.skip_hash, - })) - } - } - - /// Either an index entry for renames or another directory entry in case of copies. - #[derive(Clone, PartialEq, Debug)] - pub enum RewriteSource { - /// The source originates in the index and is detected as missing in the working tree. - /// This can also happen for copies. - RewriteFromIndex { - /// The entry that is the source of the rewrite, which means it was removed on disk, - /// equivalent to [Change::Removed]. - /// - /// Note that the [entry-id](gix_index::Entry::id) is the content-id of the source of the rewrite. - source_entry: gix_index::Entry, - /// The index of the `source_entry` for lookup in [`gix_index::State::entries()`] - useful to look at neighbors. - source_entry_index: usize, - /// The repository-relative path of the `source_entry`. - source_rela_path: BString, - /// The computed status of the `source_entry`. - source_status: gix_status::index_as_worktree::EntryStatus<(), crate::submodule::Status>, - }, - /// This source originates in the directory tree and is always the source of copies. - CopyFromDirectoryEntry { - /// The source of the copy operation, which is also an entry of the directory walk. - /// - /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the source of the rewrite. - source_dirwalk_entry: gix_dir::Entry, - /// `collapsed_directory_status` is `Some(dir_status)` if this `source_dirwalk_entry` was part of a directory with the given - /// `dir_status` that wasn't the same as the one of `source_dirwalk_entry` and - /// if [gix_dir::walk::Options::emit_collapsed] was [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). - /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). - source_dirwalk_entry_collapsed_directory_status: Option, - /// The object id as it would appear if the entry was written to the object database. - /// It's the same as [`dirwalk_entry_id`](Item::Rewrite), or `diff` is `Some(_)` to indicate that the copy - /// was determined by similarity, not by content equality. - source_dirwalk_entry_id: gix_hash::ObjectId, - }, - } - /// Access impl RewriteSource { /// The repository-relative path of this source. @@ -448,62 +451,6 @@ pub mod iter { } } - /// The item produced by the iterator - #[derive(Clone, PartialEq, Debug)] - pub enum Item { - /// A tracked file was modified, and index-specific information is passed. - Modification { - /// The entry with modifications. - entry: gix_index::Entry, - /// The index of the `entry` for lookup in [`gix_index::State::entries()`] - useful to look at neighbors. - entry_index: usize, - /// The repository-relative path of the entry. - rela_path: BString, - /// The computed status of the entry. - status: gix_status::index_as_worktree::EntryStatus<(), SubmoduleStatus>, - }, - /// An entry returned by the directory walk, without any relation to the index. - /// - /// This can happen if ignored files are returned as well, or if rename-tracking is disabled. - DirectoryContents { - /// The entry found during the disk traversal. - entry: gix_dir::Entry, - /// `collapsed_directory_status` is `Some(dir_status)` if this `entry` was part of a directory with the given - /// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was - /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). - /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). - collapsed_directory_status: Option, - }, - /// The rewrite tracking discovered a match between a deleted and added file, and considers them equal enough, - /// depending on the tracker settings. - /// - /// Note that the source of the rewrite is always the index as it detects the absence of entries, something that - /// can't be done during a directory walk. - Rewrite { - /// The source of the rewrite operation. - source: RewriteSource, - /// The untracked entry found during the disk traversal, the destination of the rewrite. - /// - /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the destination of the rewrite, and the current - /// location of the entry. - dirwalk_entry: gix_dir::Entry, - /// `collapsed_directory_status` is `Some(dir_status)` if this `dirwalk_entry` was part of a directory with the given - /// `dir_status` that wasn't the same as the one of `dirwalk_entry` and if [gix_dir::walk::Options::emit_collapsed] was - /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). - /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). - dirwalk_entry_collapsed_directory_status: Option, - /// The object id after the rename, specifically hashed in order to determine equality. - dirwalk_entry_id: gix_hash::ObjectId, - /// It's `None` if the 'source.id' is equal to `dirwalk_entry_id`, as identity made an actual diff computation unnecessary. - /// Otherwise, and if enabled, it's `Some(stats)` to indicate how similar both entries were. - diff: Option, - /// If true, this rewrite is created by copy, and 'source.id' is pointing to its source. - /// Otherwise, it's a rename, and 'source.id' points to a deleted object, - /// as renames are tracked as deletions and additions of the same or similar content. - copy: bool, - }, - } - impl Item { /// Return a simplified summary of the item as digest of its status, or `None` if this item is /// created from the directory walk and is *not untracked*, or if it is merely to communicate @@ -591,24 +538,6 @@ pub mod iter { type SubmoduleStatus = crate::submodule::Status; - /// The error returned by [Platform::into_index_worktree_iter()](crate::status::Platform::into_index_worktree_iter()). - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - Index(#[from] crate::worktree::open_index::Error), - #[error("Failed to spawn producer thread")] - #[cfg(feature = "parallel")] - SpawnThread(#[source] std::io::Error), - #[error(transparent)] - #[cfg(not(feature = "parallel"))] - IndexWorktreeStatus(#[from] crate::status::index_worktree::Error), - #[error(transparent)] - ConfigSkipHash(#[from] crate::config::boolean::Error), - #[error(transparent)] - PrepareSubmodules(#[from] crate::submodule::modules::Error), - } - /// Lifecycle impl Platform<'_, Progress> where @@ -620,105 +549,14 @@ pub mod iter { /// - Optional patterns to use to limit the paths to look at. If empty, all paths are considered. #[doc(alias = "diff_index_to_workdir", alias = "git2")] pub fn into_index_worktree_iter( - self, + mut self, patterns: impl IntoIterator, - ) -> Result { - let index = match self.index { - None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?), - Some(index) => index, - }; - - let skip_hash = self - .repo - .config - .resolved - .boolean(crate::config::tree::Index::SKIP_HASH) - .map(|res| crate::config::tree::Index::SKIP_HASH.enrich_error(res)) - .transpose() - .with_lenient_default(self.repo.config.lenient_config)? - .unwrap_or_default(); - let should_interrupt = self.should_interrupt.clone().unwrap_or_default(); - let submodule = BuiltinSubmoduleStatus::new(self.repo.clone().into_sync(), self.submodules)?; - #[cfg(feature = "parallel")] - { - let (tx, rx) = std::sync::mpsc::channel(); - let mut collect = Collect { tx }; - let patterns: Vec<_> = patterns.into_iter().collect(); - let join = std::thread::Builder::new() - .name("gix::status::index_worktree::iter::producer".into()) - .spawn({ - let repo = self.repo.clone().into_sync(); - let options = self.index_worktree_options; - let should_interrupt = should_interrupt.clone(); - let mut progress = self.progress; - move || -> Result<_, crate::status::index_worktree::Error> { - let repo = repo.to_thread_local(); - let out = repo.index_worktree_status( - &index, - patterns, - &mut collect, - gix_status::index_as_worktree::traits::FastEq, - submodule, - &mut progress, - &should_interrupt, - options, - )?; - Ok(Outcome { - index_worktree: out, - index, - changes: None, - skip_hash, - }) - } - }) - .map_err(Error::SpawnThread)?; - - Ok(super::Iter { - rx_and_join: Some((rx, join)), - should_interrupt, - changes: Vec::new(), - out: None, - }) - } - #[cfg(not(feature = "parallel"))] - { - let mut collect = Collect { items: Vec::new() }; - - let repo = self.repo.clone().into_sync(); - let options = self.index_worktree_options; - let mut progress = self.progress; - let repo = repo.to_thread_local(); - let out = repo.index_worktree_status( - &index, - patterns, - &mut collect, - gix_status::index_as_worktree::traits::FastEq, - submodule, - &mut progress, - &should_interrupt, - options, - )?; - let mut out = Outcome { - index_worktree: out, - index, - changes: None, - skip_hash, - }; - let mut iter = super::Iter { - items: Vec::new().into_iter(), - changes: Vec::new(), - out: None, - }; - let items = collect - .items - .into_iter() - .filter_map(|item| iter.maybe_keep_index_change(item)) - .collect::>(); - out.changes = (!iter.changes.is_empty()).then(|| std::mem::take(&mut iter.changes)); - iter.items = items.into_iter(); - iter.out = Some(out); - Ok(iter) - } + ) -> Result { + // deactivate the tree-iteration + self.head_tree = None; + Ok(index_worktree::Iter { + inner: self.into_iter(patterns)?, + }) } } @@ -726,107 +564,32 @@ pub mod iter { type Item = Result; fn next(&mut self) -> Option { - #[cfg(feature = "parallel")] - loop { - let (rx, _join) = self.rx_and_join.as_ref()?; - match rx.recv().ok() { - Some(item) => { - if let Some(item) = self.maybe_keep_index_change(item) { - break Some(Ok(item)); - } - continue; - } - None => { - let (_rx, handle) = self.rx_and_join.take()?; - break match handle.join().expect("no panic") { - Ok(mut out) => { - out.changes = Some(std::mem::take(&mut self.changes)); - self.out = Some(out); - None - } - Err(err) => Some(Err(err)), - }; + self.inner.next().map(|res| { + res.map(|item| match item { + crate::status::Item::IndexWorktree(item) => item, + crate::status::Item::TreeIndex(_) => unreachable!("BUG: we deactivated this kind of traversal"), + }) + .map_err(|err| match err { + crate::status::iter::Error::IndexWorktree(err) => err, + crate::status::iter::Error::TreeIndex(_) => { + unreachable!("BUG: we deactivated this kind of traversal") } - } - } - #[cfg(not(feature = "parallel"))] - self.items.next().map(Ok) + }) + }) } } /// Access impl super::Iter { /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed. - pub fn outcome_mut(&mut self) -> Option<&mut Outcome> { - self.out.as_mut() + pub fn outcome_mut(&mut self) -> Option<&mut crate::status::Outcome> { + self.inner.out.as_mut() } /// Turn the iterator into the iteration outcome, which is `None` on error or if the iteration /// isn't complete. - pub fn into_outcome(mut self) -> Option { - self.out.take() - } - } - - impl super::Iter { - fn maybe_keep_index_change(&mut self, item: Item) -> Option { - let change = match item { - Item::Modification { - status: gix_status::index_as_worktree::EntryStatus::NeedsUpdate(stat), - entry_index, - .. - } => (entry_index, ApplyChange::NewStat(stat)), - Item::Modification { - status: - gix_status::index_as_worktree::EntryStatus::Change( - gix_status::index_as_worktree::Change::Modification { - set_entry_stat_size_zero, - .. - }, - ), - entry_index, - .. - } if set_entry_stat_size_zero => (entry_index, ApplyChange::SetSizeToZero), - _ => return Some(item), - }; - - self.changes.push(change); - None - } - } - - #[cfg(feature = "parallel")] - impl Drop for super::Iter { - fn drop(&mut self) { - crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt); - } - } - - struct Collect { - #[cfg(feature = "parallel")] - tx: std::sync::mpsc::Sender, - #[cfg(not(feature = "parallel"))] - items: Vec, - } - - impl<'index> gix_status::index_as_worktree_with_renames::VisitEntry<'index> for Collect { - type ContentChange = ::Output; - type SubmoduleStatus = - ::Output; - - fn visit_entry( - &mut self, - entry: gix_status::index_as_worktree_with_renames::Entry< - 'index, - Self::ContentChange, - Self::SubmoduleStatus, - >, - ) { - // NOTE: we assume that the receiver triggers interruption so the operation will stop if the receiver is down. - #[cfg(feature = "parallel")] - self.tx.send(entry.into()).ok(); - #[cfg(not(feature = "parallel"))] - self.items.push(entry.into()); + pub fn into_outcome(mut self) -> Option { + self.inner.out.take() } } } diff --git a/gix/src/status/iter/mod.rs b/gix/src/status/iter/mod.rs new file mode 100644 index 00000000000..6df82c5f8cd --- /dev/null +++ b/gix/src/status/iter/mod.rs @@ -0,0 +1,330 @@ +use crate::bstr::BString; +use crate::config::cache::util::ApplyLeniencyDefault; +use crate::status::index_worktree::BuiltinSubmoduleStatus; +use crate::status::{index_worktree, tree_index, Platform}; +use crate::worktree::IndexPersistedOrInMemory; +use gix_status::index_as_worktree::{Change, EntryStatus}; +use std::sync::atomic::Ordering; + +pub(super) mod types; +use types::{ApplyChange, Item, Iter, Outcome}; + +/// Lifecycle +impl Platform<'_, Progress> +where + Progress: gix_features::progress::Progress, +{ + /// Turn the platform into an iterator for changes between the head-tree and the index, and the index and the working tree, + /// while optionally listing untracked and/or ignored files. + /// + /// * `patterns` + /// - Optional patterns to use to limit the paths to look at. If empty, all paths are considered. + #[doc(alias = "diff_index_to_workdir", alias = "git2")] + pub fn into_iter( + self, + patterns: impl IntoIterator, + ) -> Result { + let index = match self.index { + None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?), + Some(index) => index, + }; + + let obtain_tree_id = || -> Result, crate::status::into_iter::Error> { + Ok(match self.head_tree { + Some(None) => Some(self.repo.head_tree_id()?.into()), + Some(Some(tree_id)) => Some(tree_id), + None => None, + }) + }; + + let skip_hash = self + .repo + .config + .resolved + .boolean(crate::config::tree::Index::SKIP_HASH) + .map(|res| crate::config::tree::Index::SKIP_HASH.enrich_error(res)) + .transpose() + .with_lenient_default(self.repo.config.lenient_config)? + .unwrap_or_default(); + let should_interrupt = self.should_interrupt.clone().unwrap_or_default(); + let submodule = BuiltinSubmoduleStatus::new(self.repo.clone().into_sync(), self.submodules)?; + #[cfg(feature = "parallel")] + { + let (tx, rx) = std::sync::mpsc::channel(); + let patterns: Vec<_> = patterns.into_iter().collect(); + let join_tree_index = if let Some(tree_id) = obtain_tree_id()? { + std::thread::Builder::new() + .name("gix::status::tree_index::producer".into()) + .spawn({ + let repo = self.repo.clone().into_sync(); + let should_interrupt = should_interrupt.clone(); + let tx = tx.clone(); + let tree_index_renames = self.tree_index_renames; + let index = index.clone(); + let crate::Pathspec { repo: _, stack, search } = self + .repo + .index_worktree_status_pathspec::( + &patterns, + &index, + self.index_worktree_options.dirwalk_options.as_ref(), + )?; + move || -> Result<_, _> { + let repo = repo.to_thread_local(); + let mut pathspec = crate::Pathspec { + repo: &repo, + stack, + search, + }; + repo.tree_index_status( + &tree_id, + &index, + Some(&mut pathspec), + tree_index_renames, + |change, _, _| { + let action = if tx.send(change.into_owned().into()).is_err() + || should_interrupt.load(Ordering::Acquire) + { + gix_diff::index::Action::Cancel + } else { + gix_diff::index::Action::Continue + }; + Ok::<_, std::convert::Infallible>(action) + }, + ) + } + }) + .map_err(crate::status::into_iter::Error::SpawnThread)? + .into() + } else { + None + }; + let mut collect = Collect { tx }; + let join_index_worktree = std::thread::Builder::new() + .name("gix::status::index_worktree::producer".into()) + .spawn({ + let repo = self.repo.clone().into_sync(); + let options = self.index_worktree_options; + let should_interrupt = should_interrupt.clone(); + let mut progress = self.progress; + move || -> Result<_, index_worktree::Error> { + let repo = repo.to_thread_local(); + let out = repo.index_worktree_status( + &index, + patterns, + &mut collect, + gix_status::index_as_worktree::traits::FastEq, + submodule, + &mut progress, + &should_interrupt, + options, + )?; + Ok(Outcome { + index_worktree: out, + tree_index: None, + worktree_index: index, + changes: None, + skip_hash, + }) + } + }) + .map_err(crate::status::into_iter::Error::SpawnThread)?; + + Ok(Iter { + rx_and_join: Some((rx, join_index_worktree, join_tree_index)), + should_interrupt, + index_changes: Vec::new(), + out: None, + }) + } + #[cfg(not(feature = "parallel"))] + { + let mut collect = Collect { items: Vec::new() }; + + let repo = self.repo; + let options = self.index_worktree_options; + let mut progress = self.progress; + let patterns: Vec = patterns.into_iter().collect(); + let (mut items, tree_index) = match obtain_tree_id()? { + Some(tree_id) => { + let mut pathspec = repo.index_worktree_status_pathspec::( + &patterns, + &index, + self.index_worktree_options.dirwalk_options.as_ref(), + )?; + let mut items = Vec::new(); + let tree_index = self.repo.tree_index_status( + &tree_id, + &index, + Some(&mut pathspec), + self.tree_index_renames, + |change, _, _| { + items.push(change.into_owned().into()); + let action = if should_interrupt.load(Ordering::Acquire) { + gix_diff::index::Action::Cancel + } else { + gix_diff::index::Action::Continue + }; + Ok::<_, std::convert::Infallible>(action) + }, + )?; + (items, Some(tree_index)) + } + None => (Vec::new(), None), + }; + let out = repo.index_worktree_status( + &index, + patterns, + &mut collect, + gix_status::index_as_worktree::traits::FastEq, + submodule, + &mut progress, + &should_interrupt, + options, + )?; + let mut iter = Iter { + items: Vec::new().into_iter(), + index_changes: Vec::new(), + out: None, + }; + let mut out = Outcome { + index_worktree: out, + worktree_index: index, + tree_index, + changes: None, + skip_hash, + }; + items.extend( + collect + .items + .into_iter() + .filter_map(|item| iter.maybe_keep_index_change(item)), + ); + out.changes = (!iter.index_changes.is_empty()).then(|| std::mem::take(&mut iter.index_changes)); + iter.items = items.into_iter(); + iter.out = Some(out); + Ok(iter) + } + } +} + +/// The error returned for each item returned by [`Iter`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + IndexWorktree(#[from] index_worktree::Error), + #[error(transparent)] + TreeIndex(#[from] tree_index::Error), +} + +impl Iterator for Iter { + type Item = Result; + + fn next(&mut self) -> Option { + #[cfg(feature = "parallel")] + loop { + let (rx, _join_worktree, _join_tree) = self.rx_and_join.as_ref()?; + match rx.recv().ok() { + Some(item) => { + if let Some(item) = self.maybe_keep_index_change(item) { + break Some(Ok(item)); + } + continue; + } + None => { + let (_rx, worktree_handle, tree_handle) = self.rx_and_join.take()?; + let tree_index = if let Some(handle) = tree_handle { + match handle.join().expect("no panic") { + Ok(out) => Some(out), + Err(err) => break Some(Err(err.into())), + } + } else { + None + }; + break match worktree_handle.join().expect("no panic") { + Ok(mut out) => { + out.changes = Some(std::mem::take(&mut self.index_changes)); + out.tree_index = tree_index; + self.out = Some(out); + None + } + Err(err) => Some(Err(err.into())), + }; + } + } + } + #[cfg(not(feature = "parallel"))] + self.items.next().map(Ok) + } +} + +/// Access +impl Iter { + /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed. + pub fn outcome_mut(&mut self) -> Option<&mut Outcome> { + self.out.as_mut() + } + + /// Turn the iterator into the iteration outcome, which is `None` on error or if the iteration + /// isn't complete. + pub fn into_outcome(mut self) -> Option { + self.out.take() + } +} + +impl Iter { + fn maybe_keep_index_change(&mut self, item: Item) -> Option { + let change = match item { + Item::IndexWorktree(index_worktree::Item::Modification { + status: EntryStatus::NeedsUpdate(stat), + entry_index, + .. + }) => (entry_index, ApplyChange::NewStat(stat)), + Item::IndexWorktree(index_worktree::Item::Modification { + status: + EntryStatus::Change(Change::Modification { + set_entry_stat_size_zero, + .. + }), + entry_index, + .. + }) if set_entry_stat_size_zero => (entry_index, ApplyChange::SetSizeToZero), + _ => return Some(item), + }; + + self.index_changes.push(change); + None + } +} + +#[cfg(feature = "parallel")] +impl Drop for Iter { + fn drop(&mut self) { + crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt); + } +} + +struct Collect { + #[cfg(feature = "parallel")] + tx: std::sync::mpsc::Sender, + #[cfg(not(feature = "parallel"))] + items: Vec, +} + +impl<'index> gix_status::index_as_worktree_with_renames::VisitEntry<'index> for Collect { + type ContentChange = + ::Output; + type SubmoduleStatus = ::Output; + + fn visit_entry( + &mut self, + entry: gix_status::index_as_worktree_with_renames::Entry<'index, Self::ContentChange, Self::SubmoduleStatus>, + ) { + // NOTE: we assume that the receiver triggers interruption so the operation will stop if the receiver is down. + let item = Item::IndexWorktree(entry.into()); + #[cfg(feature = "parallel")] + self.tx.send(item).ok(); + #[cfg(not(feature = "parallel"))] + self.items.push(item); + } +} diff --git a/gix/src/status/iter/types.rs b/gix/src/status/iter/types.rs new file mode 100644 index 00000000000..6087ca25110 --- /dev/null +++ b/gix/src/status/iter/types.rs @@ -0,0 +1,139 @@ +use crate::bstr::BStr; +use crate::status::{index_worktree, tree_index}; +use crate::worktree::IndexPersistedOrInMemory; + +/// An iterator for changes between the index and the worktree and the head-tree and the index. +/// +/// Note that depending on the underlying configuration, there might be a significant delay until the first +/// item is received due to the buffering necessary to perform rename tracking and/or sorting. +/// +/// ### Submodules +/// +/// Note that submodules can be set to 'inactive', which will not exclude them from the status operation, similar to +/// how `git status` includes them. +/// +/// ### Index Changes +/// +/// Changes to the index are collected, and it's possible to write the index back using [Outcome::write_changes()]. +/// Note that these changes are not observable, they will always be kept. +/// +/// ### Parallel Operation +/// +/// Note that without the `parallel` feature, the iterator becomes 'serial', which means all status will be computed in advance, +/// and it's non-interruptible, yielding worse performance for is-dirty checks for instance as interruptions won't happen. +/// It's a crutch that is just there to make single-threaded applications possible at all, as it's not really an iterator +/// anymore. If this matters, better run [Repository::index_worktree_status()](crate::Repository::index_worktree_status) by hand +/// as it provides all control one would need, just not as an iterator. +/// +/// Also, even with `parallel` set, the first call to `next()` will block until there is an item available, without a chance +/// to interrupt unless [`status::Platform::should_interrupt_*()`](crate::status::Platform::should_interrupt_shared()) was +/// configured. +pub struct Iter { + #[cfg(feature = "parallel")] + #[allow(clippy::type_complexity)] + pub(super) rx_and_join: Option<( + std::sync::mpsc::Receiver, + std::thread::JoinHandle>, + Option>>, + )>, + #[cfg(feature = "parallel")] + pub(super) should_interrupt: crate::status::OwnedOrStaticAtomicBool, + /// Without parallelization, the iterator has to buffer all changes in advance. + #[cfg(not(feature = "parallel"))] + pub(super) items: std::vec::IntoIter, + /// The outcome of the operation, only available once the operation has ended. + pub(in crate::status) out: Option, + /// The set of `(entry_index, change)` we extracted in order to potentially write back the worktree index with the changes applied. + pub(super) index_changes: Vec<(usize, ApplyChange)>, +} + +/// The item produced by the [iterator](Iter). +#[derive(Clone, PartialEq, Debug)] +pub enum Item { + /// A change between the index and the worktree. + /// + /// Note that untracked changes are also collected here. + IndexWorktree(index_worktree::Item), + /// A change between the three of `HEAD` and the index. + TreeIndex(gix_diff::index::Change), +} + +/// The data the thread sends over to the receiving iterator. +pub struct Outcome { + /// The outcome of the index-to-worktree comparison operation. + pub index_worktree: gix_status::index_as_worktree_with_renames::Outcome, + /// The outcome of the diff between `HEAD^{tree}` and the index, or `None` if this outcome + /// was produced with the [`into_index_worktree_iter()`](crate::status::Platform::into_index_worktree_iter()). + pub tree_index: Option, + /// The worktree index that was used for the operation. + pub worktree_index: IndexPersistedOrInMemory, + pub(super) skip_hash: bool, + pub(super) changes: Option>, +} + +impl Outcome { + /// Returns `true` if the index has received currently unapplied changes that *should* be written back. + /// + /// If they are not written back, subsequent `status` operations will take longer to complete, whereas the + /// additional work can be prevented by writing the changes back to the index. + pub fn has_changes(&self) -> bool { + self.changes.as_ref().map_or(false, |changes| !changes.is_empty()) + } + + /// Write the changes if there are any back to the index file. + /// This can only be done once as the changes are consumed in the process, if there were any. + pub fn write_changes(&mut self) -> Option> { + let _span = gix_features::trace::coarse!("gix::status::index_worktree::Outcome::write_changes()"); + let changes = self.changes.take()?; + let mut index = match &self.worktree_index { + IndexPersistedOrInMemory::Persisted(persisted) => (***persisted).clone(), + IndexPersistedOrInMemory::InMemory(index) => index.clone(), + }; + + let entries = index.entries_mut(); + for (entry_index, change) in changes { + let entry = &mut entries[entry_index]; + match change { + ApplyChange::SetSizeToZero => { + entry.stat.size = 0; + } + ApplyChange::NewStat(new_stat) => { + entry.stat = new_stat; + } + } + } + + Some(index.write(crate::index::write::Options { + extensions: Default::default(), + skip_hash: self.skip_hash, + })) + } +} + +pub(super) enum ApplyChange { + SetSizeToZero, + NewStat(crate::index::entry::Stat), +} + +impl From for Item { + fn from(value: index_worktree::Item) -> Self { + Item::IndexWorktree(value) + } +} + +impl From for Item { + fn from(value: gix_diff::index::Change) -> Self { + Item::TreeIndex(value) + } +} + +/// Access +impl Item { + /// Return the relative path at which the item can currently be found in the working tree or index. + pub fn location(&self) -> &BStr { + match self { + Item::IndexWorktree(change) => change.rela_path(), + Item::TreeIndex(change) => change.fields().0, + } + } +} diff --git a/gix/src/status/mod.rs b/gix/src/status/mod.rs index e165d3b5ee5..5e6bfdc3777 100644 --- a/gix/src/status/mod.rs +++ b/gix/src/status/mod.rs @@ -11,8 +11,10 @@ where repo: &'repo Repository, progress: Progress, index: Option, + head_tree: Option>, submodules: Submodule, index_worktree_options: index_worktree::Options, + tree_index_renames: tree_index::TrackRenames, should_interrupt: Option, } @@ -104,6 +106,8 @@ impl Repository { index: None, submodules: Submodule::default(), should_interrupt: None, + head_tree: Some(None), + tree_index_renames: Default::default(), index_worktree_options: index_worktree::Options { sorting: None, dirwalk_options: Some(self.dirwalk_options()?), @@ -139,7 +143,7 @@ pub mod is_dirty { #[error(transparent)] StatusPlatform(#[from] crate::status::Error), #[error(transparent)] - CreateStatusIterator(#[from] crate::status::index_worktree::iter::Error), + CreateStatusIterator(#[from] crate::status::into_iter::Error), #[error(transparent)] TreeIndexStatus(#[from] crate::status::tree_index::Error), #[error(transparent)] @@ -157,6 +161,9 @@ pub mod is_dirty { /// * submodules are taken in consideration, along with their `ignore` and `isActive` configuration /// /// Note that *untracked files* do *not* affect this flag. + // TODO(performance): this could be its very own implementation with parallelism and the special: + // stop once there is a change flag, but without using the iterator for + // optimal resource usage. pub fn is_dirty(&self) -> Result { { let head_tree_id = self.head_tree_id()?; @@ -193,6 +200,37 @@ pub mod is_dirty { } } +/// +pub mod into_iter { + /// The error returned by [status::Platform::into_iter()](crate::status::Platform::into_iter()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Index(#[from] crate::worktree::open_index::Error), + #[error("Failed to spawn producer thread")] + #[cfg(feature = "parallel")] + SpawnThread(#[source] std::io::Error), + #[error(transparent)] + #[cfg(not(feature = "parallel"))] + IndexWorktreeStatus(#[from] crate::status::index_worktree::Error), + #[error(transparent)] + ConfigSkipHash(#[from] crate::config::boolean::Error), + #[error(transparent)] + PrepareSubmodules(#[from] crate::submodule::modules::Error), + #[error("Could not create an index for the head tree to compare with the worktree index")] + HeadTreeIndex(#[from] crate::repository::index_from_tree::Error), + #[error("Could not obtain the tree id pointed to by `HEAD`")] + HeadTreeId(#[from] crate::reference::head_tree_id::Error), + #[error(transparent)] + AttributesAndExcludes(#[from] crate::repository::attributes::Error), + #[error(transparent)] + Pathspec(#[from] crate::pathspec::init::Error), + #[error(transparent)] + HeadTreeDiff(#[from] crate::status::tree_index::Error), + } +} + mod platform; /// @@ -200,3 +238,7 @@ pub mod index_worktree; /// pub mod tree_index; + +/// +pub mod iter; +pub use iter::types::{Item, Iter, Outcome}; diff --git a/gix/src/status/platform.rs b/gix/src/status/platform.rs index 43c5fbdeabf..b9086152c46 100644 --- a/gix/src/status/platform.rs +++ b/gix/src/status/platform.rs @@ -1,4 +1,4 @@ -use crate::status::{index_worktree, OwnedOrStaticAtomicBool, Platform, Submodule, UntrackedFiles}; +use crate::status::{index_worktree, tree_index, OwnedOrStaticAtomicBool, Platform, Submodule, UntrackedFiles}; use std::sync::atomic::AtomicBool; /// Builder @@ -107,4 +107,20 @@ where cb(&mut self.index_worktree_options); self } + + /// Set the tree at which the `HEAD` ref ought to reside. + /// Setting this explicitly allows to compare the index to a tree that it possibly didn't originate from. + /// + /// If not set explicitly, it will be read via `HEAD^{tree}`. + pub fn head_tree(mut self, tree: impl Into) -> Self { + self.head_tree = Some(Some(tree.into())); + self + } + + /// Configure how rename tracking should be performed when looking at changes between the [head tree](Self::head_tree()) + /// and the index. + pub fn tree_index_track_renames(mut self, renames: tree_index::TrackRenames) -> Self { + self.tree_index_renames = renames; + self + } } diff --git a/gix/src/status/tree_index.rs b/gix/src/status/tree_index.rs index c9beb25f41a..06828dfa777 100644 --- a/gix/src/status/tree_index.rs +++ b/gix/src/status/tree_index.rs @@ -16,10 +16,11 @@ pub enum Error { } /// Specify how to perform rewrite tracking [Repository::tree_index_status()]. -#[derive(Debug, Copy, Clone)] +#[derive(Default, Debug, Copy, Clone)] pub enum TrackRenames { /// Check `status.renames` and then `diff.renames` if the former isn't set. Otherwise, default to performing rewrites if nothing /// is set. + #[default] AsConfigured, /// Track renames according ot the given configuration. Given(gix_diff::Rewrites), @@ -67,6 +68,7 @@ impl Repository { where E: Into>, { + let _span = gix_trace::coarse!("gix::tree_index_status"); let tree_index: gix_index::State = self.index_from_tree(tree_id)?.into(); let rewrites = match renames { TrackRenames::AsConfigured => { @@ -106,7 +108,8 @@ impl Repository { .into(); } - let pathspec = pathspec.unwrap_or(pathspec_storage.as_mut().expect("set if pathspec isn't set by user")); + let pathspec = + pathspec.unwrap_or_else(|| pathspec_storage.as_mut().expect("set if pathspec isn't set by user")); let rewrite = gix_diff::index( &tree_index, worktree_index, diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 4e595887bb2..d1a80bd083a 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -299,9 +299,9 @@ pub mod status { #[error(transparent)] StatusPlatform(#[from] crate::status::Error), #[error(transparent)] - Status(#[from] crate::status::index_worktree::iter::Error), + StatusIter(#[from] crate::status::into_iter::Error), #[error(transparent)] - IndexWorktreeStatus(#[from] crate::status::index_worktree::Error), + NextStatusItem(#[from] crate::status::iter::Error), } impl Submodule<'_> { @@ -327,11 +327,6 @@ pub mod status { /// A reason to change them might be to enable sorting to enjoy deterministic order of changes. /// /// The status allows to easily determine if a submodule [has changes](Status::is_dirty). - /// - /// ### Incomplete Implementation Warning - /// - /// Currently, changes between the head and the index aren't computed. - // TODO: Run the full status, including tree->index once available. #[doc(alias = "submodule_status", alias = "git2")] pub fn status_opts( &self, @@ -390,7 +385,7 @@ pub mod status { opts.dirwalk_options = None; } }) - .into_index_worktree_iter(Vec::new())?; + .into_iter(None)?; let mut changes = Vec::new(); for change in statuses { changes.push(change?); @@ -444,7 +439,7 @@ pub mod status { /// /// `None` if the computation wasn't performed as the computation was skipped early, or if no working tree was /// available or repository was available. - pub changes: Option>, + pub changes: Option>, } } } diff --git a/gix/src/util.rs b/gix/src/util.rs index e9d1e10f8e9..789bcc9b1c7 100644 --- a/gix/src/util.rs +++ b/gix/src/util.rs @@ -53,11 +53,16 @@ impl From> for OwnedOrStaticAtomicBool { } } #[cfg(feature = "parallel")] -pub fn parallel_iter_drop( - mut rx_and_join: Option<(std::sync::mpsc::Receiver, std::thread::JoinHandle)>, +#[allow(clippy::type_complexity)] +pub fn parallel_iter_drop( + mut rx_and_join: Option<( + std::sync::mpsc::Receiver, + std::thread::JoinHandle, + Option>, + )>, should_interrupt: &OwnedOrStaticAtomicBool, ) { - let Some((rx, handle)) = rx_and_join.take() else { + let Some((rx, handle, maybe_handle)) = rx_and_join.take() else { return; }; let prev = should_interrupt.swap(true, std::sync::atomic::Ordering::Relaxed); @@ -66,11 +71,14 @@ pub fn parallel_iter_drop( OwnedOrStaticAtomicBool::Owned { flag, private: false } => flag.as_ref(), OwnedOrStaticAtomicBool::Owned { private: true, .. } => { // Leak the handle to let it shut down in the background, so drop returns more quickly. - drop((rx, handle)); + drop((rx, handle, maybe_handle)); return; } }; // Wait until there is time to respond before we undo the change. + if let Some(handle) = maybe_handle { + handle.join().ok(); + } handle.join().ok(); undo.fetch_update( std::sync::atomic::Ordering::SeqCst, diff --git a/gix/tests/fixtures/generated-archives/make_submodules.tar b/gix/tests/fixtures/generated-archives/make_submodules.tar index 2a38f8e7deff219962e6ab6417f63f62fa063f6e..941446ce8853831684593e3dc346f3853a4f1f3f 100644 GIT binary patch delta 36015 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ℜP4r@UM;f}9m+~u9y@fNZz>4>Gc9+Ia=f-EH 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 a_r?>osVk++%1yr&SZBbj(qF{?um4|s!zk7O delta 91531 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 diff --git a/gix/tests/fixtures/make_submodules.sh b/gix/tests/fixtures/make_submodules.sh index be7fcd48887..b0f4ddb636c 100755 --- a/gix/tests/fixtures/make_submodules.sh +++ b/gix/tests/fixtures/make_submodules.sh @@ -68,6 +68,12 @@ git init modified-untracked-and-submodule-head-changed-and-modified touch untracked ) +cp -Rv modified-untracked-and-submodule-head-changed-and-modified git-mv-and-untracked-and-submodule-head-changed-and-modified +(cd git-mv-and-untracked-and-submodule-head-changed-and-modified + git checkout this + git mv this that +) + git init with-submodules (cd with-submodules mkdir dir diff --git a/gix/tests/gix/status.rs b/gix/tests/gix/status.rs index 0e5b97bc60c..981bc34a392 100644 --- a/gix/tests/gix/status.rs +++ b/gix/tests/gix/status.rs @@ -16,16 +16,102 @@ pub fn repo(name: &str) -> crate::Result { )?) } +mod into_iter { + use crate::status::{repo, submodule_repo}; + use crate::util::hex_to_id; + use gix::status::tree_index::TrackRenames; + use gix::status::Item; + use gix_diff::Rewrites; + use gix_testtools::size_ok; + + #[test] + fn item_size() { + let actual = std::mem::size_of::(); + let expected = 264; + assert!( + size_ok(actual, expected), + "The size is the same as the one for the index-worktree-item: {actual} <~ {expected}" + ); + } + + #[test] + fn submodule_tree_index_modification() -> crate::Result { + let repo = submodule_repo("git-mv-and-untracked-and-submodule-head-changed-and-modified")?; + let mut status = repo + .status(gix::progress::Discard)? + .index_worktree_options_mut(|opts| { + opts.sorting = + Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); + }) + .tree_index_track_renames(TrackRenames::Given(Rewrites { + track_empty: true, + ..Default::default() + })) + .into_iter(None)?; + let mut items: Vec<_> = status.by_ref().filter_map(Result::ok).collect(); + items.sort_by(|a, b| a.location().cmp(b.location())); + assert_eq!(items.len(), 3, "1 untracked, 1 move, 1 submodule modification"); + insta::assert_debug_snapshot!(&items[1], @r#" + TreeIndex( + Rewrite { + source_location: "this", + source_index: 2, + source_entry_mode: Mode( + FILE, + ), + source_id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + location: "that", + index: 2, + entry_mode: Mode( + FILE, + ), + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + copy: false, + }, + ) + "#); + Ok(()) + } + + #[test] + fn error_during_tree_traversal_causes_failure() -> crate::Result { + let repo = repo("untracked-only")?; + let platform = repo.status(gix::progress::Discard)?.head_tree(hex_to_id( + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", /* empty blob, invalid tree*/ + )); + let expected_err = "Could not create index from tree at e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"; + if cfg!(feature = "parallel") { + let mut items: Vec<_> = platform.into_iter(None)?.collect(); + assert_eq!( + items.len(), + 3, + "2 untracked and one error, which is detected only in the end." + ); + assert_eq!(items.pop().expect("last item").unwrap_err().to_string(), expected_err); + } else { + match platform.into_iter(None) { + Ok(_) => { + unreachable!("errors would be detected early here as everything is done ahead of time") + } + Err(err) => { + assert_eq!(err.to_string(), expected_err); + } + } + } + Ok(()) + } +} + mod index_worktree { mod iter { use crate::status::{repo, submodule_repo}; - use gix::status::index_worktree::iter::Item; + use gix::status::index_worktree::Item; use gix_testtools::size_ok; use pretty_assertions::assert_eq; #[test] fn item_size() { - let actual = std::mem::size_of::(); + let actual = std::mem::size_of::(); let expected = 264; assert!( size_ok(actual, expected), @@ -42,7 +128,7 @@ mod index_worktree { opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); }) - .into_index_worktree_iter(Vec::new())?; + .into_index_worktree_iter(None)?; let items: Vec<_> = status.by_ref().filter_map(Result::ok).collect(); assert_eq!(items.len(), 3, "1 untracked, 1 modified file, 1 submodule modification"); Ok(()) @@ -57,7 +143,7 @@ mod index_worktree { opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); }) - .into_index_worktree_iter(Vec::new())?; + .into_index_worktree_iter(None)?; let items: Vec<_> = status.filter_map(Result::ok).collect(); assert_eq!( items, @@ -103,7 +189,7 @@ mod index_worktree { opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); }) - .into_index_worktree_iter(Vec::new())?; + .into_index_worktree_iter(None)?; let items: Vec<_> = status.by_ref().filter_map(Result::ok).collect(); assert_eq!(items, [], "no untracked files are found…"); assert_eq!( @@ -124,7 +210,7 @@ mod index_worktree { opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); }) - .into_index_worktree_iter(Vec::new())? + .into_index_worktree_iter(None)? .next() .is_some(); assert!(is_dirty, "this should abort the work as quickly as possible"); From a987e682aefa352ceaffa05d56545c9bc9c14934 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 2 Jan 2025 11:51:11 +0100 Subject: [PATCH 16/17] fix: `Submodule::status()` now konws about tree-index changes as well. This completes the status implementation. --- gix/src/submodule/mod.rs | 8 ++++ .../generated-archives/make_submodules.tar | Bin 1997824 -> 2120192 bytes gix/tests/fixtures/make_submodules.sh | 11 +++++ gix/tests/gix/submodule.rs | 39 ++++++++++++++++-- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index d1a80bd083a..dc80da3531d 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -323,6 +323,11 @@ pub mod status { /// Return the status of the submodule, just like [`status`](Self::status), but allows to adjust options /// for more control over how the status is performed. /// + /// If `check_dirty` is `true`, the computation will stop once the first in a ladder operations + /// ordered from cheap to expensive shows that the submodule is dirty. When checking for detailed + /// status information (i.e. untracked file, modifications, HEAD-index changes) only the first change + /// will be kept to stop as early as possible. + /// /// Use `&mut std::convert::identity` for `adjust_options` if no specific options are desired. /// A reason to change them might be to enable sorting to enjoy deterministic order of changes. /// @@ -389,6 +394,9 @@ pub mod status { let mut changes = Vec::new(); for change in statuses { changes.push(change?); + if check_dirty { + break; + } } status.changes = Some(changes); Ok(status) diff --git a/gix/tests/fixtures/generated-archives/make_submodules.tar b/gix/tests/fixtures/generated-archives/make_submodules.tar index 941446ce8853831684593e3dc346f3853a4f1f3f..4a3566aabdfa490b91c64243ca1539001d4c4ba6 100644 GIT binary patch delta 9923 zcmcIqd013Ow)ghF0S(=BH%m7ypn|gWLU*GAKxF^S<}J(SME~^{Z3o zoI2-sPF+~=^Xvt?7gE(BD*XqcW$JGFlM}F{+O<;KK`^#=`e)(Ctw9ls5@4qjm7v820mzmqs zN#?MuQnR#dmxy*BRa$FShZa;RY?(HvwX4nQ$e{muQQlHkYpJp9J|PPJc}F_6F3jcI zUnDfvEZI9#5QKy093;=N)l}@C-iyF7dWs68$RAh))YVAfcxhOBM>g;aN!G6`@yK!d zw+2cJ5$Z3mAA$%dS;Xb5bg_Y&s?~x{UJ1QsHz| zv~)nJSq7ZdF^qM{PaXaXhBD;oM>zIZ)%8~HMo|IQDh@j)X5%eiFhqN8363pC0 zx~;DfG^6Vb`8w?ZItEC5%pW;!mxcUQphO5DF=7Slc3rLU%8C247fmVn{e{R}`}3Sh zowKmp^@2DC*l>k&$spD(wQgYx=Z4`Ay`zO@##|)4%(+tppC%0HP8d$7i^Zo)%?9s& zfsRgDugxqq>rhgKN=(esYT+^?yea#Z!B2q6NU|p1&~Th8b?7fUvk*3U@<^cQY_J*;>uOgk=Lw^Sb}X za@)Kc2W)FkQ)9c*Ag7*sRpP#M>Pv+QIjx0GE5yZ*2;*pwQ1_YEU&NtFFtej(xKYkP z$)0g%Fy{iwqGk<#2CP65Bl6j-O1QVB>E@)3B{$1QD}P_Nrbk-8!;9G1*Ct9@f)nbv zBsigJPf1$1!8|K5XA|}^>EY6A5@@Pv-xyf;Wv053ov5zg&6+a!=Q~XrC6GrrKZM2l@3M9bV zgHnzwjQEJOSx8zf{+m?x4*7#%J~PV>-y#wJ6v&&;bAE79uGYXrNWD~O17;)1ntgqt z&vTaX(o1@-h6pGzVjWQ>3*zqnM89?G)4vR@Hzb}Axn3$e5YIQ$dlU=#g>|Gw7aj`$5ts6us@L;edD) zn0U6Tc!;M`mB)YpZ{tR_Mimg%kdS`|-8eG3Z-jA&l8yTf5O;ORV6E^|$aKDA|Lq zjo_wp;=pCQAp%N_nBz}pO?mFDmQE$3Y~g?Vu2DQ#_T9=IW0>D;709XDv`}ubqCj5= z?F9lIk8E5i0&V?VXkjqPH-={7WHz8=z7!ge%vVDLh$$wF4SPaD(+y~CLPA$3wswp! znd5)hG8Fj=BwkY!$$X@t*?9NWyaN}%Eo;_Ig+gMn>#pxER%#R&p`+vG2$zhyvHrxv9Qat#!(l#G(S3hX!OP# zBU@@PB}mjsv}JKj20IBcZh_hpLTiPsS*cEDl6R=jkV(t(g+!f5rw-AB z`B087L!h=X6}!}BS)`w!c^>z^P=l%DiTaTgx=9V5f+im*u@n8cZc=X>-i2;Li4kwI zZoa&icVhX30VBQAomq$O|CqI7I(3V6v-X9WCs$sm!F;nW{Bp2cXk}A)@Z{s&9-c_2 zAC);u%}V5!ObS?ZYxt+6TTJ@haFi`&lAJ>&=t^=rTd=J)bmD_>I7^mh_9~eH7LjBZ zZ(K#L`Z}0IRgZ>xDDmzBm`_#|sQmYeQ5b;4yIa^~Oa37|UK`inQ~cJkL63#Cs%0C# zm3KJ(>Esu%jK8RT7_R!ByaSMHM*5Ro0u|lkWW=0+l`QYI2o0TFu6x5b(w(8;a1}A5 z3pC<2&B_8z5**L7P=w12AaPzrOy^F@8#S+WQm)lF;Iy8OEa({vEZD6X6I2+r!?iq} zGGFVY!PuOJ_U7q9xJGNo1{Fwcp*B5mhHmgyYf>06K;J|Ib5cp;IG7+%vR8cVF{c(v zlUCl%MF=1<;t5*|V=jg-x!2|M9*KN4!SvY&xH42w}y|xRS0vz;cZHN;zM(wyD z$GnA+Ryu97+KzlclY%>vr)L#LrXU`rd!7>HFpG;KX9fE?EwTgYCzJkFBuk}9$EGwP z;~P$1!Nx!HQEAejnjzbjLW$Ql5LBAkVY@wd($H|GcGBF?Oo_2z%D_MK@0yqK4lm+YwH|sgUEt7mP@E}60yZ0B2IKl?E zRoF6daN?haA2?rD{p3;N?V67U)ZyS(>#UGZ*}!dvmtUm|9Yx>$QgrGlZ!ifE>*UNQNG@n+_o{>|0cjKLVhic-wogRq2UawC@Ddl;! zQvzPo!0R=79BK>!6Q`%Oq98sc3P-s0h3Wo@d18mH!VD_n3eGk874S^v>=m zEK%OyQ@QHv_{%h8V(%(LJfyi^C>$kGR%UgHVfw9ry!}_)?A1jvQPZNH!G=((n-}{K zIDsVV#MQ2xt1UHjt~&}5P+|ml5w$(9Vc-i{cOOU_qw)Q2j^a?p zrYIiD=o=kK8M)EHms^`39qc$|h8${G)C-&I`yf`To8NajS_6_@ zyYeH!53-kY56>Cv956#FU3qWA#&_2qeVhPY0sK%A4fvrvnvWk=M*)5q74sCn6JFQ6 zH8&=OPOZ|pJgqy$w7V!8T>9b|?pHT7$68{obh3|t_bWgFbdKeTrX`jWO_mtY8DR7x z&d7-M1k*%GY$r$)-BJGOOu&?BvBj@(3iFo4Ceh|Z%uAa!;J~@D!M@kV8+p2FyfGD% zLIScEkwTJ@hZN8$=g3o1j4Md*ne>UqtN)i7DBl#r#Jpv?6X(4oSBNJT6^{FW1|La= zB16DHBz9Ud+BslR7c$f@ayjM0tU?(0eNmE;k>TLQc(ERD6u9JRF_S4qbVDu?_*7Kj zWiV)ptyX>&YcK}fp~!CIJ*5&u=Ei_Gky<^G+EgbHknXIZT+Htdu2_DQ_45jK+~ zrw=wHzfT{4k7$=_HCq*|Hu!&wqTi4AXMWFoEe!ob-i72Zz8-2&m@dH@?nselZ^CAS zY!bPwysmG?5HQAwEg3Fe%vOx)uLluy@ z7iZWU8EwiKYIRJjiVW&846THN?@`Hksy0HQQ2R3S5?*cAT0~B(ouj8CvX>7Go%}Hyh9TdCS%p?I^S)5X+wyY+ z2CY{D3jq21(gTrFmC}EaqkRX0Te3L$Z=is{*g>-M-BMxnP`a>Q)Sh0_&8QVR{C3q) z&nb!#>{7_uh|ulV$CSTPOlxb5lcI-u3hQkWz(Wp_K|GOCijmbR1Z^sc{3^td0{Kju z3Qd|6WfwbKcE3Fg_4feIDCX$m5Bf}~t4RiRaZ1<|$t~dzu`bkAto(&Bmpt!Rl71dM zuTd3GFCAjk3Y=~ChogCgh~^?vor}Lb7DILGA~Qse41ZjKE*RD(BCN|v^MRv$Vf};u zJf{3onE&=xz(Wd*PJut&@DWdmBUa+=6$;QX#~ULBjyC5QYOVHjimnuimyy+#{tv}v zAMdWhyTLp{FR6~yh#Z_mX)jSxXf*jH0sYD__U9Fg@q}^8_YonsF6|>UeE^Bq=KCKB znk_p&F3CQZJoTY>*`Ti)mJN)4XALSIdgX2EIZ2k~*pzai1yBzEq*sA4a7?pj@ZZfE zj6tJzj%fmCkg3FrYH^yx>dLVCLyIa|L{##63bWCmRg+<;$H37t&@-Gif2zU(g0Jy6;tgpUV1&g&R%T%5KZT*F^V@*MM5+XK z{&N`nAPP=J9B5$czlfn1qXD10<$JY_0I*s)kqI0CI%4R$5Rh(87w88*e^9!S6F_GSMd@$lr-~GeLFo_12|hYW!x(cI zn6de*$bupDB2e_@N)4FB$v`(qk|-wGtGwCPM>5C_dAbO()5-~W@QzHpgDqx$OS@r= z1t2F<^{#lA{LO zG`B1LO|w67qTR+H>s*Yf7n$g#Ze4`PY4<0Aua_(Ry*v-$;b%6n#m=7v`B)`zf~kl% zjpq>D5WM(=0_W{fh65^f_%9dtkG+Hw!-xaHrShfJaI6#j@e#dbrcT3@_cs-DK0&j; zN&8N&^!1fdcuPmXV&K{CpQpbBJ&9tddZoN=_9*aK0G{tw8`oze`V5dd);*?Z0(kgN zv8IzxVY>Y#IsC4|hReq72XX&$>$pO?{bUpTq)EG10Qz>hh%j+KFBD??;FGk#L_SFw z>a;X&RAdr``u`p*bw<%dI$TG+ P%~-0GYpG4eCfWZ0p`-jw delta 5675 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 diff --git a/gix/tests/fixtures/make_submodules.sh b/gix/tests/fixtures/make_submodules.sh index b0f4ddb636c..906170711ed 100755 --- a/gix/tests/fixtures/make_submodules.sh +++ b/gix/tests/fixtures/make_submodules.sh @@ -21,6 +21,17 @@ git init submodule-head-changed cd m1 && git checkout @~1 ) +git init submodule-index-changed +(cd submodule-index-changed + git submodule add ../module1 m1 + git commit -m "add submodule" + + (cd m1 + git mv subdir subdir-renamed + git mv this that + ) +) + git init submodule-head-changed-no-worktree (cd submodule-head-changed-no-worktree git submodule add ../module1 m1 diff --git a/gix/tests/gix/submodule.rs b/gix/tests/gix/submodule.rs index 6f1105a5b37..cb86129c9bf 100644 --- a/gix/tests/gix/submodule.rs +++ b/gix/tests/gix/submodule.rs @@ -186,6 +186,37 @@ mod open { Ok(()) } + #[test] + fn modified_in_index_only() -> crate::Result { + let repo = repo("submodule-index-changed")?; + let sm = repo.submodules()?.into_iter().flatten().next().expect("one submodule"); + + for mode in [ + gix::submodule::config::Ignore::Untracked, + gix::submodule::config::Ignore::None, + ] { + for check_dirty in [false, true] { + let status = sm.status_opts(mode, check_dirty, &mut |platform| platform)?; + assert_eq!( + status.is_dirty(), + Some(true), + "two files were renamed using `git mv` for an HEAD^{{tree}}-index change" + ); + assert_eq!( + status.changes.expect("present").len(), + if check_dirty { 1 } else { 3 }, + "in is-dirty mode, we don't collect all changes" + ); + } + } + + assert!( + repo.is_dirty()?, + "superproject should see submodule changes in the index as well" + ); + Ok(()) + } + #[test] fn modified_and_untracked() -> crate::Result { let repo = repo("modified-and-untracked")?; @@ -194,7 +225,7 @@ mod open { let status = sm.status(gix::submodule::config::Ignore::Dirty, false)?; assert_eq!(status.is_dirty(), Some(false), "Dirty skips worktree changes entirely"); - let status = sm.status_opts( + let mut status = sm.status_opts( gix::submodule::config::Ignore::None, false, &mut |status: gix::status::Platform<'_, gix::progress::Discard>| { @@ -217,16 +248,18 @@ mod open { let status_with_dirty_check = sm.status_opts( gix::submodule::config::Ignore::None, - true, + true, /* check-dirty */ &mut |status: gix::status::Platform<'_, gix::progress::Discard>| { status.index_worktree_options_mut(|opts| { opts.sorting = Some(gix_status::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); }) }, )?; + status.changes.as_mut().expect("two changes").pop(); assert_eq!( status_with_dirty_check, status, - "it cannot abort early as the only change it sees is the modification check" + "it cannot abort early as the only change it sees is the modification check.\ + However, with check-dirty, it would only gather the changes" ); let status = sm.status(gix::submodule::config::Ignore::Untracked, false)?; From d6ed2e20631227cbb1f1811ecdaf7fbbf6ea7ace Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 3 Jan 2025 17:27:10 +0100 Subject: [PATCH 17/17] feat: `gix status` now performs HEAD^{tree}-index comparisons as well. --- gitoxide-core/src/repository/status.rs | 54 ++++++++++++++++++++------ 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/gitoxide-core/src/repository/status.rs b/gitoxide-core/src/repository/status.rs index 6264ddd5a7d..75bf33f9343 100644 --- a/gitoxide-core/src/repository/status.rs +++ b/gitoxide-core/src/repository/status.rs @@ -1,6 +1,6 @@ use anyhow::bail; use gix::bstr::{BStr, BString, ByteSlice}; -use gix::status::index_worktree::Item; +use gix::status::{self, index_worktree}; use gix_status::index_as_worktree::{Change, Conflict, EntryStatus}; use std::path::Path; @@ -109,21 +109,54 @@ pub fn show( } None => gix::status::Submodule::AsConfigured { check_dirty: false }, }) - .into_index_worktree_iter(pathspecs)?; + .into_iter(pathspecs)?; for item in iter.by_ref() { let item = item?; match item { - Item::Modification { + status::Item::TreeIndex(change) => { + let (location, _, _, _) = change.fields(); + let status = match change { + gix::diff::index::Change::Addition { .. } => "A", + gix::diff::index::Change::Deletion { .. } => "D", + gix::diff::index::Change::Modification { .. } => "M", + gix::diff::index::Change::Rewrite { + ref source_location, .. + } => { + let source_location = gix::path::from_bstr(source_location.as_ref()); + let source_location = gix::path::relativize_with_prefix(&source_location, prefix); + writeln!( + out, + "{status: >2} {source_rela_path} → {dest_rela_path}", + status = "R", + source_rela_path = source_location.display(), + dest_rela_path = + gix::path::relativize_with_prefix(&gix::path::from_bstr(location), prefix).display(), + )?; + continue; + } + gix::diff::index::Change::Unmerged { .. } => { + // Unmerged entries from the worktree-index are displayed as part of the index-worktree comparison. + // Here we have nothing to do with them and can ignore. + continue; + } + }; + writeln!( + out, + "{status: >2} {rela_path}", + rela_path = gix::path::relativize_with_prefix(&gix::path::from_bstr(location), prefix).display(), + )?; + } + status::Item::IndexWorktree(index_worktree::Item::Modification { entry: _, entry_index: _, rela_path, status, - } => print_index_entry_status(&mut out, prefix, rela_path.as_ref(), status)?, - Item::DirectoryContents { + }) => print_index_entry_status(&mut out, prefix, rela_path.as_ref(), status)?, + status::Item::IndexWorktree(index_worktree::Item::DirectoryContents { entry, collapsed_directory_status, - } => { + }) => { if collapsed_directory_status.is_none() { writeln!( out, @@ -139,12 +172,12 @@ pub fn show( )?; } } - Item::Rewrite { + status::Item::IndexWorktree(index_worktree::Item::Rewrite { source, dirwalk_entry, copy: _, // TODO: how to visualize copies? .. - } => { + }) => { // TODO: handle multi-status characters, there can also be modifications at the same time as determined by their ID and potentially diffstats. writeln!( out, @@ -175,9 +208,8 @@ pub fn show( writeln!(err, "{outcome:#?}", outcome = out.index_worktree).ok(); } - writeln!(err, "\nhead -> index isn't implemented yet")?; - progress.init(Some(out.index.entries().len()), gix::progress::count("files")); - progress.set(out.index.entries().len()); + progress.init(Some(out.worktree_index.entries().len()), gix::progress::count("files")); + progress.set(out.worktree_index.entries().len()); progress.show_throughput(start); Ok(()) }