diff --git a/pkg/commands/git_commands/bisect.go b/pkg/commands/git_commands/bisect.go index e554af117..96fed84ae 100644 --- a/pkg/commands/git_commands/bisect.go +++ b/pkg/commands/git_commands/bisect.go @@ -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. diff --git a/pkg/commands/git_commands/bisect_info.go b/pkg/commands/git_commands/bisect_info.go index 7eb5cda59..c364b1fa4 100644 --- a/pkg/commands/git_commands/bisect_info.go +++ b/pkg/commands/git_commands/bisect_info.go @@ -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 } diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 20714788f..d1356c2aa 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -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() { diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index d2036aa2e..28fc356bf 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -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 diff --git a/pkg/integration/tests/bisect/mark_candidates_while_testing_merge_base.go b/pkg/integration/tests/bisect/mark_candidates_while_testing_merge_base.go index 7e8994dc6..a9aa717eb 100644 --- a/pkg/integration/tests/bisect/mark_candidates_while_testing_merge_base.go +++ b/pkg/integration/tests/bisect/mark_candidates_while_testing_merge_base.go @@ -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 diff --git a/pkg/integration/tests/bisect/select_current_commit_in_long_range.go b/pkg/integration/tests/bisect/select_current_commit_in_long_range.go index 5f0caa183..6a733ed49 100644 --- a/pkg/integration/tests/bisect/select_current_commit_in_long_range.go +++ b/pkg/integration/tests/bisect/select_current_commit_in_long_range.go @@ -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("?")) }, })