2022-02-06 15:54:26 +11:00
package helpers
2022-01-30 20:03:08 +11:00
import (
"fmt"
"strings"
2023-07-28 21:27:15 -04:00
"github.com/jesseduffield/gocui"
2022-02-06 15:54:26 +11:00
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
2022-01-30 20:03:08 +11:00
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/types"
2022-02-06 15:54:26 +11:00
"github.com/jesseduffield/lazygit/pkg/utils"
2023-07-24 13:06:42 +10:00
"github.com/samber/lo"
2022-01-30 20:03:08 +11:00
)
2022-02-06 15:54:26 +11:00
type MergeAndRebaseHelper struct {
2023-03-23 12:35:07 +11:00
c * HelperCommon
2022-08-06 18:05:00 +10:00
refsHelper * RefsHelper
2022-01-30 20:03:08 +11:00
}
2022-02-06 15:54:26 +11:00
func NewMergeAndRebaseHelper (
2023-03-23 12:35:07 +11:00
c * HelperCommon ,
2022-02-06 15:54:26 +11:00
refsHelper * RefsHelper ,
) * MergeAndRebaseHelper {
return & MergeAndRebaseHelper {
2022-08-06 18:05:00 +10:00
c : c ,
refsHelper : refsHelper ,
2022-01-30 20:03:08 +11:00
}
}
type RebaseOption string
const (
REBASE_OPTION_CONTINUE string = "continue"
REBASE_OPTION_ABORT string = "abort"
REBASE_OPTION_SKIP string = "skip"
)
2022-02-06 15:54:26 +11:00
func ( self * MergeAndRebaseHelper ) CreateRebaseOptionsMenu ( ) error {
2022-03-27 17:19:31 +11:00
type optionAndKey struct {
option string
key types . Key
}
options := [ ] optionAndKey {
{ option : REBASE_OPTION_CONTINUE , key : 'c' } ,
{ option : REBASE_OPTION_ABORT , key : 'a' } ,
}
2022-01-30 20:03:08 +11:00
2023-03-23 12:53:18 +11:00
if self . c . Git ( ) . Status . WorkingTreeState ( ) == enums . REBASE_MODE_REBASING {
2022-03-27 17:19:31 +11:00
options = append ( options , optionAndKey {
option : REBASE_OPTION_SKIP , key : 's' ,
} )
2022-01-30 20:03:08 +11:00
}
2023-07-24 13:06:42 +10:00
menuItems := lo . Map ( options , func ( row optionAndKey , _ int ) * types . MenuItem {
2022-03-19 19:12:58 +11:00
return & types . MenuItem {
2022-05-08 14:23:32 +10:00
Label : row . option ,
2022-01-30 20:03:08 +11:00
OnPress : func ( ) error {
2022-03-27 17:19:31 +11:00
return self . genericMergeCommand ( row . option )
2022-01-30 20:03:08 +11:00
} ,
2022-03-27 17:19:31 +11:00
Key : row . key ,
2022-01-30 20:03:08 +11:00
}
2022-03-19 19:12:58 +11:00
} )
2022-01-30 20:03:08 +11:00
var title string
2023-03-23 12:53:18 +11:00
if self . c . Git ( ) . Status . WorkingTreeState ( ) == enums . REBASE_MODE_MERGING {
2022-01-30 20:03:08 +11:00
title = self . c . Tr . MergeOptionsTitle
} else {
title = self . c . Tr . RebaseOptionsTitle
}
return self . c . Menu ( types . CreateMenuOptions { Title : title , Items : menuItems } )
}
2022-02-06 15:54:26 +11:00
func ( self * MergeAndRebaseHelper ) genericMergeCommand ( command string ) error {
2023-03-23 12:53:18 +11:00
status := self . c . Git ( ) . Status . WorkingTreeState ( )
2022-01-30 20:03:08 +11:00
if status != enums . REBASE_MODE_MERGING && status != enums . REBASE_MODE_REBASING {
return self . c . ErrorMsg ( self . c . Tr . NotMergingOrRebasing )
}
self . c . LogAction ( fmt . Sprintf ( "Merge/Rebase: %s" , command ) )
commandType := ""
switch status {
case enums . REBASE_MODE_MERGING :
commandType = "merge"
case enums . REBASE_MODE_REBASING :
commandType = "rebase"
default :
// shouldn't be possible to land here
}
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == enums . REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self . c . UserConfig . Git . Merging . ManualCommit {
// TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction
return self . c . RunSubprocessAndRefresh (
2023-03-23 12:53:18 +11:00
self . c . Git ( ) . Rebase . GenericMergeOrRebaseActionCmdObj ( commandType , command ) ,
2022-01-30 20:03:08 +11:00
)
}
2023-03-23 12:53:18 +11:00
result := self . c . Git ( ) . Rebase . GenericMergeOrRebaseAction ( commandType , command )
2022-01-30 20:03:08 +11:00
if err := self . CheckMergeOrRebase ( result ) ; err != nil {
return err
}
return nil
}
var conflictStrings = [ ] string {
"Failed to merge in the changes" ,
"When you have resolved this problem" ,
"fix conflicts" ,
"Resolve all conflicts manually" ,
2023-02-25 11:39:24 +11:00
"Merge conflict in file" ,
2022-01-30 20:03:08 +11:00
}
func isMergeConflictErr ( errStr string ) bool {
for _ , str := range conflictStrings {
if strings . Contains ( errStr , str ) {
return true
}
}
return false
}
2023-08-03 19:22:51 +02:00
func ( self * MergeAndRebaseHelper ) CheckMergeOrRebaseWithRefreshOptions ( result error , refreshOptions types . RefreshOptions ) error {
if err := self . c . Refresh ( refreshOptions ) ; err != nil {
2022-01-30 20:03:08 +11:00
return err
}
if result == nil {
return nil
} else if strings . Contains ( result . Error ( ) , "No changes - did you forget to use" ) {
return self . genericMergeCommand ( REBASE_OPTION_SKIP )
} else if strings . Contains ( result . Error ( ) , "The previous cherry-pick is now empty" ) {
return self . genericMergeCommand ( REBASE_OPTION_CONTINUE )
} else if strings . Contains ( result . Error ( ) , "No rebase in progress?" ) {
// assume in this case that we're already done
return nil
2023-07-13 18:36:39 +10:00
} else {
return self . CheckForConflicts ( result )
}
}
2023-08-03 19:22:51 +02:00
func ( self * MergeAndRebaseHelper ) CheckMergeOrRebase ( result error ) error {
return self . CheckMergeOrRebaseWithRefreshOptions ( result , types . RefreshOptions { Mode : types . ASYNC } )
}
2023-07-13 18:36:39 +10:00
func ( self * MergeAndRebaseHelper ) CheckForConflicts ( result error ) error {
if result == nil {
return nil
}
if isMergeConflictErr ( result . Error ( ) ) {
return self . PromptForConflictHandling ( )
2022-01-30 20:03:08 +11:00
} else {
return self . c . ErrorMsg ( result . Error ( ) )
}
}
2023-07-13 18:36:39 +10:00
func ( self * MergeAndRebaseHelper ) PromptForConflictHandling ( ) error {
mode := self . workingTreeStateNoun ( )
return self . c . Menu ( types . CreateMenuOptions {
Title : self . c . Tr . FoundConflictsTitle ,
Items : [ ] * types . MenuItem {
{
Label : self . c . Tr . ViewConflictsMenuItem ,
OnPress : func ( ) error {
return self . c . PushContext ( self . c . Contexts ( ) . Files )
} ,
Key : 'v' ,
} ,
{
Label : fmt . Sprintf ( self . c . Tr . AbortMenuItem , mode ) ,
OnPress : func ( ) error {
return self . genericMergeCommand ( REBASE_OPTION_ABORT )
} ,
Key : 'a' ,
} ,
} ,
HideCancel : true ,
} )
}
2022-02-06 15:54:26 +11:00
func ( self * MergeAndRebaseHelper ) AbortMergeOrRebaseWithConfirm ( ) error {
2022-01-30 20:03:08 +11:00
// prompt user to confirm that they want to abort, then do it
mode := self . workingTreeStateNoun ( )
2022-03-30 08:48:29 +02:00
return self . c . Confirm ( types . ConfirmOpts {
2022-01-30 20:03:08 +11:00
Title : fmt . Sprintf ( self . c . Tr . AbortTitle , mode ) ,
Prompt : fmt . Sprintf ( self . c . Tr . AbortPrompt , mode ) ,
HandleConfirm : func ( ) error {
return self . genericMergeCommand ( REBASE_OPTION_ABORT )
} ,
} )
}
2022-02-06 15:54:26 +11:00
func ( self * MergeAndRebaseHelper ) workingTreeStateNoun ( ) string {
2023-03-23 12:53:18 +11:00
workingTreeState := self . c . Git ( ) . Status . WorkingTreeState ( )
2022-01-30 20:03:08 +11:00
switch workingTreeState {
case enums . REBASE_MODE_NONE :
return ""
case enums . REBASE_MODE_MERGING :
return "merge"
default :
return "rebase"
}
}
// PromptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress
2022-02-06 15:54:26 +11:00
func ( self * MergeAndRebaseHelper ) PromptToContinueRebase ( ) error {
2022-03-30 08:48:29 +02:00
return self . c . Confirm ( types . ConfirmOpts {
2023-05-25 21:11:51 +10:00
Title : self . c . Tr . Continue ,
2022-01-30 20:03:08 +11:00
Prompt : self . c . Tr . ConflictsResolved ,
HandleConfirm : func ( ) error {
return self . genericMergeCommand ( REBASE_OPTION_CONTINUE )
} ,
} )
}
2022-02-06 15:54:26 +11:00
func ( self * MergeAndRebaseHelper ) RebaseOntoRef ( ref string ) error {
checkedOutBranch := self . refsHelper . GetCheckedOutRef ( ) . Name
2023-03-29 00:53:14 +02:00
menuItems := [ ] * types . MenuItem {
{
Label : self . c . Tr . SimpleRebase ,
Key : 's' ,
OnPress : func ( ) error {
self . c . LogAction ( self . c . Tr . Actions . RebaseBranch )
2023-07-28 21:27:15 -04:00
return self . c . WithWaitingStatus ( self . c . Tr . RebasingStatus , func ( task gocui . Task ) error {
baseCommit := self . c . Modes ( ) . MarkedBaseCommit . GetSha ( )
var err error
if baseCommit != "" {
err = self . c . Git ( ) . Rebase . RebaseBranchFromBaseCommit ( ref , baseCommit )
} else {
err = self . c . Git ( ) . Rebase . RebaseBranch ( ref )
}
err = self . CheckMergeOrRebase ( err )
if err == nil {
2023-08-12 13:17:01 +10:00
return self . ResetMarkedBaseCommit ( )
2023-07-28 21:27:15 -04:00
}
return err
} )
2023-03-29 00:53:14 +02:00
} ,
} ,
{
Label : self . c . Tr . InteractiveRebase ,
Key : 'i' ,
Tooltip : self . c . Tr . InteractiveRebaseTooltip ,
OnPress : func ( ) error {
self . c . LogAction ( self . c . Tr . Actions . RebaseBranch )
2023-06-11 08:08:55 +02:00
baseCommit := self . c . Modes ( ) . MarkedBaseCommit . GetSha ( )
var err error
if baseCommit != "" {
err = self . c . Git ( ) . Rebase . EditRebaseFromBaseCommit ( ref , baseCommit )
} else {
err = self . c . Git ( ) . Rebase . EditRebase ( ref )
}
2023-04-15 10:42:36 +02:00
if err = self . CheckMergeOrRebase ( err ) ; err != nil {
return err
}
2023-08-12 13:17:01 +10:00
if err = self . ResetMarkedBaseCommit ( ) ; err != nil {
return err
}
2023-03-23 12:53:18 +11:00
return self . c . PushContext ( self . c . Contexts ( ) . LocalCommits )
2023-03-29 00:53:14 +02:00
} ,
} ,
}
title := utils . ResolvePlaceholderString (
2023-06-11 08:08:55 +02:00
lo . Ternary ( self . c . Modes ( ) . MarkedBaseCommit . GetSha ( ) != "" ,
self . c . Tr . RebasingFromBaseCommitTitle ,
self . c . Tr . RebasingTitle ) ,
2022-02-06 15:54:26 +11:00
map [ string ] string {
"checkedOutBranch" : checkedOutBranch ,
2023-03-29 00:53:14 +02:00
"ref" : ref ,
2022-02-06 15:54:26 +11:00
} ,
)
2023-03-29 00:53:14 +02:00
return self . c . Menu ( types . CreateMenuOptions {
Title : title ,
Items : menuItems ,
2022-02-06 15:54:26 +11:00
} )
}
func ( self * MergeAndRebaseHelper ) MergeRefIntoCheckedOutBranch ( refName string ) error {
2023-03-23 12:53:18 +11:00
if self . c . Git ( ) . Branch . IsHeadDetached ( ) {
2022-02-06 15:54:26 +11:00
return self . c . ErrorMsg ( "Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on" )
}
checkedOutBranchName := self . refsHelper . GetCheckedOutRef ( ) . Name
if checkedOutBranchName == refName {
return self . c . ErrorMsg ( self . c . Tr . CantMergeBranchIntoItself )
}
prompt := utils . ResolvePlaceholderString (
self . c . Tr . ConfirmMerge ,
map [ string ] string {
"checkedOutBranch" : checkedOutBranchName ,
"selectedBranch" : refName ,
} ,
)
2022-03-30 08:48:29 +02:00
return self . c . Confirm ( types . ConfirmOpts {
2022-03-27 17:29:22 +11:00
Title : self . c . Tr . MergeConfirmTitle ,
2022-02-06 15:54:26 +11:00
Prompt : prompt ,
HandleConfirm : func ( ) error {
self . c . LogAction ( self . c . Tr . Actions . Merge )
2023-03-23 12:53:18 +11:00
err := self . c . Git ( ) . Branch . Merge ( refName , git_commands . MergeOpts { } )
2022-02-06 15:54:26 +11:00
return self . CheckMergeOrRebase ( err )
} ,
} )
}
2023-06-11 08:08:55 +02:00
func ( self * MergeAndRebaseHelper ) ResetMarkedBaseCommit ( ) error {
self . c . Modes ( ) . MarkedBaseCommit . Reset ( )
return self . c . PostRefreshUpdate ( self . c . Contexts ( ) . LocalCommits )
}