diff --git a/docs/Config.md b/docs/Config.md index 6dea632fc..ab701623c 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -205,6 +205,7 @@ keybinding: resetCherryPick: '' copyCommitMessageToClipboard: '' openLogMenu: '' + viewBisectOptions: 'b' stash: popStash: 'g' commitFiles: @@ -389,6 +390,7 @@ gui: ``` You can use wildcard to set a unified color in case your are lazy to customize the color for every author or you just want a single color for all/other authors: + ```yaml gui: authorColors: diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index 8b0c769c2..1dd27fef3 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -69,6 +69,7 @@ For a given custom command, here are the allowed fields: | prompts | a list of prompts that will request user input before running the final command | no | | loadingText | text to display while waiting for command to finish | no | | description | text to display in the keybindings menu that appears when you press 'x' | no | +| stream | whether you want to stream the command's output to the Command Log panel | no | ### Contexts diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index 53f3db648..f0b2f78a8 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -149,6 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: reset cherry-picked (copied) commits selection ctrl+y: copy commit message to clipboard o: open commit in browser + b: view bisect options ## Commits Panel (Reflog Tab) diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 81a9a2ba9..b0a9966cc 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -149,6 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: reset cherry-picked (gekopieerde) commits selectie ctrl+y: kopieer commit bericht naar klembord o: open commit in browser + b: view bisect options ## Commits Paneel (Reflog Tabblad) diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 88dc6841c..7384c97fa 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -149,6 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: reset cherry-picked (copied) commits selection ctrl+y: copy commit message to clipboard o: open commit in browser + b: view bisect options ## Commity Panel (Reflog Tab) diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md index d978c561c..b482aaf40 100644 --- a/docs/keybindings/Keybindings_zh.md +++ b/docs/keybindings/Keybindings_zh.md @@ -149,6 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: 重置已拣选(复制)的提交 ctrl+y: 将提交消息复制到剪贴板 o: open commit in browser + b: view bisect options ## 提交 面板 (Reflog) 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,12 +157,36 @@ 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 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() @@ -158,7 +194,7 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest( 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: "", OpenLogMenu: "", 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 100644 --- a/pkg/gui/filetree/commit_file_manager.go +++ b/pkg/gui/filetree/commit_file_manager.go @@ -3,7 +3,6 @@ package filetree import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/patch" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/sirupsen/logrus" ) @@ -114,6 +113,6 @@ func (m *CommitFileManager) Render(diffName string, patchManager *patch.PatchMan status = patch.PART } - return presentation.GetCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status) + return getCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status) }) } diff --git a/pkg/gui/filetree/file_manager.go b/pkg/gui/filetree/file_manager.go index 699e20332..b028ef961 100644 --- a/pkg/gui/filetree/file_manager.go +++ b/pkg/gui/filetree/file_manager.go @@ -4,7 +4,6 @@ import ( "sync" "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/sirupsen/logrus" ) @@ -136,6 +135,6 @@ func (m *FileManager) Render(diffName string, submoduleConfigs []*models.Submodu return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string { castN := n.(*FileNode) - return presentation.GetFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File) + return getFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File) }) } diff --git a/pkg/gui/presentation/files.go b/pkg/gui/filetree/presentation.go similarity index 56% rename from pkg/gui/presentation/files.go rename to pkg/gui/filetree/presentation.go index 4d00b7e84..6cecb78ad 100644 --- a/pkg/gui/presentation/files.go +++ b/pkg/gui/filetree/presentation.go @@ -1,13 +1,16 @@ -package presentation +package filetree import ( "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" ) -func GetFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string { +// TODO: move this back into presentation package and fix the import cycle + +func getFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string { // potentially inefficient to be instantiating these color // objects with each render partiallyModifiedColor := style.FgYellow @@ -51,3 +54,43 @@ func GetFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, di return output } + +func getCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) string { + var colour style.TextStyle + if diffName == name { + colour = theme.DiffTerminalColor + } else { + switch status { + case patch.WHOLE: + colour = style.FgGreen + case patch.PART: + colour = style.FgYellow + case patch.UNSELECTED: + colour = theme.DefaultTextColor + } + } + + name = utils.EscapeSpecialChars(name) + if commitFile == nil { + return colour.Sprint(name) + } + + return getColorForChangeStatus(commitFile.ChangeStatus).Sprint(commitFile.ChangeStatus) + " " + colour.Sprint(name) +} + +func getColorForChangeStatus(changeStatus string) style.TextStyle { + switch changeStatus { + case "A": + return style.FgGreen + case "M", "R": + return style.FgYellow + case "D": + return style.FgRed + case "C": + return style.FgCyan + case "T": + return style.FgMagenta + default: + return theme.DefaultTextColor + } +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 50291c8bf..731e56b5b 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -12,6 +12,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_config" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" @@ -309,6 +310,7 @@ type guiState struct { RemoteBranches []*models.RemoteBranch Tags []*models.Tag MenuItems []*menuItem + BisectInfo *git_commands.BisectInfo Updating bool Panels *panelStates SplitMainPanel bool @@ -394,6 +396,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) { FilteredReflogCommits: make([]*models.Commit, 0), ReflogCommits: make([]*models.Commit, 0), StashEntries: make([]*models.StashEntry, 0), + BisectInfo: gui.Git.Bisect.GetInfo(), Panels: &panelStates{ // TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now Files: &filePanelState{listPanelState{SelectedLineIdx: -1}}, diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index a3e998416..584a276bb 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -908,6 +908,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleOpenCommitInBrowser, Description: gui.Tr.LcOpenCommitInBrowser, }, + { + ViewName: "commits", + Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)}, + Key: gui.getKey(config.Commits.ViewBisectOptions), + Handler: gui.handleOpenBisectMenu, + Description: gui.Tr.LcViewBisectOptions, + OpensMenu: true, + }, { ViewName: "commits", Contexts: []string{string(REFLOG_COMMITS_CONTEXT_KEY)}, diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go index e5ad6c034..f05375b67 100644 --- a/pkg/gui/list_context_config.go +++ b/pkg/gui/list_context_config.go @@ -4,6 +4,7 @@ import ( "log" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/style" ) @@ -177,6 +178,7 @@ func (gui *Gui) branchCommitsListContext() IListContext { startIdx, length, gui.shouldShowGraph(), + gui.State.BisectInfo, ) }, SelectedItem: func() (ListItem, bool) { @@ -218,6 +220,7 @@ func (gui *Gui) subCommitsListContext() IListContext { startIdx, length, gui.shouldShowGraph(), + git_commands.NewNullBisectInfo(), ) }, SelectedItem: func() (ListItem, bool) { diff --git a/pkg/gui/modes.go b/pkg/gui/modes.go index 1f92d1c84..b60fafc8a 100644 --- a/pkg/gui/modes.go +++ b/pkg/gui/modes.go @@ -1,6 +1,8 @@ package gui import ( + "fmt" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/gui/style" ) @@ -16,11 +18,13 @@ func (gui *Gui) modeStatuses() []modeStatus { { isActive: gui.State.Modes.Diffing.Active, description: func() string { - return style.FgMagenta.Sprintf( - "%s %s %s", - gui.Tr.LcShowingGitDiff, - "git diff "+gui.diffStr(), - style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses), + return gui.withResetButton( + fmt.Sprintf( + "%s %s", + gui.Tr.LcShowingGitDiff, + "git diff "+gui.diffStr(), + ), + style.FgMagenta, ) }, reset: gui.exitDiffMode, @@ -28,22 +32,20 @@ func (gui *Gui) modeStatuses() []modeStatus { { isActive: gui.Git.Patch.PatchManager.Active, description: func() string { - return style.FgYellow.SetBold().Sprintf( - "%s %s", - gui.Tr.LcBuildingPatch, - style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses), - ) + return gui.withResetButton(gui.Tr.LcBuildingPatch, style.FgYellow.SetBold()) }, reset: gui.handleResetPatch, }, { isActive: gui.State.Modes.Filtering.Active, description: func() string { - return style.FgRed.SetBold().Sprintf( - "%s '%s' %s", - gui.Tr.LcFilteringBy, - gui.State.Modes.Filtering.GetPath(), - style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses), + return gui.withResetButton( + fmt.Sprintf( + "%s '%s'", + gui.Tr.LcFilteringBy, + gui.State.Modes.Filtering.GetPath(), + ), + style.FgRed, ) }, reset: gui.exitFilterMode, @@ -51,10 +53,12 @@ func (gui *Gui) modeStatuses() []modeStatus { { isActive: gui.State.Modes.CherryPicking.Active, description: func() string { - return style.FgCyan.Sprintf( - "%d commits copied %s", - len(gui.State.Modes.CherryPicking.CherryPickedCommits), - style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses), + return gui.withResetButton( + fmt.Sprintf( + "%d commits copied", + len(gui.State.Modes.CherryPicking.CherryPickedCommits), + ), + style.FgCyan, ) }, reset: gui.exitCherryPickingMode, @@ -65,13 +69,28 @@ func (gui *Gui) modeStatuses() []modeStatus { }, description: func() string { workingTreeState := gui.Git.Status.WorkingTreeState() - return style.FgYellow.Sprintf( - "%s %s", - formatWorkingTreeState(workingTreeState), - style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses), + return gui.withResetButton( + formatWorkingTreeState(workingTreeState), style.FgYellow, ) }, reset: gui.abortMergeOrRebaseWithConfirm, }, + { + isActive: func() bool { + return gui.State.BisectInfo.Started() + }, + description: func() string { + return gui.withResetButton("bisecting", style.FgGreen) + }, + reset: gui.resetBisect, + }, } } + +func (gui *Gui) withResetButton(content string, textStyle style.TextStyle) string { + return textStyle.Sprintf( + "%s %s", + content, + style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses), + ) +} diff --git a/pkg/gui/presentation/commit_files.go b/pkg/gui/presentation/commit_files.go deleted file mode 100644 index 6a351be48..000000000 --- a/pkg/gui/presentation/commit_files.go +++ /dev/null @@ -1,49 +0,0 @@ -package presentation - -import ( - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/commands/patch" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/theme" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func GetCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) string { - var colour style.TextStyle - if diffName == name { - colour = theme.DiffTerminalColor - } else { - switch status { - case patch.WHOLE: - colour = style.FgGreen - case patch.PART: - colour = style.FgYellow - case patch.UNSELECTED: - colour = theme.DefaultTextColor - } - } - - name = utils.EscapeSpecialChars(name) - if commitFile == nil { - return colour.Sprint(name) - } - - return getColorForChangeStatus(commitFile.ChangeStatus).Sprint(commitFile.ChangeStatus) + " " + colour.Sprint(name) -} - -func getColorForChangeStatus(changeStatus string) style.TextStyle { - switch changeStatus { - case "A": - return style.FgGreen - case "M", "R": - return style.FgYellow - case "D": - return style.FgRed - case "C": - return style.FgCyan - case "T": - return style.FgMagenta - default: - return theme.DefaultTextColor - } -} diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index 68cd554fa..0bc84d5e8 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -4,6 +4,7 @@ import ( "strings" "sync" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/presentation/authors" "github.com/jesseduffield/lazygit/pkg/gui/presentation/graph" @@ -21,6 +22,14 @@ type pipeSetCacheKey struct { var pipeSetCache = make(map[pipeSetCacheKey][][]*graph.Pipe) var mutex sync.Mutex +type BisectProgress int + +const ( + BeforeNewCommit BisectProgress = iota + InbetweenCommits + AfterOldCommit +) + func GetCommitListDisplayStrings( commits []*models.Commit, fullDescription bool, @@ -31,6 +40,7 @@ func GetCommitListDisplayStrings( startIdx int, length int, showGraph bool, + bisectInfo *git_commands.BisectInfo, ) [][]string { mutex.Lock() defer mutex.Unlock() @@ -77,12 +87,94 @@ func GetCommitListDisplayStrings( } lines := make([][]string, 0, len(filteredCommits)) + bisectProgress := BeforeNewCommit + var bisectStatus BisectStatus for i, commit := range filteredCommits { - lines = append(lines, displayCommit(commit, cherryPickedCommitShaMap, diffName, parseEmoji, getGraphLine(i), fullDescription)) + bisectStatus, bisectProgress = getBisectStatus(commit.Sha, bisectInfo, bisectProgress) + lines = append(lines, displayCommit( + commit, + cherryPickedCommitShaMap, + diffName, + parseEmoji, + getGraphLine(i), + fullDescription, + bisectStatus, + bisectInfo, + )) } return lines } +// similar to the git_commands.BisectStatus but more gui-focused +type BisectStatus int + +const ( + BisectStatusNone BisectStatus = iota + BisectStatusOld + BisectStatusNew + BisectStatusSkipped + // adding candidate here which isn't present in the commands package because + // we need to actually go through the commits to get this info + BisectStatusCandidate + // also adding this + BisectStatusCurrent +) + +func getBisectStatus(commitSha string, bisectInfo *git_commands.BisectInfo, bisectProgress BisectProgress) (BisectStatus, BisectProgress) { + if !bisectInfo.Started() { + return BisectStatusNone, bisectProgress + } + + if bisectInfo.GetCurrentSha() == commitSha { + return BisectStatusCurrent, bisectProgress + } + + status, ok := bisectInfo.Status(commitSha) + if ok { + switch status { + case git_commands.BisectStatusNew: + return BisectStatusNew, InbetweenCommits + case git_commands.BisectStatusOld: + return BisectStatusOld, AfterOldCommit + case git_commands.BisectStatusSkipped: + return BisectStatusSkipped, bisectProgress + } + } else { + if bisectProgress == InbetweenCommits { + return BisectStatusCandidate, bisectProgress + } else { + return BisectStatusNone, bisectProgress + } + } + + // should never land here + return BisectStatusNone, bisectProgress +} + +func getBisectStatusText(bisectStatus BisectStatus, bisectInfo *git_commands.BisectInfo) string { + if bisectStatus == BisectStatusNone { + return "" + } + + style := getBisectStatusColor(bisectStatus) + + switch bisectStatus { + case BisectStatusNew: + return style.Sprintf("<-- " + bisectInfo.NewTerm()) + case BisectStatusOld: + return style.Sprintf("<-- " + bisectInfo.OldTerm()) + case BisectStatusCurrent: + // TODO: i18n + return style.Sprintf("<-- current") + case BisectStatusSkipped: + return style.Sprintf("<-- skipped") + case BisectStatusCandidate: + return style.Sprintf("?") + } + + return "" +} + func displayCommit( commit *models.Commit, cherryPickedCommitShaMap map[string]bool, @@ -90,9 +182,11 @@ func displayCommit( parseEmoji bool, graphLine string, fullDescription bool, + bisectStatus BisectStatus, + bisectInfo *git_commands.BisectInfo, ) []string { - - shaColor := getShaColor(commit, diffName, cherryPickedCommitShaMap) + shaColor := getShaColor(commit, diffName, cherryPickedCommitShaMap, bisectStatus, bisectInfo) + bisectString := getBisectStatusText(bisectStatus, bisectInfo) actionString := "" if commit.Action != "" { @@ -122,6 +216,7 @@ func displayCommit( cols := make([]string, 0, 5) cols = append(cols, shaColor.Sprint(commit.ShortSha())) + cols = append(cols, bisectString) if fullDescription { cols = append(cols, style.FgBlue.Sprint(utils.UnixToDate(commit.UnixTimestamp))) } @@ -133,10 +228,39 @@ func displayCommit( ) return cols - } -func getShaColor(commit *models.Commit, diffName string, cherryPickedCommitShaMap map[string]bool) style.TextStyle { +func getBisectStatusColor(status BisectStatus) style.TextStyle { + switch status { + case BisectStatusNone: + return style.FgBlack + case BisectStatusNew: + return style.FgRed + case BisectStatusOld: + return style.FgGreen + case BisectStatusSkipped: + return style.FgYellow + case BisectStatusCurrent: + return style.FgMagenta + case BisectStatusCandidate: + return style.FgBlue + } + + // shouldn't land here + return style.FgWhite +} + +func getShaColor( + commit *models.Commit, + diffName string, + cherryPickedCommitShaMap map[string]bool, + bisectStatus BisectStatus, + bisectInfo *git_commands.BisectInfo, +) style.TextStyle { + if bisectInfo.Started() { + return getBisectStatusColor(bisectStatus) + } + diffed := commit.Sha == diffName shaColor := theme.DefaultTextColor switch commit.Status { diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 3f559b400..300571ad3 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -28,6 +28,8 @@ const ( REMOTES STATUS SUBMODULES + // not actually a view. Will refactor this later + BISECT_INFO ) func getScopeNames(scopes []RefreshableView) []string { @@ -105,12 +107,12 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error { f := func() { var scopeMap map[RefreshableView]bool if len(options.scope) == 0 { - scopeMap = arrToMap([]RefreshableView{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS}) + scopeMap = arrToMap([]RefreshableView{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS, BISECT_INFO}) } else { scopeMap = arrToMap(options.scope) } - if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] { + if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] || scopeMap[BISECT_INFO] { wg.Add(1) func() { if options.mode == ASYNC { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index b815b6b70..8899cc6fc 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -453,7 +453,24 @@ type TranslationSet struct { SortCommits string CantChangeContextSizeError string LcOpenCommitInBrowser string + LcViewBisectOptions string Actions Actions + Bisect Bisect +} + +type Bisect struct { + MarkStart string + MarkSkipCurrent string + MarkSkipSelected string + ResetTitle string + ResetPrompt string + ResetOption string + BisectMenuTitle string + Mark string + Skip string + CompleteTitle string + CompletePrompt string + CompletePromptIndeterminate string } type Actions struct { @@ -541,6 +558,10 @@ type Actions struct { OpenMergeTool string OpenCommitInBrowser string OpenPullRequest string + StartBisect string + ResetBisect string + BisectSkip string + BisectMark string } const englishIntroPopupMessage = ` @@ -1005,6 +1026,7 @@ func EnglishTranslationSet() TranslationSet { SortCommits: "commit sort order", CantChangeContextSizeError: "Cannot change context while in patch building mode because we were too lazy to support it when releasing the feature. If you really want it, please let us know!", LcOpenCommitInBrowser: "open commit in browser", + LcViewBisectOptions: "view bisect options", Actions: Actions{ // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) CheckoutCommit: "Checkout commit", @@ -1091,6 +1113,22 @@ func EnglishTranslationSet() TranslationSet { OpenMergeTool: "Open merge tool", OpenCommitInBrowser: "Open commit in browser", OpenPullRequest: "Open pull request in browser", + StartBisect: "Start bisect", + ResetBisect: "Reset bisect", + BisectSkip: "Bisect skip", + BisectMark: "Bisect mark", + }, + Bisect: Bisect{ + Mark: "mark %s as %s", + MarkStart: "mark %s as %s (start bisect)", + Skip: "skip %s", + ResetTitle: "Reset 'git bisect'", + ResetPrompt: "Are you sure you want to reset 'git bisect'?", + ResetOption: "reset bisect", + BisectMenuTitle: "Bisect", + CompleteTitle: "Bisect complete", + CompletePrompt: "Bisect complete! The following commit introduced the change:\n\n%s\n\nDo you want to reset 'git bisect' now?", + CompletePromptIndeterminate: "Bisect complete! Some commits were skipped, so any of the following commits may have introduced the change:\n\n%s\n\nDo you want to reset 'git bisect' now?", }, } } diff --git a/pkg/utils/formatting.go b/pkg/utils/formatting.go index 80717bfad..d33028063 100644 --- a/pkg/utils/formatting.go +++ b/pkg/utils/formatting.go @@ -121,3 +121,10 @@ func SafeTruncate(str string, limit int) string { return str } } + +func ShortSha(sha string) string { + if len(sha) < 8 { + return sha + } + return sha[:8] +} diff --git a/test/integration/bisect/expected/.git_keep/BISECT_ANCESTORS_OK b/test/integration/bisect/expected/.git_keep/BISECT_ANCESTORS_OK new file mode 100644 index 000000000..e69de29bb diff --git a/test/integration/bisect/expected/.git_keep/BISECT_EXPECTED_REV b/test/integration/bisect/expected/.git_keep/BISECT_EXPECTED_REV new file mode 100644 index 000000000..f8c370a35 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/BISECT_EXPECTED_REV @@ -0,0 +1 @@ +dd9d90ed2d1fa5a284adba081199f18458977547 diff --git a/test/integration/bisect/expected/.git_keep/BISECT_LOG b/test/integration/bisect/expected/.git_keep/BISECT_LOG new file mode 100644 index 000000000..9d0807f9e --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/BISECT_LOG @@ -0,0 +1,16 @@ +git bisect start +# bad: [1e2780095cd8e95b93f89268f72cda21d528ab38] commit 19 +git bisect bad 1e2780095cd8e95b93f89268f72cda21d528ab38 +# good: [fbbb6006074afe8cc9009b649fae19f920b604ea] commit 10 +git bisect good fbbb6006074afe8cc9009b649fae19f920b604ea +# skip: [1b06712fea4c03c8fce8e2b3862c059f8d7f8268] commit 11 +git bisect skip 1b06712fea4c03c8fce8e2b3862c059f8d7f8268 +# skip: [ee930b55b61910c0830b0c6ea1cf9ada066d27fc] commit 12 +git bisect skip ee930b55b61910c0830b0c6ea1cf9ada066d27fc +# bad: [688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed] commit 15 +git bisect bad 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed +# good: [ac324deab67f1749eeec1531b37dfaff41e559ca] commit 13 +git bisect good ac324deab67f1749eeec1531b37dfaff41e559ca +# bad: [dd9d90ed2d1fa5a284adba081199f18458977547] commit 14 +git bisect bad dd9d90ed2d1fa5a284adba081199f18458977547 +# first bad commit: [dd9d90ed2d1fa5a284adba081199f18458977547] commit 14 diff --git a/test/integration/bisect/expected/.git_keep/BISECT_NAMES b/test/integration/bisect/expected/.git_keep/BISECT_NAMES new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/BISECT_NAMES @@ -0,0 +1 @@ + diff --git a/test/integration/bisect/expected/.git_keep/BISECT_START b/test/integration/bisect/expected/.git_keep/BISECT_START new file mode 100644 index 000000000..1f7391f92 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/BISECT_START @@ -0,0 +1 @@ +master diff --git a/test/integration/bisect/expected/.git_keep/BISECT_TERMS b/test/integration/bisect/expected/.git_keep/BISECT_TERMS new file mode 100644 index 000000000..25dd30b14 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/BISECT_TERMS @@ -0,0 +1,2 @@ +bad +good diff --git a/test/integration/bisect/expected/.git_keep/COMMIT_EDITMSG b/test/integration/bisect/expected/.git_keep/COMMIT_EDITMSG new file mode 100644 index 000000000..e6cf6d392 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/COMMIT_EDITMSG @@ -0,0 +1 @@ +commit 20 diff --git a/test/integration/bisect/expected/.git_keep/FETCH_HEAD b/test/integration/bisect/expected/.git_keep/FETCH_HEAD new file mode 100644 index 000000000..e69de29bb diff --git a/test/integration/bisect/expected/.git_keep/HEAD b/test/integration/bisect/expected/.git_keep/HEAD new file mode 100644 index 000000000..cea9d05ed --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/HEAD @@ -0,0 +1 @@ +ref: refs/heads/test diff --git a/test/integration/bisect/expected/.git_keep/config b/test/integration/bisect/expected/.git_keep/config new file mode 100644 index 000000000..8ae104545 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/config @@ -0,0 +1,10 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[user] + email = CI@example.com + name = CI diff --git a/test/integration/bisect/expected/.git_keep/description b/test/integration/bisect/expected/.git_keep/description new file mode 100644 index 000000000..498b267a8 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/test/integration/bisect/expected/.git_keep/index b/test/integration/bisect/expected/.git_keep/index new file mode 100644 index 000000000..61a57e2ef Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/index differ diff --git a/test/integration/bisect/expected/.git_keep/info/exclude b/test/integration/bisect/expected/.git_keep/info/exclude new file mode 100644 index 000000000..8e9f2071f --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/info/exclude @@ -0,0 +1,7 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ +.DS_Store diff --git a/test/integration/bisect/expected/.git_keep/logs/HEAD b/test/integration/bisect/expected/.git_keep/logs/HEAD new file mode 100644 index 000000000..f336115a9 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/logs/HEAD @@ -0,0 +1,25 @@ +0000000000000000000000000000000000000000 b7cd988a171f7f99b7e190ca2b46060074cb379a CI 1642807341 +1100 commit (initial): commit 1 +b7cd988a171f7f99b7e190ca2b46060074cb379a 810c10a66b1dabfe117eecdfb0f638bb1cd0ede5 CI 1642807341 +1100 commit: commit 2 +810c10a66b1dabfe117eecdfb0f638bb1cd0ede5 98c3099431a8777741ea114272919d6645418037 CI 1642807341 +1100 commit: commit 3 +98c3099431a8777741ea114272919d6645418037 f8174a4db5bb2082ebe73b29a47448a300fea7ae CI 1642807342 +1100 commit: commit 4 +f8174a4db5bb2082ebe73b29a47448a300fea7ae 305a009f27eb14858ea0a3a1a740a5346a543537 CI 1642807342 +1100 commit: commit 5 +305a009f27eb14858ea0a3a1a740a5346a543537 5530322be194fc9dea08ef86c9306bddeacb92db CI 1642807342 +1100 commit: commit 6 +5530322be194fc9dea08ef86c9306bddeacb92db 2cdabd5c24e74e22323744543a8ebcbfb33c7f6e CI 1642807342 +1100 commit: commit 7 +2cdabd5c24e74e22323744543a8ebcbfb33c7f6e 42fb40334713a02429d4f8d72f7fe7376caef15b CI 1642807342 +1100 commit: commit 8 +42fb40334713a02429d4f8d72f7fe7376caef15b 27584027b768a0d33ba92ad8784c09589de325b9 CI 1642807342 +1100 commit: commit 9 +27584027b768a0d33ba92ad8784c09589de325b9 fbbb6006074afe8cc9009b649fae19f920b604ea CI 1642807342 +1100 commit: commit 10 +fbbb6006074afe8cc9009b649fae19f920b604ea 1b06712fea4c03c8fce8e2b3862c059f8d7f8268 CI 1642807342 +1100 commit: commit 11 +1b06712fea4c03c8fce8e2b3862c059f8d7f8268 ee930b55b61910c0830b0c6ea1cf9ada066d27fc CI 1642807342 +1100 commit: commit 12 +ee930b55b61910c0830b0c6ea1cf9ada066d27fc ac324deab67f1749eeec1531b37dfaff41e559ca CI 1642807342 +1100 commit: commit 13 +ac324deab67f1749eeec1531b37dfaff41e559ca dd9d90ed2d1fa5a284adba081199f18458977547 CI 1642807342 +1100 commit: commit 14 +dd9d90ed2d1fa5a284adba081199f18458977547 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed CI 1642807342 +1100 commit: commit 15 +688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed 2ce3bf88f382c762d97ac069eea18bed43a1bab2 CI 1642807342 +1100 commit: commit 16 +2ce3bf88f382c762d97ac069eea18bed43a1bab2 c09b924073b6a6cc1b2208f9a00f7b73bec2add2 CI 1642807342 +1100 commit: commit 17 +c09b924073b6a6cc1b2208f9a00f7b73bec2add2 12a951328c3156482355edebf6c81ded5480aff4 CI 1642807342 +1100 commit: commit 18 +12a951328c3156482355edebf6c81ded5480aff4 1e2780095cd8e95b93f89268f72cda21d528ab38 CI 1642807342 +1100 commit: commit 19 +1e2780095cd8e95b93f89268f72cda21d528ab38 1fd41e04d86ee95083d607da4e22abef9a570abc CI 1642807342 +1100 commit: commit 20 +1fd41e04d86ee95083d607da4e22abef9a570abc dd9d90ed2d1fa5a284adba081199f18458977547 CI 1642807346 +1100 checkout: moving from master to dd9d90ed2d1fa5a284adba081199f18458977547 +dd9d90ed2d1fa5a284adba081199f18458977547 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed CI 1642807348 +1100 checkout: moving from dd9d90ed2d1fa5a284adba081199f18458977547 to 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed +688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed ac324deab67f1749eeec1531b37dfaff41e559ca CI 1642807352 +1100 checkout: moving from 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed to ac324deab67f1749eeec1531b37dfaff41e559ca +ac324deab67f1749eeec1531b37dfaff41e559ca dd9d90ed2d1fa5a284adba081199f18458977547 CI 1642807354 +1100 checkout: moving from ac324deab67f1749eeec1531b37dfaff41e559ca to dd9d90ed2d1fa5a284adba081199f18458977547 +dd9d90ed2d1fa5a284adba081199f18458977547 1fd41e04d86ee95083d607da4e22abef9a570abc CI 1642807358 +1100 checkout: moving from dd9d90ed2d1fa5a284adba081199f18458977547 to test diff --git a/test/integration/bisect/expected/.git_keep/logs/refs/heads/master b/test/integration/bisect/expected/.git_keep/logs/refs/heads/master new file mode 100644 index 000000000..4ea0e17aa --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/logs/refs/heads/master @@ -0,0 +1,20 @@ +0000000000000000000000000000000000000000 b7cd988a171f7f99b7e190ca2b46060074cb379a CI 1642807341 +1100 commit (initial): commit 1 +b7cd988a171f7f99b7e190ca2b46060074cb379a 810c10a66b1dabfe117eecdfb0f638bb1cd0ede5 CI 1642807341 +1100 commit: commit 2 +810c10a66b1dabfe117eecdfb0f638bb1cd0ede5 98c3099431a8777741ea114272919d6645418037 CI 1642807341 +1100 commit: commit 3 +98c3099431a8777741ea114272919d6645418037 f8174a4db5bb2082ebe73b29a47448a300fea7ae CI 1642807342 +1100 commit: commit 4 +f8174a4db5bb2082ebe73b29a47448a300fea7ae 305a009f27eb14858ea0a3a1a740a5346a543537 CI 1642807342 +1100 commit: commit 5 +305a009f27eb14858ea0a3a1a740a5346a543537 5530322be194fc9dea08ef86c9306bddeacb92db CI 1642807342 +1100 commit: commit 6 +5530322be194fc9dea08ef86c9306bddeacb92db 2cdabd5c24e74e22323744543a8ebcbfb33c7f6e CI 1642807342 +1100 commit: commit 7 +2cdabd5c24e74e22323744543a8ebcbfb33c7f6e 42fb40334713a02429d4f8d72f7fe7376caef15b CI 1642807342 +1100 commit: commit 8 +42fb40334713a02429d4f8d72f7fe7376caef15b 27584027b768a0d33ba92ad8784c09589de325b9 CI 1642807342 +1100 commit: commit 9 +27584027b768a0d33ba92ad8784c09589de325b9 fbbb6006074afe8cc9009b649fae19f920b604ea CI 1642807342 +1100 commit: commit 10 +fbbb6006074afe8cc9009b649fae19f920b604ea 1b06712fea4c03c8fce8e2b3862c059f8d7f8268 CI 1642807342 +1100 commit: commit 11 +1b06712fea4c03c8fce8e2b3862c059f8d7f8268 ee930b55b61910c0830b0c6ea1cf9ada066d27fc CI 1642807342 +1100 commit: commit 12 +ee930b55b61910c0830b0c6ea1cf9ada066d27fc ac324deab67f1749eeec1531b37dfaff41e559ca CI 1642807342 +1100 commit: commit 13 +ac324deab67f1749eeec1531b37dfaff41e559ca dd9d90ed2d1fa5a284adba081199f18458977547 CI 1642807342 +1100 commit: commit 14 +dd9d90ed2d1fa5a284adba081199f18458977547 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed CI 1642807342 +1100 commit: commit 15 +688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed 2ce3bf88f382c762d97ac069eea18bed43a1bab2 CI 1642807342 +1100 commit: commit 16 +2ce3bf88f382c762d97ac069eea18bed43a1bab2 c09b924073b6a6cc1b2208f9a00f7b73bec2add2 CI 1642807342 +1100 commit: commit 17 +c09b924073b6a6cc1b2208f9a00f7b73bec2add2 12a951328c3156482355edebf6c81ded5480aff4 CI 1642807342 +1100 commit: commit 18 +12a951328c3156482355edebf6c81ded5480aff4 1e2780095cd8e95b93f89268f72cda21d528ab38 CI 1642807342 +1100 commit: commit 19 +1e2780095cd8e95b93f89268f72cda21d528ab38 1fd41e04d86ee95083d607da4e22abef9a570abc CI 1642807342 +1100 commit: commit 20 diff --git a/test/integration/bisect/expected/.git_keep/logs/refs/heads/test b/test/integration/bisect/expected/.git_keep/logs/refs/heads/test new file mode 100644 index 000000000..83f64a37e --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/logs/refs/heads/test @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 1fd41e04d86ee95083d607da4e22abef9a570abc CI 1642807358 +1100 branch: Created from master diff --git a/test/integration/bisect/expected/.git_keep/objects/00/750edc07d6415dcc07ae0351e9397b0222b7ba b/test/integration/bisect/expected/.git_keep/objects/00/750edc07d6415dcc07ae0351e9397b0222b7ba new file mode 100644 index 000000000..d3c45d51e Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/00/750edc07d6415dcc07ae0351e9397b0222b7ba differ diff --git a/test/integration/bisect/expected/.git_keep/objects/07/552205114379b7c1abd7cb39575cb7a30a2e8c b/test/integration/bisect/expected/.git_keep/objects/07/552205114379b7c1abd7cb39575cb7a30a2e8c new file mode 100644 index 000000000..2ea049967 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/07/552205114379b7c1abd7cb39575cb7a30a2e8c differ diff --git a/test/integration/bisect/expected/.git_keep/objects/0c/fbf08886fca9a91cb753ec8734c84fcbe52c9f b/test/integration/bisect/expected/.git_keep/objects/0c/fbf08886fca9a91cb753ec8734c84fcbe52c9f new file mode 100644 index 000000000..da246e1fe Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/0c/fbf08886fca9a91cb753ec8734c84fcbe52c9f differ diff --git a/test/integration/bisect/expected/.git_keep/objects/11/0046b8d92b877def6cda61639cf8f37bc2829c b/test/integration/bisect/expected/.git_keep/objects/11/0046b8d92b877def6cda61639cf8f37bc2829c new file mode 100644 index 000000000..ad217ecf4 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/11/0046b8d92b877def6cda61639cf8f37bc2829c differ diff --git a/test/integration/bisect/expected/.git_keep/objects/12/a951328c3156482355edebf6c81ded5480aff4 b/test/integration/bisect/expected/.git_keep/objects/12/a951328c3156482355edebf6c81ded5480aff4 new file mode 100644 index 000000000..4419e3037 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/objects/12/a951328c3156482355edebf6c81ded5480aff4 @@ -0,0 +1,3 @@ +xK +0@]d2U1LP,׬!z + QJ릺Mv{5_XkNIq9†T8yqqGi'](uO53;P \ No newline at end of file diff --git a/test/integration/bisect/expected/.git_keep/objects/83/51c19397f4fcd5238d10034fa7fa384f14d580 b/test/integration/bisect/expected/.git_keep/objects/83/51c19397f4fcd5238d10034fa7fa384f14d580 new file mode 100644 index 000000000..12805e960 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/83/51c19397f4fcd5238d10034fa7fa384f14d580 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/91/36f315e5952043f1e7ecdc0d28c208eaeaed71 b/test/integration/bisect/expected/.git_keep/objects/91/36f315e5952043f1e7ecdc0d28c208eaeaed71 new file mode 100644 index 000000000..5108746ca Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/91/36f315e5952043f1e7ecdc0d28c208eaeaed71 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/98/c3099431a8777741ea114272919d6645418037 b/test/integration/bisect/expected/.git_keep/objects/98/c3099431a8777741ea114272919d6645418037 new file mode 100644 index 000000000..5d376c14c Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/98/c3099431a8777741ea114272919d6645418037 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/98/d9bcb75a685dfbfd60f611c309410152935b3d b/test/integration/bisect/expected/.git_keep/objects/98/d9bcb75a685dfbfd60f611c309410152935b3d new file mode 100644 index 000000000..f6e750c5d Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/98/d9bcb75a685dfbfd60f611c309410152935b3d differ diff --git a/test/integration/bisect/expected/.git_keep/objects/ac/324deab67f1749eeec1531b37dfaff41e559ca b/test/integration/bisect/expected/.git_keep/objects/ac/324deab67f1749eeec1531b37dfaff41e559ca new file mode 100644 index 000000000..ee72d6df2 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/ac/324deab67f1749eeec1531b37dfaff41e559ca differ diff --git a/test/integration/bisect/expected/.git_keep/objects/ae/95e9aa3b8881aedb7a526c86ec5d60f371ca6c b/test/integration/bisect/expected/.git_keep/objects/ae/95e9aa3b8881aedb7a526c86ec5d60f371ca6c new file mode 100644 index 000000000..175cab4cc Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/ae/95e9aa3b8881aedb7a526c86ec5d60f371ca6c differ diff --git a/test/integration/bisect/expected/.git_keep/objects/af/f6316148f1524977997c486bcfe624c9094c4e b/test/integration/bisect/expected/.git_keep/objects/af/f6316148f1524977997c486bcfe624c9094c4e new file mode 100644 index 000000000..7e3df8fbf Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/af/f6316148f1524977997c486bcfe624c9094c4e differ diff --git a/test/integration/bisect/expected/.git_keep/objects/b1/bd38b62a0800a4f6a80c34e21c5acffae52c7e b/test/integration/bisect/expected/.git_keep/objects/b1/bd38b62a0800a4f6a80c34e21c5acffae52c7e new file mode 100644 index 000000000..268d0f459 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/b1/bd38b62a0800a4f6a80c34e21c5acffae52c7e differ diff --git a/test/integration/bisect/expected/.git_keep/objects/b4/de3947675361a7770d29b8982c407b0ec6b2a0 b/test/integration/bisect/expected/.git_keep/objects/b4/de3947675361a7770d29b8982c407b0ec6b2a0 new file mode 100644 index 000000000..6d65f13b4 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/b4/de3947675361a7770d29b8982c407b0ec6b2a0 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/b5/31696093a6482eca9ad4bcab63407172225b93 b/test/integration/bisect/expected/.git_keep/objects/b5/31696093a6482eca9ad4bcab63407172225b93 new file mode 100644 index 000000000..268e52a98 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/objects/b5/31696093a6482eca9ad4bcab63407172225b93 @@ -0,0 +1 @@ +x+)JMU06b040031QHIeؑ_3[}Ybfk \ No newline at end of file diff --git a/test/integration/bisect/expected/.git_keep/objects/b6/a7d89c68e0ca66e96a9a51892cc33db66fb8a3 b/test/integration/bisect/expected/.git_keep/objects/b6/a7d89c68e0ca66e96a9a51892cc33db66fb8a3 new file mode 100644 index 000000000..02dc68891 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/b6/a7d89c68e0ca66e96a9a51892cc33db66fb8a3 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/b7/cd988a171f7f99b7e190ca2b46060074cb379a b/test/integration/bisect/expected/.git_keep/objects/b7/cd988a171f7f99b7e190ca2b46060074cb379a new file mode 100644 index 000000000..2c359b84f --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/objects/b7/cd988a171f7f99b7e190ca2b46060074cb379a @@ -0,0 +1,2 @@ +xA + @Ѯ= Q'PB cԑ*`O=@?֞0UIԑ~pWRXd3}u-~-6RplWDkY?u-# \ No newline at end of file diff --git a/test/integration/bisect/expected/.git_keep/objects/b8/626c4cff2849624fb67f87cd0ad72b163671ad b/test/integration/bisect/expected/.git_keep/objects/b8/626c4cff2849624fb67f87cd0ad72b163671ad new file mode 100644 index 000000000..0761b6105 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/b8/626c4cff2849624fb67f87cd0ad72b163671ad differ diff --git a/test/integration/bisect/expected/.git_keep/objects/ba/8e7277a0ee7cdf84cd5c6138057adb85947a90 b/test/integration/bisect/expected/.git_keep/objects/ba/8e7277a0ee7cdf84cd5c6138057adb85947a90 new file mode 100644 index 000000000..c9ed3ff6b Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/ba/8e7277a0ee7cdf84cd5c6138057adb85947a90 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/c0/9b924073b6a6cc1b2208f9a00f7b73bec2add2 b/test/integration/bisect/expected/.git_keep/objects/c0/9b924073b6a6cc1b2208f9a00f7b73bec2add2 new file mode 100644 index 000000000..28a96df4f Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/c0/9b924073b6a6cc1b2208f9a00f7b73bec2add2 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/d0/0491fd7e5bb6fa28c517a0bb32b8b506539d4d b/test/integration/bisect/expected/.git_keep/objects/d0/0491fd7e5bb6fa28c517a0bb32b8b506539d4d new file mode 100644 index 000000000..8dab6a9ea Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/d0/0491fd7e5bb6fa28c517a0bb32b8b506539d4d differ diff --git a/test/integration/bisect/expected/.git_keep/objects/d5/42aa84743f8ba1380358d4009408f03dbfb247 b/test/integration/bisect/expected/.git_keep/objects/d5/42aa84743f8ba1380358d4009408f03dbfb247 new file mode 100644 index 000000000..776eafd0b Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/d5/42aa84743f8ba1380358d4009408f03dbfb247 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/d6/b24041cf04154f8f902651969675021f4d93a5 b/test/integration/bisect/expected/.git_keep/objects/d6/b24041cf04154f8f902651969675021f4d93a5 new file mode 100644 index 000000000..1ece9675c Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/d6/b24041cf04154f8f902651969675021f4d93a5 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/d9/cc608eedd5d2cc63c262272b7a0f6ab6aed5dd b/test/integration/bisect/expected/.git_keep/objects/d9/cc608eedd5d2cc63c262272b7a0f6ab6aed5dd new file mode 100644 index 000000000..dcf2f12d5 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/d9/cc608eedd5d2cc63c262272b7a0f6ab6aed5dd differ diff --git a/test/integration/bisect/expected/.git_keep/objects/dd/9d90ed2d1fa5a284adba081199f18458977547 b/test/integration/bisect/expected/.git_keep/objects/dd/9d90ed2d1fa5a284adba081199f18458977547 new file mode 100644 index 000000000..eb499d17c Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/dd/9d90ed2d1fa5a284adba081199f18458977547 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/ea/684d3f868c358400465f2ec16a640c319ea6a3 b/test/integration/bisect/expected/.git_keep/objects/ea/684d3f868c358400465f2ec16a640c319ea6a3 new file mode 100644 index 000000000..fa152b466 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/ea/684d3f868c358400465f2ec16a640c319ea6a3 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/ec/635144f60048986bc560c5576355344005e6e7 b/test/integration/bisect/expected/.git_keep/objects/ec/635144f60048986bc560c5576355344005e6e7 new file mode 100644 index 000000000..0207e4b4b Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/ec/635144f60048986bc560c5576355344005e6e7 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/ee/930b55b61910c0830b0c6ea1cf9ada066d27fc b/test/integration/bisect/expected/.git_keep/objects/ee/930b55b61910c0830b0c6ea1cf9ada066d27fc new file mode 100644 index 000000000..54ba68c9a Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/ee/930b55b61910c0830b0c6ea1cf9ada066d27fc differ diff --git a/test/integration/bisect/expected/.git_keep/objects/f2/7c6ae26adb8396d3861976ba268f87ad8afa0b b/test/integration/bisect/expected/.git_keep/objects/f2/7c6ae26adb8396d3861976ba268f87ad8afa0b new file mode 100644 index 000000000..1bb91768c Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/f2/7c6ae26adb8396d3861976ba268f87ad8afa0b differ diff --git a/test/integration/bisect/expected/.git_keep/objects/f5/99e28b8ab0d8c9c57a486c89c4a5132dcbd3b2 b/test/integration/bisect/expected/.git_keep/objects/f5/99e28b8ab0d8c9c57a486c89c4a5132dcbd3b2 new file mode 100644 index 000000000..674e37f3f Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/f5/99e28b8ab0d8c9c57a486c89c4a5132dcbd3b2 differ diff --git a/test/integration/bisect/expected/.git_keep/objects/f8/174a4db5bb2082ebe73b29a47448a300fea7ae b/test/integration/bisect/expected/.git_keep/objects/f8/174a4db5bb2082ebe73b29a47448a300fea7ae new file mode 100644 index 000000000..470a7c545 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/f8/174a4db5bb2082ebe73b29a47448a300fea7ae differ diff --git a/test/integration/bisect/expected/.git_keep/objects/fb/bb6006074afe8cc9009b649fae19f920b604ea b/test/integration/bisect/expected/.git_keep/objects/fb/bb6006074afe8cc9009b649fae19f920b604ea new file mode 100644 index 000000000..0c80c5a33 Binary files /dev/null and b/test/integration/bisect/expected/.git_keep/objects/fb/bb6006074afe8cc9009b649fae19f920b604ea differ diff --git a/test/integration/bisect/expected/.git_keep/packed-refs b/test/integration/bisect/expected/.git_keep/packed-refs new file mode 100644 index 000000000..250f18738 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/packed-refs @@ -0,0 +1 @@ +# pack-refs with: peeled fully-peeled sorted diff --git a/test/integration/bisect/expected/.git_keep/refs/bisect/bad b/test/integration/bisect/expected/.git_keep/refs/bisect/bad new file mode 100644 index 000000000..f8c370a35 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/refs/bisect/bad @@ -0,0 +1 @@ +dd9d90ed2d1fa5a284adba081199f18458977547 diff --git a/test/integration/bisect/expected/.git_keep/refs/bisect/good-ac324deab67f1749eeec1531b37dfaff41e559ca b/test/integration/bisect/expected/.git_keep/refs/bisect/good-ac324deab67f1749eeec1531b37dfaff41e559ca new file mode 100644 index 000000000..519678c18 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/refs/bisect/good-ac324deab67f1749eeec1531b37dfaff41e559ca @@ -0,0 +1 @@ +ac324deab67f1749eeec1531b37dfaff41e559ca diff --git a/test/integration/bisect/expected/.git_keep/refs/bisect/good-fbbb6006074afe8cc9009b649fae19f920b604ea b/test/integration/bisect/expected/.git_keep/refs/bisect/good-fbbb6006074afe8cc9009b649fae19f920b604ea new file mode 100644 index 000000000..434b6868d --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/refs/bisect/good-fbbb6006074afe8cc9009b649fae19f920b604ea @@ -0,0 +1 @@ +fbbb6006074afe8cc9009b649fae19f920b604ea diff --git a/test/integration/bisect/expected/.git_keep/refs/bisect/skip-1b06712fea4c03c8fce8e2b3862c059f8d7f8268 b/test/integration/bisect/expected/.git_keep/refs/bisect/skip-1b06712fea4c03c8fce8e2b3862c059f8d7f8268 new file mode 100644 index 000000000..3fe630517 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/refs/bisect/skip-1b06712fea4c03c8fce8e2b3862c059f8d7f8268 @@ -0,0 +1 @@ +1b06712fea4c03c8fce8e2b3862c059f8d7f8268 diff --git a/test/integration/bisect/expected/.git_keep/refs/bisect/skip-ee930b55b61910c0830b0c6ea1cf9ada066d27fc b/test/integration/bisect/expected/.git_keep/refs/bisect/skip-ee930b55b61910c0830b0c6ea1cf9ada066d27fc new file mode 100644 index 000000000..1d961835e --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/refs/bisect/skip-ee930b55b61910c0830b0c6ea1cf9ada066d27fc @@ -0,0 +1 @@ +ee930b55b61910c0830b0c6ea1cf9ada066d27fc diff --git a/test/integration/bisect/expected/.git_keep/refs/heads/master b/test/integration/bisect/expected/.git_keep/refs/heads/master new file mode 100644 index 000000000..765bc55e3 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/refs/heads/master @@ -0,0 +1 @@ +1fd41e04d86ee95083d607da4e22abef9a570abc diff --git a/test/integration/bisect/expected/.git_keep/refs/heads/test b/test/integration/bisect/expected/.git_keep/refs/heads/test new file mode 100644 index 000000000..765bc55e3 --- /dev/null +++ b/test/integration/bisect/expected/.git_keep/refs/heads/test @@ -0,0 +1 @@ +1fd41e04d86ee95083d607da4e22abef9a570abc diff --git a/test/integration/bisect/expected/file b/test/integration/bisect/expected/file new file mode 100644 index 000000000..209e3ef4b --- /dev/null +++ b/test/integration/bisect/expected/file @@ -0,0 +1 @@ +20 diff --git a/test/integration/bisect/recording.json b/test/integration/bisect/recording.json new file mode 100644 index 000000000..89e7d4e40 --- /dev/null +++ b/test/integration/bisect/recording.json @@ -0,0 +1 @@ +{"KeyEvents":[{"Timestamp":628,"Mod":0,"Key":259,"Ch":0},{"Timestamp":773,"Mod":0,"Key":259,"Ch":0},{"Timestamp":1123,"Mod":0,"Key":258,"Ch":0},{"Timestamp":1378,"Mod":0,"Key":256,"Ch":98},{"Timestamp":1860,"Mod":0,"Key":13,"Ch":13},{"Timestamp":2476,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2564,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2896,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2913,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2929,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2946,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2962,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2979,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2996,"Mod":0,"Key":258,"Ch":0},{"Timestamp":3283,"Mod":0,"Key":256,"Ch":98},{"Timestamp":3811,"Mod":0,"Key":258,"Ch":0},{"Timestamp":4092,"Mod":0,"Key":13,"Ch":13},{"Timestamp":4659,"Mod":0,"Key":258,"Ch":0},{"Timestamp":4788,"Mod":0,"Key":258,"Ch":0},{"Timestamp":5108,"Mod":0,"Key":258,"Ch":0},{"Timestamp":5283,"Mod":0,"Key":256,"Ch":98},{"Timestamp":5915,"Mod":0,"Key":258,"Ch":0},{"Timestamp":6067,"Mod":0,"Key":258,"Ch":0},{"Timestamp":6292,"Mod":0,"Key":13,"Ch":13},{"Timestamp":6932,"Mod":0,"Key":257,"Ch":0},{"Timestamp":7154,"Mod":0,"Key":256,"Ch":98},{"Timestamp":7588,"Mod":0,"Key":258,"Ch":0},{"Timestamp":7722,"Mod":0,"Key":258,"Ch":0},{"Timestamp":7987,"Mod":0,"Key":257,"Ch":0},{"Timestamp":8404,"Mod":0,"Key":258,"Ch":0},{"Timestamp":8667,"Mod":0,"Key":13,"Ch":13},{"Timestamp":9139,"Mod":0,"Key":257,"Ch":0},{"Timestamp":9276,"Mod":0,"Key":257,"Ch":0},{"Timestamp":9395,"Mod":0,"Key":257,"Ch":0},{"Timestamp":9732,"Mod":0,"Key":256,"Ch":98},{"Timestamp":10364,"Mod":0,"Key":13,"Ch":13},{"Timestamp":11187,"Mod":0,"Key":256,"Ch":98},{"Timestamp":11572,"Mod":0,"Key":258,"Ch":0},{"Timestamp":11819,"Mod":0,"Key":13,"Ch":13},{"Timestamp":12451,"Mod":0,"Key":256,"Ch":98},{"Timestamp":13187,"Mod":0,"Key":13,"Ch":13},{"Timestamp":14352,"Mod":0,"Key":27,"Ch":0},{"Timestamp":14707,"Mod":0,"Key":260,"Ch":0},{"Timestamp":15339,"Mod":0,"Key":256,"Ch":110},{"Timestamp":15467,"Mod":0,"Key":256,"Ch":116},{"Timestamp":15580,"Mod":0,"Key":256,"Ch":101},{"Timestamp":15795,"Mod":0,"Key":256,"Ch":115},{"Timestamp":15843,"Mod":0,"Key":256,"Ch":116},{"Timestamp":16036,"Mod":0,"Key":13,"Ch":13},{"Timestamp":16459,"Mod":0,"Key":256,"Ch":113}],"ResizeEvents":[{"Timestamp":0,"Width":272,"Height":74}]} \ No newline at end of file diff --git a/test/integration/bisect/setup.sh b/test/integration/bisect/setup.sh new file mode 100644 index 000000000..a15abc2bc --- /dev/null +++ b/test/integration/bisect/setup.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +cd $1 + +git init + +git config user.email "CI@example.com" +git config user.name "CI" + +for i in {1..20} +do + echo "$i" > file + git add . + git commit -m "commit $i" +done diff --git a/test/integration/bisect/test.json b/test/integration/bisect/test.json new file mode 100644 index 000000000..ed8478d5c --- /dev/null +++ b/test/integration/bisect/test.json @@ -0,0 +1,4 @@ +{ + "description": "Basic git bisect usage", + "speed": 20 +}