@@ -11,8 +11,8 @@ use but_core::{
1111 commit:: { add_conflict_markers, write_conflicted_tree} ,
1212} ;
1313use but_rebase:: graph_rebase:: {
14- Editor , LookupStep , Selector , Step , SuccessfulRebase , ToSelector ,
15- mutate:: { InsertSide , SegmentDelimiter , SelectorSet } ,
14+ Editor , ExtraRef , GraphEditorOptions , LookupStep , Selector , Step , SuccessfulRebase , ToSelector ,
15+ mutate:: { SegmentDelimiter , SelectorSet } ,
1616} ;
1717use but_rebase:: { commit:: DateMode , graph_rebase:: commit:: MergeCommitChangesOutcome } ;
1818use gix:: { prelude:: ObjectIdExt as _, remote:: Direction } ;
@@ -44,6 +44,11 @@ pub enum InteractiveIntegrationStep {
4444 /// Optionally, the message to use for the squash commit.
4545 message : Option < String > ,
4646 } ,
47+ /// Merge a commit into the previous one.
48+ Merge {
49+ /// The SHA of the commit to squash.
50+ commit_id : gix:: ObjectId ,
51+ } ,
4752}
4853
4954impl fmt:: Display for InteractiveIntegrationStep {
@@ -52,6 +57,7 @@ impl fmt::Display for InteractiveIntegrationStep {
5257 Self :: Skip { commit_id } => write ! ( f, "skip {commit_id}" ) ,
5358 Self :: Pick { commit_id } => write ! ( f, "pick {commit_id}" ) ,
5459 Self :: PickUpstream { commit_id } => write ! ( f, "pick-upstream {commit_id}" ) ,
60+ Self :: Merge { commit_id } => write ! ( f, "merge {commit_id}" ) ,
5561 Self :: Squash { commits, message } => {
5662 write ! ( f, "squash" ) ?;
5763 for commit_id in commits {
@@ -294,21 +300,28 @@ fn parse_quoted_string(input: &str, line_no: usize) -> Result<String> {
294300
295301/// Integrate the upstream changes in the order of the provided steps.
296302///
297- /// `editor` - The graph editor handle.
298- ///
299303/// `ref_name` - The full reference name of the local branch we're integrating the upstream changes into.
300304///
301305/// `steps` - The vector of steps in the application order (parent to child) that describe the actions to perform
302306/// for the integration of the changes.
303307pub fn integrate_branch_with_steps < ' ws , ' meta , M : RefMetadata > (
304- mut editor : Editor < ' ws , ' meta , M > ,
305308 ref_name : & gix:: refs:: FullNameRef ,
306309 integration : InteractiveIntegration ,
310+ workspace : & ' ws mut but_graph:: Workspace ,
311+ meta : & ' meta mut M ,
312+ repo : & gix:: Repository ,
307313) -> Result < SuccessfulRebase < ' ws , ' meta , M > > {
308314 if integration. steps . is_empty ( ) {
309315 bail ! ( "Integration steps cannot be empty" )
310316 }
317+ let ( _, upstream_ref_name, _) = get_branch_tips_and_upstream ( ref_name, repo) ?;
311318
319+ let upstream_ref_name = upstream_ref_name. as_ref ( ) ;
320+ let editor_options = GraphEditorOptions {
321+ extra_refs : vec ! [ ExtraRef :: immutable( upstream_ref_name) ] ,
322+ ..GraphEditorOptions :: default ( )
323+ } ;
324+ let mut editor = Editor :: create_with_opts ( workspace, meta, repo, & editor_options) ?;
312325 let delimiter_child = editor. select_reference ( ref_name) ?;
313326 let delimiter_parent = editor. select_commit ( integration. merge_base ) ?;
314327 let segment_delimiter = SegmentDelimiter {
@@ -383,7 +396,7 @@ fn integration_steps_into_segment_nodes<M: RefMetadata>(
383396 continue ;
384397 }
385398
386- parent_most = editor . insert ( parent_most , step , InsertSide :: Below ) ?;
399+ parent_most = connect_parent_step ( editor , parent_most , step ) ?;
387400 }
388401
389402 // Step 3: Append the merge base at the bottom
@@ -394,7 +407,7 @@ fn integration_steps_into_segment_nodes<M: RefMetadata>(
394407 {
395408 existing_parent
396409 } else {
397- editor . insert ( parent_most , merge_base_step , InsertSide :: Below ) ?
410+ connect_parent_step ( editor , parent_most , merge_base_step ) ?
398411 } ;
399412
400413 Ok ( SegmentDelimiter {
@@ -424,6 +437,40 @@ fn already_connected_parent_for_step<M: RefMetadata>(
424437 . find_map ( |( parent, _) | ( parent == existing_pick) . then_some ( parent) ) )
425438}
426439
440+ /// Connects `child` to `parent_step`, choosing the smallest available edge order.
441+ ///
442+ /// Prefers order `0` when free; otherwise picks the next smallest unused order.
443+ fn connect_parent_step < M : RefMetadata > (
444+ editor : & mut Editor < ' _ , ' _ , M > ,
445+ child : Selector ,
446+ parent_step : Step ,
447+ ) -> Result < Selector > {
448+ let parent = match parent_step {
449+ Step :: Pick ( pick) => {
450+ if let Some ( existing_pick) = editor. try_select_commit ( pick. id ) {
451+ existing_pick
452+ } else {
453+ editor. add_step ( Step :: Pick ( pick) ) ?
454+ }
455+ }
456+ Step :: Reference { refname } => editor. select_reference ( refname. as_ref ( ) ) ?,
457+ Step :: None => bail ! ( "BUG: trying to connect to none" ) ,
458+ } ;
459+
460+ let used_orders = editor
461+ . direct_parents ( child) ?
462+ . into_iter ( )
463+ . map ( |( _, order) | order)
464+ . collect :: < HashSet < _ > > ( ) ;
465+ let mut order = 0 ;
466+ while used_orders. contains ( & order) {
467+ order += 1 ;
468+ }
469+
470+ editor. add_edge ( child, parent, order) ?;
471+ Ok ( parent)
472+ }
473+
427474/// Converts user-provided integration steps into graph `Step`s in insertion order.
428475///
429476/// While translating, it applies graph detachments for skip instructions and prepares
@@ -451,6 +498,28 @@ fn integration_steps_to_segment_steps_for_editor<M: RefMetadata>(
451498 InteractiveIntegrationStep :: Squash { commits, message } => {
452499 out. push ( squash_step_for_editor ( editor, commits, message. as_deref ( ) ) ?) ;
453500 }
501+ InteractiveIntegrationStep :: Merge { commit_id } => {
502+ let mut merge_commit = editor. empty_commit ( ) ?;
503+ merge_commit. message = format ! ( "Merge {commit_id} into previous commit" ) . into ( ) ;
504+ let merge_commit =
505+ editor. new_commit_untracked ( merge_commit, DateMode :: CommitterKeepAuthorKeep ) ?;
506+ let preserved_parents = editor
507+ . find_commit ( * commit_id) ?
508+ . inner
509+ . parents
510+ . iter ( )
511+ . copied ( )
512+ . collect :: < Vec < _ > > ( ) ;
513+ let mut commit_to_merge = Step :: new_untracked_pick ( * commit_id) ;
514+ let Step :: Pick ( pick) = & mut commit_to_merge else {
515+ bail ! ( "BUG: expected merge side parent to be a pick step" ) ;
516+ } ;
517+ pick. preserved_parents = Some ( preserved_parents) ;
518+ let commit_to_merge = editor. add_step ( commit_to_merge) ?;
519+ let merge_commit = editor. add_step ( Step :: new_untracked_pick ( merge_commit) ) ?;
520+ editor. add_edge ( merge_commit, commit_to_merge, 1 ) ?;
521+ out. push ( editor. lookup_step ( merge_commit) ?) ;
522+ }
454523 }
455524 }
456525
@@ -564,7 +633,7 @@ fn existing_or_new_pick_step<M: RefMetadata>(
564633 child : existing,
565634 parent : existing,
566635 } ,
567- SelectorSet :: All ,
636+ SelectorSet :: None ,
568637 parents_to_disconnect,
569638 true ,
570639 ) ?;
0 commit comments