12 Commits

Author SHA1 Message Date
Sameen Karim 9421bd66a3 Fix for replaying amended parent commits during rebase (#333)
* Avoid replaying amended parent commits

Preserve a branch's last valid base when its parent is rewritten, and only
use verified ancestor commits as rebase boundaries. Recover previously
corrupted metadata from the parent reflog when possible, otherwise stop
safely instead of replaying superseded parent commits.

* Record adopted branch merge bases

Store the actual common ancestor when adding an existing branch so cascade rebases replay only that branch's unique commits while retaining the amended-parent safety guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 30bcbf2a-5ef1-4bdb-a0fc-618294ae8ded

---------

Copilot-Session: 30bcbf2a-5ef1-4bdb-a0fc-618294ae8ded
2026-07-29 13:32:06 -04:00
Sameen Karim a5cae7bbe0 Rebase stacks onto the latest remote trunk (#330)
* Rebase stacks onto the latest remote trunk

Fetch the configured trunk explicitly before sync or rebase and use that
fetched ref whenever the local trunk cannot be safely updated, while
preserving local-only and locally-ahead trunks. Fail instead of reporting
success when the fetch or rebase never starts, carry the resolved trunk
through conflict recovery, and verify the resulting ancestry before sync
pushes or either command reports success.

* Restore stacks after incomplete cascade rebases

Roll back branches already rewritten when a later rebase cannot start or
final ancestry verification fails, preventing retries from replaying stale
history. Preserve retryable modify state without repeating completed work,
and add regression coverage for remote-qualified trunk normalization.
2026-07-29 13:32:06 -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 a82dc3ef1d Migrate to new Stacks REST API (#177)
* Add stack Number field to local model and schema

The new Stacks REST API exposes a human-facing stack number (shown in the
github.com UI) alongside the internal stack id. Add a Number field to the
stack.Stack model and document it in schema.json so it can be persisted in
the .git/gh-stack file. Purely additive; behavior is unchanged until callers
populate it.

Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740

* Cut over stack operations to the public Stacks REST API

Replace the private cli_internal stack endpoints with the new public
Stacks REST API (/repos/{owner}/{repo}/stacks):
- ListStacks / FindStackForPR (?pull_request= filter) / GetStack for reads
- CreateStack, which now returns the created stack including its number
- AddToStack for delta-only appends (there is no full-replace endpoint)
- Unstack for server-driven removal (204 dissolved / 200 partial / 422)

Migrate all callers (checkout, submit, link, sync, unstack, utils) and
drop the client-side unstack eligibility pre-check — the server now
decides which PRs can be unstacked. checkout discovers stacks via the
pull_request filter; submit/link express updates as append-only deltas;
unstack adopts partial-unstack semantics, keeping local tracking when
PRs remain stacked on GitHub.

RemoteStack now carries the stack number, and stack updates resolve a
stack's number from its internal id for stack files that predate the
Number field.

Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740

* Remove the personal access token (PAT) limitation

The new Stacks REST API is public, so any user authenticated with the
GitHub CLI (including via a PAT with repo scope) can perform stack
operations once the feature is enabled for their repository. Remove the
PAT detection and the private-preview gating:

- Delete Config.WarnIfPAT / IsPersonalAccessToken and the TokenForHostFn
  test hook (internal/config/auth.go is no longer needed).
- Drop the submit pre-flight that aborted on a PAT.
- Rename warnStacksUnavailableOrPAT to warnStacksUnavailable and simplify
  it to the "stacked PRs not enabled" message.

Copilot-Session: 03673c26-a245-42da-93ed-dfcebc92a740

* address review comments
2026-07-15 12:07:44 -04:00
Sameen Karim b6bcce1bfe Add an interactive submit TUI for customizing each PR's title, description, 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
2026-06-29 20:11:08 -04:00
Sameen Karim 756002b4c9 ensure local trunk branch for required operations (#127)
* Ensure trunk branch exists locally before commands that need it

When a user starts a stack after renaming their initial branch
(e.g. `git branch -m newbranch`), the trunk branch (e.g. main) may
not exist as a local branch. Commands that pass the trunk name to git
operations like merge-base, rebase, or rev-parse then fail with:

  fatal: Not a valid object name main

Add an `ensureLocalTrunk` helper that checks whether the trunk branch
exists locally and, if not, fetches it from the remote and creates a
local tracking branch. This mirrors the pattern already used in the
checkout command for importing stacks.

Commands updated:
- modify: call ensureLocalTrunk before the linearity check in
  CheckStackLinearity, which uses IsAncestor(trunk, branch). This was
  the originally reported failure.
- rebase: call ensureLocalTrunk after fetch and before fastForwardTrunk
  and the cascade rebase. git rebase requires a locally resolvable ref;
  the remote tracking ref alone is not sufficient.
- trunk: call ensureLocalTrunk before CheckoutBranch so that
  `gh stack trunk` works even when trunk was never created locally.
- checkout: refactor the existing inline BranchExists + CreateBranch
  block to use the shared helper.

Also fix an incorrect comment in fastForwardTrunk that claimed "the
remote tracking ref is sufficient for rebasing" — verified empirically
that `git rebase main` fails when main has no local branch, even after
fetching origin/main.

Commands that were already safe and required no changes:
- sync: fetches trunk explicitly and fastForwardTrunk guards with
  BranchExists
- push, switch, navigate, unstack: do not reference trunk
- add, submit: do not require trunk as a local git ref
- view: handles IsAncestor errors gracefully (false positive is
  acceptable since rebase will fix it)

* add check to avoid unnecessary remote selection prompt
2026-06-15 13:54:20 -04:00
Sameen Karim d235a21a9c alert for unsupported auth tokens (#113)
When users authenticate the GitHub CLI with a personal access token
(PAT) instead of OAuth (`gh auth login`), the `cli_internal` stacks
API endpoints return 404. The CLI previously interpreted this as
"Stacked PRs are not enabled for this repository," which is misleading
— the feature may be enabled, but the token type simply cannot access
the internal endpoints.

This is a recurring source of user confusion. The docs already note
that PATs are not supported, but users don't always read them before
hitting the error.

This change adds token-type detection by inspecting the `gh` auth
token prefix:

  - `gho_`        → OAuth (supported)
  - `ghs_`        → GitHub App installation token (supported)
  - `ghp_`        → Classic PAT (NOT supported)
  - `github_pat_` → Fine-grained PAT (NOT supported)

When a PAT is detected, the CLI now shows:

  ⚠ Personal access tokens are not supported by gh stack
    Run `gh auth login` to authenticate with OAuth instead.

Instead of the misleading:

  ⚠ Stacked PRs are not enabled for this repository

Changes:

- Add `internal/config/auth.go` with auth detection methods on Config:
  `IsPersonalAccessToken()`, `WarnIfPAT()`, and `RepoHost()`. Uses a
  `TokenForHostFn` field on Config for test overrides, following the
  same pattern as `GitHubClientOverride`.

- Add a pre-flight PAT check in `cmd/submit.go` before the
  `ListStacks` call. If a PAT is detected, the command aborts early
  with a clear error instead of making a doomed API call.

- Update all 404 handlers for `cli_internal` endpoints to check the
  token type and show the appropriate message:
  - `cmd/submit.go` (createNewStack)
  - `cmd/link.go` (listStacksSafe, createLink)
  - `cmd/checkout.go` (checkoutRemoteStack)

- Add `warnStacksUnavailableOrPAT()` helper in `cmd/utils.go` that
  shows the PAT-specific warning when applicable, falling back to the
  generic "not enabled" message for non-PAT tokens.

- Add unit tests in `internal/config/auth_test.go` for token prefix
  detection and warning output.

- Add integration tests in `cmd/submit_test.go` verifying that both
  classic PATs (`ghp_`) and fine-grained PATs (`github_pat_`) trigger
  the pre-flight check and abort before any API calls.

- Add `warnStacksUnavailableOrPAT` tests in `cmd/utils_test.go`
  verifying correct message selection based on token type.

- Update existing 404 tests to explicitly set an OAuth token so they
  continue exercising the ListStacks 404 path.
2026-06-15 13:54:17 -04:00
Sameen Karim e7acfc1b16 fix for rebase in sync cmd (#95)
* fix: sync performs cascade rebase even when trunk is already up-to-date

Previously, `gh stack sync` gated the cascade rebase on whether trunk
or stack branches were fast-forwarded during the current run. This meant
that if the user had already updated trunk locally (e.g., `git pull`),
sync would skip the rebase entirely even though stack branches hadn't
been rebased onto the current trunk.

This change:
- Adds `stackNeedsRebase()` to detect stale branches regardless of
  whether trunk was updated in this run
- Extracts shared helpers (`fastForwardTrunk`, `cascadeRebase`,
  `resolveOriginalRefs`) from duplicated code in sync.go and rebase.go
  into utils.go, reducing ~450 lines of duplication
- Fixes rebase.go to skip queued branches (was only skipping merged),
  consistent with sync's behavior via `IsSkipped()`
- Refactors rebase --continue to reuse the shared cascade helper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address review feedback: error handling and index validation

- resolveOriginalRefs now returns (map, error) instead of silently
  swallowing RevParseMap failures; sync warns and skips rebase, rebase
  aborts with a clear error
- cascadeRebase uses a new Err field on the result struct to distinguish
  fatal errors (e.g. checkout failure) from recoverable conflicts;
  callers no longer enter conflict-recovery flow for non-conflict errors
- continueRebase validates that remaining branch indices are contiguous
  in stack order, erroring out if the stack was reordered between
  conflict and --continue

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-26 17:39:36 -04:00
Sameen Karim 8cddfd4e21 Optimize view/modify TUI load time with parallel fetching (#79)
- 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>
2026-05-11 11:09:32 -04:00
Sameen Karim 42e43a47ae ignore stale merged/closed PRs for reused branch names (#49)
* ignore stale merged/closed PRs for reused branch names

* guard for edge case of deleted PR
2026-04-20 11:18:19 -04:00
Sameen Karim e06284ff5c skip pulling merged branches during remote checkout (#16)
* skip pulling merged branches during remote checkout

* show message if stack fully merged
2026-04-13 18:36:35 -04:00
Sameen Karim b01754e4a9 Initial release 2026-04-10 03:32:08 -04:00