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:
164
pkg/commands/git_commands/bisect.go
Normal file
164
pkg/commands/git_commands/bisect.go
Normal 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
|
||||
}
|
103
pkg/commands/git_commands/bisect_info.go
Normal file
103
pkg/commands/git_commands/bisect_info.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
Reference in New Issue
Block a user