@@ -11,6 +11,7 @@ import (
1111 "github.com/cli/go-gh/v2/pkg/prompter"
1212 "github.com/github/gh-stack/internal/config"
1313 "github.com/github/gh-stack/internal/git"
14+ "github.com/github/gh-stack/internal/github"
1415 "github.com/github/gh-stack/internal/stack"
1516)
1617
@@ -231,27 +232,126 @@ func resolveStack(sf *stack.StackFile, branch string, cfg *config.Config) (*stac
231232}
232233
233234// syncStackPRs discovers and updates pull request metadata for branches in a stack.
234- // For each branch, it queries GitHub for the most recent PR and updates the
235- // PullRequestRef including merge status. Branches with already-merged PRs are skipped.
235+ //
236+ // When the stack has a remote ID, the stack API is the source of truth: the
237+ // authoritative PR list is fetched from the server and matched to local
238+ // branches by head branch name. PRs remain associated even if closed.
239+ //
240+ // When no remote stack exists, branch-name-based discovery is used:
241+ //
242+ // 1. No tracked PR — look for an OPEN PR by head branch name.
243+ // 2. Tracked PR (not merged) — refresh status by number; if closed,
244+ // clear the association and fall through to path 1.
245+ // 3. Tracked PR (merged) — skip; the merged state is final.
246+ //
236247// The transient Queued flag is also populated from the API response.
237248func syncStackPRs (cfg * config.Config , s * stack.Stack ) {
238249 client , err := cfg .GitHubClient ()
239250 if err != nil {
240251 return
241252 }
242253
254+ // When the stack has a remote ID, the stack API is the source of truth.
255+ if s .ID != "" {
256+ if syncStackPRsFromRemote (client , s ) {
257+ return
258+ }
259+ }
260+
261+ // No remote stack (or remote sync failed) — local discovery.
243262 for i := range s .Branches {
244263 b := & s .Branches [i ]
245264
246265 if b .IsMerged () {
247266 continue
248267 }
249268
250- pr , err := client .FindAnyPRForBranch (b .Branch )
269+ if b .PullRequest != nil && b .PullRequest .Number != 0 {
270+ // Tracked PR — refresh its state.
271+ pr , err := client .FindPRByNumber (b .PullRequest .Number )
272+ if err != nil {
273+ continue // API error — keep existing tracked PR
274+ }
275+ if pr == nil {
276+ // PR not found — clear stale ref and fall through
277+ // to the open-PR lookup below.
278+ b .PullRequest = nil
279+ b .Queued = false
280+ } else {
281+ b .PullRequest = & stack.PullRequestRef {
282+ Number : pr .Number ,
283+ ID : pr .ID ,
284+ URL : pr .URL ,
285+ Merged : pr .Merged ,
286+ }
287+ b .Queued = pr .IsQueued ()
288+
289+ // If the PR was closed (not merged), remove the association
290+ // so we fall through to the open-PR lookup below.
291+ if pr .State == "CLOSED" {
292+ b .PullRequest = nil
293+ b .Queued = false
294+ } else {
295+ continue
296+ }
297+ }
298+ }
299+
300+ // No tracked PR (or just cleared) — only adopt OPEN PRs to avoid
301+ // picking up stale merged/closed PRs from a previous use of this
302+ // branch name.
303+ pr , err := client .FindPRForBranch (b .Branch )
304+ if err != nil || pr == nil {
305+ continue
306+ }
307+ b .PullRequest = & stack.PullRequestRef {
308+ Number : pr .Number ,
309+ ID : pr .ID ,
310+ URL : pr .URL ,
311+ }
312+ b .Queued = pr .IsQueued ()
313+ }
314+ }
315+
316+ // syncStackPRsFromRemote uses the stack API to sync PR state. The remote
317+ // stack's PR list is the source of truth — PRs stay associated even if
318+ // closed. Returns true if the sync succeeded, false if we should fall
319+ // back to local discovery (e.g. stack not found remotely, API error).
320+ func syncStackPRsFromRemote (client github.ClientOps , s * stack.Stack ) bool {
321+ stacks , err := client .ListStacks ()
322+ if err != nil {
323+ return false
324+ }
325+
326+ // Find our stack in the remote list.
327+ var remotePRNumbers []int
328+ for _ , rs := range stacks {
329+ if strconv .Itoa (rs .ID ) == s .ID {
330+ remotePRNumbers = rs .PullRequests
331+ break
332+ }
333+ }
334+ if remotePRNumbers == nil {
335+ return false
336+ }
337+
338+ // Fetch each remote PR's details and index by head branch name.
339+ prByBranch := make (map [string ]* github.PullRequest , len (remotePRNumbers ))
340+ for _ , num := range remotePRNumbers {
341+ pr , err := client .FindPRByNumber (num )
251342 if err != nil || pr == nil {
252343 continue
253344 }
345+ prByBranch [pr .HeadRefName ] = pr
346+ }
254347
348+ // Match remote PRs to local branches.
349+ for i := range s .Branches {
350+ b := & s .Branches [i ]
351+ pr , ok := prByBranch [b .Branch ]
352+ if ! ok {
353+ continue
354+ }
255355 b .PullRequest = & stack.PullRequestRef {
256356 Number : pr .Number ,
257357 ID : pr .ID ,
@@ -260,6 +360,8 @@ func syncStackPRs(cfg *config.Config, s *stack.Stack) {
260360 }
261361 b .Queued = pr .IsQueued ()
262362 }
363+
364+ return true
263365}
264366
265367// updateBaseSHAs refreshes the Base and Head SHAs for all active branches
0 commit comments