Skip to content

Commit 97754d7

Browse files
committed
Simplify sync reconciliation: reuse syncStack instead of a parallel path
Review feedback noted the change felt heavier than the fix warranted. The weight came from `syncRemoteStack` (cmd/sync.go), a near-duplicate of submit's `syncStack` — same <2-PR guard, ListStacks, and update/create dispatch — that existed only to add an "already up to date" short-circuit. That one optimization is what spawned the second entry point, the pre-fetched-list threading, and the double-ListStacks it then required. Collapse it to a single reconciliation path: - Remove `syncRemoteStack`; `gh stack sync` now calls the shared `syncStack` directly. One path, one ListStacks per sync. - Fold `createNewStack` into `reconcileUntrackedStack` (renamed from `adoptRemoteStack`) so it returns a single `synced bool` instead of a `(handled, synced)` tuple and owns its own ListStacks again. - Inline `stackPRNumbers` back into `syncStack` (it was only extracted to share with the now-removed `syncRemoteStack`). - Drop the now-unused `strconv`/`github` imports from cmd/sync.go. Behavior note: a routine re-sync of an already-tracked stack now prints "Stack updated on GitHub with N PRs" instead of "Stack already up to date on GitHub". This is accurate (sync does PUT the current state) and matches submit. The "Stack synced" / "Branches synced" summary is unchanged, and submit's behavior is unchanged.
1 parent 43c715a commit 97754d7

4 files changed

Lines changed: 26 additions & 84 deletions

File tree

cmd/submit.go

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -684,19 +684,6 @@ func clearPendingModifyState(cfg *config.Config, gitDir string) {
684684
cfg.Successf("Stack recreated on GitHub to match local state")
685685
}
686686

