From 8f4bf49aff22059e4c9c8cc2e734bd1c39342918 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 16 Nov 2025 12:54:14 +0100 Subject: [PATCH 1/5] Cleanup: remove unused field --- pkg/gui/gui.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2727df3c4..3610c9c23 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -233,7 +233,6 @@ type GuiRepoState struct { Modes *types.Modes SplitMainPanel bool - LimitCommits bool SearchState *types.SearchState StartupStage types.StartupStage // Allows us to not load everything at once From aff6b642ea29dd016fc9b83c27788f877febb483 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 16 Nov 2025 09:10:54 +0100 Subject: [PATCH 2/5] Trigger immediate background fetch when switching repos --- pkg/gui/background.go | 44 ++++++++++++++++++++++++++++++------------ pkg/gui/controllers.go | 2 +- pkg/gui/gui.go | 8 ++++++++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 6eaf041d4..aa0726568 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -17,6 +17,9 @@ type BackgroundRoutineMgr struct { // we typically want to pause some things that are running like background // file refreshes pauseBackgroundRefreshes bool + + // a channel to trigger an immediate background fetch; we use this when switching repos + triggerFetch chan struct{} } func (self *BackgroundRoutineMgr) PauseBackgroundRefreshes(pause bool) { @@ -86,7 +89,7 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() { _ = fetch() userConfig := self.gui.UserConfig() - self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, fetch) + self.triggerFetch = self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, fetch) } func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh(refreshInterval int) { @@ -98,29 +101,40 @@ func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh(refreshInterval in }) } -func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func() error) { +// returns a channel that can be used to trigger the callback immediately +func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func() error) chan struct{} { done := make(chan struct{}) + retrigger := make(chan struct{}) go utils.Safe(func() { ticker := time.NewTicker(interval) defer ticker.Stop() + doit := func() { + if self.pauseBackgroundRefreshes { + return + } + self.gui.c.OnWorker(func(gocui.Task) error { + _ = function() + done <- struct{}{} + return nil + }) + // waiting so that we don't bunch up refreshes if the refresh takes longer than the + // interval, or if a retrigger comes in while we're still processing a timer-based one + // (or vice versa) + <-done + } for { select { case <-ticker.C: - if self.pauseBackgroundRefreshes { - continue - } - self.gui.c.OnWorker(func(gocui.Task) error { - _ = function() - done <- struct{}{} - return nil - }) - // waiting so that we don't bunch up refreshes if the refresh takes longer than the interval - <-done + doit() + case <-retrigger: + ticker.Reset(interval) + doit() case <-stop: return } } }) + return retrigger } func (self *BackgroundRoutineMgr) backgroundFetch() (err error) { @@ -134,3 +148,9 @@ func (self *BackgroundRoutineMgr) backgroundFetch() (err error) { return err } + +func (self *BackgroundRoutineMgr) triggerImmediateFetch() { + if self.triggerFetch != nil { + self.triggerFetch <- struct{}{} + } +} diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 2f729a7b5..702ed826d 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -26,7 +26,7 @@ func (gui *Gui) resetHelpersAndControllers() { helperCommon := gui.c recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon) - reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo) + reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onSwitchToNewRepo) rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon) refsHelper := helpers.NewRefsHelper(helperCommon, rebaseHelper) suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 3610c9c23..b4b295430 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -305,6 +305,14 @@ func (self *GuiRepoState) GetSplitMainPanel() bool { return self.SplitMainPanel } +func (gui *Gui) onSwitchToNewRepo(startArgs appTypes.StartArgs, contextKey types.ContextKey) error { + err := gui.onNewRepo(startArgs, contextKey) + if err == nil && gui.UserConfig().Git.AutoFetch && gui.UserConfig().Refresher.FetchInterval > 0 { + gui.BackgroundRoutineMgr.triggerImmediateFetch() + } + return err +} + func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.ContextKey) error { var err error gui.git, err = commands.NewGitCommand( From 472b964da3cc362c48f410ef2f21cdb661b27352 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 16 Nov 2025 13:26:33 +0100 Subject: [PATCH 3/5] Cleanup: don't pass refreshInterval to startBackgroundFilesRefresh It has access to UserConfig, so it can easily get it from there. This is how we do it for startBackgroundFetch too, so be consistent. --- pkg/gui/background.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/gui/background.go b/pkg/gui/background.go index aa0726568..362c23c6f 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -43,7 +43,7 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() { if userConfig.Git.AutoRefresh { refreshInterval := userConfig.Refresher.RefreshInterval if refreshInterval > 0 { - go utils.Safe(func() { self.startBackgroundFilesRefresh(refreshInterval) }) + go utils.Safe(self.startBackgroundFilesRefresh) } else { self.gui.c.Log.Errorf( "Value of config option 'refresher.refreshInterval' (%d) is invalid, disabling auto-refresh", @@ -92,10 +92,11 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() { self.triggerFetch = self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, fetch) } -func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh(refreshInterval int) { +func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh() { self.gui.waitForIntro.Wait() - self.goEvery(time.Second*time.Duration(refreshInterval), self.gui.stopChan, func() error { + userConfig := self.gui.UserConfig() + self.goEvery(time.Second*time.Duration(userConfig.Refresher.RefreshInterval), self.gui.stopChan, func() error { self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) return nil }) From d45f27b6ee9c7b8fe9f77e6c1722fc9109ebd798 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 16 Nov 2025 13:12:14 +0100 Subject: [PATCH 4/5] Add methods for converting RefresherConfig times to time.Durations --- pkg/config/user_config.go | 8 ++++++++ pkg/gui/background.go | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 324e425fd..582d98f78 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -49,6 +49,14 @@ type RefresherConfig struct { FetchInterval int `yaml:"fetchInterval" jsonschema:"minimum=0"` } +func (c *RefresherConfig) RefreshIntervalDuration() time.Duration { + return time.Second * time.Duration(c.RefreshInterval) +} + +func (c *RefresherConfig) FetchIntervalDuration() time.Duration { + return time.Second * time.Duration(c.FetchInterval) +} + type GuiConfig struct { // See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color AuthorColors map[string]string `yaml:"authorColors"` diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 362c23c6f..9d034ffa1 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -89,14 +89,14 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() { _ = fetch() userConfig := self.gui.UserConfig() - self.triggerFetch = self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, fetch) + self.triggerFetch = self.goEvery(userConfig.Refresher.FetchIntervalDuration(), self.gui.stopChan, fetch) } func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh() { self.gui.waitForIntro.Wait() userConfig := self.gui.UserConfig() - self.goEvery(time.Second*time.Duration(userConfig.Refresher.RefreshInterval), self.gui.stopChan, func() error { + self.goEvery(userConfig.Refresher.RefreshIntervalDuration(), self.gui.stopChan, func() error { self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) return nil }) From 2af56de5d26d7f606b3e7c8f6cf77611367999a3 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 16 Nov 2025 13:15:31 +0100 Subject: [PATCH 5/5] Trigger background fetch on repo switch only if enough time has passed since the last one --- pkg/gui/background.go | 10 ++++++++++ pkg/gui/gui.go | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 9d034ffa1..73964838c 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -79,6 +79,16 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() { self.gui.waitForIntro.Wait() fetch := func() error { + // Do this on the UI thread so that we don't have to deal with synchronization around the + // access of the repo state. + self.gui.onUIThread(func() error { + // There's a race here, where we might be recording the time stamp for a different repo + // than where the fetch actually ran. It's not very likely though, and not harmful if it + // does happen; guarding against it would be more effort than it's worth. + self.gui.State.LastBackgroundFetchTime = time.Now() + return nil + }) + return self.gui.helpers.AppStatus.WithWaitingStatusImpl(self.gui.Tr.FetchingStatus, func(gocui.Task) error { return self.backgroundFetch() }, nil) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index b4b295430..a7e17ef9f 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -12,6 +12,7 @@ import ( "sort" "strings" "sync" + "time" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazycore/pkg/boxlayout" @@ -253,6 +254,8 @@ type GuiRepoState struct { ScreenMode types.ScreenMode CurrentPopupOpts *types.CreatePopupPanelOpts + + LastBackgroundFetchTime time.Time } var _ types.IRepoStateAccessor = new(GuiRepoState) @@ -308,7 +311,9 @@ func (self *GuiRepoState) GetSplitMainPanel() bool { func (gui *Gui) onSwitchToNewRepo(startArgs appTypes.StartArgs, contextKey types.ContextKey) error { err := gui.onNewRepo(startArgs, contextKey) if err == nil && gui.UserConfig().Git.AutoFetch && gui.UserConfig().Refresher.FetchInterval > 0 { - gui.BackgroundRoutineMgr.triggerImmediateFetch() + if time.Since(gui.State.LastBackgroundFetchTime) > gui.UserConfig().Refresher.FetchIntervalDuration() { + gui.BackgroundRoutineMgr.triggerImmediateFetch() + } } return err }