gix_merge/tree/
function.rs

1use std::{borrow::Cow, convert::Infallible};
2
3use bstr::{BString, ByteSlice};
4use gix_diff::{tree::recorder::Location, tree_with_rewrites::Change};
5use gix_hash::ObjectId;
6use gix_object::{
7    tree,
8    tree::{EntryKind, EntryMode},
9    FindExt,
10};
11
12use crate::tree::{
13    utils::{
14        apply_change, perform_blob_merge, possibly_rewritten_location, rewrite_location_with_renamed_directory,
15        to_components, track, unique_path_in_tree, ChangeList, ChangeListRef, PossibleConflict, TrackedChange,
16        TreeNodes,
17    },
18    Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping,
19    ConflictMapping::{Original, Swapped},
20    ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure, ResolveWith,
21};
22
23/// Perform a merge between `our_tree` and `their_tree`, using `base_tree` as merge-base.
24/// Note that `base_tree` can be an empty tree to indicate 'no common ancestor between the two sides'.
25///
26/// * `labels` are relevant for text-merges and will be shown in conflicts.
27/// * `objects` provides access to trees when diffing them.
28/// * `write_blob_to_odb(content) -> Result<ObjectId, E>` writes newly merged content into the odb to obtain an id
29///   that will be used in merged trees.
30/// * `diff_state` is state used for diffing trees.
31/// * `diff_resource_cache` is used for similarity checks.
32/// * `blob_merge` is a pre-configured platform to merge any content.
33///     - Note that it shouldn't be allowed to read from the worktree, given that this is a tree-merge.
34/// * `options` are used to affect how the merge is performed.
35///
36/// ### Unbiased (Ours x Theirs == Theirs x Ours)
37///
38/// The algorithm is implemented so that the result is the same no matter how the sides are ordered.
39///
40/// ### Differences to Merge-ORT
41///
42/// Merge-ORT (Git) defines the desired outcomes where are merely mimicked here. The algorithms are different, and it's
43/// clear that Merge-ORT is significantly more elaborate and general.
44///
45/// It also writes out trees once it's done with them in a form of reduction process, here an editor is used
46/// to keep only the changes, to be written by the caller who receives it as part of the result.
47/// This may use more memory in the worst case scenario, but in average *shouldn't* perform much worse due to the
48/// natural sparsity of the editor.
49///
50/// Our rename-tracking also produces copy information, but we discard it and simply treat it like an addition.
51///
52/// Finally, our algorithm will consider reasonable solutions to merge-conflicts as conflicts that are resolved, leaving
53/// only content with conflict markers as unresolved ones.
54///
55/// ### Performance
56///
57/// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval.
58#[allow(clippy::too_many_arguments)]
59pub fn tree<'objects, E>(
60    base_tree: &gix_hash::oid,
61    our_tree: &gix_hash::oid,
62    their_tree: &gix_hash::oid,
63    mut labels: crate::blob::builtin_driver::text::Labels<'_>,
64    objects: &'objects impl gix_object::FindObjectOrHeader,
65    mut write_blob_to_odb: impl FnMut(&[u8]) -> Result<ObjectId, E>,
66    diff_state: &mut gix_diff::tree::State,
67    diff_resource_cache: &mut gix_diff::blob::Platform,
68    blob_merge: &mut crate::blob::Platform,
69    options: Options,
70) -> Result<Outcome<'objects>, Error>
71where
72    E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
73{
74    let ours_needs_diff = base_tree != our_tree;
75    let theirs_needs_diff = base_tree != their_tree;
76    let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels);
77    let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new());
78    let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?;
79    let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind());
80    let ancestor_tree = gix_object::TreeRefIter::from_bytes(&base_buf);
81    let tree_conflicts = options.tree_conflicts;
82
83    let mut our_changes = Vec::new();
84    if ours_needs_diff {
85        let our_tree = objects.find_tree_iter(our_tree, &mut side_buf)?;
86        gix_diff::tree_with_rewrites(
87            ancestor_tree,
88            our_tree,
89            diff_resource_cache,
90            diff_state,
91            objects,
92            |change| -> Result<_, Infallible> {
93                track(change, &mut our_changes);
94                Ok(gix_diff::tree_with_rewrites::Action::Continue)
95            },
96            gix_diff::tree_with_rewrites::Options {
97                location: Some(Location::Path),
98                rewrites: options.rewrites,
99            },
100        )?;
101    }
102
103    let mut our_tree = TreeNodes::new();
104    for (idx, change) in our_changes.iter().enumerate() {
105        our_tree.track_change(&change.inner, idx);
106    }
107
108    let mut their_changes = Vec::new();
109    if theirs_needs_diff {
110        let their_tree = objects.find_tree_iter(their_tree, &mut side_buf)?;
111        gix_diff::tree_with_rewrites(
112            ancestor_tree,
113            their_tree,
114            diff_resource_cache,
115            diff_state,
116            objects,
117            |change| -> Result<_, Infallible> {
118                track(change, &mut their_changes);
119                Ok(gix_diff::tree_with_rewrites::Action::Continue)
120            },
121            gix_diff::tree_with_rewrites::Options {
122                location: Some(Location::Path),
123                rewrites: options.rewrites,
124            },
125        )?;
126    }
127
128    let mut their_tree = TreeNodes::new();
129    for (idx, change) in their_changes.iter().enumerate() {
130        their_tree.track_change(&change.inner, idx);
131    }
132
133    let mut conflicts = Vec::new();
134    let mut failed_on_first_conflict = false;
135    let mut should_fail_on_conflict = |mut conflict: Conflict| -> bool {
136        if tree_conflicts.is_some() {
137            if let Err(failure) = conflict.resolution {
138                conflict.resolution = Ok(Resolution::Forced(failure));
139            }
140        }
141        if let Some(how) = options.fail_on_conflict {
142            if conflict.resolution.is_err() || conflict.is_unresolved(how) {
143                failed_on_first_conflict = true;
144            }
145        }
146        conflicts.push(conflict);
147        failed_on_first_conflict
148    };
149
150    let ((mut our_changes, mut our_tree), (mut their_changes, mut their_tree)) =
151        ((&mut our_changes, &mut our_tree), (&mut their_changes, &mut their_tree));
152    let mut outer_side = Original;
153    if their_changes.is_empty() {
154        ((our_changes, our_tree), (their_changes, their_tree)) = ((their_changes, their_tree), (our_changes, our_tree));
155        (labels.current, labels.other) = (labels.other, labels.current);
156        outer_side = outer_side.swapped();
157    }
158
159    #[derive(Debug)]
160    enum MatchKind {
161        /// A tree is supposed to be superseded by something else.
162        EraseTree,
163        /// A leaf node is superseded by a tree
164        EraseLeaf,
165    }
166
167    'outer: while their_changes.iter().rev().any(|c| !c.was_written) {
168        let mut segment_start = 0;
169        let mut last_seen_len = their_changes.len();
170
171        while segment_start != last_seen_len {
172            for theirs_idx in segment_start..last_seen_len {
173                // `their` can be a tree, and it could be used to efficiently prune child-changes as these
174                // trees are always rewrites with parent ids (of course we validate), so child-changes could be handled
175                // quickly. However, for now the benefit of having these trees is to have them as part of the match-tree
176                // on *our* side so that it's clear that we passed a renamed directory (by identity).
177                let TrackedChange {
178                    inner: theirs,
179                    was_written,
180                    needs_tree_insertion,
181                    rewritten_location,
182                } = &their_changes[theirs_idx];
183                if theirs.entry_mode().is_tree() || *was_written {
184                    continue;
185                }
186
187                if needs_tree_insertion.is_some() {
188                    their_tree.insert(theirs, theirs_idx);
189                }
190
191                match our_tree
192                    .check_conflict(
193                        rewritten_location
194                            .as_ref()
195                            .map_or_else(|| theirs.source_location(), |t| t.0.as_bstr()),
196                    )
197                    .filter(|ours| {
198                        ours.change_idx()
199                            .zip(needs_tree_insertion.flatten())
200                            .map_or(true, |(ours_idx, ignore_idx)| ours_idx != ignore_idx)
201                            && our_tree.is_not_same_change_in_possible_conflict(theirs, ours, our_changes)
202                    }) {
203                    None => {
204                        if let Some((rewritten_location, ours_idx)) = rewritten_location {
205                            // `no_entry` to the index because that's not a conflict at all,
206                            // but somewhat advanced rename tracking.
207                            if should_fail_on_conflict(Conflict::with_resolution(
208                                Resolution::SourceLocationAffectedByRename {
209                                    final_location: rewritten_location.to_owned(),
210                                },
211                                (&our_changes[*ours_idx].inner, theirs, Original, outer_side),
212                                [None, None, None],
213                            )) {
214                                break 'outer;
215                            }
216                            editor.remove(to_components(theirs.location()))?;
217                        }
218                        apply_change(&mut editor, theirs, rewritten_location.as_ref().map(|t| &t.0))?;
219                        their_changes[theirs_idx].was_written = true;
220                    }
221                    Some(candidate) => {
222                        use crate::tree::utils::to_components_bstring_ref as toc;
223                        debug_assert!(
224                            rewritten_location.is_none(),
225                            "We should probably handle the case where a rewritten location is passed down here"
226                        );
227
228                        let (ours_idx, match_kind) = match candidate {
229                            PossibleConflict::PassedRewrittenDirectory { change_idx } => {
230                                let ours = &our_changes[change_idx];
231                                let location_after_passed_rename =
232                                    rewrite_location_with_renamed_directory(theirs.location(), &ours.inner);
233                                if let Some(new_location) = location_after_passed_rename {
234                                    their_tree.remove_existing_leaf(theirs.location());
235                                    push_deferred_with_rewrite(
236                                        (theirs.clone(), Some(change_idx)),
237                                        Some((new_location, change_idx)),
238                                        their_changes,
239                                    );
240                                } else {
241                                    apply_change(&mut editor, theirs, None)?;
242                                    their_changes[theirs_idx].was_written = true;
243                                }
244                                their_changes[theirs_idx].was_written = true;
245                                continue;
246                            }
247                            PossibleConflict::TreeToNonTree { change_idx: Some(idx) }
248                                if matches!(
249                                    our_changes[idx].inner,
250                                    Change::Deletion { .. } | Change::Addition { .. } | Change::Rewrite { .. }
251                                ) =>
252                            {
253                                (Some(idx), Some(MatchKind::EraseTree))
254                            }
255                            PossibleConflict::NonTreeToTree { change_idx } => (change_idx, Some(MatchKind::EraseLeaf)),
256                            PossibleConflict::Match { change_idx: ours_idx } => (Some(ours_idx), None),
257                            _ => (None, None),
258                        };
259
260                        let Some(ours_idx) = ours_idx else {
261                            let ours = match candidate {
262                                PossibleConflict::TreeToNonTree { change_idx, .. }
263                                | PossibleConflict::NonTreeToTree { change_idx, .. } => change_idx,
264                                PossibleConflict::Match { change_idx }
265                                | PossibleConflict::PassedRewrittenDirectory { change_idx } => Some(change_idx),
266                            }
267                            .map(|idx| &mut our_changes[idx]);
268
269                            if let Some(ours) = ours {
270                                gix_trace::debug!("Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}");
271                                let conflict = Conflict::unknown((&ours.inner, theirs, Original, outer_side));
272                                if let Some(ResolveWith::Ours) = tree_conflicts {
273                                    apply_our_resolution(&ours.inner, theirs, outer_side, &mut editor)?;
274                                    *match outer_side {
275                                        Original => &mut ours.was_written,
276                                        Swapped => &mut their_changes[theirs_idx].was_written,
277                                    } = true;
278                                }
279                                if should_fail_on_conflict(conflict) {
280                                    break 'outer;
281                                }
282                            } else if matches!(candidate, PossibleConflict::TreeToNonTree { .. }) {
283                                let (mode, id) = theirs.entry_mode_and_id();
284                                let location = theirs.location();
285                                let renamed_location = unique_path_in_tree(
286                                    location.as_bstr(),
287                                    &editor,
288                                    their_tree,
289                                    labels.other.unwrap_or_default(),
290                                )?;
291                                match tree_conflicts {
292                                    None => {
293                                        editor.upsert(toc(&renamed_location), mode.kind(), id.to_owned())?;
294                                    }
295                                    Some(ResolveWith::Ours) => {
296                                        if outer_side.is_swapped() {
297                                            editor.upsert(to_components(location), mode.kind(), id.to_owned())?;
298                                        }
299                                    }
300                                    Some(ResolveWith::Ancestor) => {
301                                        // we found no matching node of 'ours', so nothing to apply here.
302                                    }
303                                }
304
305                                let conflict = Conflict::without_resolution(
306                                    ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed {
307                                        renamed_unique_path_of_theirs: renamed_location,
308                                    },
309                                    (theirs, theirs, Original, outer_side),
310                                    [
311                                        None,
312                                        None,
313                                        index_entry_at_path(
314                                            &mode.kind().into(),
315                                            &id.to_owned(),
316                                            ConflictIndexEntryPathHint::RenamedOrTheirs,
317                                        ),
318                                    ],
319                                );
320                                their_changes[theirs_idx].was_written = true;
321                                if should_fail_on_conflict(conflict) {
322                                    break 'outer;
323                                }
324                            } else if matches!(candidate, PossibleConflict::NonTreeToTree { .. }) {
325                                // We are writing on top of what was a file, a conflict we probably already saw and dealt with.
326                                let location = theirs.location();
327                                let (mode, id) = theirs.entry_mode_and_id();
328                                editor.upsert(to_components(location), mode.kind(), id.to_owned())?;
329                                their_changes[theirs_idx].was_written = true;
330                            } else {
331                                gix_trace::debug!("Couldn't figure out how to handle {match_kind:?} theirs: {theirs:#?} candidate: {candidate:#?}");
332                            }
333                            continue;
334                        };
335
336                        let ours = &our_changes[ours_idx].inner;
337                        match (ours, theirs) {
338                            (
339                                Change::Modification {
340                                    previous_id,
341                                    previous_entry_mode,
342                                    id: our_id,
343                                    location: our_location,
344                                    entry_mode: our_mode,
345                                    ..
346                                },
347                                Change::Rewrite {
348                                    source_id: their_source_id,
349                                    id: their_id,
350                                    location: their_location,
351                                    entry_mode: their_mode,
352                                    source_location,
353                                    ..
354                                },
355                            )
356                            | (
357                                Change::Rewrite {
358                                    source_id: their_source_id,
359                                    id: their_id,
360                                    location: their_location,
361                                    entry_mode: their_mode,
362                                    source_location,
363                                    ..
364                                },
365                                Change::Modification {
366                                    previous_id,
367                                    previous_entry_mode,
368                                    id: our_id,
369                                    location: our_location,
370                                    entry_mode: our_mode,
371                                    ..
372                                },
373                            ) => {
374                                let side = if matches!(ours, Change::Modification { .. }) {
375                                    Original
376                                } else {
377                                    Swapped
378                                };
379                                if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) {
380                                    debug_assert_eq!(
381                                        previous_id, their_source_id,
382                                        "both refer to the same base, so should always match"
383                                    );
384                                    let their_rewritten_location = possibly_rewritten_location(
385                                        pick_our_tree(side, our_tree, their_tree),
386                                        their_location.as_ref(),
387                                        pick_our_changes(side, our_changes, their_changes),
388                                    );
389                                    let renamed_without_change = their_source_id == their_id;
390                                    let (merged_blob_id, resolution) = if renamed_without_change {
391                                        (*our_id, None)
392                                    } else {
393                                        let (our_location, our_id, our_mode, their_location, their_id, their_mode) =
394                                            match side {
395                                                Original => (
396                                                    our_location,
397                                                    our_id,
398                                                    our_mode,
399                                                    their_location,
400                                                    their_id,
401                                                    their_mode,
402                                                ),
403                                                Swapped => (
404                                                    their_location,
405                                                    their_id,
406                                                    their_mode,
407                                                    our_location,
408                                                    our_id,
409                                                    our_mode,
410                                                ),
411                                            };
412                                        let (merged_blob_id, resolution) = perform_blob_merge(
413                                            labels,
414                                            objects,
415                                            blob_merge,
416                                            &mut diff_state.buf1,
417                                            &mut write_blob_to_odb,
418                                            (our_location, *our_id, *our_mode),
419                                            (their_location, *their_id, *their_mode),
420                                            (source_location, *previous_id, *previous_entry_mode),
421                                            (0, outer_side),
422                                            &options,
423                                        )?;
424                                        (merged_blob_id, Some(resolution))
425                                    };
426
427                                    editor.remove(toc(our_location))?;
428                                    pick_our_tree(side, our_tree, their_tree)
429                                        .remove_existing_leaf(our_location.as_bstr());
430                                    let final_location = their_rewritten_location.clone();
431                                    let new_change = Change::Addition {
432                                        location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()),
433                                        relation: None,
434                                        entry_mode: merged_mode,
435                                        id: merged_blob_id,
436                                    };
437                                    if should_fail_on_conflict(Conflict::with_resolution(
438                                        Resolution::OursModifiedTheirsRenamedAndChangedThenRename {
439                                            merged_mode: (merged_mode != *their_mode).then_some(merged_mode),
440                                            merged_blob: resolution.map(|resolution| ContentMerge {
441                                                resolution,
442                                                merged_blob_id,
443                                            }),
444                                            final_location,
445                                        },
446                                        (ours, theirs, side, outer_side),
447                                        [
448                                            index_entry(previous_entry_mode, previous_id),
449                                            index_entry(our_mode, our_id),
450                                            index_entry(their_mode, their_id),
451                                        ],
452                                    )) {
453                                        break 'outer;
454                                    }
455
456                                    // The other side gets the addition, not our side.
457                                    push_deferred(
458                                        (new_change, None),
459                                        pick_our_changes_mut(side, their_changes, our_changes),
460                                    );
461                                } else {
462                                    match tree_conflicts {
463                                        None => {
464                                            // keep both states - 'our_location' is the previous location as well.
465                                            editor.upsert(toc(our_location), our_mode.kind(), *our_id)?;
466                                            editor.upsert(toc(their_location), their_mode.kind(), *their_id)?;
467                                        }
468                                        Some(ResolveWith::Ours) => {
469                                            editor.remove(toc(source_location))?;
470                                            if side.to_global(outer_side).is_swapped() {
471                                                editor.upsert(toc(their_location), their_mode.kind(), *their_id)?;
472                                            } else {
473                                                editor.upsert(toc(our_location), our_mode.kind(), *our_id)?;
474                                            }
475                                        }
476                                        Some(ResolveWith::Ancestor) => {}
477                                    }
478
479                                    if should_fail_on_conflict(Conflict::without_resolution(
480                                        ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch,
481                                        (ours, theirs, side, outer_side),
482                                        [
483                                            index_entry_at_path(
484                                                previous_entry_mode,
485                                                previous_id,
486                                                ConflictIndexEntryPathHint::RenamedOrTheirs,
487                                            ),
488                                            None,
489                                            index_entry_at_path(
490                                                their_mode,
491                                                their_id,
492                                                ConflictIndexEntryPathHint::RenamedOrTheirs,
493                                            ),
494                                        ],
495                                    )) {
496                                        break 'outer;
497                                    }
498                                }
499                            }
500                            (
501                                Change::Modification {
502                                    location,
503                                    previous_id,
504                                    previous_entry_mode,
505                                    entry_mode: our_mode,
506                                    id: our_id,
507                                    ..
508                                },
509                                Change::Modification {
510                                    entry_mode: their_mode,
511                                    id: their_id,
512                                    ..
513                                },
514                            ) if !involves_submodule(our_mode, their_mode)
515                                && merge_modes(*our_mode, *their_mode).is_some()
516                                && our_id != their_id =>
517                            {
518                                let (merged_blob_id, resolution) = perform_blob_merge(
519                                    labels,
520                                    objects,
521                                    blob_merge,
522                                    &mut diff_state.buf1,
523                                    &mut write_blob_to_odb,
524                                    (location, *our_id, *our_mode),
525                                    (location, *their_id, *their_mode),
526                                    (location, *previous_id, *previous_entry_mode),
527                                    (0, outer_side),
528                                    &options,
529                                )?;
530
531                                let merged_mode = merge_modes_prev(*our_mode, *their_mode, *previous_entry_mode)
532                                    .expect("BUG: merge_modes() reports a valid mode, this one should do too");
533
534                                editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?;
535                                if should_fail_on_conflict(Conflict::with_resolution(
536                                    Resolution::OursModifiedTheirsModifiedThenBlobContentMerge {
537                                        merged_blob: ContentMerge {
538                                            resolution,
539                                            merged_blob_id,
540                                        },
541                                    },
542                                    (ours, theirs, Original, outer_side),
543                                    [
544                                        index_entry(previous_entry_mode, previous_id),
545                                        index_entry(our_mode, our_id),
546                                        index_entry(their_mode, their_id),
547                                    ],
548                                )) {
549                                    break 'outer;
550                                }
551                            }
552                            (
553                                Change::Addition {
554                                    location,
555                                    entry_mode: our_mode,
556                                    id: our_id,
557                                    ..
558                                },
559                                Change::Addition {
560                                    entry_mode: their_mode,
561                                    id: their_id,
562                                    ..
563                                },
564                            ) if !involves_submodule(our_mode, their_mode) && our_id != their_id => {
565                                let conflict = if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) {
566                                    let side = if our_mode == their_mode || matches!(our_mode.kind(), EntryKind::Blob) {
567                                        outer_side
568                                    } else {
569                                        outer_side.swapped()
570                                    };
571                                    let (merged_blob_id, resolution) = perform_blob_merge(
572                                        labels,
573                                        objects,
574                                        blob_merge,
575                                        &mut diff_state.buf1,
576                                        &mut write_blob_to_odb,
577                                        (location, *our_id, merged_mode),
578                                        (location, *their_id, merged_mode),
579                                        (location, their_id.kind().null(), merged_mode),
580                                        (0, side),
581                                        &options,
582                                    )?;
583                                    editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?;
584                                    Conflict::with_resolution(
585                                        Resolution::OursModifiedTheirsModifiedThenBlobContentMerge {
586                                            merged_blob: ContentMerge {
587                                                resolution,
588                                                merged_blob_id,
589                                            },
590                                        },
591                                        (ours, theirs, Original, outer_side),
592                                        [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)],
593                                    )
594                                } else {
595                                    // Actually this has a preference, as symlinks are always left in place with the other side renamed.
596                                    let (
597                                        logical_side,
598                                        label_of_side_to_be_moved,
599                                        (our_mode, our_id, our_path_hint),
600                                        (their_mode, their_id, their_path_hint),
601                                    ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) {
602                                        (
603                                            Original,
604                                            labels.other.unwrap_or_default(),
605                                            (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
606                                            (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
607                                        )
608                                    } else {
609                                        (
610                                            Swapped,
611                                            labels.current.unwrap_or_default(),
612                                            (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
613                                            (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
614                                        )
615                                    };
616                                    let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree);
617                                    let renamed_location = unique_path_in_tree(
618                                        location.as_bstr(),
619                                        &editor,
620                                        tree_with_rename,
621                                        label_of_side_to_be_moved,
622                                    )?;
623                                    let mut conflict = Conflict::without_resolution(
624                                        ResolutionFailure::OursAddedTheirsAddedTypeMismatch {
625                                            their_unique_location: renamed_location.clone(),
626                                        },
627                                        (ours, theirs, logical_side, outer_side),
628                                        [
629                                            None,
630                                            index_entry_at_path(&our_mode, &our_id, our_path_hint),
631                                            index_entry_at_path(&their_mode, &their_id, their_path_hint),
632                                        ],
633                                    );
634                                    match tree_conflicts {
635                                        None => {
636                                            let new_change = Change::Addition {
637                                                location: renamed_location,
638                                                entry_mode: their_mode,
639                                                id: their_id,
640                                                relation: None,
641                                            };
642                                            editor.upsert(toc(location), our_mode.kind(), our_id)?;
643                                            tree_with_rename.remove_existing_leaf(location.as_bstr());
644                                            push_deferred(
645                                                (new_change, None),
646                                                pick_our_changes_mut(logical_side, their_changes, our_changes),
647                                            );
648                                        }
649                                        Some(resolve) => {
650                                            conflict.entries = Default::default();
651                                            match resolve {
652                                                ResolveWith::Ours => match outer_side {
653                                                    Original => {
654                                                        editor.upsert(toc(location), our_mode.kind(), our_id)?;
655                                                    }
656                                                    Swapped => {
657                                                        editor.upsert(toc(location), their_mode.kind(), their_id)?;
658                                                    }
659                                                },
660                                                ResolveWith::Ancestor => {
661                                                    // Do nothing - this discards both sides.
662                                                    // Note that one of these adds might be the result of a rename, which
663                                                    // means we effectively loose the original and can't get it back as that information is degenerated.
664                                                }
665                                            }
666                                        }
667                                    }
668                                    conflict
669                                };
670
671                                if should_fail_on_conflict(conflict) {
672                                    break 'outer;
673                                }
674                            }
675                            (
676                                Change::Modification {
677                                    location,
678                                    entry_mode,
679                                    id,
680                                    previous_entry_mode,
681                                    previous_id,
682                                },
683                                Change::Deletion { .. },
684                            )
685                            | (
686                                Change::Deletion { .. },
687                                Change::Modification {
688                                    location,
689                                    entry_mode,
690                                    id,
691                                    previous_entry_mode,
692                                    previous_id,
693                                },
694                            ) => {
695                                let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) {
696                                    (labels.current.unwrap_or_default(), Original)
697                                } else {
698                                    (labels.other.unwrap_or_default(), Swapped)
699                                };
700                                let deletion_prefaces_addition_of_directory = {
701                                    let change_on_right = match side {
702                                        Original => their_changes.get(theirs_idx + 1),
703                                        Swapped => our_changes.get(ours_idx + 1),
704                                    };
705                                    change_on_right
706                                        .map(|change| {
707                                            change.inner.entry_mode().is_tree()
708                                                && change.inner.location() == location
709                                                && matches!(change.inner, Change::Addition { .. })
710                                        })
711                                        .unwrap_or_default()
712                                };
713
714                                let should_break = if deletion_prefaces_addition_of_directory {
715                                    let entries = [
716                                        index_entry(previous_entry_mode, previous_id),
717                                        index_entry(entry_mode, id),
718                                        None,
719                                    ];
720                                    match tree_conflicts {
721                                        None => {
722                                            let our_tree = pick_our_tree(side, our_tree, their_tree);
723                                            let renamed_path = unique_path_in_tree(
724                                                location.as_bstr(),
725                                                &editor,
726                                                our_tree,
727                                                label_of_side_to_be_moved,
728                                            )?;
729                                            editor.remove(toc(location))?;
730                                            our_tree.remove_existing_leaf(location.as_bstr());
731
732                                            let new_change = Change::Addition {
733                                                location: renamed_path.clone(),
734                                                relation: None,
735                                                entry_mode: *entry_mode,
736                                                id: *id,
737                                            };
738                                            let should_break = should_fail_on_conflict(Conflict::without_resolution(
739                                                ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed {
740                                                    renamed_unique_path_to_modified_blob: renamed_path,
741                                                },
742                                                (ours, theirs, side, outer_side),
743                                                entries,
744                                            ));
745
746                                            // Since we move *our* side, our tree needs to be modified.
747                                            push_deferred(
748                                                (new_change, None),
749                                                pick_our_changes_mut(side, our_changes, their_changes),
750                                            );
751                                            should_break
752                                        }
753                                        Some(ResolveWith::Ours) => {
754                                            match side.to_global(outer_side) {
755                                                Original => {
756                                                    // ours is modification
757                                                    editor.upsert(toc(location), entry_mode.kind(), *id)?;
758                                                }
759                                                Swapped => {
760                                                    // ours is deletion
761                                                    editor.remove(toc(location))?;
762                                                }
763                                            }
764                                            should_fail_on_conflict(Conflict::without_resolution(
765                                                ResolutionFailure::OursModifiedTheirsDeleted,
766                                                (ours, theirs, side, outer_side),
767                                                entries,
768                                            ))
769                                        }
770                                        Some(ResolveWith::Ancestor) => {
771                                            should_fail_on_conflict(Conflict::without_resolution(
772                                                ResolutionFailure::OursModifiedTheirsDeleted,
773                                                (ours, theirs, side, outer_side),
774                                                entries,
775                                            ))
776                                        }
777                                    }
778                                } else {
779                                    let entries = [
780                                        index_entry(previous_entry_mode, previous_id),
781                                        index_entry(entry_mode, id),
782                                        None,
783                                    ];
784                                    match tree_conflicts {
785                                        None => {
786                                            editor.upsert(toc(location), entry_mode.kind(), *id)?;
787                                        }
788                                        Some(ResolveWith::Ours) => {
789                                            let ours = match outer_side {
790                                                Original => ours,
791                                                Swapped => theirs,
792                                            };
793
794                                            match ours {
795                                                Change::Modification { .. } => {
796                                                    editor.upsert(toc(location), entry_mode.kind(), *id)?;
797                                                }
798                                                Change::Deletion { .. } => {
799                                                    editor.remove(toc(location))?;
800                                                }
801                                                _ => unreachable!("parent-match assures this"),
802                                            }
803                                        }
804                                        Some(ResolveWith::Ancestor) => {}
805                                    }
806                                    should_fail_on_conflict(Conflict::without_resolution(
807                                        ResolutionFailure::OursModifiedTheirsDeleted,
808                                        (ours, theirs, side, outer_side),
809                                        entries,
810                                    ))
811                                };
812                                if should_break {
813                                    break 'outer;
814                                }
815                            }
816                            (
817                                Change::Modification { .. },
818                                Change::Addition {
819                                    location,
820                                    entry_mode,
821                                    id,
822                                    ..
823                                },
824                            ) if ours.location() != theirs.location() => {
825                                match tree_conflicts {
826                                    None => {
827                                        unreachable!("modification/deletion pair should prevent modification/addition from happening")
828                                    }
829                                    Some(ResolveWith::Ancestor) => {}
830                                    Some(ResolveWith::Ours) => {
831                                        if outer_side.is_swapped() {
832                                            editor.upsert(toc(location), entry_mode.kind(), *id)?;
833                                        }
834                                        // we have already taken care of the 'root' of this -
835                                        // everything that follows can safely be ignored
836                                    }
837                                }
838                            }
839                            (
840                                Change::Rewrite {
841                                    source_location,
842                                    source_entry_mode,
843                                    source_id,
844                                    entry_mode: our_mode,
845                                    id: our_id,
846                                    location: our_location,
847                                    ..
848                                },
849                                Change::Rewrite {
850                                    entry_mode: their_mode,
851                                    id: their_id,
852                                    location: their_location,
853                                    ..
854                                },
855                                // NOTE: renames are only tracked among these kinds of types anyway, but we make sure.
856                            ) if our_mode.is_blob_or_symlink() && their_mode.is_blob_or_symlink() => {
857                                let (merged_blob_id, mut resolution) = if our_id == their_id {
858                                    (*our_id, None)
859                                } else {
860                                    let (id, resolution) = perform_blob_merge(
861                                        labels,
862                                        objects,
863                                        blob_merge,
864                                        &mut diff_state.buf1,
865                                        &mut write_blob_to_odb,
866                                        (our_location, *our_id, *our_mode),
867                                        (their_location, *their_id, *their_mode),
868                                        (source_location, *source_id, *source_entry_mode),
869                                        (1, outer_side),
870                                        &options,
871                                    )?;
872                                    (id, Some(resolution))
873                                };
874
875                                let merged_mode =
876                                    merge_modes(*our_mode, *their_mode).expect("this case was assured earlier");
877
878                                if matches!(tree_conflicts, None | Some(ResolveWith::Ours)) {
879                                    editor.remove(toc(source_location))?;
880                                    our_tree.remove_existing_leaf(source_location.as_bstr());
881                                    their_tree.remove_existing_leaf(source_location.as_bstr());
882                                }
883
884                                let their_location =
885                                    possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes)
886                                        .map_or(Cow::Borrowed(their_location.as_bstr()), Cow::Owned);
887                                let our_location =
888                                    possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes)
889                                        .map_or(Cow::Borrowed(our_location.as_bstr()), Cow::Owned);
890                                let (our_addition, their_addition) = if our_location == their_location {
891                                    (
892                                        None,
893                                        Some(Change::Addition {
894                                            location: our_location.into_owned(),
895                                            relation: None,
896                                            entry_mode: merged_mode,
897                                            id: merged_blob_id,
898                                        }),
899                                    )
900                                } else {
901                                    if should_fail_on_conflict(Conflict::without_resolution(
902                                        ResolutionFailure::OursRenamedTheirsRenamedDifferently {
903                                            merged_blob: resolution.take().map(|resolution| ContentMerge {
904                                                resolution,
905                                                merged_blob_id,
906                                            }),
907                                        },
908                                        (ours, theirs, Original, outer_side),
909                                        [
910                                            index_entry_at_path(
911                                                source_entry_mode,
912                                                source_id,
913                                                ConflictIndexEntryPathHint::Source,
914                                            ),
915                                            index_entry_at_path(
916                                                our_mode,
917                                                &merged_blob_id,
918                                                ConflictIndexEntryPathHint::Current,
919                                            ),
920                                            index_entry_at_path(
921                                                their_mode,
922                                                &merged_blob_id,
923                                                ConflictIndexEntryPathHint::RenamedOrTheirs,
924                                            ),
925                                        ],
926                                    )) {
927                                        break 'outer;
928                                    }
929                                    match tree_conflicts {
930                                        None => {
931                                            let our_addition = Change::Addition {
932                                                location: our_location.into_owned(),
933                                                relation: None,
934                                                entry_mode: merged_mode,
935                                                id: merged_blob_id,
936                                            };
937                                            let their_addition = Change::Addition {
938                                                location: their_location.into_owned(),
939                                                relation: None,
940                                                entry_mode: merged_mode,
941                                                id: merged_blob_id,
942                                            };
943                                            (Some(our_addition), Some(their_addition))
944                                        }
945                                        Some(ResolveWith::Ancestor) => (None, None),
946                                        Some(ResolveWith::Ours) => {
947                                            let our_addition = Change::Addition {
948                                                location: match outer_side {
949                                                    Original => our_location,
950                                                    Swapped => their_location,
951                                                }
952                                                .into_owned(),
953                                                relation: None,
954                                                entry_mode: merged_mode,
955                                                id: merged_blob_id,
956                                            };
957                                            (Some(our_addition), None)
958                                        }
959                                    }
960                                };
961
962                                if let Some(resolution) = resolution {
963                                    if should_fail_on_conflict(Conflict::with_resolution(
964                                        Resolution::OursModifiedTheirsModifiedThenBlobContentMerge {
965                                            merged_blob: ContentMerge {
966                                                resolution,
967                                                merged_blob_id,
968                                            },
969                                        },
970                                        (ours, theirs, Original, outer_side),
971                                        [
972                                            index_entry_at_path(
973                                                source_entry_mode,
974                                                source_id,
975                                                ConflictIndexEntryPathHint::Source,
976                                            ),
977                                            index_entry_at_path(
978                                                our_mode,
979                                                &merged_blob_id,
980                                                ConflictIndexEntryPathHint::Current,
981                                            ),
982                                            index_entry_at_path(
983                                                their_mode,
984                                                &merged_blob_id,
985                                                ConflictIndexEntryPathHint::RenamedOrTheirs,
986                                            ),
987                                        ],
988                                    )) {
989                                        break 'outer;
990                                    }
991                                }
992                                if let Some(addition) = our_addition {
993                                    push_deferred((addition, Some(ours_idx)), our_changes);
994                                }
995                                if let Some(addition) = their_addition {
996                                    push_deferred((addition, Some(theirs_idx)), their_changes);
997                                }
998                            }
999                            (
1000                                Change::Deletion { .. },
1001                                Change::Rewrite {
1002                                    source_location,
1003                                    entry_mode: rewritten_mode,
1004                                    id: rewritten_id,
1005                                    location,
1006                                    ..
1007                                },
1008                            )
1009                            | (
1010                                Change::Rewrite {
1011                                    source_location,
1012                                    entry_mode: rewritten_mode,
1013                                    id: rewritten_id,
1014                                    location,
1015                                    ..
1016                                },
1017                                Change::Deletion { .. },
1018                            ) if !rewritten_mode.is_commit() => {
1019                                let side = if matches!(ours, Change::Deletion { .. }) {
1020                                    Original
1021                                } else {
1022                                    Swapped
1023                                };
1024
1025                                match tree_conflicts {
1026                                    None | Some(ResolveWith::Ours) => {
1027                                        editor.remove(toc(source_location))?;
1028                                        pick_our_tree(side, our_tree, their_tree)
1029                                            .remove_existing_leaf(source_location.as_bstr());
1030                                    }
1031                                    Some(ResolveWith::Ancestor) => {}
1032                                }
1033
1034                                let their_rewritten_location = possibly_rewritten_location(
1035                                    pick_our_tree(side, our_tree, their_tree),
1036                                    location.as_ref(),
1037                                    pick_our_changes(side, our_changes, their_changes),
1038                                )
1039                                .unwrap_or_else(|| location.to_owned());
1040                                let our_addition = Change::Addition {
1041                                    location: their_rewritten_location,
1042                                    relation: None,
1043                                    entry_mode: *rewritten_mode,
1044                                    id: *rewritten_id,
1045                                };
1046
1047                                if should_fail_on_conflict(Conflict::without_resolution(
1048                                    ResolutionFailure::OursDeletedTheirsRenamed,
1049                                    (ours, theirs, side, outer_side),
1050                                    [
1051                                        None,
1052                                        None,
1053                                        index_entry_at_path(
1054                                            rewritten_mode,
1055                                            rewritten_id,
1056                                            ConflictIndexEntryPathHint::RenamedOrTheirs,
1057                                        ),
1058                                    ],
1059                                )) {
1060                                    break 'outer;
1061                                }
1062
1063                                let ours_is_rewrite = side.is_swapped();
1064                                if tree_conflicts.is_none()
1065                                    || (matches!(tree_conflicts, Some(ResolveWith::Ours)) && ours_is_rewrite)
1066                                {
1067                                    push_deferred(
1068                                        (our_addition, None),
1069                                        pick_our_changes_mut(side, their_changes, our_changes),
1070                                    );
1071                                }
1072                            }
1073                            (
1074                                Change::Rewrite {
1075                                    source_location,
1076                                    source_entry_mode,
1077                                    source_id,
1078                                    entry_mode: our_mode,
1079                                    id: our_id,
1080                                    location,
1081                                    ..
1082                                },
1083                                Change::Addition {
1084                                    id: their_id,
1085                                    entry_mode: their_mode,
1086                                    location: add_location,
1087                                    ..
1088                                },
1089                            )
1090                            | (
1091                                Change::Addition {
1092                                    id: their_id,
1093                                    entry_mode: their_mode,
1094                                    location: add_location,
1095                                    ..
1096                                },
1097                                Change::Rewrite {
1098                                    source_location,
1099                                    source_entry_mode,
1100                                    source_id,
1101                                    entry_mode: our_mode,
1102                                    id: our_id,
1103                                    location,
1104                                    ..
1105                                },
1106                            ) if !involves_submodule(our_mode, their_mode) => {
1107                                let side = if matches!(ours, Change::Rewrite { .. }) {
1108                                    Original
1109                                } else {
1110                                    Swapped
1111                                };
1112                                if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) {
1113                                    let (merged_blob_id, resolution) = if our_id == their_id {
1114                                        (*our_id, None)
1115                                    } else {
1116                                        let (id, resolution) = perform_blob_merge(
1117                                            labels,
1118                                            objects,
1119                                            blob_merge,
1120                                            &mut diff_state.buf1,
1121                                            &mut write_blob_to_odb,
1122                                            (location, *our_id, *our_mode),
1123                                            (location, *their_id, *their_mode),
1124                                            (source_location, source_id.kind().null(), *source_entry_mode),
1125                                            (0, outer_side),
1126                                            &options,
1127                                        )?;
1128                                        (id, Some(resolution))
1129                                    };
1130
1131                                    editor.remove(toc(source_location))?;
1132                                    pick_our_tree(side, our_tree, their_tree).remove_leaf(source_location.as_bstr());
1133
1134                                    if let Some(resolution) = resolution {
1135                                        if should_fail_on_conflict(Conflict::with_resolution(
1136                                            Resolution::OursModifiedTheirsModifiedThenBlobContentMerge {
1137                                                merged_blob: ContentMerge {
1138                                                    resolution,
1139                                                    merged_blob_id,
1140                                                },
1141                                            },
1142                                            (ours, theirs, Original, outer_side),
1143                                            [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)],
1144                                        )) {
1145                                            break 'outer;
1146                                        }
1147                                    }
1148
1149                                    // Because this constellation can only be found by the lookup tree, there is
1150                                    // no need to put it as addition, we know it's not going to intersect on the other side.
1151                                    editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?;
1152                                } else {
1153                                    // We always remove the source from the tree - it might be re-added later.
1154                                    let ours_is_rename =
1155                                        tree_conflicts == Some(ResolveWith::Ours) && side == outer_side;
1156                                    let remove_rename_source =
1157                                        tree_conflicts.is_none() || ours_is_rename || add_location != source_location;
1158                                    if remove_rename_source {
1159                                        editor.remove(toc(source_location))?;
1160                                        pick_our_tree(side, our_tree, their_tree)
1161                                            .remove_leaf(source_location.as_bstr());
1162                                    }
1163
1164                                    let (
1165                                        logical_side,
1166                                        label_of_side_to_be_moved,
1167                                        (our_mode, our_id, our_path_hint),
1168                                        (their_mode, their_id, their_path_hint),
1169                                    ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) {
1170                                        (
1171                                            Original,
1172                                            labels.other.unwrap_or_default(),
1173                                            (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
1174                                            (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
1175                                        )
1176                                    } else {
1177                                        (
1178                                            Swapped,
1179                                            labels.current.unwrap_or_default(),
1180                                            (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs),
1181                                            (*our_mode, *our_id, ConflictIndexEntryPathHint::Current),
1182                                        )
1183                                    };
1184                                    let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree);
1185                                    let renamed_location = unique_path_in_tree(
1186                                        location.as_bstr(),
1187                                        &editor,
1188                                        tree_with_rename,
1189                                        label_of_side_to_be_moved,
1190                                    )?;
1191
1192                                    let upsert_rename_destination = tree_conflicts.is_none() || ours_is_rename;
1193                                    if upsert_rename_destination {
1194                                        editor.upsert(toc(location), our_mode.kind(), our_id)?;
1195                                        tree_with_rename.remove_existing_leaf(location.as_bstr());
1196                                    }
1197
1198                                    let conflict = Conflict::without_resolution(
1199                                        ResolutionFailure::OursAddedTheirsAddedTypeMismatch {
1200                                            their_unique_location: renamed_location.clone(),
1201                                        },
1202                                        (ours, theirs, side, outer_side),
1203                                        [
1204                                            None,
1205                                            index_entry_at_path(&our_mode, &our_id, our_path_hint),
1206                                            index_entry_at_path(&their_mode, &their_id, their_path_hint),
1207                                        ],
1208                                    );
1209
1210                                    if tree_conflicts.is_none() {
1211                                        let new_change_with_rename = Change::Addition {
1212                                            location: renamed_location,
1213                                            entry_mode: their_mode,
1214                                            id: their_id,
1215                                            relation: None,
1216                                        };
1217                                        push_deferred(
1218                                            (
1219                                                new_change_with_rename,
1220                                                Some(pick_idx(logical_side, theirs_idx, ours_idx)),
1221                                            ),
1222                                            pick_our_changes_mut(logical_side, their_changes, our_changes),
1223                                        );
1224                                    }
1225
1226                                    if should_fail_on_conflict(conflict) {
1227                                        break 'outer;
1228                                    }
1229                                }
1230                            }
1231                            _unknown => {
1232                                debug_assert!(
1233                                    match_kind.is_none()
1234                                        || (ours.location() == theirs.location()
1235                                        || ours.source_location() == theirs.source_location()),
1236                                    "BUG: right now it's not known to be possible to match changes from different paths: {match_kind:?} {candidate:?}"
1237                                );
1238                                if let Some(ResolveWith::Ours) = tree_conflicts {
1239                                    apply_our_resolution(ours, theirs, outer_side, &mut editor)?;
1240                                }
1241                                if should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) {
1242                                    break 'outer;
1243                                }
1244                            }
1245                        }
1246                        their_changes[theirs_idx].was_written = true;
1247                        our_changes[ours_idx].was_written = true;
1248                    }
1249                }
1250            }
1251            segment_start = last_seen_len;
1252            last_seen_len = their_changes.len();
1253        }
1254
1255        ((our_changes, our_tree), (their_changes, their_tree)) = ((their_changes, their_tree), (our_changes, our_tree));
1256        (labels.current, labels.other) = (labels.other, labels.current);
1257        outer_side = outer_side.swapped();
1258    }
1259
1260    Ok(Outcome {
1261        tree: editor,
1262        conflicts,
1263        failed_on_first_unresolved_conflict: failed_on_first_conflict,
1264    })
1265}
1266
1267fn apply_our_resolution(
1268    local_ours: &Change,
1269    local_theirs: &Change,
1270    outer_side: ConflictMapping,
1271    editor: &mut gix_object::tree::Editor<'_>,
1272) -> Result<(), Error> {
1273    let ours = match outer_side {
1274        Original => local_ours,
1275        Swapped => local_theirs,
1276    };
1277    Ok(apply_change(editor, ours, None)?)
1278}
1279
1280fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool {
1281    a.is_commit() || b.is_commit()
1282}
1283
1284/// Allows equal modes or prefers executables bits in case of blobs
1285///
1286/// Note that this is often not correct as the previous mode of each side should be taken into account so that:
1287///
1288/// on | on = on
1289/// off | off = off
1290/// on | off || off | on = conflict
1291fn merge_modes(a: EntryMode, b: EntryMode) -> Option<EntryMode> {
1292    match (a.kind(), b.kind()) {
1293        (_, _) if a == b => Some(a),
1294        (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob)
1295        | (EntryKind::Blob, EntryKind::BlobExecutable) => Some(EntryKind::BlobExecutable.into()),
1296        _ => None,
1297    }
1298}
1299
1300/// Use this version if there is a single common `prev` value for both `a` and `b` to detect
1301/// if the mode was turned on or off.
1302fn merge_modes_prev(a: EntryMode, b: EntryMode, prev: EntryMode) -> Option<EntryMode> {
1303    match (a.kind(), b.kind()) {
1304        (_, _) if a == b => Some(a),
1305        (a @ EntryKind::BlobExecutable, b @ (EntryKind::BlobExecutable | EntryKind::Blob))
1306        | (a @ EntryKind::Blob, b @ EntryKind::BlobExecutable) => {
1307            let prev = prev.kind();
1308            let changed = if a == prev { b } else { a };
1309            Some(
1310                match (prev, changed) {
1311                    (EntryKind::Blob, EntryKind::BlobExecutable) => EntryKind::BlobExecutable,
1312                    (EntryKind::BlobExecutable, EntryKind::Blob) => EntryKind::Blob,
1313                    _ => unreachable!("upper match already assured we only deal with blobs"),
1314                }
1315                .into(),
1316            )
1317        }
1318        _ => None,
1319    }
1320}
1321
1322fn push_deferred(change_and_idx: (Change, Option<usize>), changes: &mut ChangeList) {
1323    push_deferred_with_rewrite(change_and_idx, None, changes);
1324}
1325
1326fn push_deferred_with_rewrite(
1327    (change, ours_idx): (Change, Option<usize>),
1328    new_location: Option<(BString, usize)>,
1329    changes: &mut ChangeList,
1330) {
1331    changes.push(TrackedChange {
1332        inner: change,
1333        was_written: false,
1334        needs_tree_insertion: Some(ours_idx),
1335        rewritten_location: new_location,
1336    });
1337}
1338
1339fn pick_our_tree<'a>(side: ConflictMapping, ours: &'a mut TreeNodes, theirs: &'a mut TreeNodes) -> &'a mut TreeNodes {
1340    match side {
1341        Original => ours,
1342        Swapped => theirs,
1343    }
1344}
1345
1346fn pick_our_changes<'a>(
1347    side: ConflictMapping,
1348    ours: &'a ChangeListRef,
1349    theirs: &'a ChangeListRef,
1350) -> &'a ChangeListRef {
1351    match side {
1352        Original => ours,
1353        Swapped => theirs,
1354    }
1355}
1356
1357fn pick_idx(side: ConflictMapping, ours: usize, theirs: usize) -> usize {
1358    match side {
1359        Original => ours,
1360        Swapped => theirs,
1361    }
1362}
1363
1364fn pick_our_changes_mut<'a>(
1365    side: ConflictMapping,
1366    ours: &'a mut ChangeList,
1367    theirs: &'a mut ChangeList,
1368) -> &'a mut ChangeList {
1369    match side {
1370        Original => ours,
1371        Swapped => theirs,
1372    }
1373}
1374
1375fn index_entry(mode: &gix_object::tree::EntryMode, id: &gix_hash::ObjectId) -> Option<ConflictIndexEntry> {
1376    Some(ConflictIndexEntry {
1377        mode: *mode,
1378        id: *id,
1379        path_hint: None,
1380    })
1381}
1382
1383fn index_entry_at_path(
1384    mode: &gix_object::tree::EntryMode,
1385    id: &gix_hash::ObjectId,
1386    hint: ConflictIndexEntryPathHint,
1387) -> Option<ConflictIndexEntry> {
1388    Some(ConflictIndexEntry {
1389        mode: *mode,
1390        id: *id,
1391        path_hint: Some(hint),
1392    })
1393}