Skip to content

Commit e856ca8

Browse files
committed
feat: provide a way to record and apply index changes.
These changes will then be applicable to an index that is created from the written tree editor.
1 parent 465e4b1 commit e856ca8

File tree

8 files changed

+127
-2
lines changed

8 files changed

+127
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-merge/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ gix-quote = { version = "^0.4.13", path = "../gix-quote" }
3232
gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] }
3333
gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" }
3434
gix-diff = { version = "^0.47.0", path = "../gix-diff", default-features = false, features = ["blob"] }
35+
gix-index = { version = "^0.36.0", path = "../gix-index" }
3536

3637
thiserror = "2.0.0"
3738
imara-diff = { version = "0.1.7" }

gix-merge/src/tree/mod.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ impl Outcome<'_> {
8181
pub fn has_unresolved_conflicts(&self, how: TreatAsUnresolved) -> bool {
8282
self.conflicts.iter().any(|c| c.is_unresolved(how))
8383
}
84+
85+
/// Returns `true` if `index` changed as we applied conflicting stages to it.
86+
/// It's important that `index` is at the state of [`Self::tree`].
87+
///
88+
/// Note that in practice, whenever there is a single [conflict], this function will return `true`.
89+
pub fn index_changed_after_applying_conflicts(
90+
&self,
91+
index: &mut gix_index::State,
92+
) -> Result<bool, apply_index_entries::Error> {
93+
apply_index_entries(&self.conflicts, index)
94+
}
8495
}
8596

8697
/// A description of a conflict (i.e. merge issue without an auto-resolution) as seen during a [tree-merge](crate::tree()).
@@ -99,11 +110,25 @@ pub struct Conflict {
99110
pub ours: Change,
100111
/// The change representing *their* side.
101112
pub theirs: Change,
113+
/// An array to store an entry for each stage of the conflict.
114+
/// Note that ours and theirs might be swapped, so one should access it through [`Self::entries()`].
115+
pub entries: [Option<ConflictIndexEntry>; 3],
102116
/// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`]
103117
/// and [`Self::into_parts_by_resolution()`].
104118
map: ConflictMapping,
105119
}
106120

121+
/// A conflicting entry for insertion into the index.
122+
/// It will always be either on stage 1 (ancestor/base), 2 (ours) or 3 (theirs)
123+
#[derive(Debug, Clone, Copy)]
124+
pub struct ConflictIndexEntry {
125+
/// The kind of object at this stage.
126+
/// Note that it's possible that this is a directory, for instance if a directory was replaced with a file.
127+
pub mode: gix_object::tree::EntryMode,
128+
/// The id defining the state of the object.
129+
pub id: gix_hash::ObjectId,
130+
}
131+
107132
/// A utility to help define which side is what in the [`Conflict`] type.
108133
#[derive(Debug, Clone, Copy)]
109134
enum ConflictMapping {
@@ -178,6 +203,14 @@ impl Conflict {
178203
}
179204
}
180205

