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" ) type BisectController struct { baseController *controllerCommon } var _ types.IController = &BisectController{} func NewBisectController( common *controllerCommon, ) *BisectController { return &BisectController{ baseController: baseController{}, controllerCommon: common, } } func (self *BisectController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { Key: opts.GetKey(opts.Config.Commits.ViewBisectOptions), Handler: opts.Guards.OutsideFilterMode(self.checkSelected(self.openMenu)), Description: self.c.Tr.LcViewBisectOptions, 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.git.Bisect.GetInfo() if info.Started() { return self.openMidBisectMenu(info, commit) } else { 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.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha // 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.git.Bisect.ReachableFromStart(info) menuItems := []*types.MenuItem{ { DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.BisectMark) if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { return self.c.Error(err) } return self.afterMark(selectCurrentAfter, waitToReselect) }, Key: 'b', }, { DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.BisectMark) if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { return self.c.Error(err) } return self.afterMark(selectCurrentAfter, waitToReselect) }, Key: 'g', }, { DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Skip, commit.ShortSha()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.BisectSkip) if err := self.git.Bisect.Skip(commit.Sha); err != nil { return self.c.Error(err) } return self.afterMark(selectCurrentAfter, waitToReselect) }, Key: 's', }, { DisplayString: self.c.Tr.Bisect.ResetOption, OnPress: func() error { return self.helpers.Bisect.Reset() }, Key: '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{ { DisplayString: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.StartBisect) if err := self.git.Bisect.Start(); err != nil { return self.c.Error(err) } if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { return self.c.Error(err) } return self.helpers.Bisect.PostBisectCommandRefresh() }, Key: 'b', }, { DisplayString: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.StartBisect) if err := self.git.Bisect.Start(); err != nil { return self.c.Error(err) } if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { return self.c.Error(err) } return self.helpers.Bisect.PostBisectCommandRefresh() }, Key: 'g', }, }, }) } func (self *BisectController) showBisectCompleteMessage(candidateShas []string) error { prompt := self.c.Tr.Bisect.CompletePrompt if len(candidateShas) > 1 { prompt = self.c.Tr.Bisect.CompletePromptIndeterminate } formattedCommits, err := self.git.Commit.GetCommitsOneline(candidateShas) if err != nil { return self.c.Error(err) } return 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.git.Bisect.Reset(); err != nil { return self.c.Error(err) } return self.helpers.Bisect.PostBisectCommandRefresh() }, }) } func (self *BisectController) afterMark(selectCurrent bool, waitToReselect bool) error { done, candidateShas, err := self.git.Bisect.IsDone() if err != nil { return self.c.Error(err) } if err := self.afterBisectMarkRefresh(selectCurrent, waitToReselect); err != nil { return self.c.Error(err) } if done { return self.showBisectCompleteMessage(candidateShas) } return nil } func (self *BisectController) afterBisectMarkRefresh(selectCurrent bool, waitToReselect bool) error { selectFn := func() { if selectCurrent { self.selectCurrentBisectCommit() } } if waitToReselect { return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{}, Then: selectFn}) } else { selectFn() return self.helpers.Bisect.PostBisectCommandRefresh() } } func (self *BisectController) selectCurrentBisectCommit() { info := self.git.Bisect.GetInfo() if info.GetCurrentSha() != "" { // find index of commit with that sha, move cursor to that. for i, commit := range self.model.Commits { if commit.Sha == info.GetCurrentSha() { self.context().SetSelectedLineIdx(i) _ = self.context().HandleFocus() break } } } } func (self *BisectController) checkSelected(callback func(*models.Commit) error) func() error { return func() error { commit := self.context().GetSelected() if commit == nil { return nil } return callback(commit) } } func (self *BisectController) Context() types.Context { return self.context() } func (self *BisectController) context() *context.LocalCommitsContext { return self.contexts.LocalCommits }