687-
// stackPRNumbers returns the PR numbers for a stack in order (bottom to top),
688-
// including merged PRs. The stacks API expects the full list — omitting merged
689-
// PRs causes a "Stack contents have changed" rejection.
690-
func stackPRNumbers(s *stack.Stack) []int {
691-
var prNumbers []int
692-
for _, b := range s.Branches {
693-
if b.PullRequest != nil {
694-
prNumbers = append(prNumbers, b.PullRequest.Number)
695-
}
696-
}
697-
return prNumbers
698-
}
699-
700687
// syncStack creates or updates a stack on GitHub from the active PRs.
701688
// If the stack already exists (s.ID is set), it calls the PUT endpoint with
702689
// the full list of PRs to keep the remote stack in sync. If no stack exists
@@ -708,7 +695,15 @@ func stackPRNumbers(s *stack.Stack) []int {
708695
// (created, updated, or already in sync) and false otherwise (fewer than two
709696
// PRs, an unresolved divergence, stacked PRs unavailable, or an API failure).
710697
func syncStack(cfg *config.Config, client github.ClientOps, s *stack.Stack) bool {
711-
prNumbers := stackPRNumbers(s)
698+
// Collect PR numbers in stack order (bottom to top), including merged PRs.
699+
// The API expects the full list — omitting merged PRs causes a
700+
// "Stack contents have changed" rejection.
701+
var prNumbers []int
702+
for _, b := range s.Branches {
703+
if b.PullRequest != nil {
704+
prNumbers = append(prNumbers, b.PullRequest.Number)
705+
}
706+
}
712707

713708
// The API requires at least 2 PRs to form a stack.
714709
if len(prNumbers) < 2 {
@@ -721,32 +716,27 @@ func syncStack(cfg *config.Config, client github.ClientOps, s *stack.Stack) bool
721716

722717
// No locally tracked stack ID. The stack may already exist on GitHub
723718
// (created from the web UI or another clone) without being recorded
724-
// locally. Inspect the remote stacks and adopt a match instead of blindly
725-
// creating a new one, which the API rejects because the PRs are already
726-
// part of a stack.
719+
// locally. Adopt it instead of blindly creating a new one, which the API
720+
// rejects because the PRs are already part of a stack.
721+
return reconcileUntrackedStack(cfg, client, s, prNumbers)
722+
}
723+
724+
// reconcileUntrackedStack reconciles a locally untracked stack (s.ID == "")
725+
// with the stacks that already exist on GitHub. The PRs in s may already belong
726+
// to a remote stack created from the web UI or another clone; in that case we
727+
// adopt that stack rather than POST a new one (which the API rejects because the
728+
// PRs are already stacked). It creates a new stack when none match, refuses to
729+
// modify a divergent or PR-dropping stack, adopts a matching stack, or updates a
730+
// partially-formed one. It returns true when the remote stack object now
731+
// reflects the local stack.
732+
func reconcileUntrackedStack(cfg *config.Config, client github.ClientOps, s *stack.Stack, prNumbers []int) bool {
727733
stacks, err := client.ListStacks()
728734
if err != nil {
729735
// Couldn't inspect remote state — fall back to the create path, which
730736
// reports its own errors (handleCreate422 covers "already stacked").
731737
return createNewStack(cfg, client, s, prNumbers)
732738
}
733739

734-
return reconcileUntrackedStack(cfg, client, s, prNumbers, stacks)
735-
}
736-
737-
// reconcileUntrackedStack reconciles a locally untracked stack (s.ID == "")
738-
// against the already-fetched remote stacks. The PRs in s may already belong to
739-
// a remote stack created from the web UI or another clone; in that case we adopt
740-
// that stack rather than POST a new one (which the API rejects because the PRs
741-
// are already stacked). It creates a new stack when none match, refuses to
742-
// modify a divergent or PR-dropping stack, adopts a matching stack, or updates a
743-
// partially-formed one.
744-
//
745-
// Callers pass the pre-fetched stack list so a single ListStacks round-trip is
746-
// shared across the reconciliation flow (sync fetches the list once for its
747-
// already-up-to-date short-circuit and reuses it here). It returns true when the
748-
// remote stack object now reflects the local stack.
749-
func reconcileUntrackedStack(cfg *config.Config, client github.ClientOps, s *stack.Stack, prNumbers []int, stacks []github.RemoteStack) bool {
750740
matched, err := findMatchingStack(stacks, prNumbers)
751741
if err != nil {
752742
// Our PRs are spread across more than one remote stack. A PR can only

cmd/sync.go

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ package cmd
33
import (
44
"errors"
55
"fmt"
6-
"strconv"
76
"strings"
87

98
"github.com/cli/go-gh/v2/pkg/prompter"
109
"github.com/github/gh-stack/internal/config"
1110
"github.com/github/gh-stack/internal/git"
12-
"github.com/github/gh-stack/internal/github"
1311
"github.com/github/gh-stack/internal/modify"
1412
"github.com/github/gh-stack/internal/stack"
1513
"github.com/spf13/cobra"
@@ -240,7 +238,7 @@ func runSync(cfg *config.Config, opts *syncOptions) error {
240238
// summary message below.
241239
stackSynced := false
242240
if client, err := cfg.GitHubClient(); err == nil {
243-
stackSynced = syncRemoteStack(cfg, client, s)
241+
stackSynced = syncStack(cfg, client, s)
244242
}
245243

246244
// --- Step 6: Prune merged branches (optional) ---
@@ -350,52 +348,6 @@ func runSync(cfg *config.Config, opts *syncOptions) error {
350348
return nil
351349
}
352350

353-
// syncRemoteStack reconciles the stack object on GitHub with the local stack's
354-
// open PRs. It only links existing PRs into a stack — it never opens PRs (use
355-
// `gh stack submit` for that). It returns true when the remote stack object now
356-
// reflects the local stack (created, updated, adopted, or already in sync), and
357-
// false when there is nothing to sync or the remote stack could not be
358-
// reconciled (fewer than two PRs, stacked PRs unavailable, a divergence across
359-
// multiple stacks, or an API failure).
360-
//
361-
// A stack on GitHub requires at least two open PRs, so a single-PR or PR-less
362-
// stack reconciles to false and the caller reports only the branches as synced.
363-
func syncRemoteStack(cfg *config.Config, client github.ClientOps, s *stack.Stack) bool {
364-
prNumbers := stackPRNumbers(s)
365-
if len(prNumbers) < 2 {
366-
return false
367-
}
368-
369-
// Inspect the remote stacks once so a routine sync that has not changed the
370-
// PR membership does not issue a redundant — and misleading — update, and so
371-
// the create/adopt path can reuse the same list (one ListStacks per sync).
372-
stacks, err := client.ListStacks()
373-
if err != nil {
374-
// Couldn't inspect remote state; attempt a direct create/update and let
375-
// those helpers surface their own availability/PAT/create errors.
376-
if s.ID != "" {
377-
return updateStack(cfg, client, s, prNumbers)
378-
}
379-
return createNewStack(cfg, client, s, prNumbers)
380-
}
381-
382-
if matched, mErr := findMatchingStack(stacks, prNumbers); mErr == nil &&
383-
matched != nil && slicesEqual(matched.PullRequests, prNumbers) {
384-
// The remote stack already lists exactly these PRs — record its ID so
385-
// future operations stay cheap and report it as in sync.
386-
s.ID = strconv.Itoa(matched.ID)
387-
cfg.Successf("Stack already up to date on GitHub")
388-
return true
389-
}
390-
391-
// Membership differs (or no stack exists yet). Reuse the list we already
392-
// fetched for the create/adopt/update path.
393-
if s.ID != "" {
394-
return updateStack(cfg, client, s, prNumbers)
395-
}
396-
return reconcileUntrackedStack(cfg, client, s, prNumbers, stacks)
397-
}
398-
399351
// restoreBranches resets each branch to its original SHA, collecting any errors.
400352
func restoreBranches(originalRefs map[string]string) []string {
401353
var errors []string

cmd/sync_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1733,7 +1733,7 @@ func TestSync_AdoptsExistingEqualRemoteStack(t *testing.T) {
17331733

17341734
output := runSyncWithGitHub(t, newSyncMockNoRebase(tmpDir, "b1"), ghMock)
17351735

1736-
assert.Contains(t, output, "Stack already up to date on GitHub")
1736+
assert.Contains(t, output, "already up to date")
17371737
assert.Contains(t, output, "Stack synced")
17381738
assert.NotContains(t, output, "Branches synced")
17391739

skills/gh-stack/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ gh stack sync [flags]
643643
- `✓ Pushed N branches`
644644
- `✓ PR #N (<branch>) — Open` per branch
645645
- `Merged: #N, #M` for merged branches
646-
- `✓ Stack created on GitHub with N PRs` / `✓ Stack updated on GitHub with N PRs` / `Stack already up to date on GitHub` (when two or more PRs exist)
646+
- `✓ Stack created on GitHub with N PRs` / `✓ Stack updated on GitHub with N PRs` / `Linked to the existing stack on GitHub` (when two or more PRs exist)
647647
- `✓ Pruned <branch> (merged)` per pruned branch (when pruning)
648648
- `✓ Stack synced` when the stack object on GitHub was created/updated to match local, or `✓ Branches synced` when only the branches were synced (fewer than two PRs, stacked PRs unavailable, or a divergence)
649649

0 commit comments

Comments
 (0)