Bounce PULL_REQUESTS model updates onto the UI thread

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 <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-07-03 15:25:04 +02:00
parent 4c9fdc4221
commit 4c3f8b51ea

View file

@ -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
})