mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-28 18:24:17 -05:00
This is preparation for upcoming commits that will bounce refresh-scope model updates (e.g. Model.Files) onto the UI thread by enqueuing the write via OnUIThread instead of applying it directly on the worker goroutine. Once that lands, a Then callback that reads the model must run after that queued write has been processed, not synchronously at wg.Wait() time — at that point the workers have returned, but a bounce they queued may not have been processed yet. Queuing Then via OnUIThread here, ahead of that change, guarantees the right ordering once it lands: a bounce queued earlier in the same refresh is already sitting in the channel by the time wg.Wait() returns, so Then enqueued after it will always be processed after, and see the post-refresh model. The signature change to func() error lets Then propagate errors through gocui's normal error handler (the same path key-handler errors take). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
314 lines
9.4 KiB
Go
314 lines
9.4 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type BisectController struct {
|
|
baseController
|
|
*ListControllerTrait[*models.Commit]
|
|
c *ControllerCommon
|
|
}
|
|
|
|
var _ types.IController = &BisectController{}
|
|
|
|
func NewBisectController(
|
|
c *ControllerCommon,
|
|
) *BisectController {
|
|
return &BisectController{
|
|
baseController: baseController{},
|
|
c: c,
|
|
ListControllerTrait: NewListControllerTrait(
|
|
c,
|
|
c.Contexts().LocalCommits,
|
|
c.Contexts().LocalCommits.GetSelected,
|
|
c.Contexts().LocalCommits.GetSelectedItems,
|
|
),
|
|
}
|
|
}
|
|
|
|
func (self *BisectController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
bindings := []*types.Binding{
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Commits.ViewBisectOptions),
|
|
Handler: opts.Guards.OutsideFilterMode(self.withItem(self.openMenu)),
|
|
Description: self.c.Tr.ViewBisectOptions,
|
|
OpensMenu: true,
|
|
},
|
|
}
|
|
|
|
return bindings
|
|
}
|
|
|
|
func (self *BisectController) openMenu(commit *models.Commit) error {
|
|
// no shame in getting this directly rather than using the cached value
|
|
// given how cheap it is to obtain
|
|
info := self.c.Git().Bisect.GetInfo()
|
|
if info.Started() {
|
|
return self.openMidBisectMenu(info, commit)
|
|
}
|
|
return self.openStartBisectMenu(info, commit)
|
|
}
|
|
|
|
func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
|
|
// if there is not yet a 'current' bisect commit, or if we have
|
|
// selected the current commit, we need to jump to the next 'current' commit
|
|
// after we perform a bisect action. The reason we don't unconditionally jump
|
|
// is that sometimes the user will want to go and mark a few commits as skipped
|
|
// in a row and they wouldn't want to be jumped back to the current bisect
|
|
// commit each time.
|
|
// Originally we were allowing the user to, from the bisect menu, select whether
|
|
// they were talking about the selected commit or the current bisect commit,
|
|
// and that was a bit confusing (and required extra keypresses).
|
|
selectCurrentAfter := info.GetCurrentHash() == "" || info.GetCurrentHash() == commit.Hash()
|
|
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
|
|
// ref, because we'll be reloading our commits in that case.
|
|
waitToReselect := selectCurrentAfter && !self.c.Git().Bisect.ReachableFromStart(info)
|
|
|
|
// If we have a current hash already, then we always want to use that one. If
|
|
// not, we're still picking the initial commits before we really start, so
|
|
// use the selected commit in that case.
|
|
|
|
bisecting := info.GetCurrentHash() != ""
|
|
hashToMark := lo.Ternary(bisecting, info.GetCurrentHash(), commit.Hash())
|
|
shortHashToMark := utils.ShortHash(hashToMark)
|
|
|
|
// For marking a commit as bad, when we're not already bisecting, we require
|
|
// a single item selected, but once we are bisecting, it doesn't matter because
|
|
// the action applies to the HEAD commit rather than the selected commit.
|
|
var singleItemIfNotBisecting *types.DisabledReason
|
|
if !bisecting {
|
|
singleItemIfNotBisecting = self.require(self.singleItemSelected())()
|
|
}
|
|
|
|
menuItems := []*types.MenuItem{
|
|
{
|
|
Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, shortHashToMark, info.NewTerm()),
|
|
OnPress: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.BisectMark)
|
|
if err := self.c.Git().Bisect.Mark(hashToMark, info.NewTerm()); err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.afterMark(selectCurrentAfter, waitToReselect)
|
|
},
|
|
DisabledReason: singleItemIfNotBisecting,
|
|
Keys: menuKey('b'),
|
|
},
|
|
{
|
|
Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, shortHashToMark, info.OldTerm()),
|
|
OnPress: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.BisectMark)
|
|
if err := self.c.Git().Bisect.Mark(hashToMark, info.OldTerm()); err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.afterMark(selectCurrentAfter, waitToReselect)
|
|
},
|
|
DisabledReason: singleItemIfNotBisecting,
|
|
Keys: menuKey('g'),
|
|
},
|
|
{
|
|
Label: fmt.Sprintf(self.c.Tr.Bisect.SkipCurrent, shortHashToMark),
|
|
OnPress: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.BisectSkip)
|
|
if err := self.c.Git().Bisect.Skip(hashToMark); err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.afterMark(selectCurrentAfter, waitToReselect)
|
|
},
|
|
DisabledReason: singleItemIfNotBisecting,
|
|
Keys: menuKey('s'),
|
|
},
|
|
}
|
|
if info.GetCurrentHash() != "" && info.GetCurrentHash() != commit.Hash() {
|
|
menuItems = append(menuItems, lo.ToPtr(types.MenuItem{
|
|
Label: fmt.Sprintf(self.c.Tr.Bisect.SkipSelected, commit.ShortHash()),
|
|
OnPress: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.BisectSkip)
|
|
if err := self.c.Git().Bisect.Skip(commit.Hash()); err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.afterMark(selectCurrentAfter, waitToReselect)
|
|
},
|
|
DisabledReason: self.require(self.singleItemSelected())(),
|
|
Keys: menuKey('S'),
|
|
}))
|
|
}
|
|
menuItems = append(menuItems, lo.ToPtr(types.MenuItem{
|
|
Label: self.c.Tr.Bisect.ResetOption,
|
|
OnPress: func() error {
|
|
return self.c.Helpers().Bisect.Reset()
|
|
},
|
|
Keys: menuKey('r'),
|
|
}))
|
|
|
|
return self.c.Menu(types.CreateMenuOptions{
|
|
Title: self.c.Tr.Bisect.BisectMenuTitle,
|
|
Items: menuItems,
|
|
})
|
|
}
|
|
|
|
func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
|
|
return self.c.Menu(types.CreateMenuOptions{
|
|
Title: self.c.Tr.Bisect.BisectMenuTitle,
|
|
Items: []*types.MenuItem{
|
|
{
|
|
Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortHash(), info.NewTerm()),
|
|
OnPress: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.StartBisect)
|
|
if err := self.c.Git().Bisect.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := self.c.Git().Bisect.Mark(commit.Hash(), info.NewTerm()); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.Helpers().Bisect.PostBisectCommandRefresh()
|
|
return nil
|
|
},
|
|
DisabledReason: self.require(self.singleItemSelected())(),
|
|
Keys: menuKey('b'),
|
|
},
|
|
{
|
|
Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortHash(), info.OldTerm()),
|
|
OnPress: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.StartBisect)
|
|
if err := self.c.Git().Bisect.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := self.c.Git().Bisect.Mark(commit.Hash(), info.OldTerm()); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.Helpers().Bisect.PostBisectCommandRefresh()
|
|
return nil
|
|
},
|
|
DisabledReason: self.require(self.singleItemSelected())(),
|
|
Keys: menuKey('g'),
|
|
},
|
|
{
|
|
Label: self.c.Tr.Bisect.ChooseTerms,
|
|
OnPress: func() error {
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: self.c.Tr.Bisect.OldTermPrompt,
|
|
HandleConfirm: func(oldTerm string) error {
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: self.c.Tr.Bisect.NewTermPrompt,
|
|
HandleConfirm: func(newTerm string) error {
|
|
self.c.LogAction(self.c.Tr.Actions.StartBisect)
|
|
if err := self.c.Git().Bisect.StartWithTerms(oldTerm, newTerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.Helpers().Bisect.PostBisectCommandRefresh()
|
|
return nil
|
|
},
|
|
})
|
|
return nil
|
|
},
|
|
})
|
|
return nil
|
|
},
|
|
Keys: menuKey('t'),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func (self *BisectController) showBisectCompleteMessage(candidateHashes []string) error {
|
|
prompt := self.c.Tr.Bisect.CompletePrompt
|
|
if len(candidateHashes) > 1 {
|
|
prompt = self.c.Tr.Bisect.CompletePromptIndeterminate
|
|
}
|
|
|
|
formattedCommits, err := self.c.Git().Commit.GetCommitsOneline(candidateHashes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.Bisect.CompleteTitle,
|
|
Prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)),
|
|
HandleConfirm: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.ResetBisect)
|
|
if err := self.c.Git().Bisect.Reset(); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.Helpers().Bisect.PostBisectCommandRefresh()
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *BisectController) afterMark(selectCurrent bool, waitToReselect bool) error {
|
|
done, candidateHashes, err := self.c.Git().Bisect.IsDone()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := self.afterBisectMarkRefresh(selectCurrent, waitToReselect); err != nil {
|
|
return err
|
|
}
|
|
|
|
if done {
|
|
return self.showBisectCompleteMessage(candidateHashes)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *BisectController) afterBisectMarkRefresh(selectCurrent bool, waitToReselect bool) error {
|
|
selectFn := func() error {
|
|
if selectCurrent {
|
|
self.selectCurrentBisectCommit()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if waitToReselect {
|
|
self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{}, Then: selectFn})
|
|
return nil
|
|
}
|
|
|
|
if err := selectFn(); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.Helpers().Bisect.PostBisectCommandRefresh()
|
|
return nil
|
|
}
|
|
|
|
func (self *BisectController) selectCurrentBisectCommit() {
|
|
info := self.c.Git().Bisect.GetInfo()
|
|
if info.GetCurrentHash() != "" {
|
|
// find index of commit with that hash, move cursor to that.
|
|
for i, commit := range self.c.Model().Commits {
|
|
if commit.Hash() == info.GetCurrentHash() {
|
|
self.context().SetSelection(i)
|
|
self.context().HandleFocus(types.OnFocusOpts{})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *BisectController) context() *context.LocalCommitsContext {
|
|
return self.c.Contexts().LocalCommits
|
|
}
|