jesseduffield.lazygit/pkg/gui/controllers/helpers/merge_and_rebase_helper.go
Stefan Haller f9b790a1f9 Let OnUIThreadAndWait's error be about the wait, not about f
Every caller passes an f that unconditionally returns nil, so f's error
return has never carried anything: the value is dead weight, and it
occupies the one channel the wait itself needs to report that it couldn't
run f at all. Drop it, so that the error the wait returns can only ever
mean that.

Work that can fail hands its error back through a captured variable, the
way the background fetch already hands back four values, which keeps the
two outcomes distinguishable at a call site that has both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 11:04:53 +02:00

706 lines
23 KiB
Go

package helpers
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stefanhaller/git-todo-parser/todo"
)
type MergeAndRebaseHelper struct {
c *HelperCommon
// Whether the "continue the rebase/merge?" prompt is currently on screen.
// We use this to auto-dismiss it if the operation stops being in the state
// that the prompt is offering to act on (e.g. it was continued or aborted
// externally), so the user isn't left with a stale prompt. Only accessed on
// the UI thread.
continueRebasePromptShowing bool
}
func NewMergeAndRebaseHelper(
c *HelperCommon,
) *MergeAndRebaseHelper {
return &MergeAndRebaseHelper{
c: c,
}
}
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
keys []gocui.Key
}
options := []optionAndKey{
{option: REBASE_OPTION_CONTINUE, keys: menuKey('c')},
{option: REBASE_OPTION_ABORT, keys: menuKey('a')},
}
if self.c.Git().Status.WorkingTreeState().CanSkip() {
options = append(options, optionAndKey{
option: REBASE_OPTION_SKIP, keys: menuKey('s'),
})
}
menuItems := lo.Map(options, func(row optionAndKey, _ int) *types.MenuItem {
return &types.MenuItem{
Label: row.option,
OnPress: func() error {
return self.genericMergeCommand(row.option)
},
Keys: row.keys,
}
})
title := self.c.Git().Status.WorkingTreeState().OptionsMenuTitle(self.c.Tr)
return self.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems})
}
func (self *MergeAndRebaseHelper) ContinueRebase() error {
return self.genericMergeCommand(REBASE_OPTION_CONTINUE)
}
func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
// The menu/prompt/confirm handlers that reach here run on the UI thread and
// spin up a worker (via the waiting status below) to do the actual work.
return self.genericMergeCommandImpl(command, true, false)
}
// genericMergeCommandImpl runs a merge/rebase continue/skip/abort and handles
// the result. Continuing can be slow (it may replay many commits), so the
// non-subprocess path runs on a worker with a waiting status.
//
// showWaitingStatus is false only for the recursive auto-skip in
// CheckMergeOrRebaseWithRefreshOptions, which already runs on a worker, so it
// must not spin up a second waiting status. calledFromWorker is used only by the
// subprocess path below: it's true for that recursive worker skip and false for
// genericMergeCommand's UI-thread invocation, so the post-action refresh picks
// RefreshFromWorker vs Refresh correctly.
func (self *MergeAndRebaseHelper) genericMergeCommandImpl(command string, showWaitingStatus bool, calledFromWorker bool) error {
status := self.c.Git().Status.WorkingTreeState()
if status.None() {
return errors.New(self.c.Tr.NotMergingOrRebasing)
}
self.c.LogAction(fmt.Sprintf("Merge/Rebase: %s", command))
effectiveStatus := status.Effective()
if effectiveStatus == models.WORKING_TREE_STATE_REBASING {
todoFile, err := os.ReadFile(
filepath.Join(self.c.Git().RepoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"),
)
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
self.c.LogCommand(string(todoFile), false)
}
}
commandType := status.CommandName()
selectHeadCommitOnSuccess := command == REBASE_OPTION_CONTINUE &&
effectiveStatus == models.WORKING_TREE_STATE_MERGING
// 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
needsSubprocess := (effectiveStatus == models.WORKING_TREE_STATE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig().Git.Merging.ManualCommit) ||
// but we'll also use a subprocess if we have exec todos; those are likely to be lengthy build
// tasks whose output the user will want to see in the terminal
(effectiveStatus == models.WORKING_TREE_STATE_REBASING && command != REBASE_OPTION_ABORT && self.hasExecTodos(calledFromWorker))
if needsSubprocess {
// TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction
success, err := self.c.RunSubprocess(self.c.Git().Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command))
self.refreshAfterMergeOrRebase(types.RefreshOptions{
CommitSelection: commitSelectionAfterMerge(success && selectHeadCommitOnSuccess),
}, calledFromWorker)
self.RecordWhetherMergeOrRebaseStartedInLazygit()
return err
}
// runAction always ends up on a worker: either the waiting status below spins
// one up, or we're the recursive auto-skip reached from
// CheckMergeOrRebaseWithRefreshOptions, which already runs on one.
runAction := func() error {
result := self.c.Git().Rebase.GenericMergeOrRebaseAction(commandType, command)
return self.CheckMergeOrRebaseWithRefreshOptions(result,
types.RefreshOptions{
CommitSelection: commitSelectionAfterMerge(result == nil && selectHeadCommitOnSuccess),
})
}
if showWaitingStatus {
return self.c.WithWaitingStatus(status.Title(self.c.Tr), func(gocui.Task) error {
return runAction()
})
}
return runAction()
}
// commitSelectionAfterMerge maps whether a merge/rebase/pull created a new
// commit at HEAD to the corresponding commit-selection behavior: select that
// new commit, or otherwise keep the previous selection by hash.
func commitSelectionAfterMerge(createdNewCommit bool) types.CommitSelectionBehavior {
if createdNewCommit {
return types.SelectHeadCommit
}
return types.KeepCommitSelectionByHash
}
func (self *MergeAndRebaseHelper) hasExecTodos(calledFromWorker bool) bool {
check := func() bool {
for _, commit := range self.c.Model().Commits {
if !commit.IsTODO() {
break
}
if commit.Action == todo.Exec {
return true
}
}
return false
}
// This reads the model, which is only safe on the UI thread, so bounce there
// when we're being called from a worker.
if !calledFromWorker {
return check()
}
result := false
_ = self.c.GocuiGui().OnUIThreadAndWait(func() {
result = check()
})
return result
}
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",
"hint: after resolving the conflicts",
"CONFLICT (content):",
}
func isMergeConflictErr(errStr string) bool {
for _, str := range conflictStrings {
if strings.Contains(errStr, str) {
return true
}
}
return false
}
// RecordWhetherMergeOrRebaseStartedInLazygit is called right after we run a
// merge/rebase/cherry-pick/revert step. If it left an operation in progress,
// that operation is one we started, which is what later lets us auto-prompt to
// continue it once its conflicts are resolved. If nothing is in progress
// anymore (the step completed or aborted the operation), we clear the flag.
func (self *MergeAndRebaseHelper) RecordWhetherMergeOrRebaseStartedInLazygit() {
self.c.State().GetRepoState().SetMergeOrRebaseStartedInLazygit(
self.c.Git().Status.WorkingTreeState().Any())
}
// CheckMergeOrRebaseWithRefreshOptions handles the result of a merge/rebase
// step and refreshes. It always runs on a worker (the WithWaitingStatus /
// WithWaitingStatusBlockingInput / WithInlineStatus handlers).
func (self *MergeAndRebaseHelper) CheckMergeOrRebaseWithRefreshOptions(result error, refreshOptions types.RefreshOptions) error {
self.refreshAfterMergeOrRebase(refreshOptions, true)
self.RecordWhetherMergeOrRebaseStartedInLazygit()
if result == nil {
return nil
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
return self.genericMergeCommandImpl(REBASE_OPTION_SKIP, false, true)
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
return self.genericMergeCommandImpl(REBASE_OPTION_SKIP, false, true)
} else if strings.Contains(result.Error(), "No rebase in progress?") {
// assume in this case that we're already done
return nil
}
return self.CheckForConflicts(result)
}
// refreshAfterMergeOrRebase issues the post-action refresh on the entry point
// that matches the thread the merge/rebase ran on: RefreshFromWorker for the
// worker callers, Refresh for the merge/rebase-continue subprocess path that
// stays on the UI thread.
func (self *MergeAndRebaseHelper) refreshAfterMergeOrRebase(refreshOptions types.RefreshOptions, calledFromWorker bool) {
if calledFromWorker {
self.c.RefreshFromWorker(refreshOptions)
} else {
self.c.Refresh(refreshOptions)
}
}
func (self *MergeAndRebaseHelper) CheckMergeOrRebase(result error) error {
return self.CheckMergeOrRebaseWithRefreshOptions(result, types.RefreshOptions{})
}
// Like CheckMergeOrRebase, but for operations that create a new commit at HEAD
// (a merge, or a pull that merges): on success it selects that new commit,
// which the keep-selection-by-hash logic can't do since the commit didn't exist
// before the refresh.
func (self *MergeAndRebaseHelper) CheckMergeOrRebaseAndSelectHeadCommit(result error) error {
return self.CheckMergeOrRebaseWithRefreshOptions(result,
types.RefreshOptions{CommitSelection: commitSelectionAfterMerge(result == nil)})
}
func (self *MergeAndRebaseHelper) CheckForConflicts(result error) error {
if result == nil {
return nil
}
if isMergeConflictErr(result.Error()) {
return self.PromptForConflictHandling()
}
return result
}
func (self *MergeAndRebaseHelper) PromptForConflictHandling() error {
mode := self.c.Git().Status.WorkingTreeState().CommandName()
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.FoundConflictsTitle,
Items: []*types.MenuItem{
{
Label: self.c.Tr.ViewConflictsMenuItem,
OnPress: func() error {
self.c.Context().Push(self.c.Contexts().Files, types.OnFocusOpts{})
return nil
},
},
{
Label: fmt.Sprintf(self.c.Tr.AbortMenuItem, mode),
OnPress: func() error {
return self.genericMergeCommand(REBASE_OPTION_ABORT)
},
Keys: menuKey('a'),
},
},
HideCancel: true,
})
}
func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error {
// prompt user to confirm that they want to abort, then do it
mode := self.c.Git().Status.WorkingTreeState().CommandName()
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)
},
})
return nil
}
// PromptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress
func (self *MergeAndRebaseHelper) PromptToContinueRebase() {
self.continueRebasePromptShowing = true
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.Continue,
Prompt: fmt.Sprintf(self.c.Tr.ConflictsResolved, self.c.Git().Status.WorkingTreeState().CommandName()),
HandleClose: func() error {
self.continueRebasePromptShowing = false
return nil
},
HandleConfirm: func() error {
self.continueRebasePromptShowing = false
// By the time we get here, we might have unstaged changes again,
// e.g. if the user had to fix build errors after resolving the
// conflicts, but after lazygit opened the prompt already. Ask again
// to auto-stage these.
// Need to refresh the files to be really sure if this is the case.
// We would otherwise be relying on lazygit's auto-refresh on focus,
// but this is not supported by all terminals or on all platforms.
//
// The model.Files update is bounced onto the UI thread, so we have
// to read it in Then; reading it inline here would see the previous
// model.
self.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.FILES},
Then: func() error {
unstagedFiles := GetUnstagedFilesExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
if len(unstagedFiles) > 0 {
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.Continue,
Prompt: self.c.Tr.UnstagedFilesAfterConflictsResolved,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
if err := self.c.Git().WorkingTree.StageFiles(unstagedFiles, []string{}); err != nil {
return err
}
return self.genericMergeCommand(REBASE_OPTION_CONTINUE)
},
})
return nil
}
return self.genericMergeCommand(REBASE_OPTION_CONTINUE)
},
})
return nil
},
})
}
// DismissContinueRebasePromptIfShowing closes the "continue the rebase/merge?"
// prompt if it's currently on screen. It's called when the operation is no
// longer in the state the prompt is offering to act on (e.g. it was continued
// or aborted outside lazygit, or new conflicts have appeared), so that the
// user isn't left with a prompt whose "continue" would now be wrong or fail.
// Must be called on the UI thread.
func (self *MergeAndRebaseHelper) DismissContinueRebasePromptIfShowing() {
if !self.continueRebasePromptShowing {
return
}
self.continueRebasePromptShowing = false
// Guard against popping something else: while our prompt is up no other
// popup can open, and confirming or closing it would have cleared the flag,
// so if it's set the confirmation context is ours.
if self.c.Context().Current() == self.c.Contexts().Confirmation {
self.c.Context().Pop()
}
}
func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
checkedOutBranch := self.c.Model().Branches[0]
checkedOutBranchName := checkedOutBranch.Name
var disabledReason, baseBranchDisabledReason *types.DisabledReason
if checkedOutBranchName == ref {
disabledReason = &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
}
baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(checkedOutBranch, self.c.Model().MainBranches)
if err != nil {
return err
}
if baseBranch == "" {
baseBranch = self.c.Tr.CouldNotDetermineBaseBranch
baseBranchDisabledReason = &types.DisabledReason{Text: self.c.Tr.CouldNotDetermineBaseBranch}
}
menuItems := []*types.MenuItem{
{
Label: utils.ResolvePlaceholderString(self.c.Tr.SimpleRebase,
map[string]string{"ref": ref},
),
Keys: menuKey('s'),
DisabledReason: disabledReason,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(task gocui.Task) error {
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 {
self.c.OnUIThread(func() error {
return self.ResetMarkedBaseCommit()
})
}
return err
})
},
},
{
Label: utils.ResolvePlaceholderString(self.c.Tr.InteractiveRebase,
map[string]string{"ref": ref},
),
Keys: menuKey('i'),
DisabledReason: disabledReason,
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(task gocui.Task) error {
var err error
if baseCommit != "" {
err = self.c.Git().Rebase.EditRebaseFromBaseCommit(ref, baseCommit)
} else {
err = self.c.Git().Rebase.EditRebase(ref)
}
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
self.c.OnUIThread(func() error {
if err := self.ResetMarkedBaseCommit(); err != nil {
return err
}
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
return nil
})
return nil
})
},
},
{
Label: utils.ResolvePlaceholderString(self.c.Tr.RebaseOntoBaseBranch,
map[string]string{"baseBranch": ShortBranchName(baseBranch)},
),
Keys: menuKey('b'),
DisabledReason: baseBranchDisabledReason,
Tooltip: self.c.Tr.RebaseOntoBaseBranchTooltip,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(task gocui.Task) error {
var err error
if baseCommit != "" {
err = self.c.Git().Rebase.RebaseBranchFromBaseCommit(baseBranch, baseCommit)
} else {
err = self.c.Git().Rebase.RebaseBranch(baseBranch)
}
err = self.CheckMergeOrRebase(err)
if err == nil {
self.c.OnUIThread(func() error {
return self.ResetMarkedBaseCommit()
})
}
return err
})
},
},
}
title := utils.ResolvePlaceholderString(
lo.Ternary(self.c.Modes().MarkedBaseCommit.GetHash() != "",
self.c.Tr.RebasingFromBaseCommitTitle,
self.c.Tr.RebasingTitle),
map[string]string{
"checkedOutBranch": checkedOutBranchName,
},
)
return self.c.Menu(types.CreateMenuOptions{
Title: title,
Items: menuItems,
})
}
func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) error {
if self.c.Git().Branch.IsHeadDetached() {
return errors.New("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.c.Model().Branches[0].Name
if checkedOutBranchName == refName {
return errors.New(self.c.Tr.CantMergeBranchIntoItself)
}
wantFastForward, wantNonFastForward := self.fastForwardMergeUserPreference()
canFastForward := self.c.Git().Branch.CanDoFastForwardMerge(refName)
var firstRegularMergeItem *types.MenuItem
var secondRegularMergeItem *types.MenuItem
var fastForwardMergeItem *types.MenuItem
if !wantNonFastForward && (wantFastForward || canFastForward) {
firstRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR),
Keys: menuKey('m'),
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
fastForwardMergeItem = firstRegularMergeItem
secondRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeNonFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_NON_FAST_FORWARD),
Keys: menuKey('n'),
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeNonFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
} else {
firstRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeNonFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR),
Keys: menuKey('m'),
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeNonFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
secondRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_FAST_FORWARD),
Keys: menuKey('f'),
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
fastForwardMergeItem = secondRegularMergeItem
}
if !canFastForward {
fastForwardMergeItem.DisabledReason = &types.DisabledReason{
Text: utils.ResolvePlaceholderString(
self.c.Tr.CannotFastForwardMerge,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
}
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Merge,
Items: []*types.MenuItem{
firstRegularMergeItem,
secondRegularMergeItem,
{
Label: self.c.Tr.SquashMergeUncommitted,
OnPress: self.SquashMergeUncommitted(refName),
Keys: menuKey('s'),
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.SquashMergeUncommittedTooltip,
map[string]string{
"selectedBranch": refName,
},
),
},
{
Label: self.c.Tr.SquashMergeCommitted,
OnPress: self.SquashMergeCommitted(refName, checkedOutBranchName),
Keys: menuKey('S'),
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.SquashMergeCommittedTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
},
},
})
}
func (self *MergeAndRebaseHelper) RegularMerge(refName string, variant git_commands.MergeVariant) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.Merge)
return self.c.WithWaitingStatus(self.c.Tr.MergingStatus, func(gocui.Task) error {
err := self.c.Git().Branch.Merge(refName, variant)
return self.CheckMergeOrRebaseAndSelectHeadCommit(err)
})
}
}
func (self *MergeAndRebaseHelper) SquashMergeUncommitted(refName string) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.SquashMerge)
return self.c.WithWaitingStatus(self.c.Tr.MergingStatus, func(gocui.Task) error {
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_SQUASH)
return self.CheckMergeOrRebase(err)
})
}
}
func (self *MergeAndRebaseHelper) SquashMergeCommitted(refName, checkedOutBranchName string) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.SquashMerge)
return self.c.WithWaitingStatus(self.c.Tr.MergingStatus, func(gocui.Task) error {
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_SQUASH)
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
message := utils.ResolvePlaceholderString(self.c.UserConfig().Git.Merging.SquashMergeMessage, map[string]string{
"selectedRef": refName,
"currentBranch": checkedOutBranchName,
})
err = self.c.Git().Commit.CommitCmdObj(message, "", false).Run()
if err != nil {
return err
}
self.c.RefreshFromWorker(types.RefreshOptions{})
return nil
})
}
}
// Returns wantsFastForward, wantsNonFastForward. These will never both be true, but they can both be false.
func (self *MergeAndRebaseHelper) fastForwardMergeUserPreference() (bool, bool) {
// Check user config first, because it takes precedence over git config
mergingArgs := self.c.UserConfig().Git.Merging.Args
if strings.Contains(mergingArgs, "--ff") { // also covers "--ff-only"
return true, false
}
if strings.Contains(mergingArgs, "--no-ff") {
return false, true
}
// Then check git config
mergeFfConfig := self.c.Git().Config.GetMergeFF()
if mergeFfConfig == "true" || mergeFfConfig == "only" {
return true, false
}
if mergeFfConfig == "false" {
return false, true
}
return false, false
}
func (self *MergeAndRebaseHelper) ResetMarkedBaseCommit() error {
self.c.Modes().MarkedBaseCommit.Reset()
self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
return nil
}