* Support addressing a stack by its stack number
checkout now interprets a bare integer as a stack number first (the
identifier shown in the github.com stack UI), falling back to a locally
tracked PR number, then a PR number discovered from GitHub, then a branch
name. A new checkoutStackByNumber resolves the stack via GetStack and
checks out its top-most unmerged branch; the reconcile/import logic is
shared with the PR-number path.
unstack gains an optional <stack-number> positional argument to unstack a
specific locally tracked stack instead of the current one.
Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740
* Surface the stack number in output and TUIs
Show the human-facing stack number wherever it is known:
- Append a "(stack #N)" label to submit, link, checkout, and unstack
success messages.
- Add a "Stack #N" header line to the view command (short and static)
and the stackview TUI header.
- Add a "Stack #N" info line to the submit TUI header when submitting
an already-created stack.
Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740
* Update docs and agent instructions for the new API
- cli.md: document checkout/unstack by stack number and drop the
"PATs are not supported" note (any gh-authenticated user can now run
stack operations).
- quick-start.md: drop the PAT-not-supported note.
- AGENTS.md / copilot-instructions.md: ClientOps is now 13 methods over
the public Stacks REST API; remove the TokenForHostFn test hook; note
the stack file's id/number identity.
- SKILL.md: add checkout/unstack-by-stack-number quick references.
Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740
* address review comments
* 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.
* 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.
* prune merged branches
* interactively prompt for prune
* delete remote tracking ref too
* disable selecting merged branches in TUIs
* include full list (including merged PRs) in PUT request to stacks API
* add prune to docs
* addressing review comments
* increment skill file version
- Deduplicate API calls: syncStackPRs now returns PRDetails for
LoadBranchNodes to reuse, eliminating redundant FindPRDetailsForBranch calls
- Parallelize API calls in syncStackPRs (capped at 6 concurrent requests)
- Parallelize git operations in LoadBranchNodes (capped at 4 concurrent)
- Show "Loading stack..." indicator for interactive sessions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Audit all 7 GraphQL operations and remove fields that are fetched but
never used in Go code:
- Remove FindAnyPRForBranch: entire function is dead code (zero call sites)
- FindPRForBranch: remove title, state, headRefName, merged (4 fields)
- CreatePR: remove title, state, headRefName, baseRefName, isDraft (5 fields)
- FindPRDetailsForBranch: remove id, title, headRefName, baseRefName,
comments { totalCount } (4 fields + 1 nested object)
- FindPRByNumber: remove title (1 field)
Also remove Title from PullRequest struct, and Title + CommentsCount
from PRDetails struct since they are no longer populated or read.
Total: ~15 unused fields removed across 5 queries, 1 dead query deleted,
and 1 unnecessary nested object (comments) eliminated.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* git primitives for modify cmd
* extract reusable TUI parts
* modify cmd
* recreate stack after modify
* add checks to prevent other modifications while modify is applying
* modify continue for resuming after resolving conflicts
* fix bug with duplicate stack entries after modifying
* reuse conflict resolution help msg from rebase
* additional confirmation before overwriting stack on remote
* fix recreate order of operations
Co-authored-by: Copilot <copilot@github.com>
* move base commit instead of cherry picking for fold up
* check to ensure we aren't left with zero branches
* unify and dedupe across view and modify tui
* more detailed help instructions
Co-authored-by: Copilot <copilot@github.com>
* only recommend submit if stack exists on remote
Co-authored-by: Copilot <copilot@github.com>
* tests for modify tui, apply modifications, submit modifications
* refactor submit for regular and pending modifications
* rename recover to abort
Co-authored-by: Copilot <copilot@github.com>
* docs for modify cmd
* tui styling updates
* updated tui screenshot
* addressing review comments
* Fix 4 bugs from code review
Bug 1: Move RevParseMap error check before using originalRefs.
The error from git.RevParseMap() was deferred past iteration of
originalRefs, which could panic on a nil map.
Bug 2: Differentiate cherry-pick vs rebase conflicts in modify.
Cherry-pick conflicts don't save state as 'conflict' phase, so
--continue won't work. Now prints --abort-only instructions for
cherry-pick conflicts.
Bug 3: Unwind now cleans up branches created by renames.
After restoring snapshot branches, Unwind deletes renamed branch
names that don't belong to the original snapshot.
Bug 4: Simplify push message in submit command.
Changed from 'Pushing N branches to remote...' to 'Pushing to
remote...' since individual branches may fail.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix 7 nit issues from code review
11: Add named constants for phase strings (PhaseApplying, PhaseConflict,
PhasePendingSubmit) in state.go; replace remaining raw literals in
state.go CheckStateGuard.
14: Fix bottomLines comment mismatch — listed 3 items but value is 2.
15: Extract magic number 88 to MinWidthForArt constant in header.go.
16: Remove unused stackview import anchor in model.go — the import
is used via types.go where BranchNode is embedded.
17: Simplify CheckStackLinearity parent resolution — ActiveBaseBranch
already handles skipping merged branches.
18: Fix rename undo matching any rename — add NewName check so only
the specific rename being undone is matched.
20: Add TestUndoRename and TestUndoRename_DoesNotAffectOtherRenames
to validate rename undo behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make cherry-pick conflicts recoverable via --continue
Previously, cherry-pick conflicts during fold-down operations could only
be resolved with --abort. Now they save full conflict state (phase,
conflict type, fold branch/target, remaining branches) to the state file,
enabling recovery via 'gh stack modify --continue'.
Changes:
- Add ConflictType field to StateFile (rebase or cherry_pick)
- Add FoldBranch/FoldTarget fields for cherry-pick context
- Add CherryPickContinue to git package (cherry-pick --continue)
- Save cherry-pick conflict state in ApplyPlan with remaining branches
- ContinueApply handles both rebase and cherry-pick conflicts
- Unified conflict messaging in cmd/modify.go (both types show --continue)
- Updated test to verify cherry-pick conflict state is saved correctly
* Apply suggestions from code review
Co-authored-by: Luke Ghenco <lukeghenco@github.com>
Co-authored-by: Sameen Karim <skarim@github.com>
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Luke Ghenco <lukeghenco@github.com>