mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-06 13:00:59 -04:00
Ask git which commits a bisect hasn't ruled out yet
The commits view marks the commits that a bisect hasn't ruled out yet with a "?". To find them, it looks for the bad commit and the first good commit in the list, and marks the commits between them. If there's no good commit in the list, it marks none. This is the case while git tests the merge base of a good and a bad commit on different branches. It's also the case early in a long bisect, when the good commit is further down than the commits that we load. With merges, the commits between the two can also include some that the bisect has ruled out. Ask git instead, with "git rev-list <bad> --not <good>...", when refreshing the commits while git knows both a good and a bad commit. This takes about as long as the git log that loads the commits, or less, and it gets faster as the bisect goes on. Keep only the commits that the commits view has loaded, because git may list many more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fd7178a3a9
commit
0b87c9e610
@@ -5,6 +5,10 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type BisectCommands struct {
|
||||
@@ -181,6 +185,40 @@ func (self *BisectCommands) IsDone() (bool, []string, error) {
|
||||
return done, candidates, nil
|
||||
}
|
||||
|
||||
// Asks git which of the given commits the bisect hasn't ruled out yet, and
|
||||
// stores them in the bisect info. Git may list many more commits than these,
|
||||
// so we don't keep the others.
|
||||
func (self *BisectCommands) LoadCandidates(info *BisectInfo, commits []*models.Commit) error {
|
||||
if !info.Bisecting() {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldHashes := lo.Keys(lo.PickBy(info.statusMap, func(_ string, status BisectStatus) bool {
|
||||
return status == BisectStatusOld
|
||||
}))
|
||||
cmdArgs := NewGitCmd("rev-list").
|
||||
Arg(info.GetNewHash(), "--not").
|
||||
Arg(oldHashes...).
|
||||
ToArgv()
|
||||
|
||||
loadedHashes := set.NewFromSlice(lo.Map(commits, func(commit *models.Commit, _ int) string {
|
||||
return commit.Hash()
|
||||
}))
|
||||
candidates := set.New[string]()
|
||||
err := self.cmd.New(cmdArgs).DontLog().RunAndProcessLines(func(line string) (bool, error) {
|
||||
if hash := strings.TrimSpace(line); loadedHashes.Includes(hash) {
|
||||
candidates.Add(hash)
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info.candidates = candidates
|
||||
return nil
|
||||
}
|
||||
|
||||
// tells us whether the 'start' ref that we'll be sent back to after we're done
|
||||
// bisecting is actually a descendant of our current bisect commit. If it's not, we need to
|
||||
// render the commits from the bad commit.
|
||||
|
||||
@@ -2,6 +2,7 @@ package git_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -34,6 +35,11 @@ type BisectInfo struct {
|
||||
|
||||
// the hash of the commit that's under test
|
||||
current string
|
||||
|
||||
// the commits that the bisect hasn't ruled out yet, among those that the
|
||||
// commits view has loaded. Nil until BisectCommands.LoadCandidates is
|
||||
// called.
|
||||
candidates *set.Set[string]
|
||||
}
|
||||
|
||||
type BisectStatus int
|
||||
@@ -78,6 +84,14 @@ func (self *BisectInfo) HasCommitToTest() bool {
|
||||
return !marked
|
||||
}
|
||||
|
||||
// Whether the bisect hasn't ruled out the commit yet. This is the case if the
|
||||
// new commit can reach it, but no old one can, so it includes the new commit
|
||||
// and the skipped ones. Only known for the commits that the commits view has
|
||||
// loaded.
|
||||
func (self *BisectInfo) IsCandidate(commitHash string) bool {
|
||||
return self.candidates != nil && self.candidates.Includes(commitHash)
|
||||
}
|
||||
|
||||
func (self *BisectInfo) GetStartHash() string {
|
||||
return self.start
|
||||
}
|
||||
|
||||
@@ -871,6 +871,9 @@ func (self *RefreshHelper) refreshCommitsWithLimit(captured capturedCommitState,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := env.git.Bisect.LoadCandidates(bisectInfo, commits); err != nil {
|
||||
self.c.Log.Warnf("Failed to find the commits that the bisect hasn't ruled out yet: %v", err)
|
||||
}
|
||||
workingTreeState := env.git.Status.WorkingTreeState()
|
||||
|
||||
self.onUIThreadUnlessRepoChanged(env, func() {
|
||||
|
||||
@@ -36,11 +36,6 @@ var (
|
||||
pipeSetCacheAuthorColors = authors.ColorsVersion()
|
||||
)
|
||||
|
||||
type bisectBounds struct {
|
||||
newIndex int
|
||||
oldIndex int
|
||||
}
|
||||
|
||||
func GetCommitListDisplayStrings(
|
||||
common *common.Common,
|
||||
commits []*models.Commit,
|
||||
@@ -78,8 +73,6 @@ func GetCommitListDisplayStrings(
|
||||
|
||||
filteredCommits := commits[startIdx:endIdx]
|
||||
|
||||
bisectBounds := getbisectBounds(commits, bisectInfo)
|
||||
|
||||
// function expects to be passed the index of the commit in terms of the `commits` slice
|
||||
var getGraphLine func(int) string
|
||||
if showGraph {
|
||||
@@ -187,14 +180,14 @@ func GetCommitListDisplayStrings(
|
||||
|
||||
reservedWidths := getReservedColumnWidths(
|
||||
commits, common.UserConfig().Gui.CommitHashLength, fullDescription,
|
||||
timeFormat, shortTimeFormat, now, bisectInfo, bisectBounds)
|
||||
timeFormat, shortTimeFormat, now, bisectInfo)
|
||||
|
||||
lines := make([][]string, 0, len(filteredCommits))
|
||||
var bisectStatus BisectStatus
|
||||
willBeRebased := markedBaseCommit == ""
|
||||
for i, commit := range filteredCommits {
|
||||
unfilteredIdx := i + startIdx
|
||||
bisectStatus = getBisectStatus(unfilteredIdx, commit.Hash(), bisectInfo, bisectBounds)
|
||||
bisectStatus = getBisectStatus(commit.Hash(), bisectInfo)
|
||||
isMarkedBaseCommit := commit.Hash() != "" && commit.Hash() == markedBaseCommit
|
||||
if isMarkedBaseCommit {
|
||||
willBeRebased = true
|
||||
@@ -243,16 +236,15 @@ func getReservedColumnWidths(
|
||||
shortTimeFormat string,
|
||||
now time.Time,
|
||||
bisectInfo *git_commands.BisectInfo,
|
||||
bisectBounds *bisectBounds,
|
||||
) reservedColumnWidths {
|
||||
result := reservedColumnWidths{}
|
||||
|
||||
for i, commit := range commits {
|
||||
for _, commit := range commits {
|
||||
result.hash = max(result.hash, utils.StringWidth(getHashText(commit, hashLength)))
|
||||
if commit.IsTODO() {
|
||||
result.action = max(result.action, utils.StringWidth(getActionText(commit)))
|
||||
}
|
||||
bisectStatus := getBisectStatus(i, commit.Hash(), bisectInfo, bisectBounds)
|
||||
bisectStatus := getBisectStatus(commit.Hash(), bisectInfo)
|
||||
result.bisect = max(result.bisect,
|
||||
utils.StringWidth(getBisectStatusText(bisectStatus, bisectInfo)))
|
||||
}
|
||||
@@ -274,29 +266,6 @@ func getReservedColumnWidths(
|
||||
return result
|
||||
}
|
||||
|
||||
func getbisectBounds(commits []*models.Commit, bisectInfo *git_commands.BisectInfo) *bisectBounds {
|
||||
if !bisectInfo.Bisecting() {
|
||||
return nil
|
||||
}
|
||||
|
||||
bisectBounds := &bisectBounds{}
|
||||
|
||||
for i, commit := range commits {
|
||||
if commit.Hash() == bisectInfo.GetNewHash() {
|
||||
bisectBounds.newIndex = i
|
||||
}
|
||||
|
||||
status, ok := bisectInfo.Status(commit.Hash())
|
||||
if ok && status == git_commands.BisectStatusOld {
|
||||
bisectBounds.oldIndex = i
|
||||
return bisectBounds
|
||||
}
|
||||
}
|
||||
|
||||
// shouldn't land here
|
||||
return nil
|
||||
}
|
||||
|
||||
// precondition: slice is not empty
|
||||
func indexOfFirstNonTODOCommit(commits []*models.Commit) int {
|
||||
for i, commit := range commits {
|
||||
@@ -345,14 +314,13 @@ const (
|
||||
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
|
||||
// a commit that has no mark, and that the bisect hasn't ruled out yet
|
||||
BisectStatusCandidate
|
||||
// also adding this
|
||||
BisectStatusCurrent
|
||||
)
|
||||
|
||||
func getBisectStatus(index int, commitHash string, bisectInfo *git_commands.BisectInfo, bisectBounds *bisectBounds) BisectStatus {
|
||||
func getBisectStatus(commitHash string, bisectInfo *git_commands.BisectInfo) BisectStatus {
|
||||
if !bisectInfo.Started() {
|
||||
return BisectStatusNone
|
||||
}
|
||||
@@ -372,7 +340,7 @@ func getBisectStatus(index int, commitHash string, bisectInfo *git_commands.Bise
|
||||
return BisectStatusSkipped
|
||||
}
|
||||
} else {
|
||||
if bisectBounds != nil && index >= bisectBounds.newIndex && index <= bisectBounds.oldIndex {
|
||||
if bisectInfo.IsCandidate(commitHash) {
|
||||
return BisectStatusCandidate
|
||||
}
|
||||
return BisectStatusNone
|
||||
|
||||
@@ -28,10 +28,7 @@ var MarkCandidatesWhileTestingMergeBase = NewIntegrationTest(NewIntegrationTestA
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("CI commit-05").Contains("<-- bad"),
|
||||
/* EXPECTED:
|
||||
MatchesRegexp(`\?.*commit-04`),
|
||||
ACTUAL: */
|
||||
Contains("CI commit-04").DoesNotContain("?"),
|
||||
Contains("CI commit-02").Contains("<-- current").IsSelected(),
|
||||
// This is an ancestor of the good commit-03, so the bisect has
|
||||
// ruled it out
|
||||
|
||||
@@ -28,9 +28,6 @@ var SelectCurrentCommitInLongRange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
// to be tested. The good one is further down than the commits that
|
||||
// we load.
|
||||
SelectNextItem().
|
||||
/* EXPECTED:
|
||||
SelectedLine(Contains("?"))
|
||||
ACTUAL: */
|
||||
SelectedLine(DoesNotContain("?"))
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user