From 66158d283553787217c722f0cd9f1b87ca588a96 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 17 Aug 2025 09:52:05 +0200 Subject: [PATCH 1/3] fix: Add `PartialEq, Eq, PartialOrd, Ord` to more types. --- gix-index/src/entry/flags.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gix-index/src/entry/flags.rs b/gix-index/src/entry/flags.rs index 1a770350df9..2c4e30ca4c7 100644 --- a/gix-index/src/entry/flags.rs +++ b/gix-index/src/entry/flags.rs @@ -6,7 +6,7 @@ bitflags! { /// In-memory flags. /// /// Notably, not all of these will be persisted but can be used to aid all kinds of operations. - #[derive(Debug, Clone, Copy, Eq, PartialEq)] + #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] pub struct Flags: u32 { // TODO: could we use the pathlen ourselves to save 8 bytes? And how to handle longer paths than that? 0 as sentinel maybe? /// The mask to obtain the length of the path associated with this entry, up to 4095 characters without extension. From 38b28c6e778a9a35472b3703e72e2e5e1ade3c7c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 17 Aug 2025 07:02:46 +0200 Subject: [PATCH 2/3] feat!: Make all conflict entries available in index-worktree status. That way it's possible to rely on the status information without the need to access the index separately. Note that to make this work, `PartialOrd|Ord|PartialEq|Eq` had to be removed from the type. --- gix-status/src/index_as_worktree/function.rs | 44 +++- gix-status/src/index_as_worktree/mod.rs | 2 +- gix-status/src/index_as_worktree/types.rs | 44 +++- .../src/index_as_worktree_with_renames/mod.rs | 2 +- .../index_as_worktree_with_renames/types.rs | 8 +- gix-status/tests/status/index_as_worktree.rs | 224 ++++++++++++++++-- .../status/index_as_worktree_with_renames.rs | 2 +- gix-status/tests/status/mod.rs | 4 + 8 files changed, 293 insertions(+), 37 deletions(-) diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index 474c9c6c1b0..6000dd12d2f 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -11,6 +11,7 @@ use gix_features::parallel::{in_parallel_if, Reduce}; use gix_filter::pipeline::convert::ToGitOutcome; use gix_object::FindExt; +use crate::index_as_worktree::types::ConflictIndexEntry; use crate::{ index_as_worktree::{ traits, @@ -165,7 +166,7 @@ where return None; } Conflict::try_from_entry(all_entries, state.path_backing, absolute_entry_index, entry_path) - .map(|(_conflict, offset)| offset) + .map(|(_conflict, offset, _entries)| offset) }); if let Some(entries_to_skip_as_conflict_originates_in_previous_chunk) = offset { // skip current entry as it's done, along with following conflict entries @@ -287,10 +288,22 @@ impl<'index> State<'_, 'index> { } let status = if entry.stage_raw() != 0 { Ok( - Conflict::try_from_entry(entries, self.path_backing, entry_index, path).map(|(conflict, offset)| { - *outer_entry_index += offset; // let out loop skip over entries related to the conflict - EntryStatus::Conflict(conflict) - }), + Conflict::try_from_entry(entries, self.path_backing, entry_index, path).map( + |(conflict, offset, entries)| { + *outer_entry_index += offset; // let out loop skip over entries related to the conflict + EntryStatus::Conflict { + summary: conflict, + entries: Box::new({ + let mut a: [Option; 3] = Default::default(); + let src = entries.into_iter().map(|e| e.map(ConflictIndexEntry::from)); + for (a, b) in a.iter_mut().zip(src) { + *a = b; + } + a + }), + } + }, + ), ) } else { self.compute_status(entry, path, diff, submodule, objects) @@ -622,20 +635,23 @@ impl Conflict { /// Also return the amount of extra-entries that were part of the conflict declaration (not counting the entry at `start_index`) /// /// If for some reason entry at `start_index` isn't in conflicting state, `None` is returned. - pub fn try_from_entry( - entries: &[gix_index::Entry], + /// + /// Return `(Self, num_consumed_entries, three_possibly_entries)`. + pub fn try_from_entry<'entry>( + entries: &'entry [gix_index::Entry], path_backing: &gix_index::PathStorageRef, start_index: usize, entry_path: &BStr, - ) -> Option<(Self, usize)> { + ) -> Option<(Self, usize, [Option<&'entry gix_index::Entry>; 3])> { use Conflict::*; let mut mask = None::; + let mut seen: [Option<&gix_index::Entry>; 3] = Default::default(); - let mut count = 0_usize; - for stage in (start_index..(start_index + 3).min(entries.len())).filter_map(|idx| { + let mut num_consumed_entries = 0_usize; + for (stage, entry) in (start_index..(start_index + 3).min(entries.len())).filter_map(|idx| { let entry = &entries[idx]; let stage = entry.stage_raw(); - (stage > 0 && entry.path_in(path_backing) == entry_path).then_some(stage) + (stage > 0 && entry.path_in(path_backing) == entry_path).then_some((stage, entry)) }) { // This could be `1 << (stage - 1)` but let's be specific. *mask.get_or_insert(0) |= match stage { @@ -644,7 +660,8 @@ impl Conflict { 3 => 0b100, _ => 0, }; - count += 1; + num_consumed_entries = stage as usize - 1; + seen[num_consumed_entries] = Some(entry); } mask.map(|mask| { @@ -659,7 +676,8 @@ impl Conflict { 0b111 => BothModified, _ => unreachable!("BUG: bitshifts and typical entry layout doesn't allow for more"), }, - count - 1, + num_consumed_entries, + seen, ) }) } diff --git a/gix-status/src/index_as_worktree/mod.rs b/gix-status/src/index_as_worktree/mod.rs index 7dce0c43306..8c55c913ab4 100644 --- a/gix-status/src/index_as_worktree/mod.rs +++ b/gix-status/src/index_as_worktree/mod.rs @@ -1,7 +1,7 @@ //! Changes between an index and a worktree. /// mod types; -pub use types::{Change, Conflict, Context, EntryStatus, Error, Options, Outcome, VisitEntry}; +pub use types::{Change, Conflict, ConflictIndexEntry, Context, EntryStatus, Error, Options, Outcome, VisitEntry}; mod recorder; pub use recorder::{Record, Recorder}; diff --git a/gix-status/src/index_as_worktree/types.rs b/gix-status/src/index_as_worktree/types.rs index f9b4360e351..26d26981412 100644 --- a/gix-status/src/index_as_worktree/types.rs +++ b/gix-status/src/index_as_worktree/types.rs @@ -1,6 +1,7 @@ use std::sync::atomic::AtomicBool; use bstr::{BStr, BString}; +use gix_index::entry; /// The error returned by [index_as_worktree()`](crate::index_as_worktree()). #[derive(Debug, thiserror::Error)] @@ -144,11 +145,48 @@ pub enum Change { SubmoduleModification(U), } +/// Like [`gix_index::Entry`], but without disk-metadata. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ConflictIndexEntry { + /// The object id for this entry's ODB representation (assuming it's up-to-date with it). + pub id: gix_hash::ObjectId, + /// Additional flags for use in algorithms and for efficiently storing stage information, primarily + /// to obtain the [stage](entry::Flags::stage()). + pub flags: entry::Flags, + /// The kind of item this entry represents - it's not all blobs in the index anymore. + pub mode: entry::Mode, +} + +impl From<&gix_index::Entry> for ConflictIndexEntry { + fn from( + gix_index::Entry { + stat: _, + id, + flags, + mode, + .. + }: &gix_index::Entry, + ) -> Self { + ConflictIndexEntry { + id: *id, + flags: *flags, + mode: *mode, + } + } +} + /// Information about an entry. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum EntryStatus { - /// The entry is in a conflicting state, and we didn't collect any more information about it. - Conflict(Conflict), + /// The entry is in a conflicting state, and we provide all related entries along with a summary. + Conflict { + /// An analysis on the conflict itself based on the observed index entries. + summary: Conflict, + /// The entries from stage 1 to stage 3, where stage 1 is at index 0 and stage 3 at index 2. + /// Note that when there are conflicts, there is no stage 0. + /// Further, all entries are looking at the same path. + entries: Box<[Option; 3]>, + }, /// There is no conflict and a change was discovered. Change(Change), /// The entry didn't change, but its state caused extra work that can be avoided next time if its stats would be updated to the diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index d592a7f86a9..afa42a8553f 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -460,7 +460,7 @@ pub(super) mod function { fn kind(&self) -> ChangeKind { match self { ModificationOrDirwalkEntry::Modification(m) => match &m.status { - EntryStatus::Conflict(_) | EntryStatus::IntentToAdd | EntryStatus::NeedsUpdate(_) => { + EntryStatus::Conflict { .. } | EntryStatus::IntentToAdd | EntryStatus::NeedsUpdate(_) => { ChangeKind::Modification } EntryStatus::Change(c) => match c { diff --git a/gix-status/src/index_as_worktree_with_renames/types.rs b/gix-status/src/index_as_worktree_with_renames/types.rs index e3dd9d4efe0..d0e528c1e4b 100644 --- a/gix-status/src/index_as_worktree_with_renames/types.rs +++ b/gix-status/src/index_as_worktree_with_renames/types.rs @@ -37,7 +37,7 @@ pub enum Sorting { } /// Provide additional information collected during the runtime of [`index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()). -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub struct Outcome { /// The outcome of the modification check of tracked files. pub tracked_file_modification: crate::index_as_worktree::Outcome, @@ -49,7 +49,7 @@ pub struct Outcome { } /// Either an index entry for renames or another directory entry in case of copies. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub enum RewriteSource<'index, ContentChange, SubmoduleStatus> { /// The source originates in the index and is detected as missing in the working tree. /// This can also happen for copies. @@ -86,7 +86,7 @@ pub enum RewriteSource<'index, ContentChange, SubmoduleStatus> { } /// An 'entry' in the sense of a merge of modified tracked files and results from a directory walk. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub enum Entry<'index, ContentChange, SubmoduleStatus> { /// A tracked file was modified, and index-specific information is passed. Modification { @@ -218,7 +218,7 @@ impl Entry<'_, ContentChange, SubmoduleStatus> { pub fn summary(&self) -> Option { Some(match self { Entry::Modification { - status: EntryStatus::Conflict(_), + status: EntryStatus::Conflict { .. }, .. } => Summary::Conflict, Entry::Modification { diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index d0f7a4a57f0..cbdafe94d12 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -17,7 +17,10 @@ use gix_status::{ }, }; -use crate::fixture_path; +use crate::{fixture_path, hex_to_id}; +use gix_index::entry::{Flags, Mode}; +use gix_status::index_as_worktree::ConflictIndexEntry; +use pretty_assertions::assert_eq; // since tests are fixtures a bunch of stat information (like inode number) // changes when extracting the data so we need to disable all advanced stat @@ -235,7 +238,7 @@ pub(super) fn records_to_tuple<'index>( fn deracify_status(status: EntryStatus) -> Option { Some(match status { - EntryStatus::Conflict(c) => EntryStatus::Conflict(c), + EntryStatus::Conflict { summary, entries } => EntryStatus::Conflict { summary, entries }, EntryStatus::Change(c) => match c { Change::Removed => Change::Removed, Change::Type { worktree_mode } => Change::Type { worktree_mode }, @@ -473,7 +476,30 @@ fn conflict() { assert_eq!( fixture( "status_conflict", - &[(BStr::new(b"content"), 0, EntryStatus::Conflict(Conflict::BothModified))], + &[( + BStr::new(b"content"), + 0, + EntryStatus::Conflict { + summary: Conflict::BothModified, + entries: Box::new([ + Some(ConflictIndexEntry { + id: hex_to_id("df967b96a579e45a18b8251732d16804b2e56a55"), + flags: Flags::from_bits_retain(0x1000), + mode: Mode::FILE, + },), + Some(ConflictIndexEntry { + id: hex_to_id("d244dd0bf67758236f793fd7749a1c814fbfeac4"), + flags: Flags::from_bits_retain(0x2000), + mode: Mode::FILE, + },), + Some(ConflictIndexEntry { + id: hex_to_id("c7747099cf9e073babc68f52cdfb4d280ba5689f"), + flags: Flags::STAGE_MASK, + mode: Mode::FILE, + }), + ]) + } + )], ), Outcome { entries_to_process: 3, @@ -491,9 +517,54 @@ fn conflict_both_deleted_and_added_by_them_and_added_by_us() { conflict_fixture( "both-deleted", &[ - (BStr::new(b"added-by-them"), 0, EntryStatus::Conflict(AddedByThem)), - (BStr::new(b"added-by-us"), 1, EntryStatus::Conflict(AddedByUs)), - (BStr::new(b"file"), 2, EntryStatus::Conflict(BothDeleted)), + ( + BStr::new(b"added-by-them"), + 0, + EntryStatus::Conflict { + summary: AddedByThem, + entries: Box::new([ + None, + None, + Some(ConflictIndexEntry { + id: hex_to_id("9daeafb9864cf43055ae93beb0afd6c7d144bfa4"), + flags: Flags::STAGE_MASK, + mode: Mode::FILE, + }), + ]) + } + ), + ( + BStr::new(b"added-by-us"), + 1, + EntryStatus::Conflict { + summary: AddedByUs, + entries: Box::new([ + None, + Some(ConflictIndexEntry { + id: hex_to_id("9daeafb9864cf43055ae93beb0afd6c7d144bfa4"), + flags: Flags::from_bits_retain(0x2000), + mode: Mode::FILE, + },), + None, + ]) + } + ), + ( + BStr::new(b"file"), + 2, + EntryStatus::Conflict { + summary: BothDeleted, + entries: Box::new([ + Some(ConflictIndexEntry { + id: hex_to_id("9daeafb9864cf43055ae93beb0afd6c7d144bfa4"), + flags: Flags::from_bits_retain(0x1000), + mode: Mode::FILE, + }), + None, + None, + ]) + } + ), ], ), Outcome { @@ -511,8 +582,46 @@ fn conflict_both_added_and_deleted_by_them() { conflict_fixture( "both-added", &[ - (BStr::new(b"both-added"), 0, EntryStatus::Conflict(BothAdded)), - (BStr::new(b"deleted-by-them"), 2, EntryStatus::Conflict(DeletedByThem)), + ( + BStr::new(b"both-added"), + 0, + EntryStatus::Conflict { + summary: BothAdded, + entries: Box::new([ + None, + Some(ConflictIndexEntry { + id: hex_to_id("ba2906d0666cf726c7eaadd2cd3db615dedfdf3a"), + flags: Flags::from_bits_retain(0x2000), + mode: Mode::FILE, + },), + Some(ConflictIndexEntry { + id: hex_to_id("e019be006cf33489e2d0177a3837a2384eddebc5"), + flags: Flags::STAGE_MASK, + mode: Mode::FILE, + },), + ]) + } + ), + ( + BStr::new(b"deleted-by-them"), + 2, + EntryStatus::Conflict { + summary: DeletedByThem, + entries: Box::new([ + Some(ConflictIndexEntry { + id: hex_to_id("b1b716105590454bfc4c0247f193a04088f39c7f"), + flags: Flags::from_bits_retain(0x1000), + mode: Mode::FILE, + },), + Some(ConflictIndexEntry { + id: hex_to_id("7d5ae6def200acda76d2ccf7c93170a9d88d6cb1"), + flags: Flags::from_bits_retain(0x2000), + mode: Mode::FILE, + },), + None, + ]) + } + ), ], ), Outcome { @@ -526,15 +635,83 @@ fn conflict_both_added_and_deleted_by_them() { #[test] fn conflict_detailed_single() { use Conflict::*; - for (name, expected, entry_index, entries_to_process, entries_processed) in [ - ("deleted-by-them", DeletedByThem, 0, 2, 1), - ("deleted-by-us", DeletedByUs, 0, 2, 1), - ("both-modified", BothModified, 0, 3, 1), + for (name, expected, expected_entries, entry_index, entries_to_process, entries_processed) in [ + ( + "deleted-by-them", + DeletedByThem, + [ + Some(ConflictIndexEntry { + id: hex_to_id("dde77be9fbfb155ff0473e7fe31781d56d50e5d3"), + flags: Flags::from_bits_retain(0x1000), + mode: Mode::FILE, + }), + Some(ConflictIndexEntry { + id: hex_to_id("e14959721a622239cc8de786a4b8cfcefea8304c"), + flags: Flags::from_bits_retain(0x2000), + mode: Mode::FILE, + }), + None, + ], + 0, + 2, + 1, + ), + ( + "deleted-by-us", + DeletedByUs, + [ + Some(ConflictIndexEntry { + id: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + flags: Flags::from_bits_retain(0x1000), + mode: Mode::FILE, + }), + None, + Some(ConflictIndexEntry { + id: hex_to_id("0835e4f9714005ed591f68d306eea0d6d2ae8fd7"), + flags: Flags::STAGE_MASK, + mode: Mode::FILE, + }), + ], + 0, + 2, + 1, + ), + ( + "both-modified", + BothModified, + [ + Some(ConflictIndexEntry { + id: hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + flags: Flags::from_bits_retain(0x1000), + mode: Mode::FILE, + }), + Some(ConflictIndexEntry { + id: hex_to_id("e45c9c2666d44e0327c1f9c239a74c508336053e"), + flags: Flags::from_bits_retain(0x2000), + mode: Mode::FILE, + }), + Some(ConflictIndexEntry { + id: hex_to_id("aac4af54d6427ef10af2b51a524e7272c4f37c02"), + flags: Flags::STAGE_MASK, + mode: Mode::FILE, + }), + ], + 0, + 3, + 1, + ), ] { assert_eq!( conflict_fixture( name, - &[(BStr::new(b"file"), entry_index, EntryStatus::Conflict(expected))], + &[( + BStr::new(b"file"), + entry_index, + EntryStatus::Conflict { + summary: expected, + entries: Box::new(expected_entries) + } + )], ), Outcome { entries_to_process, @@ -551,7 +728,26 @@ fn submodule_conflict() { assert_eq!( submodule_fixture( "conflict", - &[(BStr::new(b"m1"), 1, EntryStatus::Conflict(Conflict::DeletedByUs))] + &[( + BStr::new(b"m1"), + 1, + EntryStatus::Conflict { + summary: Conflict::DeletedByUs, + entries: Box::new([ + Some(ConflictIndexEntry { + id: hex_to_id("3189cd3cb0af8586c39a838aa3e54fd72a872a41"), + flags: Flags::from_bits_retain(0x1000), + mode: Mode::DIR | Mode::SYMLINK + }), + None, + Some(ConflictIndexEntry { + id: hex_to_id("e376f96e6a7f1c9335ca16c3f62e172166146bda"), + flags: Flags::STAGE_MASK, + mode: Mode::DIR | Mode::SYMLINK, + }), + ]) + } + )] ), Outcome { entries_to_process: 3, diff --git a/gix-status/tests/status/index_as_worktree_with_renames.rs b/gix-status/tests/status/index_as_worktree_with_renames.rs index 7a8a7e8aa51..003166e79c0 100644 --- a/gix-status/tests/status/index_as_worktree_with_renames.rs +++ b/gix-status/tests/status/index_as_worktree_with_renames.rs @@ -393,7 +393,7 @@ impl Expectation<'_> { pub fn summary(&self) -> Option { Some(match self { Expectation::Modification { status, .. } => match status { - EntryStatus::Conflict(_) => Summary::Conflict, + EntryStatus::Conflict { .. } => Summary::Conflict, EntryStatus::Change(change) => match change { Change::Removed => Summary::Removed, Change::Type { .. } => Summary::TypeChange, diff --git a/gix-status/tests/status/mod.rs b/gix-status/tests/status/mod.rs index 43135b103db..e097aaee274 100644 --- a/gix-status/tests/status/mod.rs +++ b/gix-status/tests/status/mod.rs @@ -10,3 +10,7 @@ pub fn fixture_path(name: &str) -> std::path::PathBuf { .expect("script works"); dir } + +fn hex_to_id(hex: &str) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex") +} From 5da38e5a473979965b622869c4676f0f21bed58b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 17 Aug 2025 10:27:22 +0200 Subject: [PATCH 3/3] adapt to changes in `gix-status` --- gitoxide-core/src/repository/status.rs | 2 +- gix/src/status/index_worktree.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitoxide-core/src/repository/status.rs b/gitoxide-core/src/repository/status.rs index 77d94322e35..670804ad729 100644 --- a/gitoxide-core/src/repository/status.rs +++ b/gitoxide-core/src/repository/status.rs @@ -220,7 +220,7 @@ fn print_index_entry_status( ) -> std::io::Result<()> { let char_storage; let status = match status { - EntryStatus::Conflict(conflict) => as_str(conflict), + EntryStatus::Conflict { summary, entries: _ } => as_str(summary), EntryStatus::Change(change) => { char_storage = change_to_char(&change); std::str::from_utf8(std::slice::from_ref(&char_storage)).expect("valid ASCII") diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index fa125acb317..d987cfd24a2 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -489,7 +489,7 @@ pub mod iter { use gix_status::index_as_worktree_with_renames::Summary::*; Some(match self { Item::Modification { status, .. } => match status { - EntryStatus::Conflict(_) => Conflict, + EntryStatus::Conflict { .. } => Conflict, EntryStatus::Change(change) => match change { Change::Removed => Removed, Change::Type { .. } => TypeChange,