@@ -260,8 +260,11 @@ func loadStack(cfg *config.Config, branch string) (*loadStackResult, error) {
260260}
261261
262262// loadStackByNumber loads the locally tracked stack whose stack number matches
263- // the given value. It prints a helpful error and returns a non-nil error when
264- // no local stack has that number.
263+ // the given value. Stack files created before the number was tracked store only
264+ // the internal ID (Number == 0); such legacy stacks are resolved by mapping
265+ // their ID to a remote stack number so they can still be targeted by number. It
266+ // prints a helpful error and returns a non-nil error when no local stack
267+ // resolves to that number.
265268func loadStackByNumber (cfg * config.Config , number int ) (* loadStackResult , error ) {
266269 gitDir , err := git .GitDir ()
267270 if err != nil {
@@ -275,6 +278,28 @@ func loadStackByNumber(cfg *config.Config, number int) (*loadStackResult, error)
275278 return nil , fmt .Errorf ("failed to load stack state: %w" , err )
276279 }
277280
281+ // Direct match on the tracked stack number.
282+ if result := stackResultByNumber (sf , gitDir , number ); result != nil {
283+ return result , nil
284+ }
285+
286+ // No direct match — backfill legacy stacks' numbers from the remote and
287+ // retry, so `gh stack unstack <number>` also works for stacks tracked
288+ // before the number was recorded locally.
289+ if backfillLegacyStackNumbers (cfg , sf , gitDir ) {
290+ if result := stackResultByNumber (sf , gitDir , number ); result != nil {
291+ return result , nil
292+ }
293+ }
294+
295+ cfg .Errorf ("stack #%d is not tracked locally" , number )
296+ cfg .Printf ("Run `%s` to check it out first" , cfg .ColorCyan (fmt .Sprintf ("gh stack checkout %d" , number )))
297+ return nil , fmt .Errorf ("stack #%d is not tracked locally" , number )
298+ }
299+
300+ // stackResultByNumber returns a loadStackResult for the locally tracked stack
301+ // whose Number matches, or nil when none does.
302+ func stackResultByNumber (sf * stack.StackFile , gitDir string , number int ) * loadStackResult {
278303 for i := range sf .Stacks {
279304 if sf .Stacks [i ].Number == number {
280305 currentBranch , _ := git .CurrentBranch ()
@@ -283,13 +308,59 @@ func loadStackByNumber(cfg *config.Config, number int) (*loadStackResult, error)
283308 StackFile : sf ,
284309 Stack : & sf .Stacks [i ],
285310 CurrentBranch : currentBranch ,
286- }, nil
311+ }
287312 }
288313 }
314+ return nil
315+ }
289316
290- cfg .Errorf ("stack #%d is not tracked locally" , number )
291- cfg .Printf ("Run `%s` to check it out first" , cfg .ColorCyan (fmt .Sprintf ("gh stack checkout %d" , number )))
292- return nil , fmt .Errorf ("stack #%d is not tracked locally" , number )
317+ // backfillLegacyStackNumbers fills in the human-facing Number for locally
318+ // tracked stacks that predate it (Number == 0 but ID set) by mapping their
319+ // internal ID to the remote stack list, persisting any updates. Returns true
320+ // when at least one number was filled in. Best-effort: returns false on any
321+ // client or API error rather than failing the caller.
322+ func backfillLegacyStackNumbers (cfg * config.Config , sf * stack.StackFile , gitDir string ) bool {
323+ needsResolve := false
324+ for i := range sf .Stacks {
325+ if sf .Stacks [i ].Number == 0 && sf .Stacks [i ].ID != "" {
326+ needsResolve = true
327+ break
328+ }
329+ }
330+ if ! needsResolve {
331+ return false
332+ }
333+
334+ client , err := cfg .GitHubClient ()
335+ if err != nil {
336+ return false
337+ }
338+ stacks , err := client .ListStacks ()
339+ if err != nil {
340+ return false
341+ }
342+ numberByID := make (map [string ]int , len (stacks ))
343+ for _ , rs := range stacks {
344+ numberByID [strconv .Itoa (rs .ID )] = rs .Number
345+ }
346+
347+ changed := false
348+ for i := range sf .Stacks {
349+ if sf .Stacks [i ].Number != 0 || sf .Stacks [i ].ID == "" {
350+ continue
351+ }
352+ if n , ok := numberByID [sf .Stacks [i ].ID ]; ok && n != 0 {
353+ sf .Stacks [i ].Number = n
354+ changed = true
355+ }
356+ }
357+ if changed {
358+ if err := stack .Save (gitDir , sf ); err != nil {
359+ // Non-fatal: the in-memory backfill still lets us resolve the target.
360+ cfg .Warningf ("could not persist stack numbers: %v" , err )
361+ }
362+ }
363+ return changed
293364}
294365
295366// handleSaveError translates a stack.Save error into the appropriate user
@@ -608,6 +679,12 @@ func syncStackPRsFromRemote(client github.ClientOps, s *stack.Stack) (map[string
608679 for _ , rs := range stacks {
609680 if strconv .Itoa (rs .ID ) == s .ID {
610681 remotePRNumbers = rs .PRNumbers ()
682+ // Backfill the human-facing stack number for stack files created
683+ // before it was tracked, so callers (view, submit TUI) can display
684+ // it. Persisted by whichever command later saves the stack file.
685+ if s .Number == 0 {
686+ s .Number = rs .Number
687+ }
611688 break
612689 }
613690 }
0 commit comments