mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-14 11:23:09 +02:00
b61ca21a84
We have a use-case to rebind 'm' to the merge action in the branches panel. There's three ways to handle this: 1) For all global keybindings, define a per-panel key that invokes it 2) Give a name to all controller actions and allow them to be invoked in custom commands 3) Allow checking for merge conflicts after running a custom command so that users can add their own 'git merge' custom command that matches the in-built action Option 1 is hairy, Option 2 though good for users introduces new backwards compatibility issues that I don't want to do right now, and option 3 is trivially easy to implement so that's what I'm doing. I've put this under an 'after' key so that we can add more things later. I'm imagining other things like being able to move the cursor to a newly added item etc. I considered always running this hook by default but I'd rather not: it's matching on the output text and I'd rather something like that be explicitly opted-into to avoid cases where we erroneously believe that there are conflicts.
286 lines
7.8 KiB
Go
286 lines
7.8 KiB
Go
package helpers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/generics/slices"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
)
|
|
|
|
type MergeAndRebaseHelper struct {
|
|
c *HelperCommon
|
|
refsHelper *RefsHelper
|
|
}
|
|
|
|
func NewMergeAndRebaseHelper(
|
|
c *HelperCommon,
|
|
refsHelper *RefsHelper,
|
|
) *MergeAndRebaseHelper {
|
|
return &MergeAndRebaseHelper{
|
|
c: c,
|
|
refsHelper: refsHelper,
|
|
}
|
|
}
|
|
|
|
type RebaseOption string
|
|
|
|
const (
|
|
REBASE_OPTION_CONTINUE string = "continue"
|
|
REBASE_OPTION_ABORT string = "abort"
|
|
REBASE_OPTION_SKIP string = "skip"
|
|
)
|
|
|
|
func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
|
|
type optionAndKey struct {
|
|
option string
|
|
key types.Key
|
|
}
|
|
|
|
options := []optionAndKey{
|
|
{option: REBASE_OPTION_CONTINUE, key: 'c'},
|
|
{option: REBASE_OPTION_ABORT, key: 'a'},
|
|
}
|
|
|
|
if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
|
options = append(options, optionAndKey{
|
|
option: REBASE_OPTION_SKIP, key: 's',
|
|
})
|
|
}
|
|
|
|
menuItems := slices.Map(options, func(row optionAndKey) *types.MenuItem {
|
|
return &types.MenuItem{
|
|
Label: row.option,
|
|
OnPress: func() error {
|
|
return self.genericMergeCommand(row.option)
|
|
},
|
|
Key: row.key,
|
|
}
|
|
})
|
|
|
|
var title string
|
|
if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_MERGING {
|
|
title = self.c.Tr.MergeOptionsTitle
|
|
} else {
|
|
title = self.c.Tr.RebaseOptionsTitle
|
|
}
|
|
|
|
return self.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems})
|
|
}
|
|
|
|
func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
|
|
status := self.c.Git().Status.WorkingTreeState()
|
|
|
|
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(
|
|
self.c.Git().Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command),
|
|
)
|
|
}
|
|
result := self.c.Git().Rebase.GenericMergeOrRebaseAction(commandType, command)
|
|
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",
|
|
"Merge conflict in file",
|
|
}
|
|
|
|
func isMergeConflictErr(errStr string) bool {
|
|
for _, str := range conflictStrings {
|
|
if strings.Contains(errStr, str) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (self *MergeAndRebaseHelper) CheckMergeOrRebase(result error) error {
|
|
if err := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
|
|
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
|
|
} else {
|
|
return self.CheckForConflicts(result)
|
|
}
|
|
}
|
|
|
|
func (self *MergeAndRebaseHelper) CheckForConflicts(result error) error {
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
|
|
if isMergeConflictErr(result.Error()) {
|
|
return self.PromptForConflictHandling()
|
|
} else {
|
|
return self.c.ErrorMsg(result.Error())
|
|
}
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error {
|
|
// prompt user to confirm that they want to abort, then do it
|
|
mode := self.workingTreeStateNoun()
|
|
return self.c.Confirm(types.ConfirmOpts{
|
|
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)
|
|
},
|
|
})
|
|
}
|
|
|
|
func (self *MergeAndRebaseHelper) workingTreeStateNoun() string {
|
|
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
|
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
|
|
func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
|
|
return self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.Continue,
|
|
Prompt: self.c.Tr.ConflictsResolved,
|
|
HandleConfirm: func() error {
|
|
return self.genericMergeCommand(REBASE_OPTION_CONTINUE)
|
|
},
|
|
})
|
|
}
|
|
|
|
func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
|
|
checkedOutBranch := self.refsHelper.GetCheckedOutRef().Name
|
|
if ref == checkedOutBranch {
|
|
return self.c.ErrorMsg(self.c.Tr.CantRebaseOntoSelf)
|
|
}
|
|
menuItems := []*types.MenuItem{
|
|
{
|
|
Label: self.c.Tr.SimpleRebase,
|
|
Key: 's',
|
|
OnPress: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
|
|
err := self.c.Git().Rebase.RebaseBranch(ref)
|
|
return self.CheckMergeOrRebase(err)
|
|
},
|
|
},
|
|
{
|
|
Label: self.c.Tr.InteractiveRebase,
|
|
Key: 'i',
|
|
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
|
|
OnPress: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
|
|
err := self.c.Git().Rebase.EditRebase(ref)
|
|
if err = self.CheckMergeOrRebase(err); err != nil {
|
|
return err
|
|
}
|
|
return self.c.PushContext(self.c.Contexts().LocalCommits)
|
|
},
|
|
},
|
|
}
|
|
|
|
title := utils.ResolvePlaceholderString(
|
|
self.c.Tr.RebasingTitle,
|
|
map[string]string{
|
|
"checkedOutBranch": checkedOutBranch,
|
|
"ref": ref,
|
|
},
|
|
)
|
|
|
|
return self.c.Menu(types.CreateMenuOptions{
|
|
Title: title,
|
|
Items: menuItems,
|
|
})
|
|
}
|
|
|
|
func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) error {
|
|
if self.c.Git().Branch.IsHeadDetached() {
|
|
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,
|
|
},
|
|
)
|
|
|
|
return self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.MergeConfirmTitle,
|
|
Prompt: prompt,
|
|
HandleConfirm: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.Merge)
|
|
err := self.c.Git().Branch.Merge(refName, git_commands.MergeOpts{})
|
|
return self.CheckMergeOrRebase(err)
|
|
},
|
|
})
|
|
}
|