23 Commits

Author SHA1 Message Date
Sameen Karim 68ce60c760 Merge Stacked PRs with merge command (#307)
* merge cmd

* Refine the merge TUI and simplify the async-merge client

Follow-up polish for `gh stack merge` (the command itself landed in the
previous commit). These changes refine the interactive wizard, enrich the
PR picker, and replace the merge client's bespoke HTTP handling with the
standard go-gh REST client.

Wizard and stepper:
- Redesign the top stepper as a segmented bar: completed steps are green,
  the active step is the brightest, and upcoming steps are dimmed. Steps
  are separated by a Powerline arrow that blends into the shading, with a
  graceful fallback to abutting segments on terminals that lack the glyph
  (e.g. Apple Terminal). Set GH_STACK_POWERLINE=1/0 to override detection.
- Show the stack number in the header ("Merge stack #123").
- Hide the header and stepper once the merge is submitted so the live
  progress view stands on its own.

PR picker:
- Render each pull request on two lines: the title (white/black, a touch
  bolder when selected) above its "#number • branch" (gray, fainter when
  deselected). Titles are fetched in one batched GraphQL query (PRTitles)
  and fall back to the branch name.
- Scroll long stacks in a fixed 10-item window with persistent "N more"
  indicators, so the list no longer jumps as those hints appear and
  disappear. Add shift+up / shift+down to jump to the top or bottom.

Progress and outcome:
- Always render a status line ("Submitting merge request...") so it does
  not pop in later and shift the view, and normalize messages to end in an
  ellipsis.
- Print the final result from the command layer rather than the TUI: a
  success line that includes the merge commit SHA
  ("Merged #1, #2 into main (abc1234)"), an atomic-rollback note on
  failure, a distinct message when the user stops watching an in-flight
  merge, and "Cancelled operation, nothing merged" on cancel.
- Clamp every rendered line to the terminal width so resizing no longer
  leaves duplicated header lines behind, and make truncation ANSI-aware.

Async-merge client:
- Use the go-gh REST client (c.rest.Put / c.rest.Get) for both the submit
  and poll endpoints, removing the bespoke http.Client, base-URL helper,
  and manual response decoding. The REST client discards non-2xx bodies,
  but that only costs the rare 400 message and 409 UUID: real merge
  failures still surface through the 200 poll body, and the in-range PRs
  are validated open, non-draft, and non-merged before submitting.
- Add classifyAsyncMergeError to map status codes to clear errors (404
  unavailable, 409 already exists, 400 no longer mergeable) and drop the
  now-unused AsyncMergeResult.StatusCode field. Rework the client tests to
  drive the REST client through a stub http.RoundTripper.

* warn merge queue unsupported

* update for new status field from api

* merge cmd docs

* more helpful error msgs

* update to support merge queue

* addressing review comments

* hide merge method step for merge queue

* set merge action explicitly

* address review comments to clarify docs on merge/api behavior
2026-07-29 13:32:07 -04:00
Sameen Karim fded736e78 Minor docs updates (#343)
* clarify push behavior

* max stack size

* clarify only pushing active branches

* no stack obj in pull_request.opened event
2026-07-29 13:32:06 -04:00
Sameen Karim 53ed88c2a2 Docs Content Updates (#185)
* stack map and merge behavior clarification

* stacked webhook event

* stacks rest api

* optimizing ci usage

* graphql api docs

* rm deprecated adopt examples for init

* clarifications on merge behavior

* document admin bypass and auto-merge limitations

* updated screenshots

* unstacking behavior clarification

* stacking recommendation banner

* correct phrasing for bypass

* clarify wording on trunk branch

* skill file fixes

* bump js-yaml to 4.3.0

* fix broken link

* move up flag references for readability
2026-07-26 11:26:04 -04:00
Copilot 6dcf9f050a Respect repository default branch in gh stack link (#265)
* Initial plan

* Respect repository default branch in gh stack link

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-24 06:54:23 -04:00
Sameen Karim 38aba1b710 Deprecate branch name prefixes (#182)
* deprecate prefix functionality

* clean up branch auto-naming

* update docs

* preserve literal hyphens when slugifying branch names
2026-07-15 12:07:46 -04:00
Sameen Karim 9dfef64ffb Append to an existing stack by stack number with link (#181)
* link append to stack by number

* updated link docs

* address review comment
2026-07-15 12:07:46 -04:00
Sameen Karim e0b2104e75 Remote unstack by stack number (#180)
* unstack as a pure api wrapper if stack not checked out locally

* update unstack docs

* address review comments
2026-07-15 12:07:45 -04:00
Sameen Karim f880f0d469 Stack number as primary identifier (#178)
* 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
2026-07-15 12:07:44 -04:00
Sameen Karim 95f04b8fed Sync remote stack with local stack state (#175)
* pull in remote updates during sync

* cancel aborts operation

* switch to nearest surviving branch

* simplified options to remote delete

* stack existing PRs from submit TUI

* docs updates

* address review comments
2026-07-15 12:07:44 -04:00
Sameen Karim e208dfc488 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.
2026-06-29 20:11:10 -04:00
Sameen Karim 754d190490 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.
2026-06-29 20:11:09 -04:00
Sameen Karim e45a88c984 rebase without trunk (#129)
* flag for rebasing without trunk

* update docs with new rebase flag
2026-06-15 13:54:20 -04:00
Sameen Karim bf2358bf12 trunk command (#108)
* Add `gh stack trunk` navigation command

Add a new navigation command that checks out the trunk branch of the
current stack.

The command is stack-aware: it requires the user to be on a branch that
is part of a stack, loads the stack metadata, and checks out `s.Trunk.Branch`.
If the user is already on the trunk branch, it prints a message and exits
without calling git checkout.

New files:
  - cmd/trunk.go: TrunkCmd (cobra command) + runTrunk implementation
  - cmd/trunk_test.go: 7 test cases covering happy path, already on
    trunk, from top of stack, not in a stack, checkout failure, custom
    trunk branch name, and positional argument rejection

Modified files:
  - cmd/root.go: register TrunkCmd in the "nav" command group
  - README.md: add `gh stack trunk` to the Navigation section
  - docs/src/content/docs/reference/cli.md: add `gh stack trunk`
    reference section

* address review comments

* increment skill version
2026-05-26 17:39:39 -04:00
Sameen Karim d3be1b577e prune merged branches (#94)
* 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
2026-05-15 14:01:04 -04:00
Sameen Karim d115ca0570 support multiple branches during init (#91)
* implicitly adopt branches in init

* deprecate adopt flag

* update docs

* address review comments
2026-05-15 14:01:03 -04:00
Sameen Karim 8dbd7c63ed improve agent friendliness of view --json (#80)
* return exit codes instead of interactive prompt for view json mode

* increment skill file version
2026-05-11 11:09:33 -04:00
Sameen Karim 13330406ef open PRs as draft by default (#76)
* open prs as draft by default

* apply suggested docs updates from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 11:09:30 -04:00
Sameen Karim 03fe8ea371 simplify unstack to only target the active stack (#74)
* rm arg from unstack so it only targets active stack

* fix typo

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 11:09:29 -04:00
Sameen Karim 7af17b55dc switch command (#51)
* switch cmd to interactively switch to another branch in the stack

* add switch to docs

* addressing review comments

* bump skill file version

* default to current branch
2026-04-20 11:20:27 -04:00
Sameen Karim a00ff54996 link command for api-only operations (#50)
* link cmd for api-only stack upserts

* accept branch names too

* push branches first

* update docs with link cmd details

* adding more logging
2026-04-20 11:19:01 -04:00
Sameen Karim 8893d274f2 preflight check for stacked PR availability in submit (#44)
* preflight check for stacked prs before submit

* close pipe read end in test to avoid FD leak

* concise var reuse
2026-04-20 11:17:20 -04:00
Sameen Karim 22c8ef460b fix for rev-parse error during sync with deleted branches (#42)
* fix for rev-parse error during sync

* clearer info msg with rebasing over merged PRs
2026-04-20 11:14:50 -04:00
Sameen Karim b01754e4a9 Initial release 2026-04-10 03:32:08 -04:00