This commit is contained in:
Stefan Kerkmann 2026-09-09 18:23:04 +09:00 committed by GitHub
commit f7d7137a5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 105 additions and 36 deletions

View file

@ -55,8 +55,29 @@ func NewCommitLoader(
} }
} }
const (
GIT_LOG_FETCH_COUNT = 400
GIT_LOG_FETCH_THRESHOLD = GIT_LOG_FETCH_COUNT / 4
)
type GitLogLimit struct {
Limit int
}
func DefaultGitLogLimit() *GitLogLimit {
return &GitLogLimit{Limit: GIT_LOG_FETCH_COUNT}
}
func (self *GitLogLimit) CanFetchMoreCommits(lineIdx, logCommitCount int) bool {
return lineIdx >= logCommitCount-GIT_LOG_FETCH_THRESHOLD && logCommitCount >= self.Limit
}
func (self *GitLogLimit) Increase(realCommitCount int) {
self.Limit = realCommitCount + GIT_LOG_FETCH_COUNT
}
type GetCommitsOptions struct { type GetCommitsOptions struct {
Limit bool LogLimit *GitLogLimit
FilterPath string FilterPath string
FilterAuthor string FilterAuthor string
IncludeRebaseCommits bool IncludeRebaseCommits bool
@ -603,6 +624,11 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
refSpec += "..." + opts.RefToShowDivergenceFrom refSpec += "..." + opts.RefToShowDivergenceFrom
} }
var limitArg string
if opts.LogLimit != nil {
limitArg = fmt.Sprintf("--max-count=%d", opts.LogLimit.Limit)
}
cmdArgs := NewGitCmd("log"). cmdArgs := NewGitCmd("log").
Arg(refSpec). Arg(refSpec).
ArgIf(gitLogOrder != "default", "--"+gitLogOrder). ArgIf(gitLogOrder != "default", "--"+gitLogOrder).
@ -611,7 +637,7 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
Arg(prettyFormat). Arg(prettyFormat).
Arg("--abbrev=40"). Arg("--abbrev=40").
ArgIf(opts.FilterAuthor != "", "--author="+opts.FilterAuthor). ArgIf(opts.FilterAuthor != "", "--author="+opts.FilterAuthor).
ArgIf(opts.Limit, "-300"). ArgIf(limitArg != "", limitArg).
ArgIf(opts.FilterPath != "", "--follow", "--name-status"). ArgIf(opts.FilterPath != "", "--follow", "--name-status").
Arg("--no-show-signature"). Arg("--no-show-signature").
ArgIf(opts.RefToShowDivergenceFrom != "", "--left-right"). ArgIf(opts.RefToShowDivergenceFrom != "", "--left-right").

View file

