1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-28 16:02:01 +03:00

add support for git bisect

This commit is contained in:
Jesse Duffield
2022-01-19 18:32:27 +11:00
parent ab84410b41
commit 4ab5e54139
117 changed files with 1013 additions and 104 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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