From 1859b9c39618b577eb0700b763a05f9f0017c58e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 19:07:39 +0000 Subject: [PATCH 1/2] enhance: do not refresh repositories in inactive tabs Every opened repository owns a file system watcher whose timer refreshes branches, commits and local changes whenever something changes on disk, even when its tab is not visible. With many tabs open, a build or a fetch in a background repository keeps running `git status`/`git log` for nothing. The watcher of a repository that is not in the active tab now keeps collecting changes but skips refreshing. Once the tab becomes active again, the pending changes are refreshed at the next tick. Explicit refreshes made by the app itself (initial load, after operations) are not affected. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KCxH1zwDXLd9kfAAkcf6PZ --- src/Models/Watcher.cs | 12 +++++++++++- src/ViewModels/Launcher.cs | 9 +++++++++ src/ViewModels/Repository.cs | 5 +++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 66e246bc07..49559e2723 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -100,6 +100,15 @@ public IDisposable Lock() return new LockContext(this); } + /// + /// Inactive repositories (not shown in the active tab) still collect changes, but do not refresh + /// anything until they become active again. + /// + public void SetActive(bool active) + { + _isActive = active; + } + public void MarkBranchUpdated() { Interlocked.Exchange(ref _updateBranch, 0); @@ -141,7 +150,7 @@ public void Dispose() private void Tick(object sender) { - if (Interlocked.Read(ref _lockCount) > 0) + if (!_isActive || Interlocked.Read(ref _lockCount) > 0) return; var now = DateTime.Now.ToFileTime(); @@ -377,6 +386,7 @@ private bool IsInSubmodule(string folder) private readonly string _root; private List _watchers; private Timer _timer; + private volatile bool _isActive = true; private long _lockCount; private long _updateWC; diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index aad821cda9..7f5ef1d97f 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -27,8 +27,17 @@ public LauncherPage ActivePage get => _activePage; set { + var old = _activePage; if (SetProperty(ref _activePage, value)) + { + if (old is { Data: Repository oldRepo }) + oldRepo.SetActive(false); + + if (value is { Data: Repository newRepo }) + newRepo.SetActive(true); + PostActivePageChanged(); + } } } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 4d3b5541c9..55823a8078 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -504,6 +504,11 @@ public void Close() _autoFetchTimer.Dispose(); } + public void SetActive(bool active) + { + _watcher?.SetActive(active); + } + public void SendNotification(string message, bool isError = false) { Models.Notification.Send(FullPath, message, isError); From ab50d2deda95819a14172ad32566ac4cda4a5c77 Mon Sep 17 00:00:00 2001 From: Bert Verhelst Date: Mon, 9 Nov 2026 08:25:55 +0100 Subject: [PATCH 2/2] enhance: keep dirty indicator of inactive tabs up to date While a repository is in an inactive tab, its watcher now runs a lightweight `git status --porcelain` once per batch of changes and only updates the dirty indicator of the tab. Local changes themselves are still refreshed once the tab becomes active again. Co-Authored-By: Claude Opus 5.5 --- src/Commands/QueryHasLocalChanges.cs | 30 ++++++++++++++++++++++++++++ src/Models/IRepository.cs | 1 + src/Models/Watcher.cs | 21 ++++++++++++++++++- src/ViewModels/Repository.cs | 17 ++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/Commands/QueryHasLocalChanges.cs diff --git a/src/Commands/QueryHasLocalChanges.cs b/src/Commands/QueryHasLocalChanges.cs new file mode 100644 index 0000000000..3d73dfdb18 --- /dev/null +++ b/src/Commands/QueryHasLocalChanges.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + /// + /// Lightweight check used by repositories in inactive tabs: only tells whether there are local changes, + /// without parsing them, so the tab's dirty indicator stays up to date. + /// + public class QueryHasLocalChanges : Command + { + public QueryHasLocalChanges(string repo, bool includeUntracked) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + Args = includeUntracked + ? "--no-optional-locks status -unormal --ignore-submodules=dirty --porcelain" + : "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain"; + } + + public async Task GetResultAsync() + { + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (!rs.IsSuccess) + return null; + + return !string.IsNullOrWhiteSpace(rs.StdOut); + } + } +} diff --git a/src/Models/IRepository.cs b/src/Models/IRepository.cs index 2fc7c6125c..96a95ff878 100644 --- a/src/Models/IRepository.cs +++ b/src/Models/IRepository.cs @@ -10,6 +10,7 @@ public interface IRepository void RefreshCommits(); void RefreshSubmodules(); void RefreshWorkingCopyChanges(); + void RefreshDirtyState(); void RefreshStashes(); } } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 49559e2723..2baaba1f47 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -150,9 +150,15 @@ public void Dispose() private void Tick(object sender) { - if (!_isActive || Interlocked.Read(ref _lockCount) > 0) + if (Interlocked.Read(ref _lockCount) > 0) return; + if (!_isActive) + { + CheckDirtyStateWhileInactive(); + return; + } + var now = DateTime.Now.ToFileTime(); var refreshCommits = false; var refreshSubmodules = false; @@ -237,6 +243,18 @@ private void Tick(object sender) _repo.RefreshCommits(); } + private void CheckDirtyStateWhileInactive() + { + // Pending refreshes are kept as-is so they run once the repository becomes active again. Only update + // the dirty indicator of the tab, once per batch of changes. + var pending = Math.Max(Interlocked.Read(ref _updateWC), Interlocked.Read(ref _updateBranch)); + if (pending <= 0 || pending == _lastDirtyStateCheck || DateTime.Now.ToFileTime() <= pending) + return; + + _lastDirtyStateCheck = pending; + _repo.RefreshDirtyState(); + } + private void OnRepositoryChanged(object o, FileSystemEventArgs e) { if (string.IsNullOrEmpty(e.Name) || e.Name.Equals(".git", StringComparison.Ordinal)) @@ -387,6 +405,7 @@ private bool IsInSubmodule(string folder) private List _watchers; private Timer _timer; private volatile bool _isActive = true; + private long _lastDirtyStateCheck = 0; private long _lockCount; private long _updateWC; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 55823a8078..90f9437942 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1373,6 +1373,23 @@ public void RefreshSubmodules() }); } + public void RefreshDirtyState() + { + if (IsBare) + return; + + var includeUntracked = _uiStates.IncludeUntrackedInLocalChanges; + Task.Run(async () => + { + var hasChanges = await new Commands.QueryHasLocalChanges(FullPath, includeUntracked) + .GetResultAsync() + .ConfigureAwait(false); + + if (hasChanges is { } value) + Dispatcher.UIThread.Post(() => GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, !value)); + }); + } + public void RefreshWorkingCopyChanges() { if (IsBare)