jesseduffield.lazygit/pkg/gui/controllers/helpers/cherry_pick_helper.go
Stefan Haller e54cb4bf42 Decouple hiding the working tree state from blocking input
Blocking keyboard input and hiding the working tree state mode are two
separate concerns; they were fused into one helper because every caller
so far wanted both. A caller that blocks input for something other than a
rebase would then hide the "Rebasing" indicator for the duration of its
operation, which has nothing to do with it.

Make it an explicit option instead, so blocking input on its own doesn't
imply anything about the modes on display.
2026-08-05 17:29:20 +02:00

176 lines
4.9 KiB
Go

package helpers
import (
"strconv"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type CherryPickHelper struct {
c *HelperCommon
rebaseHelper *MergeAndRebaseHelper
}
// I'm using the analogy of copy+paste in the terminology here because it's intuitively what's going on,
// even if in truth we're running git cherry-pick
func NewCherryPickHelper(
c *HelperCommon,
rebaseHelper *MergeAndRebaseHelper,
) *CherryPickHelper {
return &CherryPickHelper{
c: c,
rebaseHelper: rebaseHelper,
}
}
func (self *CherryPickHelper) getData() *cherrypicking.CherryPicking {
return self.c.Modes().CherryPicking
}
func (self *CherryPickHelper) CopyRange(commitsList []*models.Commit, context types.IListContext) error {
startIdx, endIdx := context.GetList().GetSelectionRange()
if err := self.resetIfNecessary(context); err != nil {
return err
}
// After a paste the buffer is hidden but not cleared, so the user
// thinks they're starting fresh. Clear it before adding so the new
// copy replaces the old one.
if self.getData().DidPaste {
self.getData().CherryPickedCommits = nil
self.getData().DidPaste = false
}
commitSet := self.getData().SelectedHashSet()
allCommitsCopied := lo.EveryBy(commitsList[startIdx:endIdx+1], func(commit *models.Commit) bool {
return commitSet.Includes(commit.Hash())
})
// if all selected commits are already copied, we'll uncopy them
if allCommitsCopied {
for index := startIdx; index <= endIdx; index++ {
commit := commitsList[index]
self.getData().Remove(commit, commitsList)
}
} else {
for index := startIdx; index <= endIdx; index++ {
commit := commitsList[index]
self.getData().Add(commit, commitsList)
}
}
self.rerender()
return nil
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied.
// Only to be called from the branch commits controller
func (self *CherryPickHelper) Paste() error {
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.CherryPick,
Prompt: utils.ResolvePlaceholderString(
self.c.Tr.SureCherryPick,
map[string]string{
"numCommits": strconv.Itoa(len(self.getData().CherryPickedCommits)),
}),
HandleConfirm: func() error {
mustStash := IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
cherryPickedCommits := self.getData().CherryPickedCommits
return self.c.WithWaitingStatusBlockingInput(types.WaitingStatusOpts{
Message: self.c.Tr.CherryPickingStatus,
HideWorkingTreeState: true,
}, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.CherryPick)
if mustStash {
if err := self.c.Git().Stash.Push(self.c.Tr.AutoStashForCherryPicking); err != nil {
return err
}
}
result := self.c.Git().Rebase.CherryPickCommits(cherryPickedCommits)
err := self.rebaseHelper.CheckMergeOrRebaseWithRefreshOptions(result,
types.RefreshOptions{BatchUIUpdates: true})
if err != nil {
return result
}
// If we're in the cherry-picking state at this point, it must
// be because there were conflicts. Don't clear the copied
// commits in this case, since we might want to abort and try
// pasting them again.
isInCherryPick, result := self.c.Git().Status.IsInCherryPick()
if result != nil {
return result
}
if !isInCherryPick {
// DidPaste and the re-render touch mode state and contexts,
// so run them on the UI thread.
self.c.OnUIThread(func() error {
self.getData().DidPaste = true
self.rerender()
return nil
})
if mustStash {
if err := self.c.Git().Stash.Pop(0); err != nil {
return err
}
self.c.RefreshFromWorker(types.RefreshOptions{
Scope: []types.RefreshableView{types.STASH, types.FILES},
})
}
}
return nil
})
},
})
return nil
}
func (self *CherryPickHelper) CanPaste() bool {
return self.getData().CanPaste()
}
func (self *CherryPickHelper) Reset() error {
self.getData().ContextKey = ""
self.getData().CherryPickedCommits = nil
self.rerender()
return nil
}
// you can only copy from one context at a time, because the order and position of commits matter
func (self *CherryPickHelper) resetIfNecessary(context types.Context) error {
oldContextKey := types.ContextKey(self.getData().ContextKey)
if oldContextKey != context.GetKey() {
// need to reset the cherry picking mode
self.getData().ContextKey = string(context.GetKey())
self.getData().CherryPickedCommits = make([]*models.Commit, 0)
}
return nil
}
func (self *CherryPickHelper) rerender() {
for _, context := range []types.Context{
self.c.Contexts().LocalCommits,
self.c.Contexts().ReflogCommits,
self.c.Contexts().SubCommits,
} {
self.c.PostRefreshUpdate(context)
}
}