@@ -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, using `how` to determine if a
86+ /// conflict should be considered unresolved.
87+ /// It's important that `index` is at the state of [`Self::tree`].
88+ ///
89+ /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`.
90+ /// Also, the unconflicted stage of such entries will be removed merely by setting a flag, so the
91+ /// in-memory entry is still present.
92+ pub fn index_changed_after_applying_conflicts ( & self , index : & mut gix_index:: State , how : TreatAsUnresolved ) -> bool {
93+ apply_index_entries ( & self . conflicts , how, 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,45 @@ 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+ ///
115+ /// * `entries[0]` => Base
116+ /// * `entries[1]` => Ours
117+ /// * `entries[2]` => Theirs
118+ ///
119+ /// Note that ours and theirs might be swapped, so one should access it through [`Self::entries()`] to compensate for that.
120+ pub entries : [ Option < ConflictIndexEntry > ; 3 ] ,
102121 /// Determine how to interpret the `ours` and `theirs` fields. This is used to implement [`Self::changes_in_resolution()`]
103122 /// and [`Self::into_parts_by_resolution()`].
104123 map : ConflictMapping ,
105124}
106125
126+ /// A conflicting entry for insertion into the index.
127+ /// It will always be either on stage 1 (ancestor/base), 2 (ours) or 3 (theirs)
128+ #[ derive( Debug , Clone , Copy ) ]
129+ pub struct ConflictIndexEntry {
130+ /// The kind of object at this stage.
131+ /// Note that it's possible that this is a directory, for instance if a directory was replaced with a file.
132+ pub mode : gix_object:: tree:: EntryMode ,
133+ /// The id defining the state of the object.
134+ pub id : gix_hash:: ObjectId ,
135+ /// Hidden, maybe one day we can do without?
136+ path_hint : Option < ConflictIndexEntryPathHint > ,
137+ }
138+
139+ /// A hint for [`apply_index_entries()`] to know which paths to use for an entry.
140+ /// This is only used when necessary.
141+ #[ derive( Debug , Clone , Copy ) ]
142+ enum ConflictIndexEntryPathHint {
143+ /// Use the previous path, i.e. rename source.
144+ Source ,
145+ /// Use the current path as it is in the tree.
146+ Current ,
147+ /// Use the path of the final destination, or *their* name.
148+ /// It's definitely finicky, as we don't store the actual path and instead refer to it.
149+ RenamedOrTheirs ,
150+ }
151+
107152/// A utility to help define which side is what in the [`Conflict`] type.
108153#[ derive( Debug , Clone , Copy ) ]
109154enum ConflictMapping {
@@ -147,7 +192,11 @@ impl Conflict {
147192 TreatAsUnresolved :: Renames | TreatAsUnresolved :: RenamesAndAutoResolvedContent => match & self . resolution {
148193 Ok ( success) => match success {
149194 Resolution :: SourceLocationAffectedByRename { .. } => false ,
150- Resolution :: OursModifiedTheirsRenamedAndChangedThenRename { .. } => true ,
195+ Resolution :: OursModifiedTheirsRenamedAndChangedThenRename {
196+ merged_blob,
197+ final_location,
198+ ..
199+ } => final_location. is_some ( ) || merged_blob. as_ref ( ) . map_or ( false , content_merge_matches) ,
151200 Resolution :: OursModifiedTheirsModifiedThenBlobContentMerge { merged_blob } => {
152201 content_merge_matches ( merged_blob)
153202 }
@@ -178,6 +227,14 @@ impl Conflict {
178227 }
179228 }
180229
230+ /// Return the index entries for insertion into the index, to match with what's returned by [`Self::changes_in_resolution()`].
231+ pub fn entries ( & self ) -> [ Option < ConflictIndexEntry > ; 3 ] {
232+ match self . map {
233+ ConflictMapping :: Original => self . entries ,
234+ ConflictMapping :: Swapped => [ self . entries [ 0 ] , self . entries [ 2 ] , self . entries [ 1 ] ] ,
235+ }
236+ }
237+
181238 /// Return information about the content merge if it was performed.
182239 pub fn content_merge ( & self ) -> Option < ContentMerge > {
183240 match & self . resolution {
@@ -308,3 +365,136 @@ pub struct Options {
308365
309366pub ( super ) mod function;
310367mod utils;
368+ pub mod apply_index_entries {
369+
370+ pub ( super ) mod function {
371+ use crate :: tree:: { Conflict , ConflictIndexEntryPathHint , Resolution , ResolutionFailure , TreatAsUnresolved } ;
372+ use bstr:: { BStr , ByteSlice } ;
373+ use std:: collections:: { hash_map, HashMap } ;
374+
375+ /// Returns `true` if `index` changed as we applied conflicting stages to it, using `how` to determine if a
376+ /// conflict should be considered unresolved.
377+ /// Once a stage of a path conflicts, the unconflicting stage is removed even though it might be the one
378+ /// that is currently checked out.
379+ /// This removal, however, is only done by flagging it with [gix_index::entry::Flags::REMOVE], which means
380+ /// these entries won't be written back to disk but will still be present in the index.
381+ /// It's important that `index` matches the tree that was produced as part of the merge that also
382+ /// brought about `conflicts`, or else this function will fail if it cannot find the path matching
383+ /// the conflicting entries.
384+ ///
385+ /// Note that in practice, whenever there is a single [conflict](Conflict), this function will return `true`.
386+ /// Errors can only occour if `index` isn't the one created from the merged tree that produced the `conflicts`.
387+ pub fn apply_index_entries (
388+ conflicts : & [ Conflict ] ,
389+ how : TreatAsUnresolved ,
390+ index : & mut gix_index:: State ,
391+ ) -> bool {
392+ if index. is_sparse ( ) {
393+ gix_trace:: error!( "Refusing to apply index entries to sparse index - it's not tested yet" ) ;
394+ return false ;
395+ }
396+ let len = index. entries ( ) . len ( ) ;
397+ let mut idx_by_path_stage = HashMap :: < ( gix_index:: entry:: Stage , & BStr ) , usize > :: default ( ) ;
398+ for conflict in conflicts. iter ( ) . filter ( |c| c. is_unresolved ( how) ) {
399+ let ( renamed_path, current_path) : ( Option < & BStr > , & BStr ) = match & conflict. resolution {
400+ Ok ( success) => match success {
401+ Resolution :: SourceLocationAffectedByRename { final_location } => {
402+ ( Some ( final_location. as_bstr ( ) ) , final_location. as_bstr ( ) )
403+ }
404+ Resolution :: OursModifiedTheirsRenamedAndChangedThenRename { final_location, .. } => (
405+ final_location. as_ref ( ) . map ( |p| p. as_bstr ( ) ) ,
406+ conflict. changes_in_resolution ( ) . 1 . location ( ) ,
407+ ) ,
408+ Resolution :: OursModifiedTheirsModifiedThenBlobContentMerge { .. } => {
409+ ( None , conflict. ours . location ( ) )
410+ }
411+ } ,
412+ Err ( failure) => match failure {
413+ ResolutionFailure :: OursRenamedTheirsRenamedDifferently { .. } => {
414+ ( Some ( conflict. theirs . location ( ) ) , conflict. ours . location ( ) )
415+ }
416+ ResolutionFailure :: OursModifiedTheirsRenamedTypeMismatch
417+ | ResolutionFailure :: OursDeletedTheirsRenamed
418+ | ResolutionFailure :: OursModifiedTheirsDeleted
419+ | ResolutionFailure :: Unknown => ( None , conflict. ours . location ( ) ) ,
420+ ResolutionFailure :: OursModifiedTheirsDirectoryThenOursRenamed {
421+ renamed_unique_path_to_modified_blob,
422+ } => (
423+ Some ( renamed_unique_path_to_modified_blob. as_bstr ( ) ) ,
424+ conflict. ours . location ( ) ,
425+ ) ,
426+ ResolutionFailure :: OursAddedTheirsAddedTypeMismatch { their_unique_location } => {
427+ ( Some ( their_unique_location. as_bstr ( ) ) , conflict. ours . location ( ) )
428+ }
429+ } ,
430+ } ;
431+ let source_path = conflict. ours . source_location ( ) ;
432+
433+ let entries_with_stage = conflict. entries ( ) . into_iter ( ) . enumerate ( ) . filter_map ( |( idx, entry) | {
434+ entry. filter ( |e| e. mode . is_no_tree ( ) ) . map ( |e| {
435+ (
436+ match idx {
437+ 0 => gix_index:: entry:: Stage :: Base ,
438+ 1 => gix_index:: entry:: Stage :: Ours ,
439+ 2 => gix_index:: entry:: Stage :: Theirs ,
440+ _ => unreachable ! ( "fixed size array with three items" ) ,
441+ } ,
442+ match e. path_hint {
443+ None => renamed_path. unwrap_or ( current_path) ,
444+ Some ( ConflictIndexEntryPathHint :: Source ) => source_path,
445+ Some ( ConflictIndexEntryPathHint :: Current ) => current_path,
446+ Some ( ConflictIndexEntryPathHint :: RenamedOrTheirs ) => {
447+ renamed_path. unwrap_or_else ( || conflict. changes_in_resolution ( ) . 1 . location ( ) )
448+ }
449+ } ,
450+ e,
451+ )
452+ } )
453+ } ) ;
454+
455+ if !entries_with_stage. clone ( ) . any ( |( _, path, _) | {
456+ index
457+ . entry_index_by_path_and_stage_bounded ( path, gix_index:: entry:: Stage :: Unconflicted , len)
458+ . is_some ( )
459+ } ) {
460+ continue ;
461+ }
462+
463+ for ( stage, path, entry) in entries_with_stage {
464+ if let Some ( pos) =
465+ index. entry_index_by_path_and_stage_bounded ( path, gix_index:: entry:: Stage :: Unconflicted , len)
466+ {
467+ index. entries_mut ( ) [ pos] . flags . insert ( gix_index:: entry:: Flags :: REMOVE ) ;
468+ } ;
469+ match idx_by_path_stage. entry ( ( stage, path) ) {
470+ hash_map:: Entry :: Occupied ( map_entry) => {
471+ // This can happen due to the way the algorithm works.
472+ // The same happens in Git, but it stores the index-related data as part of its deduplicating tree.
473+ // We store each conflict we encounter, which also may duplicate their index entries, sometimes, but
474+ // with different values. The most recent value wins.
475+ // Instead of trying to deduplicate the index entries when the merge runs, we put the cost
476+ // to the tree-assembly - there is no way around it.
477+ let index_entry = & mut index. entries_mut ( ) [ * map_entry. get ( ) ] ;
478+ index_entry. mode = entry. mode . into ( ) ;
479+ index_entry. id = entry. id ;
480+ }
481+ hash_map:: Entry :: Vacant ( map_entry) => {
482+ map_entry. insert ( index. entries ( ) . len ( ) ) ;
483+ index. dangerously_push_entry (
484+ Default :: default ( ) ,
485+ entry. id ,
486+ stage. into ( ) ,
487+ entry. mode . into ( ) ,
488+ path,
489+ ) ;
490+ }
491+ } ;
492+ }
493+ }
494+
495+ index. sort_entries ( ) ;
496+ index. entries ( ) . len ( ) != len
497+ }
498+ }
499+ }
500+ pub use apply_index_entries:: function:: apply_index_entries;
0 commit comments