From 4c3f8b51ea57787a407f104e110c35ee2093e5ce Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 3 Jul 2026 15:25:04 +0200 Subject: [PATCH] Bounce PULL_REQUESTS model updates onto the UI thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refreshGithubPullRequests and setGithubPullRequests now do their network work on the worker and write Model.PullRequests / PullRequestsMap in an onUIThreadUnlessRepoChanged bounce (the "no github remotes" and "no base remote" early-returns clear them the same way). rebuildPullRequestsMap moves into the bounce so the map is built from Model.Branches and Model.Remotes as they stand on the UI thread — after those scopes' refreshes have applied their own bounces — rather than from whatever the worker happened to see. The remaining worker-side reads of Model.Branches (to pick which upstream branches to query) are the same not-yet-addressed worker-read race that applies to the other bounced scopes. RefreshingPullRequestsMutex is left in place for the mutex cleanup. Co-Authored-By: Claude Sonnet 5 --- pkg/gui/controllers/helpers/refresh_helper.go | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 083907881..917a58ecf 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -1115,17 +1115,25 @@ func (self *RefreshHelper) refreshGithubPullRequests() { self.c.Mutexes().RefreshingPullRequestsMutex.Lock() defer self.c.Mutexes().RefreshingPullRequestsMutex.Unlock() + generation := self.c.State().GetRepoGeneration() + + clearPullRequests := func() { + self.onUIThreadUnlessRepoChanged(generation, func() error { + self.c.Model().PullRequests = nil + self.c.Model().PullRequestsMap = nil + return nil + }) + } + githubRemotes := getAuthenticatedGithubRemotes(self.getGithubRemotes(), self.c.Git().GitHub.GetAuthToken) if len(githubRemotes) == 0 { - self.c.Model().PullRequests = nil - self.c.Model().PullRequestsMap = nil + clearPullRequests() return } baseInfo := getGithubBaseRemote(githubRemotes, self.c.Git().GitHub.ConfiguredBaseRemoteName()) if baseInfo == nil { - self.c.Model().PullRequests = nil - self.c.Model().PullRequestsMap = nil + clearPullRequests() if !self.githubBaseRemotePromptDismissed[self.c.Git().RepoPaths.RepoPath()] { self.promptForBaseGithubRepo(githubRemotes) @@ -1243,6 +1251,8 @@ func (self *RefreshHelper) rebuildPullRequestsMap() { } func (self *RefreshHelper) setGithubPullRequests(baseInfo *githubRemoteInfo) { + generation := self.c.State().GetRepoGeneration() + if len(self.c.Model().Branches) == 0 { return } @@ -1260,11 +1270,14 @@ func (self *RefreshHelper) setGithubPullRequests(baseInfo *githubRemoteInfo) { return } - self.c.Model().PullRequests = prs self.savePullRequestsToCache(prs) - self.rebuildPullRequestsMap() - self.c.OnUIThread(func() error { + self.onUIThreadUnlessRepoChanged(generation, func() error { + self.c.Model().PullRequests = prs + // Rebuilding here rather than on the worker means the map is built from + // the branches and remotes as they are on the UI thread, after their + // own refreshes' bounces have applied. + self.rebuildPullRequestsMap() self.c.PostRefreshUpdate(self.c.Contexts().Branches) return nil })