From a003261fe11652246981321536f290683621862a Mon Sep 17 00:00:00 2001 From: Corentin Damman Date: Wed, 30 Sep 2026 10:18:42 +0200 Subject: [PATCH 1/2] fix: watcher missed ref updates stored in `packed-refs` or lost on buffer overflow The branch graph was sometimes not updated after a fetch, until a pull or a restart. - `packed-refs` was ignored. A ref updated or deleted only there (e.g. `fetch --prune`, or the `git maintenance run --auto` triggered after a fetch) never scheduled a refresh. - `FileSystemWatcher.Error` was not handled. A large fetch writes thousands of object files, which overflows the internal buffer (8 KB by default): Windows then drops events, including the ones about `refs/remotes/*`, and nothing noticed. Since we cannot know which events were lost, schedule a full reload instead. --- src/Models/Watcher.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index b881807b0..66e246bc0 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -43,6 +43,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) combined.Renamed += OnRepositoryChanged; combined.Changed += OnRepositoryChanged; combined.Deleted += OnRepositoryChanged; + combined.Error += OnWatcherError; combined.EnableRaisingEvents = false; _watchers.Add(combined); @@ -58,6 +59,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) wc.Renamed += OnWorkingCopyChanged; wc.Changed += OnWorkingCopyChanged; wc.Deleted += OnWorkingCopyChanged; + wc.Error += OnWatcherError; wc.EnableRaisingEvents = false; var git = new FileSystemWatcher(); @@ -69,6 +71,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) git.Renamed += OnGitDirChanged; git.Changed += OnGitDirChanged; git.Deleted += OnGitDirChanged; + git.Error += OnWatcherError; git.EnableRaisingEvents = false; _watchers.Add(wc); @@ -240,6 +243,21 @@ private void OnRepositoryChanged(object o, FileSystemEventArgs e) HandleWorkingCopyFileChanged(name, e.FullPath); } + /// + /// The internal buffer overflowed (a large fetch or checkout writes thousands of files), so + /// some events were dropped and we cannot know which. Reload everything once things settle. + /// + private void OnWatcherError(object o, ErrorEventArgs e) + { + var desired = DateTime.Now.AddSeconds(1).ToFileTime(); + Interlocked.Exchange(ref _updateBranch, desired); + Interlocked.Exchange(ref _updateTags, desired); + Interlocked.Exchange(ref _updateStashes, desired); + Interlocked.Exchange(ref _updateWC, desired); + if (_repo.MayHaveSubmodules()) + Interlocked.Exchange(ref _updateSubmodules, desired); + } + private void OnGitDirChanged(object o, FileSystemEventArgs e) { if (string.IsNullOrEmpty(e.Name)) @@ -302,6 +320,12 @@ private void HandleGitDirFileChanged(string name) { Interlocked.Exchange(ref _updateBranch, DateTime.Now.AddSeconds(.5).ToFileTime()); } + else if (name.Equals("packed-refs", StringComparison.Ordinal)) + { + var desired = DateTime.Now.AddSeconds(.5).ToFileTime(); + Interlocked.Exchange(ref _updateBranch, desired); + Interlocked.Exchange(ref _updateTags, desired); + } else if (name.StartsWith("reftable/", StringComparison.Ordinal)) { var desired = DateTime.Now.AddSeconds(.5).ToFileTime(); From 1ba58df834e9726b4e029f8c4a57d78428fdc456 Mon Sep 17 00:00:00 2001 From: Corentin Damman Date: Wed, 30 Sep 2026 10:18:43 +0200 Subject: [PATCH 2/2] fix: refresh branches and tags explicitly after fetch Fetch, fetch-into and auto-fetch relied only on the filesystem watcher to show the new remote refs. The watcher is best-effort: besides the cases fixed in the previous commit, it may not report anything at all on network drives or WSL paths. Other ref-changing operations (`AddRemote`, `DeleteBranch`, `DeleteRemote`...) already refresh explicitly; do the same here. This does not refresh twice: `MarkBranchesDirtyManually` also clears the watcher's pending flags. It also updates the graph right away instead of after the watcher's 0.5s delay. --- src/ViewModels/Fetch.cs | 4 ++++ src/ViewModels/FetchInto.cs | 1 + src/ViewModels/Repository.cs | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/ViewModels/Fetch.cs b/src/ViewModels/Fetch.cs index 330dd5d76..e2c609023 100644 --- a/src/ViewModels/Fetch.cs +++ b/src/ViewModels/Fetch.cs @@ -105,6 +105,10 @@ public override async Task Sure() log.Complete(); + _repo.MarkBranchesDirtyManually(); + if (!notags) + _repo.MarkTagsDirtyManually(); + if (navigateToUpstreamHEAD && !token.IsCancellationRequested) { var upstream = _repo.CurrentBranch?.Upstream; diff --git a/src/ViewModels/FetchInto.cs b/src/ViewModels/FetchInto.cs index 816687ec7..f99facadc 100644 --- a/src/ViewModels/FetchInto.cs +++ b/src/ViewModels/FetchInto.cs @@ -43,6 +43,7 @@ public override async Task Sure() .ExecAsync(); log.Complete(); + _repo.MarkBranchesDirtyManually(); if (_repo.SelectedViewIndex == 0 && !token.IsCancellationRequested) { diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index aa58226d9..4c288dc17 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1929,6 +1929,8 @@ private async Task AutoFetchOnUIThread() await new Commands.Fetch(FullPath, remote).Use(log).ExecAsync(); _lastFetchTime = DateTime.Now; + MarkBranchesDirtyManually(); + MarkTagsDirtyManually(); } catch {