diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/commands/git.go | 3 | ||||
-rw-r--r-- | pkg/commands/git_commands/bisect.go | 164 | ||||
-rw-r--r-- | pkg/commands/git_commands/bisect_info.go | 103 | ||||
-rw-r--r-- | pkg/commands/git_commands/commit.go | 14 | ||||
-rw-r--r-- | pkg/commands/models/commit.go | 11 | ||||
-rw-r--r-- | pkg/commands/oscommands/cmd_obj.go | 40 | ||||
-rw-r--r-- | pkg/commands/oscommands/cmd_obj_runner.go | 56 | ||||
-rw-r--r-- | pkg/config/user_config.go | 3 | ||||
-rw-r--r-- | pkg/gui/bisect.go | 204 | ||||
-rw-r--r-- | pkg/gui/commits_panel.go | 9 | ||||
-rw-r--r-- | pkg/gui/custom_commands.go | 6 | ||||
-rw-r--r-- | pkg/gui/filetree/commit_file_manager.go | 3 | ||||
-rw-r--r-- | pkg/gui/filetree/file_manager.go | 3 | ||||
-rw-r--r-- | pkg/gui/filetree/presentation.go (renamed from pkg/gui/presentation/files.go) | 47 | ||||
-rw-r--r-- | pkg/gui/gui.go | 3 | ||||
-rw-r--r-- | pkg/gui/keybindings.go | 8 | ||||
-rw-r--r-- | pkg/gui/list_context_config.go | 3 | ||||
-rw-r--r-- | pkg/gui/modes.go | 65 | ||||
-rw-r--r-- | pkg/gui/presentation/commit_files.go | 49 | ||||
-rw-r--r-- | pkg/gui/presentation/commits.go | 132 | ||||
-rw-r--r-- | pkg/gui/view_helpers.go | 6 | ||||
-rw-r--r-- | pkg/i18n/english.go | 38 | ||||
-rw-r--r-- | pkg/utils/formatting.go | 7 |
23 files changed, 874 insertions, 103 deletions
diff --git a/pkg/commands/git.go b/pkg/commands/git.go index b75410322..f6812e254 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -36,6 +36,7 @@ type GitCommand struct { Sync *git_commands.SyncCommands Tag *git_commands.TagCommands WorkingTree *git_commands.WorkingTreeCommands + Bisect *git_commands.BisectCommands Loaders Loaders } @@ -113,6 +114,7 @@ func NewGitCommandAux( // TODO: have patch manager take workingTreeCommands in its entirety patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff) patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchManager) + bisectCommands := git_commands.NewBisectCommands(gitCommon) return &GitCommand{ Branch: branchCommands, @@ -129,6 +131,7 @@ func NewGitCommandAux( Submodule: submoduleCommands, Sync: syncCommands, Tag: tagCommands, + Bisect: bisectCommands, WorkingTree: workingTreeCommands, Loaders: Loaders{ Branches: loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName, configCommands), diff --git a/pkg/commands/git_commands/bisect.go b/pkg/commands/git_commands/bisect.go new file mode 100644 index 000000000..2e362af2a --- /dev/null +++ b/pkg/commands/git_commands/bisect.go @@ -0,0 +1,164 @@ +package git_commands + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +type BisectCommands struct { + *GitCommon +} + +func NewBisectCommands(gitCommon *GitCommon) *BisectCommands { + return &BisectCommands{ + GitCommon: gitCommon, + } +} + +// This command is pretty cheap to run so we're not storing the result anywhere. +// But if it becomes problematic we can chang that. +func (self *BisectCommands) GetInfo() *BisectInfo { + var err error + info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"} + // we return nil if we're not in a git bisect session. + // we know we're in a session by the presence of a .git/BISECT_START file + + bisectStartPath := filepath.Join(self.dotGitDir, "BISECT_START") + exists, err := self.os.FileExists(bisectStartPath) + if err != nil { + self.Log.Infof("error getting git bisect info: %s", err.Error()) + return info + } + + if !exists { + return info + } + + startContent, err := os.ReadFile(bisectStartPath) + if err != nil { + self.Log.Infof("error getting git bisect info: %s", err.Error()) + return info + } + + info.started = true + info.start = strings.TrimSpace(string(startContent)) + + termsContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_TERMS")) + if err != nil { + // old git versions won't have this file so we default to bad/good + } else { + splitContent := strings.Split(string(termsContent), "\n") + info.newTerm = splitContent[0] + info.oldTerm = splitContent[1] + } + + bisectRefsDir := filepath.Join(self.dotGitDir, "refs", "bisect") + files, err := os.ReadDir(bisectRefsDir) + if err != nil { + self.Log.Infof("error getting git bisect info: %s", err.Error()) + return info + } + + info.statusMap = make(map[string]BisectStatus) + for _, file := range files { + status := BisectStatusSkipped + name := file.Name() + path := filepath.Join(bisectRefsDir, name) + + fileContent, err := os.ReadFile(path) + if err != nil { + self.Log.Infof("error getting git bisect info: %s", err.Error()) + return info + } + + sha := strings.TrimSpace(string(fileContent)) + + if name == info.newTerm { + status = BisectStatusNew + } else if strings.HasPrefix(name, info.oldTerm+"-") { + status = BisectStatusOld + } else if strings.HasPrefix(name, "skipped-") { + status = BisectStatusSkipped + } + + info.statusMap[sha] = status + } + + currentContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_EXPECTED_REV")) + if err != nil { + self.Log.Infof("error getting git bisect info: %s", err.Error()) + return info + } + currentSha := strings.TrimSpace(string(currentContent)) + info.current = currentSha + + return info +} + +func (self *BisectCommands) Reset() error { + return self.cmd.New("git bisect reset").StreamOutput().Run() +} + +func (self *BisectCommands) Mark(ref string, term string) error { + return self.cmd.New( + fmt.Sprintf("git bisect %s %s", term, ref), + ). + IgnoreEmptyError(). + StreamOutput(). + Run() +} + +func (self *BisectCommands) Skip(ref string) error { + return self.Mark(ref, "skip") +} + +func (self *BisectCommands) Start() error { + return self.cmd.New("git bisect start").StreamOutput().Run() +} + +// tells us whether we've found our problem commit(s). We return a string slice of +// commit sha's if we're done, and that slice may have more that one item if +// skipped commits are involved. +func (self *BisectCommands) IsDone() (bool, []string, error) { + info := self.GetInfo() + if !info.Bisecting() { + return false, nil, nil + } + + newSha := info.GetNewSha() + if newSha == "" { + return false, nil, nil + } + + // if we start from the new commit and reach the a good commit without + // coming across any unprocessed commits, then we're done + done := false + candidates := []string{} + + err := self.cmd.New(fmt.Sprintf("git rev-list %s", newSha)).RunAndProcessLines(func(line string) (bool, error) { + sha := strings.TrimSpace(line) + + if status, ok := info.statusMap[sha]; ok { + switch status { + case BisectStatusSkipped, BisectStatusNew: + candidates = append(candidates, sha) + return false, nil + case BisectStatusOld: + done = true + return true, nil + } + } else { + return true, nil + } + + // should never land here + return true, nil + }) + if err != nil { + return false, nil, err + } + + return done, candidates, nil +} diff --git a/pkg/commands/git_commands/bisect_info.go b/pkg/commands/git_commands/bisect_info.go new file mode 100644 index 000000000..9b812a17f --- /dev/null +++ b/pkg/commands/git_commands/bisect_info.go @@ -0,0 +1,103 @@ +package git_commands + +import "github.com/sirupsen/logrus" + +// although the typical terms in a git bisect are 'bad' and 'good', they're more +// generally known as 'new' and 'old'. Semi-recently git allowed the user to define +// their own terms e.g. when you want to used 'fixed', 'unfixed' in the event +// that you're looking for a commit that fixed a bug. + +// Git bisect only keeps track of a single 'bad' commit. Once you pick a commit +// that's older than the current bad one, it forgets about the previous one. On +// the other hand, it does keep track of all the good and skipped commits. + +type BisectInfo struct { + log *logrus.Entry + + // tells us whether all our git bisect files are there meaning we're in bisect mode. + // Doesn't necessarily mean that we've actually picked a good/bad commit yet. + started bool + + // this is the ref you started the commit from + start string // this will always be defined + + // these will be defined if we've started + newTerm string // 'bad' by default + oldTerm string // 'good' by default + + // map of commit sha's to their status + statusMap map[string]BisectStatus + + // the sha of the commit that's under test + current string +} + +type BisectStatus int + +const ( + BisectStatusOld BisectStatus = iota + BisectStatusNew + BisectStatusSkipped +) + +// null object pattern +func NewNullBisectInfo() *BisectInfo { + return &BisectInfo{started: false} +} + +func (self *BisectInfo) GetNewSha() string { + for sha, status := range self.statusMap { + if status == BisectStatusNew { + return sha + } + } + + return "" +} + +func (self *BisectInfo) GetCurrentSha() string { + return self.current +} + +func (self *BisectInfo) StartSha() string { + return self.start +} + +func (self *BisectInfo) Status(commitSha string) (BisectStatus, bool) { + status, ok := self.statusMap[commitSha] + return status, ok +} + +func (self *BisectInfo) NewTerm() string { + return self.newTerm +} + +func (self *BisectInfo) OldTerm() string { + return self.oldTerm +} + +// this is for when we have called `git bisect start`. It does not +// mean that we have actually started narrowing things down or selecting good/bad commits +func (self *BisectInfo) Started() bool { + return self.started +} + +// this is where we have both a good and bad revision and we're actually +// starting to narrow things down +func (self *BisectInfo) Bisecting() bool { + if !self.Started() { + return false + } + + if self.GetNewSha() == "" { + return false + } + + for _, status := range self.statusMap { + if status == BisectStatusOld { + return true + } + } + + return false +} diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 2858c100a..38f50bdda 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -75,7 +75,19 @@ func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) { } func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error) { - return self.cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).DontLog().RunWithOutput() + return self.GetCommitMessagesFirstLine([]string{sha}) +} + +func (self *CommitCommands) GetCommitMessagesFirstLine(shas []string) (string, error) { + return self.cmd.New( + fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", strings.Join(shas, " ")), + ).DontLog().RunWithOutput() +} + +func (self *CommitCommands) GetCommitsOneline(shas []string) (string, error) { + return self.cmd.New( + fmt.Sprintf("git show --no-patch --oneline %s", strings.Join(shas, " ")), + ).DontLog().RunWithOutput() } // AmendHead amends HEAD with whatever is staged in your working tree diff --git a/pkg/commands/models/commit.go b/pkg/commands/models/commit.go index 1b8659e77..330c525dc 100644 --- a/pkg/commands/models/commit.go +++ b/pkg/commands/models/commit.go @@ -1,6 +1,10 @@ package models -import "fmt" +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/utils" +) // Commit : A git commit type Commit struct { @@ -18,10 +22,7 @@ type Commit struct { } func (c *Commit) ShortSha() string { - if len(c.Sha) < 8 { - return c.Sha - } - return c.Sha[:8] + return utils.ShortSha(c.Sha) } func (c *Commit) RefName() string { diff --git a/pkg/commands/oscommands/cmd_obj.go b/pkg/commands/oscommands/cmd_obj.go index bc1e9dc74..3e55359de 100644 --- a/pkg/commands/oscommands/cmd_obj.go +++ b/pkg/commands/oscommands/cmd_obj.go @@ -35,6 +35,18 @@ type ICmdObj interface { // This returns false if DontLog() was called ShouldLog() bool + // when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel) + StreamOutput() ICmdObj + // returns true if StreamOutput() was called + ShouldStreamOutput() bool + + // if you call this before ShouldStreamOutput we'll consider an error with no + // stderr content as a non-error. Not yet supported for Run or RunWithOutput ( + // but adding support is trivial) + IgnoreEmptyError() ICmdObj + // returns true if IgnoreEmptyError() was called + ShouldIgnoreEmptyError() bool + PromptOnCredentialRequest() ICmdObj FailOnCredentialRequest() ICmdObj @@ -47,9 +59,15 @@ type CmdObj struct { runner ICmdObjRunner - // if set to true, we don't want to log the command to the user. + // see DontLog() dontLog bool + // see StreamOutput() + streamOutput bool + + // see IgnoreEmptyError() + ignoreEmptyError bool + // if set to true, it means we might be asked to enter a username/password by this command. credentialStrategy CredentialStrategy } @@ -98,6 +116,26 @@ func (self *CmdObj) ShouldLog() bool { return !self.dontLog } +func (self *CmdObj) StreamOutput() ICmdObj { + self.streamOutput = true + + return self +} + +func (self *CmdObj) ShouldStreamOutput() bool { + return self.streamOutput +} + +func (self *CmdObj) IgnoreEmptyError() ICmdObj { + self.ignoreEmptyError = true + + return self +} + +func (self *CmdObj) ShouldIgnoreEmptyError() bool { + return self.ignoreEmptyError +} + func (self *CmdObj) Run() error { return self.runner.Run(self) } diff --git a/pkg/commands/oscommands/cmd_obj_runner.go b/pkg/commands/oscommands/cmd_obj_runner.go index 37f46e76d..e1a38d80f 100644 --- a/pkg/commands/oscommands/cmd_obj_runner.go +++ b/pkg/commands/oscommands/cmd_obj_runner.go @@ -34,15 +34,27 @@ type cmdObjRunner struct { var _ ICmdObjRunner = &cmdObjRunner{} func (self *cmdObjRunner) Run(cmdObj ICmdObj) error { - if cmdObj.GetCredentialStrategy() == NONE { - _, err := self.RunWithOutput(cmdObj) - return err - } else { + if cmdObj.GetCredentialStrategy() != NONE { return self.runWithCredentialHandling(cmdObj) } + + if cmdObj.ShouldStreamOutput() { + return self.runAndStream(cmdObj) + } + + _, err := self.RunWithOutput(cmdObj) + return err } func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) { + if cmdObj.ShouldStreamOutput() { + err := self.runAndStream(cmdObj) + // for now we're not capturing output, just because it would take a little more + // effort and there's currently no use case for it. Some commands call RunWithOutput + // but ignore the output, hence why we've got this check here. + return "", err + } + if cmdObj.GetCredentialStrategy() != NONE { err := self.runWithCredentialHandling(cmdObj) // for now we're not capturing output, just because it would take a little more @@ -145,6 +157,14 @@ type cmdHandler struct { close func() error } +func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error { + return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) { + go func() { + _, _ = io.Copy(cmdWriter, handler.stdoutPipe) + }() + }) +} + // runAndDetectCredentialRequest detect a username / password / passphrase question in a command // promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase // The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back @@ -152,13 +172,29 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest( cmdObj ICmdObj, promptUserForCredential func(CredentialType) string, ) error { + // setting the output to english so we can parse it for a username/password request + cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8") + + return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) { + tr := io.TeeReader(handler.stdoutPipe, cmdWriter) + + go utils.Safe(func() { + self.processOutput(tr, handler.stdinPipe, promptUserForCredential) + }) + }) +} + +func (self *cmdObjRunner) runAndStreamAux( + cmdObj ICmdObj, + onRun func(*cmdHandler, io.Writer), +) error { cmdWriter := self.guiIO.newCmdWriterFn() if cmdObj.ShouldLog() { self.logCmdObj(cmdObj) } self.log.WithField("command", cmdObj.ToString()).Info("RunCommand") - cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd() + cmd := cmdObj.GetCmd() var stderr bytes.Buffer cmd.Stderr = io.MultiWriter(cmdWriter, &stderr) @@ -174,14 +210,14 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest( } }() - tr := io.TeeReader(handler.stdoutPipe, cmdWriter) - - go utils.Safe(func() { - self.processOutput(tr, handler.stdinPipe, promptUserForCredential) - }) + onRun(handler, cmdWriter) err = cmd.Wait() if err != nil { + errStr := stderr.String() + if cmdObj.ShouldIgnoreEmptyError() && errStr == "" { + return nil + } return errors.New(stderr.String()) } diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 8c1d90a0e..2b79f941f 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -247,6 +247,7 @@ type KeybindingCommitsConfig struct { CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"` OpenLogMenu string `yaml:"openLogMenu"` OpenInBrowser string `yaml:"openInBrowser"` + ViewBisectOptions string `yaml:"viewBisectOptions"` } type KeybindingStashConfig struct { @@ -293,6 +294,7 @@ type CustomCommand struct { Prompts []CustomCommandPrompt `yaml:"prompts"` LoadingText string `yaml:"loadingText"` Description string `yaml:"description"` + Stream bool `yaml:"stream"` } type CustomCommandPrompt struct { @@ -510,6 +512,7 @@ func GetDefaultConfig() *UserConfig { CopyCommitMessageToClipboard: "<c-y>", OpenLogMenu: "<c-l>", OpenInBrowser: "o", + ViewBisectOptions: "b", }, Stash: KeybindingStashConfig{ PopStash: "g", diff --git a/pkg/gui/bisect.go b/pkg/gui/bisect.go new file mode 100644 index 000000000..ca00ce87f --- /dev/null +++ b/pkg/gui/bisect.go @@ -0,0 +1,204 @@ +package gui + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" +) + +func (gui *Gui) handleOpenBisectMenu() error { + if ok, err := gui.validateNotInFilterMode(); err != nil || !ok { + return err + } + + // no shame in getting this directly rather than using the cached value + // given how cheap it is to obtain + info := gui.Git.Bisect.GetInfo() + commit := gui.getSelectedLocalCommit() + if info.Started() { + return gui.openMidBisectMenu(info, commit) + } else { + return gui.openStartBisectMenu(info, commit) + } +} + +func (gui *Gui) 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 + + menuItems := []*menuItem{ + { + displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()), + onPress: func() error { + gui.logAction(gui.Tr.Actions.BisectMark) + if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { + return gui.surfaceError(err) + } + + return gui.afterMark(selectCurrentAfter) + }, + }, + { + displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()), + onPress: func() error { + gui.logAction(gui.Tr.Actions.BisectMark) + if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { + return gui.surfaceError(err) + } + + return gui.afterMark(selectCurrentAfter) + }, + }, + { + displayString: fmt.Sprintf(gui.Tr.Bisect.Skip, commit.ShortSha()), + onPress: func() error { + gui.logAction(gui.Tr.Actions.BisectSkip) + if err := gui.Git.Bisect.Skip(commit.Sha); err != nil { + return gui.surfaceError(err) + } + + return gui.afterMark(selectCurrentAfter) + }, + }, + { + displayString: gui.Tr.Bisect.ResetOption, + onPress: func() error { + return gui.resetBisect() + }, + }, + } + + return gui.createMenu( + gui.Tr.Bisect.BisectMenuTitle, + menuItems, + createMenuOptions{showCancel: true}, + ) +} + +func (gui *Gui) openStartBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error { + return gui.createMenu( + gui.Tr.Bisect.BisectMenuTitle, + []*menuItem{ + { + displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()), + onPress: func() error { + gui.logAction(gui.Tr.Actions.StartBisect) + if err := gui.Git.Bisect.Start(); err != nil { + return gui.surfaceError(err) + } + + if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { + return gui.surfaceError(err) + } + + return gui.postBisectCommandRefresh() + }, + }, + { + displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()), + onPress: func() error { + gui.logAction(gui.Tr.Actions.StartBisect) + if err := gui.Git.Bisect.Start(); err != nil { + return gui.surfaceError(err) + } + + if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { + return gui.surfaceError(err) + } + + return gui.postBisectCommandRefresh() + }, + }, + }, + createMenuOptions{showCancel: true}, + ) +} + +func (gui *Gui) resetBisect() error { + return gui.ask(askOpts{ + title: gui.Tr.Bisect.ResetTitle, + prompt: gui.Tr.Bisect.ResetPrompt, + handleConfirm: func() error { + gui.logAction(gui.Tr.Actions.ResetBisect) + if err := gui.Git.Bisect.Reset(); err != nil { + return gui.surfaceError(err) + } + + return gui.postBisectCommandRefresh() + }, + }) +} + +func (gui *Gui) showBisectCompleteMessage(candidateShas []string) error { + prompt := gui.Tr.Bisect.CompletePrompt + if len(candidateShas) > 1 { + prompt = gui.Tr.Bisect.CompletePromptIndeterminate + } + + formattedCommits, err := gui.Git.Commit.GetCommitsOneline(candidateShas) + if err != nil { + return gui.surfaceError(err) + } + + return gui.ask(askOpts{ + title: gui.Tr.Bisect.CompleteTitle, + prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)), + handleConfirm: func() error { + gui.logAction(gui.Tr.Actions.ResetBisect) + if err := gui.Git.Bisect.Reset(); err != nil { + return gui.surfaceError(err) + } + + return gui.postBisectCommandRefresh() + }, + }) +} + +func (gui *Gui) afterMark(selectCurrent bool) error { + done, candidateShas, err := gui.Git.Bisect.IsDone() + if err != nil { + return gui.surfaceError(err) + } + + if err := gui.postBisectCommandRefresh(); err != nil { + return gui.surfaceError(err) + } + + if done { + return gui.showBisectCompleteMessage(candidateShas) + } + + if selectCurrent { + gui.selectCurrentBisectCommit() + } + + return nil +} + +func (gui *Gui) selectCurrentBisectCommit() { + info := gui.Git.Bisect.GetInfo() + if info.GetCurrentSha() != "" { + // find index of commit with that sha, move cursor to that. + for i, commit := range gui.State.Commits { + if commit.Sha == info.GetCurrentSha() { + gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(i) + _ = gui.State.Contexts.BranchCommits.HandleFocus() + break + } + } + } +} + +func (gui *Gui) postBisectCommandRefresh() error { + return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{}}) +} diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 0f63e4157..d568caf88 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -116,12 +116,19 @@ func (gui *Gui) refreshCommitsWithLimit() error { gui.Mutexes.BranchCommitsMutex.Lock() defer gui.Mutexes.BranchCommitsMutex.Unlock() + refName := "HEAD" + bisectInfo := gui.Git.Bisect.GetInfo() + gui.State.BisectInfo = bisectInfo + if bisectInfo.Started() { + refName = bisectInfo.StartSha() + } + commits, err := gui.Git.Loaders.Commits.GetCommits( loaders.GetCommitsOptions{ Limit: gui.State.Panels.Commits.LimitCommits, FilterPath: gui.State.Modes.Filtering.GetPath(), IncludeRebaseCommits: true, - RefName: "HEAD", + RefName: refName, All: gui.State.ShowWholeGitGraph, }, ) diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index c55b58a02..02293dd65 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -254,7 +254,11 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand } return gui.WithWaitingStatus(loadingText, func() error { gui.logAction(gui.Tr.Actions.CustomCommand) - err := gui.OSCommand.Cmd.NewShell(cmdStr).Run() + cmdObj := gui.OSCommand.Cmd.NewShell(cmdStr) + if customCommand.Stream { + cmdObj.StreamOutput() + } + err := cmdObj.Run() if err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/filetree/commit_file_manager.go b/pkg/gui/filetree/commit_file_manager.go index e80528bcb..852a67b09 |