206+
/// Return the index entries for insertion into the index, to match with what's returned by [`Self::changes_in_resolution()`].
207+
pub fn entries(&self) -> [Option<ConflictIndexEntry>; 3] {
208+
match self.map {
209+
ConflictMapping::Original => self.entries,
210+
ConflictMapping::Swapped => [self.entries[0], self.entries[2], self.entries[1]],
211+
}
212+
}
213+
181214
/// Return information about the content merge if it was performed.
182215
pub fn content_merge(&self) -> Option<ContentMerge> {
183216
match &self.resolution {
@@ -308,3 +341,35 @@ pub struct Options {
308341

309342
pub(super) mod function;
310343
mod utils;
344+
pub mod apply_index_entries {
345+
346+
pub(super) mod function {
347+
use crate::tree::Conflict;
348+
use bstr::BString;
349+
350+
/// The error returned by [`apply_index_entries()`].
351+
#[derive(Debug, thiserror::Error)]
352+
#[allow(missing_docs)]
353+
pub enum Error {
354+
#[error(
355+
"Could not find '{path}' in the index, even though it should have the non-conflicting variant of it."
356+
)]
357+
MissingPathInIndex { path: BString },
358+
}
359+
360+
/// Returns `true` if `index` changed as we applied conflicting stages to it.
361+
/// It's important that `index` matches the tree that was produced as part of the merge that also
362+
/// brought about `conflicts`, or else this function will fail if it cannot find the path matching
363+
/// the conflicting entries.
364+
///
365+
/// Note that in practice, whenever there is a single [conflict], this function will return `true`.
366+
/// Errors can only occour if `index` isn't the one created from the merged tree that produced the `conflicts`.
367+
pub fn apply_index_entries(_conflicts: &[Conflict], index: &mut gix_index::State) -> Result<bool, Error> {
368+
let len = index.entries().len();
369+
370+
Ok(index.entries().len() != len)
371+
}
372+
}
373+
pub use function::Error;
374+
}
375+
pub use apply_index_entries::function::apply_index_entries;

gix-merge/src/tree/utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ impl Conflict {
563563
resolution,
564564
ours: ours.clone(),
565565
theirs: theirs.clone(),
566+
entries: Default::default(),
566567
map: match outer_map {
567568
ConflictMapping::Original => map,
568569
ConflictMapping::Swapped => map.swapped(),
259 KB
Binary file not shown.

gix-merge/tests/fixtures/tree-baseline.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ function baseline () (
6666
git -c merge.conflictStyle=$conflict_style merge-tree -z --write-tree --allow-unrelated-histories "$their_committish" "$our_committish" > "$merge_info" || :
6767
echo "$dir" "$conflict_style" "$their_commit_id" "$their_committish" "$our_commit_id" "$our_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases
6868
fi
69+
70+
local index_path=.git/${conflict_style}-${our_committish}-${their_committish}.index
71+
if [ ! -e $index_path ]; then
72+
git checkout -f $our_committish
73+
git merge -m m $their_committish || :
74+
cp .git/index "$index_path"
75+
fi
76+
77+
local index_path=.git/${conflict_style}-${their_committish}-${our_committish}.index
78+
if [ ! -e $index_path ]; then
79+
git checkout -f $their_committish
80+
git merge -m m $our_committish || :
81+
cp .git/index "$index_path"
82+
fi
6983
)
7084

7185
git init simple

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ pub struct Expectation {
9696
pub their_side_name: String,
9797
pub merge_info: MergeInfo,
9898
pub case_name: String,
99+
pub index: gix_index::File,
99100
pub deviation: Option<Deviation>,
100101
}
101102

@@ -129,7 +130,7 @@ impl Iterator for Expectations<'_> {
129130
let mut tokens = line.split(' ');
130131
let (
131132
Some(subdir),
132-
Some(conflict_style),
133+
Some(conflict_style_name),
133134
Some(our_commit_id),
134135
Some(our_side_name),
135136
Some(their_commit_id),
@@ -160,7 +161,7 @@ impl Iterator for Expectations<'_> {
160161
});
161162

162163
let subdir_path = self.root.join(subdir);
163-
let conflict_style = match conflict_style {
164+
let conflict_style = match conflict_style_name {
164165
"merge" => ConflictStyle::Merge,
165166
"diff3" => ConflictStyle::Diff3,
166167
unknown => unreachable!("Unknown conflict style: '{unknown}'"),
@@ -170,6 +171,15 @@ impl Iterator for Expectations<'_> {
170171
let our_commit_id = gix_hash::ObjectId::from_hex(our_commit_id.as_bytes()).unwrap();
171172
let their_commit_id = gix_hash::ObjectId::from_hex(their_commit_id.as_bytes()).unwrap();
172173
let merge_info = parse_merge_info(std::fs::read_to_string(subdir_path.join(merge_info_filename)).unwrap());
174+
let index = gix_index::File::at(
175+
subdir_path
176+
.join(".git")
177+
.join(format!("{conflict_style_name}-{our_side_name}-{their_side_name}.index")),
178+
gix_hash::Kind::Sha1,
179+
false, /* skip hash */
180+
Default::default(),
181+
)
182+
.expect("index should be present for each combination");
173183
Some(Expectation {
174184
root: subdir_path,
175185
conflict_style,
@@ -179,6 +189,7 @@ impl Iterator for Expectations<'_> {
179189
their_commit_id,
180190
their_side_name: their_side_name.to_owned(),
181191
merge_info,
192+
index,
182193
case_name: format!(
183194
"{subdir}-{}",
184195
merge_info_filename
@@ -285,6 +296,30 @@ fn parse_info<'a>(mut lines: impl Iterator<Item = &'a str>) -> Option<ConflictIn
285296
Some(ConflictInfo { paths, kind, message })
286297
}
287298

299+
#[derive(Debug, PartialEq, Eq)]
300+
pub struct DebugIndexEntry<'a> {
301+
path: &'a BStr,
302+
id: gix_hash::ObjectId,
303+
mode: gix_index::entry::Mode,
304+
stage: gix_index::entry::Stage,
305+
}
306+
307+
pub fn clear_entries(state: &gix_index::State) -> Vec<DebugIndexEntry<'_>> {
308+
state
309+
.entries()
310+
.iter()
311+
.map(|entry| {
312+
let path = entry.path(state);
313+
DebugIndexEntry {
314+
path,
315+
id: entry.id,
316+
mode: entry.mode,
317+
stage: entry.stage(),
318+
}
319+
})
320+
.collect()
321+
}
322+
288323
pub fn visualize_tree(
289324
id: &gix_hash::oid,
290325
odb: &impl gix_object::Find,

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ fn run_baseline() -> crate::Result {
3232
their_side_name,
3333
merge_info,
3434
case_name,
35+
index,
3536
deviation,
3637
} in baseline::Expectations::new(&root, &cases)
3738
.filter(|case| new_test.map_or(true, |prefix: &str| case.case_name.starts_with(prefix)))
@@ -98,6 +99,13 @@ fn run_baseline() -> crate::Result {
9899
);
99100
}
100101
}
102+
103+
let actual_index = gix_index::State::from_tree(&actual_id, &odb, Default::default())?;
104+
pretty_assertions::assert_eq!(
105+
baseline::clear_entries(&actual_index),
106+
baseline::clear_entries(&index),
107+
"{case_name}: index mismatch"
108+
);
101109
}
102110

103111
assert_eq!(

0 commit comments

Comments
 (0)