@@ -6,6 +6,7 @@ use anyhow::{Context as _, bail};
66use bstr:: { BStr , BString , ByteSlice } ;
77use but_error:: Code ;
88use gix:: objs:: WriteTo ;
9+ use gix:: objs:: tree:: EntryKind ;
910use gix:: prelude:: ObjectIdExt ;
1011use serde:: { Deserialize , Serialize } ;
1112
@@ -698,8 +699,149 @@ impl ConflictEntries {
698699 }
699700}
700701
702+ /// Derive GitButler conflict entry metadata from a `gix` tree-merge outcome.
703+ ///
704+ /// This is the shared translation used when a merge is auto-resolved but still
705+ /// has unresolved conflicts that must be persisted in a conflicted commit/tree.
706+ /// It first asks `gix` to materialize conflict stages into an index view of the
707+ /// merged tree. If that yields no staged conflict paths, it falls back to the
708+ /// unresolved conflict list from the merge outcome itself, which is important
709+ /// for force-resolved merges where Git would otherwise omit index stages.
710+ ///
711+ /// - `repo` - is the repository that owns `merged_tree_id` and provides the index
712+ /// machinery used to derive stage entries from the merge result.
713+ ///
714+ /// - `merged_tree_id` - is the tree written from the merge result's auto-resolved
715+ /// output, which is reloaded so conflict stages can be inspected.
716+ ///
717+ /// - `merge_result` - is the original `gix` merge outcome whose unresolved
718+ /// conflicts are translated into GitButler conflict-entry metadata.
719+ ///
720+ /// - `treat_as_unresolved` - decides which conflicts should still be treated as
721+ /// unresolved when scanning both staged entries and fallback conflict records.
722+ pub fn conflict_entries_from_merge_outcome (
723+ repo : & gix:: Repository ,
724+ merged_tree_id : gix:: ObjectId ,
725+ merge_result : & gix:: merge:: tree:: Outcome < ' _ > ,
726+ treat_as_unresolved : gix:: merge:: tree:: TreatAsUnresolved ,
727+ ) -> anyhow:: Result < ConflictEntries > {
728+ use gix:: index:: entry:: Stage ;
729+
730+ let mut index = repo. index_from_tree ( & merged_tree_id. attach ( repo) ) ?;
731+ merge_result. index_changed_after_applying_conflicts (
732+ & mut index,
733+ treat_as_unresolved,
734+ gix:: merge:: tree:: apply_index_entries:: RemovalMode :: Mark ,
735+ ) ;
736+
737+ let ( mut ancestor_entries, mut our_entries, mut their_entries) =
738+ ( Vec :: new ( ) , Vec :: new ( ) , Vec :: new ( ) ) ;
739+ for entry in index. entries ( ) {
740+ let storage = match entry. stage ( ) {
741+ Stage :: Unconflicted => continue ,
742+ Stage :: Base => & mut ancestor_entries,
743+ Stage :: Ours => & mut our_entries,
744+ Stage :: Theirs => & mut their_entries,
745+ } ;
746+ storage. push ( gix:: path:: from_bstr ( entry. path ( & index) ) . into_owned ( ) ) ;
747+ }
748+
749+ let mut out = ConflictEntries {
750+ ancestor_entries,
751+ our_entries,
752+ their_entries,
753+ } ;
754+
755+ if !out. has_entries ( ) {
756+ fn push_unique ( v : & mut Vec < PathBuf > , change : & gix:: diff:: tree_with_rewrites:: Change ) {
757+ let path = gix:: path:: from_bstr ( change. location ( ) ) . into_owned ( ) ;
758+ if !v. contains ( & path) {
759+ v. push ( path) ;
760+ }
761+ }
762+
763+ for conflict in merge_result
764+ . conflicts
765+ . iter ( )
766+ . filter ( |c| c. is_unresolved ( treat_as_unresolved) )
767+ {
768+ let ( ours, theirs) = conflict. changes_in_resolution ( ) ;
769+ push_unique ( & mut out. our_entries , ours) ;
770+ push_unique ( & mut out. their_entries , theirs) ;
771+ }
772+ }
773+
774+ assert_eq ! (
775+ out. has_entries( ) ,
776+ merge_result. has_unresolved_conflicts( treat_as_unresolved) ,
777+ "Must have entries to indicate conflicting files, or bad things will happen later: {:#?}" ,
778+ merge_result. conflicts
779+ ) ;
780+
781+ Ok ( out)
782+ }
783+
701784mod conflict;
702785pub use conflict:: {
703786 add_conflict_markers, is_conflicted, message_is_conflicted,
704787 rewrite_conflict_markers_on_message_change, strip_conflict_markers,
705788} ;
789+
790+ /// Write a GitButler conflicted tree that wraps `resolved_tree_id` together
791+ /// with the conflict side trees and conflict file list.
792+ ///
793+ /// - `repo` - is the repository that owns all tree objects involved and receives
794+ /// the newly written conflicted tree.
795+ ///
796+ /// - `resolved_tree_id` - is the visible auto-resolved tree that callers want to
797+ /// present as the conflicted commit's main tree.
798+ ///
799+ /// - `base_tree_id` - is the merge-base tree used for the conflicted merge step.
800+ ///
801+ /// - `ours_tree_id` - is the accumulated tree on the "ours" side of the conflict.
802+ ///
803+ /// - `theirs_tree_id` - is the incoming tree on the "theirs" side of the conflict.
804+ ///
805+ /// - `conflict_entries` - lists the paths that should be recorded as conflicted in
806+ /// the synthetic GitButler tree metadata.
807+ pub fn write_conflicted_tree (
808+ repo : & gix:: Repository ,
809+ resolved_tree_id : gix:: ObjectId ,
810+ base_tree_id : gix:: ObjectId ,
811+ ours_tree_id : gix:: ObjectId ,
812+ theirs_tree_id : gix:: ObjectId ,
813+ conflict_entries : & ConflictEntries ,
814+ ) -> anyhow:: Result < gix:: ObjectId > {
815+ let conflicted_files_string = toml:: to_string ( conflict_entries) ?;
816+ let conflicted_files_blob = repo. write_blob ( conflicted_files_string. as_bytes ( ) ) ?;
817+
818+ let mut tree = repo. find_tree ( resolved_tree_id) ?. edit ( ) ?;
819+ tree. upsert (
820+ TreeKind :: Ours . as_tree_entry_name ( ) ,
821+ EntryKind :: Tree ,
822+ ours_tree_id,
823+ ) ?;
824+ tree. upsert (
825+ TreeKind :: Theirs . as_tree_entry_name ( ) ,
826+ EntryKind :: Tree ,
827+ theirs_tree_id,
828+ ) ?;
829+ tree. upsert (
830+ TreeKind :: Base . as_tree_entry_name ( ) ,
831+ EntryKind :: Tree ,
832+ base_tree_id,
833+ ) ?;
834+ tree. upsert (
835+ TreeKind :: AutoResolution . as_tree_entry_name ( ) ,
836+ EntryKind :: Tree ,
837+ resolved_tree_id,
838+ ) ?;
839+ tree. upsert (
840+ TreeKind :: ConflictFiles . as_tree_entry_name ( ) ,
841+ EntryKind :: Blob ,
842+ conflicted_files_blob,
843+ ) ?;
844+ tree. write ( )
845+ . context ( "failed to write conflicted tree" )
846+ . map ( |tree_id| tree_id. detach ( ) )
847+ }
0 commit comments