Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: github/gh-stack
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.0.6
Choose a base ref
...
head repository: github/gh-stack
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.0.7
Choose a head ref
  • 9 commits
  • 61 files changed
  • 5 contributors

Commits on Jun 15, 2026

  1. Bump js-yaml in /docs in the npm_and_yarn group across 1 directory (#130

    )
    
    Bumps the npm_and_yarn group with 1 update in the /docs directory: [js-yaml](https://github.com/nodeca/js-yaml).
    
    
    Updates `js-yaml` from 4.1.1 to 4.2.0
    - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/nodeca/js-yaml/commits)
    
    ---
    updated-dependencies:
    - dependency-name: js-yaml
      dependency-version: 4.2.0
      dependency-type: indirect
      dependency-group: npm_and_yarn
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Jun 15, 2026
    Configuration menu
    Copy the full SHA
    84d160b View commit details
    Browse the repository at this point in the history

Commits on Jun 17, 2026

  1. Add AGENTS.md and copilot-instructions.md for AI agent onboarding (

    …#133)
    
    * Add AGENTS.md and copilot-instructions.md for AI agent onboarding
    
    Add two complementary instruction files so AI coding agents can work
    effectively in this repository without re-discovering conventions:
    
    - AGENTS.md (7K chars): agent-agnostic open standard with full project
      structure, build/test commands, coding patterns, testing conventions,
      error handling, key interfaces, and non-obvious gotchas.
    
    - .github/copilot-instructions.md (2K chars): concise Copilot-specific
      instructions under the 4K code review limit, covering the essentials
      and referencing AGENTS.md for full details.
    
    Closes #132
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * fix: update cmd to generate an executable binary
    
    Co-authored-by: Sameen Karim <skarim@github.com>
    
    * fix: update cmd to get the build output as an exec binary
    
    Co-authored-by: Sameen Karim <skarim@github.com>
    
    * Fix build command and errors.As usage per review feedback
    
    - go build ./... compiles but does not produce a binary. Changed to
      go build -o gh-stack . which actually outputs the executable.
    - errors.As(err, &ExitError{}) panics at runtime because the value
      type ExitError does not satisfy the error interface (only *ExitError
      does). Updated to the correct two-line pattern matching cmd/root.go.
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    Co-authored-by: Sameen Karim <skarim@github.com>
    3 people authored Jun 17, 2026
    Configuration menu
    Copy the full SHA
    4759126 View commit details
    Browse the repository at this point in the history

Commits on Jun 19, 2026

  1. Unstack Updates: preflight PR eligibility before delete and improve A…

    …PI errors (#136)
    
    * unstack: preflight PR eligibility before delete and improve API errors.Block unstack delete only when all PRs in the stack are ineligible
    
    * Update cmd/unstack.go
    
    Co-authored-by: Sameen Karim <skarim@github.com>
    
    * remove cfg and update help text
    
    ---------
    
    Co-authored-by: Sameen Karim <skarim@github.com>
    wiseemily88 and skarim authored Jun 19, 2026
    Configuration menu
    Copy the full SHA
    67d118f View commit details
    Browse the repository at this point in the history

Commits on Jun 30, 2026

  1. Redesign TUI shared header and embed the GitHub logo as an image (#143)

    * Redesign the shared header and embed the GitHub logo as an image
    
    Rework the shared gh-stack header (used by `view` and `modify`, and reused
    by `submit` later in this stack) for a cleaner, more responsive look, and
    replace the braille/ASCII Invertocat with a real image of the GitHub mark.
    
    - Logo: embed the Invertocat PNG with go:embed and draw it via an inline-
      image protocol (kitty or iTerm2). It is image-or-nothing: when no protocol
      is available, stdout is not a TTY, or we are inside tmux/screen, no logo is
      drawn and the text falls back to the normal left padding. Detection is
      environment-based and cached so it never blocks the TUI, and a fixed kitty
      image id lets the header clear or replace the logo in place instead of
      leaving copies behind.
    - Layout: place the logo in the top-left corner beside the title and version,
      with the stack-info lines left-aligned beneath it on the same left margin.
      Size the box to its content for each view so there is no trailing empty row.
    - Responsiveness: hide the logo progressively — first when the viewport is too
      narrow, then a little before the rest of the header at short heights, where
      a vertical resize could otherwise leave a ghost of the inline image.
    - Add unit tests for the header's responsive thresholds.
    
    * Drop the unused HeaderHeight constant and dedupe the header config build
    
    Follow-up to review feedback on the shared-header redesign:
    
    - Remove the HeaderHeight constant. Nothing referenced it (callers compute
      height via HeaderHeightFor), and its doc described a "maximum" that
      HeaderHeightFor does not actually enforce, so the comment was misleading.
    - In the view and modify View() methods, build the header config once and reuse
      it for both RenderHeader and the height reservation instead of rebuilding it
      twice per frame. The click/scroll handlers keep deriving the height from the
      same config, so the header's dimensions remain a single source of truth.
    skarim authored Jun 30, 2026
    Configuration menu
    Copy the full SHA
    0efc764 View commit details
    Browse the repository at this point in the history
  2. Add an interactive submit TUI for customizing each PR's title, descri…

    …ption, and draft state (#147)
    
    * Add submitview data model and PR draft override plumbing
    
    Introduce the internal/tui/submitview package that will back the new
    interactive `gh stack submit` TUI, and wire its per-PR override contract
    into the submit command without changing current behavior.
    
    - submitview: BranchState model (NEW/OPEN/DRAFT/QUEUED/MERGED/CLOSED) with
      selectability/editability rules, SubmitNode UI state with edit detection,
      PRDraft override type, state derivation, title/description prefill, and
      state-badge/panel/tab styles.
    - submit: refactor ensurePR/createPR to accept an optional per-branch
      override map (title/body/draft/include); deselected NEW branches are
      pushed but get no PR.
    
    The override map is nil on the --auto / non-interactive path, so the
    agent-compat contract is unchanged. Fully unit tested.
    
    * Add single-screen submit TUI
    
    Introduce an interactive, single-screen editor for `gh stack submit`,
    built on Bubble Tea and Lip Gloss.
    
    The left panel renders the stack as a connected tree down to the trunk.
    Every branch without a PR is included by default; deselect one with its
    checkbox or `^x`. Because each PR builds on the branch below it,
    deselecting a branch also deselects the ones stacked above it, and
    re-including a branch re-includes the ones below it that it depends on.
    The cursor uses its own cyan accent so it reads distinctly from the green
    new/included color; existing PRs are shown dimmed with a no-entry glyph.
    
    The right panel edits the focused branch's PR in web-create-PR order: a
    header with the branch name and an include chip ("Creating PR" /
    "Skipped"), the title, a scrollable description (Glamour markdown preview
    and $EDITOR escape, with a scrollbar and mouse click-to-position), and a
    ready to draft segmented toggle (defaulting to ready). A footer strip
    shows the PR progress, the next branch, and the editor hints. Skipping a
    branch dims its body; branches that already have a PR show a read-only
    card linking to the PR.
    
    It shares the gh-stack header (art, title, stack info, and keyboard
    shortcuts) with `gh stack view` and `gh stack modify` for a unified look.
    Submit every included PR at once with Ctrl+S. Full keyboard and mouse
    support throughout.
    
    * Wire the single-screen submit TUI into `gh stack submit`
    
    Launch the submit editor from `gh stack submit` in interactive terminals,
    collecting per-branch PR drafts and applying them in a single batch. In
    non-interactive terminals or with --auto, fall back to auto-generated
    titles and skip the editor. Update the README and CLI reference to
    describe the single-screen flow.
    
    * Use the API PR title/body for existing PRs and fix new-PR defaults
    
    For existing PRs, the submit TUI showed a commit/template-derived draft
    instead of the pull request's real title and body. Fetch the actual title
    and body and render them in the read-only card:
    
    - open/draft/queued (tracked) and adopted-open PRs now carry title/body
      through the existing batch sync (added the fields to the GraphQL queries
      and PRDetails — no extra round trips), and
    - merged branches (which skip the live refresh) are filled in by a targeted
      enrichment step run only when the submit TUI opens.
    
    Also align the new-PR defaults with the non-TUI submit's defaultPRTitleBody:
    
    - Title: the commit subject only when the branch has exactly one commit,
      otherwise the humanized branch name (was: the oldest commit's subject even
      for multi-commit branches).
    - Description: the PR template, else the single commit's body, else empty
      (removed the bulleted commit-subject list for multi-commit branches).
    
    * Stop mouse wheel from leaking escape characters into form fields
    
    Scrolling the mouse wheel while a title or description field was focused
    could insert stray characters such as "[<65;54;51M" into the field. The
    submit TUI ran the Bubble Tea program with WithMouseAllMotion (mode 1003),
    which reports an event on every pointer move. During a wheel scroll that
    floods the input stream, and under that volume Bubble Tea splits an SGR
    mouse escape sequence ("\x1b[<Cb;Cx;Cy(M|m)") across input reads; the
    leftover bytes of a partially-parsed sequence are then emitted as key
    runes and inserted into the focused text input.
    
    Two changes fix this:
    
      - Switch to WithMouseCellMotion (mode 1002), which reports clicks, drag,
        and wheel but not idle pointer motion. That removes the per-move input
        flood, so under a real terminal's reads (up to 256 bytes) the only
        fragment that still surfaces is a single, clean burst at each wheel
        notch boundary. The TUI never used idle-hover for rendering, so
        cell-motion loses nothing.
    
      - Drop any leaked fragments before they reach a field. A split SGR mouse
        sequence surfaces as an Alt+"[" (the consumed "\x1b[") followed by
        body fragments ("<65;54;5", "1M"), or occasionally the whole body in
        one run ("[<65;54;51M"). consumeLeakedMouseKey recognises the start,
        swallows the body up to its "M"/"m" terminator, and bails out the
        moment a rune does not fit an SGR body, so ordinary typing (including
        "<", ";", digits, "M") and bracketed pastes are never eaten.
    
    Tests cover every split point of an SGR sequence, single-run tails,
    preserved real typing, a stray Alt+"[", bracketed paste, and that wheel
    events never modify the focused field. Verified end-to-end by feeding
    1,500 wheel sequences through the real parser under terminal-sized reads
    and confirming the field stays empty.
    
    * Re-enable mouse tracking after the external editor closes
    
    Opening the description in $EDITOR with ^e and then quitting left the
    mouse unresponsive: clicks and wheel scrolling stopped working while
    keyboard navigation still did.
    
    The editor is launched with tea.ExecProcess, which releases the terminal
    before running the command and calls Bubble Tea's RestoreTerminal when it
    returns. RestoreTerminal re-enables the alt-screen, bracketed paste, and
    focus reporting, but it does not re-enable mouse tracking. The editor
    (e.g. vim) disables mouse reporting on exit, so once control returns to
    the TUI the terminal no longer emits mouse events.
    
    Re-arm mouse mode when the editor-finished message arrives by batching
    tea.EnableMouseCellMotion with the handler's command. That re-enables
    cell-motion and SGR mouse reporting, matching the WithMouseCellMotion
    option the program starts with, on every editor-return path (success or
    error).
    
    * dead code cleanup
    
    * support mouse input to move cursor in title field
    
    * use textArea for title to support word wrap for long inputs
    skarim authored Jun 30, 2026
    Configuration menu
    Copy the full SHA
    b6bcce1 View commit details
    Browse the repository at this point in the history
  3. Adapt theme colors to light and dark terminals (#149)

    * Make the TUIs adapt to light and dark terminal backgrounds
    
    The submit, view, and modify TUIs were tuned for dark terminals. On light
    or solarized-light backgrounds the result was hard to read and inverted:
    primary text used ANSI white (invisible on white), dim chrome used light
    grays (too faint), and accents used bright cyan (low contrast) — so
    "active" things looked lighter than "disabled" ones.
    
    Introduce a centralized, background-aware color palette and migrate all
    three TUIs to it:
    
    - internal/tui/shared/theme.go: a semantic palette of lipgloss.AdaptiveColor
      values (primary/muted/faint text, chrome/border, accent, PR-state colors,
      badge backgrounds, row shade, button, switch). lipgloss resolves the
      light/dark variant per render from the terminal background, which Bubble
      Tea detects at startup; terminals that don't report it fall back to dark,
      preserving the original look.
    - Replace every hardcoded ANSI color in shared/, submitview/, and
      modifyview/ with palette roles. The four pre-rendered status icons now
      render at use-time so their adaptive colors resolve correctly. The submit
      markdown preview picks glamour's light or dark style from the detected
      background.
    - GH_STACK_THEME=auto|light|dark forces the palette for terminals that
      mis-detect (some SSH/tmux setups); wired via the root command's
      PersistentPreRun before any render. Documented in the README and CLI docs.
    
    Neutral text/chrome use truecolor hex (GitHub Primer-inspired) for
    predictability across themes, including solarized which repurposes ANSI
    8-15; lipgloss downsamples on terminals without truecolor.
    
    Tests verify the palette resolves differently for light vs dark and that
    GH_STACK_THEME is honored.
    
    * Apply background-aware colors to all command output
    
    Background detection and the GH_STACK_THEME override (added for the TUIs)
    only affected the interactive screens. Plain command output -- status
    messages and interactive prompts -- went through the mgutz/ansi library
    with fixed ANSI palette names (green/red/yellow/cyan/...), so it never
    adapted to the terminal background and could read poorly on light or
    solarized themes.
    
    Unify everything on the same adaptive palette so all colors react to the
    detected background and to GH_STACK_THEME.
    
    - Extract internal/theme, a foundational package with no internal
      dependencies, that owns:
        - the background-aware lipgloss.AdaptiveColor palette (moved out of
          internal/tui/shared),
        - ApplyOverride(), the GH_STACK_THEME=auto|light|dark logic, and
        - non-TUI colorizers (Success/Error/Warning/Blue/Magenta/Cyan/Gray/
          Bold) plus FgSeqs(), which returns the raw start/reset escapes used
          to color the user's echoed prompt input.
    - internal/tui/shared/theme.go now re-exports the palette, so the TUI code
      keeps referring to shared.ColorX unchanged.
    - internal/config/config.go wires the Config.Color* funcs to the theme
      colorizers and drops mgutz/ansi (now an indirect dependency only).
    - cmd/utils.go colors the prompt icon and echoed input via theme.
    - cmd/root.go calls theme.ApplyOverride() in PersistentPreRun.
    
    Detection adds no cost: because the command package imports Bubble Tea,
    its init() already triggers (and caches) the terminal background query for
    every command, so the non-TUI colorizers just read the cached value.
    Terminals that don't answer the query fall back to the dark palette;
    GH_STACK_THEME=light|dark forces it. Colors are truecolor on capable
    terminals and downsample to the nearest ANSI color elsewhere.
    
    Tests: internal/theme covers palette adaptiveness, ApplyOverride, the
    colorizers, and FgSeqs; a new internal/config test verifies the wired-up
    Config.Color* funcs adapt to the background when color is enabled.
    
    Docs: README and the CLI reference note that GH_STACK_THEME now controls
    all colored output, not just the interactive screens.
    
    No behavior change beyond colors.
    skarim authored Jun 30, 2026
    Configuration menu
    Copy the full SHA
    797c62e View commit details
    Browse the repository at this point in the history
  4. Adopt an existing remote stack on submit instead of erroring (#153)

    When a stack existed on GitHub but was not recorded in the local tracking
    file (`.git/gh-stack`, i.e. `s.ID == ""`) -- for example a stack created
    from the web UI or in another clone -- `gh stack submit` blindly called the
    create-stack API. The server rejected the request because the PRs were
    already stacked, surfacing a confusing, dead-end error:
    
        Could not create stack: Pull requests are already part of a stack.
    
    `syncStack()` only ever consulted the local `s.ID`. When it was empty it
    always POSTed to create, and the only handling for the rejection
    (`handleCreate422`) parsed the error string and at best printed "up to
    date" -- it never imported the remote stack ID, so the local/remote
    mismatch was never resolved and recurred on every submit.
    
    Detect and adopt the existing stack before creating one. When `s.ID` is
    empty and there are at least two PRs, `syncStack` now lists the
    repository's stacks and reconciles, mirroring the patterns already used by
    `checkout` and `link`:
    
    - No remote stack contains our PRs -> create a new one (unchanged).
    - The remote stack's PRs are all tracked locally (an exact match, or e.g.
      two of three already stacked with a third added on top) -> adopt the
      remote stack ID into local tracking and PUT the full, ordered PR list,
      appending any new PRs to the top. The adopted ID is persisted by the
      existing stack.Save in runSubmit.
    - The remote stack contains PRs we are not tracking locally -> refuse and
      warn rather than silently dropping them, pointing the user to
      `gh stack checkout <pr>` to import the full stack first.
    - Our PRs are spread across multiple remote stacks (an unresolvable
      divergence, since a PR can belong to only one stack) -> warn and skip.
    
    The new logic reuses `findMatchingStack`, `formatPRList`, and `slicesEqual`
    from the link command; `syncStack`'s signature is unchanged, and adoption
    is best-effort: listing failures degrade to the previous create path and
    never fail the submit, since the PRs are already pushed and created.
    
    Also broaden the create-stack 422 fallback (`handleCreate422`) to recognize
    the server's "already part of a stack" phrasing in addition to "already
    stacked", so that if the stack listing is unavailable the message is still
    actionable instead of the raw "Could not create stack" error above.
    
    Tests: six new cases in cmd/submit_test.go cover exact-match adoption (ID
    imported, no create/update call), additive two-of-three adoption (update
    with the full list), remote-superset refusal, PRs spanning multiple stacks,
    the ListStacks-error fallthrough to create, and the broadened fallback
    phrasing. Existing syncStack tests are unchanged.
    skarim authored Jun 30, 2026
    Configuration menu
    Copy the full SHA
    9b30b7f View commit details
    Browse the repository at this point in the history
  5. Fork unmerged branches into new stack (#154)

    * Fork unmerged branches into a new stack when the base stack is fully merged
    
    Once every PR that is officially part of a stack on GitHub has been merged
    -- especially after the merged branches are deleted upstream -- you can no
    longer add to that stack. A new PR on top would target the trunk directly
    instead of chaining onto the merged PRs, so the remote stack's "each PR's
    base ref is the previous PR's head ref" invariant no longer holds. On the
    next `gh stack submit`, the stack update was rejected and surfaced as a
    confusing, dead-end warning:
    
        Failed to update stack on GitHub: Pull requests must form a stack,
        where each PR's base ref is the previous PR's head ref
    
    `submit` had no handling for this: `syncStack` always sent the full PR list
    (including the merged-and-deleted ones), so the API rejected the broken
    chain even though the new PRs had already been created with correct bases.
    
    Fork the survivors into a fresh stack instead of failing. After syncing PR
    state and before pushing, `runSubmit` now calls `maybeForkFromMergedBase`:
    
    - It triggers only when every PR officially part of the tracked remote
      stack (`s.ID`) has merged. Membership is read from the stacks API, so
      open PRs that are not part of the remote stack do not count, and -- this
      is the key guard -- a normal partial, bottom-up merge (where the remote
      stack still lists an open PR) is left completely untouched. A cheap
      pre-check (the local stack must have at least one merged branch) avoids
      an extra ListStacks call on the common path.
    - The local branches are partitioned: those still in the merged remote
      stack stay behind; everything else (new branches, plus open PRs that were
      never part of that remote stack) is lifted into a brand-new stack rooted
      at the original trunk, with an empty remote ID. The bottom survivor is
      re-based onto the trunk.
    - `runSubmit` continues with the new stack, so the push loop, PR creation,
      and `syncStack` all operate on it; the empty ID routes `syncStack` through
      the adopt/create path and a fresh stack is created on GitHub.
    - The original, fully merged stack is left untouched on GitHub. Locally it
      is kept as a record only if at least one of its branches still exists in
      the working copy; otherwise it is dropped. No data is lost -- those PRs
      are already merged on GitHub.
    
    To restructure the stack file safely, add `StackFile.IndexOfStack`, which
    locates a stack by pointer identity so the fork can capture what it needs
    before `AddStack`/`RemoveStack` reallocate the underlying slice.
    
    Also soften the partial-merge case that does not fork: when an `UpdateStack`
    call fails with the "must form a stack" 422 and the stack still contains
    merged branches, report it as an informational note (the unmerged PRs were
    pushed and re-based onto the trunk) rather than a scary failure warning.
    
    Scope is limited to `submit`. `add` and `checkout` keep their existing
    "refuse and suggest `gh stack init`" behavior on fully merged stacks.
    
    Tests:
    - cmd/submit_test.go: TestSubmit_ForksWhenRemoteStackFullyMerged covers both
      disposition variants (the old stack is removed when its merged branches
      are gone locally, kept when they still exist) and asserts that only the
      new branches are pushed, the fork message is printed, a fresh stack is
      created, and the local stack file is split into two stacks.
      TestSubmit_NoForkWhenRemoteStackHasOpenPR verifies the everyday bottom-up
      merge is not forked and that the broken-chain 422 is reported calmly.
      TestUpdateStack_BrokenChainAfterMerge checks the calm-vs-warn branch.
    - internal/stack/stack_test.go: TestIndexOfStack covers identity lookup and
      the not-found case.
    
    Docs: README, the CLI reference, the stacked-PRs guide, the FAQ, and the
    agent SKILL.md note that submitting onto a fully merged stack starts a new
    stack rooted at the trunk.
    
    * Handle fully merged stacks gracefully in the view and modify TUIs
    
    Merged branches (and their PRs) are not selectable, so once an entire stack
    has landed there is nothing to act on -- yet the TUIs did not reflect that:
    
    - `gh stack view` still drew a highlighted cursor on the top branch even
      though it could not be selected. Navigation, checkout, and the per-branch
      toggles all silently did nothing, with no indication of why.
    - `gh stack modify` opened its full editor on a stack with nothing left to
      restructure, instead of short-circuiting like `gh stack submit` does when
      there is nothing to submit.
    
    Reflect the "nothing actionable" state in both TUIs.
    
    View (internal/tui/stackview/model.go):
    
    - Hide the cursor when every branch is merged. `New` now starts the cursor
      at -1 and only lands it on the current or first non-merged branch; when
      none exists the cursor stays hidden, so no row is rendered as focused. The
      existing `m.cursor >= 0` guards and merged-skipping `moveCursor` already
      make every cursor action a no-op in that state, and mouse-wheel scrolling
      still works for tall merged stacks.
    - Dim the shortcuts that depend on the cursor. `buildHeaderConfig` marks
      navigate, commits, files, open PR, and checkout as `Disabled` (rendered
      gray via the existing ShortcutEntry.Disabled styling) when all branches
      are merged, leaving only `q quit` active.
    
    Modify (cmd/modify.go):
    
    - Short-circuit before opening the TUI. After preconditions pass and PR
      state is synced, `runModify` now returns early when the stack is fully
      merged, printing "All branches in this stack have been merged" and
      pointing at `gh stack init`, exiting cleanly (exit 0) like submit's
      "nothing to submit" path. The linearity and merge-queue precondition
      checks already skip merged branches, so they do not fire spuriously.
    
    Tests:
    - internal/tui/stackview/model_test.go: the cursor is hidden (-1) when all
      branches are merged; up/down/enter do not move it or trigger a checkout;
      View renders without panicking on a hidden cursor; buildHeaderConfig
      disables every cursor-dependent shortcut (and only those) when all merged,
      and leaves them all enabled when active branches remain.
    - cmd/modify_test.go: runModify short-circuits on a fully merged stack,
      printing the message and returning no error without launching the TUI.
    skarim authored Jun 30, 2026
    Configuration menu
    Copy the full SHA
    754d190 View commit details
    Browse the repository at this point in the history
  6. Create/update stack on remote during sync (#156)

    * Create/update the remote stack on sync and fix false "Stack synced"
    
    `gh stack sync` reported "Stack synced" even when it had not created or
    updated the stack object on GitHub. After running `gh stack init` to
    adopt existing branches and then opening PRs outside the CLI, `gh stack
    sync` detected the open PRs and printed "Stack synced" — but no stack had
    ever been created on the server.
    
    There were two distinct bugs:
    
    1. Sync never reconciled the remote stack object. `runSync` called
       `syncStackPRs`, which only *reads* PR state and links PRs to local
       branches; it never called the create/update path. So the branches were
       rebased and pushed and the PRs were detected, but the stack on GitHub
       was never created.
    
    2. The final message was unconditional. `runSync` always printed "Stack
       synced", which is supposed to mean "the stack object on GitHub now
       reflects the local stack" — something that can only be true when two or
       more open PRs exist and the remote stack was actually created/updated.
    
    Fix
    
    Reconcile the remote stack from sync, and make the closing message reflect
    what actually happened.
    
    * cmd/sync.go
      - Add a reconciliation step (5b) after PR-state sync: when the stack has
        two or more open PRs, link them into a stack on GitHub via the new
        `syncRemoteStack` helper. It inspects existing stacks first and:
          - short-circuits quietly when a remote stack already lists exactly
            these PRs (records the ID, prints "Stack already up to date on
            GitHub") so routine syncs don't issue a redundant, misleading
            update;
          - otherwise delegates to `syncStack` to create a new stack, adopt an
            untracked one, or update a partially-formed one.
        Sync never opens PRs — that remains `gh stack submit`'s job.
      - Replace the unconditional "Stack synced" with a result-driven message:
        "Stack synced" when the remote stack object was created/updated/in
        sync, otherwise "Branches synced" (fewer than two PRs, stacked PRs
        unavailable, a cross-stack divergence, or no GitHub client).
      - Update the command's long description to document the stack-object
        step and the two possible closing messages.
    
    * cmd/submit.go
      - Thread a `synced bool` return through the existing, tested stack
        helpers so sync can tell whether the remote stack object now matches
        local: `syncStack`, `createNewStack`, and `updateStack` now return
        `bool`; `adoptRemoteStack` returns `(handled, synced)`; and
        `handleCreate422` returns `bool` (true only when the PRs are already
        stacked together). Extract the shared `stackPRNumbers` helper.
      - This is additive: submit's single call site ignores the new return
        value, so submit's behavior, output, and tests are unchanged. Reusing
        these helpers (instead of duplicating the 404/422 handling in sync)
        keeps the create/adopt/update logic in one tested place.
    
    Tests
    
    * cmd/sync_test.go — six new cases covering the reconciliation matrix:
      - TestSync_CreatesRemoteStackWhenPRsExist: open PRs but no remote stack
        -> CreateStack is called and the new ID is persisted to the stack file;
        output contains "Stack created on GitHub" and "Stack synced".
      - TestSync_AdoptsExistingEqualRemoteStack: a matching remote stack ->
        no create/update, ID recorded, "Stack synced".
      - TestSync_UpdatesPartialRemoteStack: a subset stack -> UpdateStack with
        the full PR list, "Stack synced".
      - TestSync_FewerThanTwoPRs_BranchesSynced: one PR -> no stack API calls,
        "Branches synced", not "Stack synced".
      - TestSync_StacksUnavailable_BranchesSynced: 404 on create -> warns,
        "Branches synced".
      - TestSync_PRsSpanMultipleStacks_BranchesSynced: PRs across two stacks ->
        divergence warning, no create/update, "Branches synced".
    
    Docs
    
    Document the new stack-object step and the "Stack synced" vs "Branches
    synced" distinction in:
      - README.md
      - docs/src/content/docs/reference/cli.md
      - skills/gh-stack/SKILL.md
      - docs/src/content/docs/introduction/overview.md
      - docs/src/content/docs/guides/stacked-prs.md
      - docs/src/content/docs/guides/workflows.md
    
    * Address PR review: one ListStacks per sync, command-neutral guidance
    
    Two follow-ups from the #156 review (both flagged optional / non-blocking).
    
    1. Remove the redundant ListStacks round-trip on sync's create path.
       syncRemoteStack fetched the stack list for its already-up-to-date
       short-circuit, then delegated to syncStack -> adoptRemoteStack, which
       listed the stacks again — two GETs on the first-sync-create and
       membership-changed paths. Refactor adoptRemoteStack into a list-accepting
       reconcileUntrackedStack(cfg, client, s, prNumbers, stacks): syncStack now
       fetches the list once and passes it down, and syncRemoteStack reuses the
       list it already fetched. Net: exactly one ListStacks per sync. This also
       drops the (handled, synced) tuple. Submit's behavior is unchanged.
    
    2. Make the divergence / dropped-PR guidance command-neutral. The shared
       helper emitted submit-specific wording ("reconcile them before
       submitting", "...then `gh stack submit`") that is now reachable from
       `gh stack sync`. Reword to "reconcile them first" and drop the trailing
       `gh stack submit` so it reads correctly from either command.
    
    Tests: assert exactly one ListStacks on the create path and that the
    divergence guidance is not submit-specific.
    
    * increment skill file version
    
    * 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.
    skarim authored Jun 30, 2026
    Configuration menu
    Copy the full SHA
    e208dfc View commit details
    Browse the repository at this point in the history
Loading