Skip to content

Commit 3e94b58

Browse files
committed
fix!: assure that tree::apply_index_entries() cannot unknowingly leave unconflicted *and* conflicted entries.
This is a massive footgun currently where incorrect usage is very likely while causing all kinds of mistakes.
1 parent 01f66eb commit 3e94b58

File tree

2 files changed

+41
-9
lines changed

2 files changed

+41
-9
lines changed

gix-merge/src/tree/mod.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,18 @@ impl Outcome<'_> {
121121

122122
/// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a
123123
/// conflict should be considered unresolved.
124+
/// `removal_mode` decides how unconflicted entries should be removed if they are superseded by
125+
/// their conflicted counterparts.
124126
/// It's important that `index` is at the state of [`Self::tree`].
125127
///
126128
/// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`.
127-
/// Also, the unconflicted stage of such entries will be removed merely by setting a flag, so the
128-
/// in-memory entry is still present.
129-
pub fn index_changed_after_applying_conflicts(&self, index: &mut gix_index::State, how: TreatAsUnresolved) -> bool {
130-
apply_index_entries(&self.conflicts, how, index)
129+
pub fn index_changed_after_applying_conflicts(
130+
&self,
131+
index: &mut gix_index::State,
132+
how: TreatAsUnresolved,
133+
removal_mode: apply_index_entries::RemovalMode,
134+
) -> bool {
135+
apply_index_entries(&self.conflicts, how, index, removal_mode)
131136
}
132137
}
133138

@@ -456,7 +461,24 @@ mod utils;
456461
///
457462
pub mod apply_index_entries {
458463

464+
/// Determines how we deal with the removal of unconflicted entries if these are superseded by their conflicted counterparts,
465+
/// i.e. stage 1, 2 and 3.
466+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
467+
pub enum RemovalMode {
468+
/// Add the [`gix_index::entry::Flags::REMOVE`] flag to entries that are to be removed.
469+
///
470+
/// **Note** that this also means that unconflicted and conflicted stages will be visible in the same index.
471+
/// When written, entries marked for removal will automatically be ignored. However, this also means that
472+
/// one must not use the in-memory index or take specific care of entries that are marked for removal.
473+
Mark,
474+
/// Entries marked for removal (even those that were already marked) will be removed from memory at the end.
475+
///
476+
/// This is an expensive step that leaves a consistent index, ready for use.
477+
Prune,
478+
}
479+
459480
pub(super) mod function {
481+
use crate::tree::apply_index_entries::RemovalMode;
460482
use crate::tree::{Conflict, ConflictIndexEntryPathHint, Resolution, ResolutionFailure, TreatAsUnresolved};
461483
use bstr::{BStr, ByteSlice};
462484
use std::collections::{hash_map, HashMap};
@@ -465,8 +487,9 @@ pub mod apply_index_entries {
465487
/// conflict should be considered unresolved.
466488
/// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one
467489
/// that is currently checked out.
468-
/// This removal, however, is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means
469-
/// these entries won't be written back to disk but will still be present in the index.
490+
/// This removal is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means
491+
/// these entries won't be written back to disk but will still be present in the index if `removal_mode`
492+
/// is [`RemovalMode::Mark`]. For proper removal, choose [`RemovalMode::Prune`].
470493
/// It's important that `index` matches the tree that was produced as part of the merge that also
471494
/// brought about `conflicts`, or else this function will fail if it cannot find the path matching
472495
/// the conflicting entries.
@@ -477,6 +500,7 @@ pub mod apply_index_entries {
477500
conflicts: &[Conflict],
478501
how: TreatAsUnresolved,
479502
index: &mut gix_index::State,
503+
removal_mode: RemovalMode,
480504
) -> bool {
481505
if index.is_sparse() {
482506
gix_trace::error!("Refusing to apply index entries to sparse index - it's not tested yet");
@@ -585,8 +609,15 @@ pub mod apply_index_entries {
585609
}
586610
}
587611

612+
let res = index.entries().len() != len;
613+
match removal_mode {
614+
RemovalMode::Mark => {}
615+
RemovalMode::Prune => {
616+
index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE));
617+
}
618+
}
588619
index.sort_entries();
589-
index.entries().len() != len
620+
res
590621
}
591622
}
592623
}

gix-merge/tests/merge/tree/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::tree::baseline::Deviation;
22
use gix_diff::Rewrites;
33
use gix_merge::commit::Options;
4+
use gix_merge::tree::apply_index_entries::RemovalMode;
45
use gix_merge::tree::{treat_as_unresolved, TreatAsUnresolved};
56
use gix_object::Write;
67
use gix_worktree::stack::state::attributes;
@@ -121,8 +122,8 @@ fn run_baseline() -> crate::Result {
121122
}
122123
};
123124
let conflicts_like_in_git = TreatAsUnresolved::git();
124-
let did_change = actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git);
125-
actual_index.remove_entries(|_, _, e| e.flags.contains(gix_index::entry::Flags::REMOVE));
125+
let did_change =
126+
actual.index_changed_after_applying_conflicts(&mut actual_index, conflicts_like_in_git, RemovalMode::Prune);
126127

127128
pretty_assertions::assert_eq!(
128129
baseline::clear_entries(&actual_index),

0 commit comments

Comments
 (0)