@ -8,6 +8,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gocui"
@ -217,11 +218,12 @@ func (self *LocalCommitsContext) ClearDropInsertionIndex() {
type LocalCommitsViewModel struct { type LocalCommitsViewModel struct {
*ListViewModel[*models.Commit] *ListViewModel[*models.Commit]
// If this is true we limit the amount of commits we load, for the sake of keeping things fast. // If this is non-nil we limit the amount of commits we load, for the sake of keeping things fast.
// If the user attempts to scroll past the end of the list, we will load more commits. // If the user attempts to scroll past the end of the list, we will load more commits.
// A nil value means no limit; we load the whole log.
// Atomic because a checkout or reset sets it from a worker goroutine while the // Atomic because a checkout or reset sets it from a worker goroutine while the
// commits refresh reads it on the UI thread to decide how many commits to load. // commits refresh reads it on the UI thread to decide how many commits to load.
limitCommits atomic.Bool gitLogLimit atomic.Pointer[git_commands.GitLogLimit]
// If this is true we'll use git log --all when fetching the commits. // If this is true we'll use git log --all when fetching the commits.
showWholeGitGraph bool showWholeGitGraph bool
@ -232,7 +234,7 @@ func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon
ListViewModel: NewListViewModel(getModel), ListViewModel: NewListViewModel(getModel),
showWholeGitGraph: c.UserConfig().Git.Log.ShowWholeGraph, showWholeGitGraph: c.UserConfig().Git.Log.ShowWholeGraph,
} }
self.limitCommits.Store(true) self.gitLogLimit.Store(git_commands.DefaultGitLogLimit())
return self return self
} }
@ -303,12 +305,12 @@ func (self *LocalCommitsContext) ModelSearchResults(searchStr string, caseSensit
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.modelToViewIndexConverter(), searchStr) return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.modelToViewIndexConverter(), searchStr)
} }
func (self *LocalCommitsViewModel) SetLimitCommits(value bool) { func (self *LocalCommitsViewModel) SetGitLogLimit(limit *git_commands.GitLogLimit) {
self.limitCommits.Store(value) self.gitLogLimit.Store(limit)
} }
func (self *LocalCommitsViewModel) GetLimitCommits() bool { func (self *LocalCommitsViewModel) GetGitLogLimit() *git_commands.GitLogLimit {
return self.limitCommits.Load() return self.gitLogLimit.Load()
} }
func (self *LocalCommitsViewModel) SetShowWholeGitGraph(value bool) { func (self *LocalCommitsViewModel) SetShowWholeGitGraph(value bool) {

View file

@ -33,8 +33,8 @@ func NewSubCommitsContext(
ListViewModel: NewListViewModel( ListViewModel: NewListViewModel(
func() []*models.Commit { return c.Model().SubCommits }, func() []*models.Commit { return c.Model().SubCommits },
), ),
ref: nil, ref: nil,
limitCommits: true, gitLogLimit: git_commands.DefaultGitLogLimit(),
} }
getDisplayStrings := func(startIdx int, endIdx int) [][]string { getDisplayStrings := func(startIdx int, endIdx int) [][]string {
@ -142,7 +142,7 @@ type SubCommitsViewModel struct {
refToShowDivergenceFrom string refToShowDivergenceFrom string
*ListViewModel[*models.Commit] *ListViewModel[*models.Commit]
limitCommits bool gitLogLimit *git_commands.GitLogLimit
showBranchHeads bool showBranchHeads bool
} }
@ -199,12 +199,12 @@ func (self *SubCommitsContext) GetCommits() []*models.Commit {
return self.getModel() return self.getModel()
} }
func (self *SubCommitsContext) SetLimitCommits(value bool) { func (self *SubCommitsContext) SetGitLogLimit(limit *git_commands.GitLogLimit) {
self.limitCommits = value self.gitLogLimit = limit
} }
func (self *SubCommitsContext) GetLimitCommits() bool { func (self *SubCommitsContext) GetGitLogLimit() *git_commands.GitLogLimit {
return self.limitCommits return self.gitLogLimit
} }
func (self *SubCommitsContext) GetDiffTerminals() []string { func (self *SubCommitsContext) GetDiffTerminals() []string {

View file

@ -759,7 +759,7 @@ func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflo
// worker computes from an immutable snapshot rather than reading state the UI // worker computes from an immutable snapshot rather than reading state the UI
// thread concurrently mutates. // thread concurrently mutates.
type capturedCommitState struct { type capturedCommitState struct {
limitCommits bool gitLogLimit *git_commands.GitLogLimit
showWholeGitGraph bool showWholeGitGraph bool
filterPath string filterPath string
filterAuthor string filterAuthor string
@ -768,6 +768,18 @@ type capturedCommitState struct {
parentIsLocalCommits bool parentIsLocalCommits bool
} }
// snapshotGitLogLimit returns a copy of the given git log limit, or nil if
// there is no limit. The limit object lives on the context, where the UI
// thread bumps it when the user scrolls close to the end of the commit list;
// the refresh's git work on a worker goroutine must read a copy taken at
// capture time rather than the live object.
func snapshotGitLogLimit(limit *git_commands.GitLogLimit) *git_commands.GitLogLimit {
if limit == nil {
return nil
}
return &git_commands.GitLogLimit{Limit: limit.Limit}
}
// captureCommitsState reads the commits refresh's model/context/mode inputs // captureCommitsState reads the commits refresh's model/context/mode inputs
// into an immutable snapshot. It must run on the UI thread. // into an immutable snapshot. It must run on the UI thread.
// The selection is captured later, when applying the refresh, so user input // The selection is captured later, when applying the refresh, so user input
@ -776,7 +788,7 @@ func (self *RefreshHelper) captureCommitsState() capturedCommitState {
parentCtx := self.c.Contexts().CommitFiles.GetParentContext() parentCtx := self.c.Contexts().CommitFiles.GetParentContext()
return capturedCommitState{ return capturedCommitState{
limitCommits: self.c.Contexts().LocalCommits.GetLimitCommits(), gitLogLimit: snapshotGitLogLimit(self.c.Contexts().LocalCommits.GetGitLogLimit()),
showWholeGitGraph: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(), showWholeGitGraph: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(),
filterPath: self.c.Modes().Filtering.GetPath(), filterPath: self.c.Modes().Filtering.GetPath(),
filterAuthor: self.c.Modes().Filtering.GetAuthor(), filterAuthor: self.c.Modes().Filtering.GetAuthor(),
@ -847,7 +859,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit(captured capturedCommitState,
refName, bisectInfo := self.refForLog(env) refName, bisectInfo := self.refForLog(env)
commits, err := env.git.Loaders.CommitLoader.GetCommits( commits, err := env.git.Loaders.CommitLoader.GetCommits(
git_commands.GetCommitsOptions{ git_commands.GetCommitsOptions{
Limit: captured.limitCommits, LogLimit: captured.gitLogLimit,
FilterPath: captured.filterPath, FilterPath: captured.filterPath,
FilterAuthor: captured.filterAuthor, FilterAuthor: captured.filterAuthor,
IncludeRebaseCommits: true, IncludeRebaseCommits: true,
@ -999,7 +1011,7 @@ func findNewConflictedCommit(previousCommits []*models.Commit, commits []*models
// work is dispatched to a worker. // work is dispatched to a worker.
type capturedSubCommitState struct { type capturedSubCommitState struct {
ref models.Ref ref models.Ref
limitCommits bool gitLogLimit *git_commands.GitLogLimit
refToShowDivergenceFrom string refToShowDivergenceFrom string
filterPath string filterPath string
filterAuthor string filterAuthor string
@ -1012,7 +1024,7 @@ type capturedSubCommitState struct {
func (self *RefreshHelper) captureSubCommitState() capturedSubCommitState { func (self *RefreshHelper) captureSubCommitState() capturedSubCommitState {
return capturedSubCommitState{ return capturedSubCommitState{
ref: self.c.Contexts().SubCommits.GetRef(), ref: self.c.Contexts().SubCommits.GetRef(),
limitCommits: self.c.Contexts().SubCommits.GetLimitCommits(), gitLogLimit: snapshotGitLogLimit(self.c.Contexts().SubCommits.GetGitLogLimit()),
refToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(), refToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(),
filterPath: self.c.Modes().Filtering.GetPath(), filterPath: self.c.Modes().Filtering.GetPath(),
filterAuthor: self.c.Modes().Filtering.GetAuthor(), filterAuthor: self.c.Modes().Filtering.GetAuthor(),
@ -1028,7 +1040,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit(captured capturedSubCommit
commits, err := env.git.Loaders.CommitLoader.GetCommits( commits, err := env.git.Loaders.CommitLoader.GetCommits(
git_commands.GetCommitsOptions{ git_commands.GetCommitsOptions{
Limit: captured.limitCommits, LogLimit: captured.gitLogLimit,
FilterPath: captured.filterPath, FilterPath: captured.filterPath,
FilterAuthor: captured.filterAuthor, FilterAuthor: captured.filterAuthor,
IncludeRebaseCommits: false, IncludeRebaseCommits: false,

View file

@ -41,7 +41,7 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
refresh := func() { refresh := func() {
// loading a heap of commits is slow so we limit them whenever doing a reset // loading a heap of commits is slow so we limit them whenever doing a reset
self.c.Contexts().LocalCommits.SetLimitCommits(true) self.c.Contexts().LocalCommits.SetGitLogLimit(git_commands.DefaultGitLogLimit())
scope := []types.RefreshableView{ scope := []types.RefreshableView{
types.COMMITS, types.COMMITS,
@ -204,7 +204,7 @@ func (self *RefsHelper) ResetToRef(ref string, strength string, envVars []string
} }
// loading a heap of commits is slow so we limit them whenever doing a reset // loading a heap of commits is slow so we limit them whenever doing a reset
self.c.Contexts().LocalCommits.SetLimitCommits(true) self.c.Contexts().LocalCommits.SetGitLogLimit(git_commands.DefaultGitLogLimit())
self.c.RefreshFromWorker(types.RefreshOptions{ self.c.RefreshFromWorker(types.RefreshOptions{
Scope: []types.RefreshableView{types.FILES, types.BRANCHES, types.REFLOG, types.COMMITS}, Scope: []types.RefreshableView{types.FILES, types.BRANCHES, types.REFLOG, types.COMMITS},

View file

@ -34,7 +34,7 @@ type ViewSubCommitsOpts struct {
func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error { func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error {
commits, err := self.c.Git().Loaders.CommitLoader.GetCommits( commits, err := self.c.Git().Loaders.CommitLoader.GetCommits(
git_commands.GetCommitsOptions{ git_commands.GetCommitsOptions{
Limit: true, LogLimit: git_commands.DefaultGitLogLimit(),
FilterPath: self.c.Modes().Filtering.GetPath(), FilterPath: self.c.Modes().Filtering.GetPath(),
FilterAuthor: self.c.Modes().Filtering.GetAuthor(), FilterAuthor: self.c.Modes().Filtering.GetAuthor(),
IncludeRebaseCommits: false, IncludeRebaseCommits: false,
@ -59,7 +59,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error {
subCommitsContext.SetTitleRef(utils.TruncateWithEllipsis(opts.TitleRef, 50)) subCommitsContext.SetTitleRef(utils.TruncateWithEllipsis(opts.TitleRef, 50))
subCommitsContext.SetRef(opts.Ref) subCommitsContext.SetRef(opts.Ref)
subCommitsContext.SetRefToShowDivergenceFrom(opts.RefToShowDivergenceFrom) subCommitsContext.SetRefToShowDivergenceFrom(opts.RefToShowDivergenceFrom)
subCommitsContext.SetLimitCommits(true) subCommitsContext.SetGitLogLimit(git_commands.DefaultGitLogLimit())
subCommitsContext.SetShowBranchHeads(opts.ShowBranchHeads) subCommitsContext.SetShowBranchHeads(opts.ShowBranchHeads)
subCommitsContext.ClearSearchString() subCommitsContext.ClearSearchString()
subCommitsContext.GetView().ClearSearch() subCommitsContext.GetView().ClearSearch()

View file

@ -18,9 +18,6 @@ import (
"github.com/stefanhaller/git-todo-parser/todo" "github.com/stefanhaller/git-todo-parser/todo"
) )
// after selecting the 200th commit, we'll load in all the rest
const COMMIT_THRESHOLD = 200
// How long a commit move may take before the drop indicator switches to a // How long a commit move may take before the drop indicator switches to a
// "moving commits here" spinner; quick moves stay free of flicker. // "moving commits here" spinner; quick moves stay free of flicker.
const commitDragMovingIndicatorDelay = 200 * time.Millisecond const commitDragMovingIndicatorDelay = 200 * time.Millisecond
@ -1679,8 +1676,8 @@ func (self *LocalCommitsController) createTag(commit *models.Commit) error {
func (self *LocalCommitsController) openSearch() error { func (self *LocalCommitsController) openSearch() error {
// we usually lazyload these commits but now that we're searching we need to load them now // we usually lazyload these commits but now that we're searching we need to load them now
if self.context().GetLimitCommits() { if self.context().GetGitLogLimit() != nil {
self.context().SetLimitCommits(false) self.context().SetGitLogLimit(nil)
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}) self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}})
} }
@ -1698,7 +1695,9 @@ func (self *LocalCommitsController) handleOpenLogMenu() error {
self.context().SetShowWholeGitGraph(!self.context().GetShowWholeGitGraph()) self.context().SetShowWholeGitGraph(!self.context().GetShowWholeGitGraph())
if self.context().GetShowWholeGitGraph() { if self.context().GetShowWholeGitGraph() {
self.context().SetLimitCommits(false) self.context().SetGitLogLimit(nil)
} else {
self.context().SetGitLogLimit(git_commands.DefaultGitLogLimit())
} }
return self.c.WithWaitingStatus(self.c.Tr.LoadingCommits, func(gocui.Task) error { return self.c.WithWaitingStatus(self.c.Tr.LoadingCommits, func(gocui.Task) error {
@ -1799,8 +1798,30 @@ func (self *LocalCommitsController) handleOpenLogMenu() error {
func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) { func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) {
return func(types.OnFocusOpts) { return func(types.OnFocusOpts) {
context := self.context() context := self.context()
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { limit := context.GetGitLogLimit()
context.SetLimitCommits(false)
if limit == nil {
return
}
lineIdx := context.GetSelectedLineIdx()
logCommitCount := len(self.c.Model().Commits)
if self.isRebasing() {
rebaseCommitCount := lo.CountBy(self.c.Model().Commits, func(c *models.Commit) bool {
return c.IsTODO()
})
if lineIdx < rebaseCommitCount {
lineIdx = 0
} else {
lineIdx -= rebaseCommitCount
}
logCommitCount -= rebaseCommitCount
}
if limit.CanFetchMoreCommits(lineIdx, logCommitCount) {
limit.Increase(logCommitCount)
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}) self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}})
} }
} }

View file

@ -64,8 +64,16 @@ func (self *SubCommitsController) GetOnRenderToMain() func() {
func (self *SubCommitsController) GetOnFocus() func(types.OnFocusOpts) { func (self *SubCommitsController) GetOnFocus() func(types.OnFocusOpts) {
return func(types.OnFocusOpts) { return func(types.OnFocusOpts) {
context := self.context() context := self.context()
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { limit := context.GetGitLogLimit()
context.SetLimitCommits(false)
if limit == nil {
return
}
logCommitCount := len(self.c.Model().SubCommits)
if limit.CanFetchMoreCommits(context.GetSelectedLineIdx(), logCommitCount) {
limit.Increase(logCommitCount)
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUB_COMMITS}}) self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUB_COMMITS}})
} }
} }