98 Commits

Author SHA1 Message Date
Sameen Karim 2bd699a544 print full urls when terminal doesn't support osc 8 hyperlinks (#479) v0.1.1 2026-08-27 13:34:37 -04:00
Sameen Karim 3765b9bb2f add: offer to initialize a stack automatically (#486)
* offer to initialize stack when running add from outside a stack

* fetch trunk if missing locally

* disambiguate local trunk branch during init
2026-08-27 11:14:03 -04:00
Sameen Karim cf65746bd7 checkout: resolve remote stack by branch name (#477)
* checkout by remote branch name

* address review comments
2026-08-27 10:56:31 -04:00
Sameen Karim 1d9a20bd60 checkout: detect remote stack from current branch (#475)
* detect remote stack from current branch

* clean up phrasing
2026-08-27 10:56:31 -04:00
Sameen Karim 39528e64d6 Make checkout picker height dynamic (#469) 2026-08-27 10:56:30 -04:00
Sameen Karim 5fe0a50cce fallback to vi for editor if none set (#484) 2026-08-26 17:22:39 -04:00
Sameen Karim 4f9188e611 navigate down/up with j/k (#468)
* Add Vim navigation to stack view

* Advertise Vim navigation in stack modify

* Add Vim navigation to stack checkout

* Add Vim navigation to stack switch

* Advertise Vim navigation in stack merge
2026-08-24 16:29:17 -04:00
dependabot[bot] ab00aa4a3f Bump js-yaml in /docs in the npm_and_yarn group across 1 directory (#432)
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.3.0 to 4.3.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.1
  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>
2026-08-17 12:53:06 -04:00
Sameen Karim 14fc42ed9b Improve skill efficiency and performance (#388)
* break up skill into multiple files

* better reordering instructions

* updated branch placement instructions

* clarify branch naming

* recommend explicit arg for merge

* refine troubleshooting text

* address review comments
2026-08-03 21:12:24 -04:00
Sameen Karim 8c9c833130 remove private preview mentions from docs (#368) 2026-07-30 11:13:25 -04:00
Sameen Karim a1b4a3d4d0 Async merge API docs (#321)
* async merge api docs

* address review comments

* Apply batched suggestions from code review

Co-authored-by: Tracy Lum <11878752+talum@users.noreply.github.com>
Co-authored-by: Sameen Karim <skarim@github.com>

---------

Co-authored-by: Tracy Lum <11878752+talum@users.noreply.github.com>
v0.1.0
2026-07-29 13:32:07 -04:00
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 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 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
dependabot[bot] c79f2b22a1 Bump postcss in /docs in the npm_and_yarn group across 1 directory (#318)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [postcss](https://github.com/postcss/postcss).


Updates `postcss` from 8.5.16 to 8.5.23
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.16...8.5.23)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.23
  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>
2026-07-26 09:03:56 -04:00
dependabot[bot] effa94bf5a Bump sharp in /docs in the npm_and_yarn group across 1 directory (#317)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [sharp](https://github.com/lovell/sharp).


Updates `sharp` from 0.34.5 to 0.35.3
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.3)

---
updated-dependencies:
- dependency-name: sharp
  dependency-version: 0.35.3
  dependency-type: direct:production
  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>
2026-07-26 09:00:01 -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
dependabot[bot] a365459e81 Bump svgo in /docs in the npm_and_yarn group across 1 directory (#266)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [svgo](https://github.com/svg/svgo).


Updates `svgo` from 4.0.1 to 4.0.2
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v4.0.1...v4.0.2)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 4.0.2
  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>
2026-07-23 18:21:44 -04:00
dependabot[bot] 171d055ff1 Bump astro in /docs in the npm_and_yarn group across 1 directory (#244)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro).


Updates `astro` from 7.0.3 to 7.1.3
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@7.1.3/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 7.1.3
  dependency-type: direct:production
  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>
2026-07-21 23:34:24 -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
v0.0.8
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 ceffffc50a Interactive stack picker for checkout (#179)
* Add an interactive stack picker for checkout

Running `gh stack checkout` with no argument previously showed a plain
text prompt of locally tracked stacks only. It could not surface stacks
that exist only on the remote, gave no sense of a stack's state, and
listed fully merged stacks that can no longer be added to.

Replace it with an interactive picker (new package
internal/tui/checkoutview) that lists every stack available to you,
reconciling the local stack file with the Stacks REST API:

- Merges local and remote stacks, matched by stack id/number, and labels
  each Local (present locally, even if also tracked on the remote) or
  Remote (only on GitHub). Fully merged stacks are filtered out.
- Shows compact columns: stack number, first...last branch, base branch,
  a muted status bar summarizing merged/open/closed/unpushed PRs, type,
  and relative created time.
- Offers All / Local / Remote tabs and `/` type-to-filter search.

The picker renders inline rather than taking over the screen: it shows
up to ten rows with a scroll indicator, shrinks to fit short terminals,
and clears itself on exit.

Selecting a locally available stack checks out its top unmerged branch;
selecting a remote-only stack clones it down through the existing
checkout-by-number import flow.

When the Stacks API is unavailable (stacks not enabled, no auth, or a
network error), the picker degrades gracefully to a local-only list.

Also update the README, overview, and CLI reference for the new behavior.

* search by entire branch list

* 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 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 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 623b7e6fbf Fix rebase treating queued PRs as merged (#173)
* Don't treat queued PRs as merged when rebasing the stack

gh stack rebase and gh stack sync share cascadeRebase, which skipped
branches via IsSkipped() (merged or queued) and then switched to a
`git rebase --onto` that drops the skipped branch's commits from every
downstream branch. That is right for a merged PR — its commits are
already in trunk — but wrong for a queued PR: its commits only exist on
its own branch, which is frozen in the merge queue, so the branches
above it were rebased onto trunk and lost work they depend on.

Handle the two cases separately. A merged branch still activates --onto
so its commits are dropped. A queued branch is still skipped (its branch
is frozen and is not rebased or pushed), but onto mode is reset so
downstream branches rebase normally onto the queued branch, keeping its
commits underneath. The --onto target search, the runRebase --onto seed,
and the continueRebase display base now key on IsMerged() instead of
IsSkipped(), so a queued predecessor no longer forces downstream
branches onto trunk. gh stack sync is fixed through the same shared
helper.

Add rebase coverage for a queued branch mid-stack, a merged branch below
a queued branch, and --upstack above a queued branch, plus a sync test
that also asserts the queued branch is excluded from the push. The
transient queued state is injected through the GitHub mock's merge-queue
entry.

* Refresh queued PR state when continuing a stack rebase

continueRebase reloads the stack from disk, where the Queued flag is
transient (json:"-") and therefore lost, and it only called syncStackPRs
after the cascade. So if the initial rebase conflicted on a branch below
a queued branch, `gh stack rebase --continue` resumed with that branch
seen as active: it rebased the frozen merge-queue branch and rebuilt the
downstream branches on a local history that differs from the queued
branch.

Call syncStackPRs right after resolving the stack — before selecting the
base and cascading the remaining branches — mirroring the refresh
runRebase already does before its cascade. The queued flag is
repopulated, so queued branches stay skipped and downstream branches
stay stacked on them.

Add TestRebase_Continue_QueuedBranchBelowConflict, which conflicts below
a queued branch and asserts the frozen branch is not rebased and the
branch above stays stacked on it. Verified to fail without the refresh.
2026-07-15 12:07:43 -04:00
Sameen Karim 46b5dc4f78 Fix link failing when an existing stack PR is queued for merge (#171)
gh stack link validated PR eligibility before it fetched the repository's
stacks, so validatePREligibility rejected any queued PR unconditionally —
including one already a member of the stack being updated. Because link is
additive (every existing stack PR must be re-listed or the update is refused
for dropping them), a stack whose bottom PR was in the merge queue could never
take new PRs on top: re-listing the queued PR failed eligibility, and omitting
it failed the drop check.

Fetch the stacks and resolve the target stack before validating eligibility,
then skip the eligibility checks (queued, auto-merge, merged, closed) for any
PR that is already a member of that stack — those PRs are not being added, so
the checks don't apply. PRs not already in the stack, and brand-new stacks,
keep the previous strict behavior. prevalidateStack now takes the resolved
stack instead of looking it up a second time.

Add coverage for linking new PRs onto a stack whose existing PR is queued,
merged, or has auto-merge enabled, and for still rejecting a queued PR that is
not yet part of the target stack.
2026-07-15 12:07:43 -04:00
Sameen Karim 0c51c08255 Fix to properly revert after aborting modify (#167)
* Add cherry-pick abort/quit/in-progress git primitives

Introduce IsCherryPickInProgress() (detects .git/CHERRY_PICK_HEAD) and split
the existing cherry-pick reset into two distinct operations:

- CherryPickQuit() runs `git cherry-pick --quit`, clearing the sequencer
  state without touching the index (used to clear stale state before starting
  a fresh cherry-pick).
- CherryPickAbort() now runs `git cherry-pick --abort`, which fully restores
  the working tree and index to the pre-cherry-pick state.

The previous CherryPickAbort() ran --quit, which leaves an unmerged index and
therefore cannot recover a conflicted fold-down. Integration tests cover the
in-progress detection, the full abort restore, and the --quit-leaves-index
behavior.

* Fix modify --abort leaving a broken stack after a conflict

When `gh stack modify` hit a rebase or cherry-pick conflict it saved state
with phase "conflict" and told the user to run `gh stack modify --abort` to
restore. But runModifyAbort had no case for PhaseConflict, so it fell into the
default branch that merely printed "unexpected modify state phase" and deleted
the state file without unwinding. The in-flight rebase/cherry-pick stayed
active, branches were left partially rewritten, and the deleted state file also
made --continue impossible: the stack was stuck in limbo.

Fixes:

- runModifyAbort now unwinds on PhaseConflict (same recovery as PhaseApplying),
  aborting the in-progress operation, resetting branch tips to their pre-modify
  SHAs, restoring stack metadata, and clearing state.
- Unwind now also aborts an in-progress cherry-pick (fold-down conflicts), not
  just a rebase. Without this the restore checkouts would fail on the unmerged
  cherry-pick index.
- ContinueApply now records a subsequent cascade-rebase conflict as
  ConflictType "rebase" instead of leaving a stale "cherry_pick", so the next
  --continue calls RebaseContinue rather than failing in CherryPickContinue.

Adds coverage for the conflict-phase abort, pending-submit no-op abort, Unwind
aborting an active cherry-pick, and the cherry-pick to rebase ConflictType
transition.

* Persist fold-branch removal when a post-fold cascade rebase conflicts

ContinueApply removes the folded branch from the in-memory stack after a
fold-down cherry-pick is resolved, but a subsequent cascade rebase conflict
only saved the modify state file, not the stack metadata. On the next
--continue the on-disk metadata (folded branch still present) was re-read, and
because ConflictType is now "rebase" the fold-removal block was skipped, so the
final save resurrected the folded branch as a phantom entry pointing at an
orphaned tip.

Persist the stack file alongside the state file on a cascade-rebase conflict,
mirroring ApplyPlan's save-on-conflict, so the fold removal survives recovery.
Adds an end-to-end regression test covering the fold-then-cascade-conflict path
across two --continue calls.
2026-07-15 12:07:42 -04:00
Sameen Karim ae5f889c2f Fully-qualify branch refspecs when pushing (#165)
* Fully-qualify branch refspecs when pushing

gh-stack builds `git push` arguments from stack branch names in `internal/git`.
The force path passed `<branch>:refs/heads/<branch>`, and the non-force path
passed bare branch names. A git refspec treats a leading `+` as "force update",
and Git allows branch names that begin with `+`, so a branch named `+feature`
was parsed as refspec syntax for `feature`: the force path pushed local
`feature` into remote `+feature`, and the non-force path force-updated remote
`feature`.

Build fully-qualified refspecs for both the source and destination of every
push: `refs/heads/<branch>:refs/heads/<branch>`. A branch name can no longer be
reinterpreted as a refspec modifier. Force updates are still requested via the
existing `--force-with-lease` flags, whose ref names were already
fully-qualified. `DeleteRemoteBranch` is fully-qualified the same way.

The `Push` signature and every call site are unchanged. Add real-git
integration tests covering the force and non-force paths with a `+`-prefixed
branch.

* rm redundant if and simplify
2026-07-15 12:07:42 -04:00
Sameen Karim 5975945dfd Ignore symlinked pull request templates (#164)
Template discovery in internal/pr read candidate template paths with
os.ReadFile, which follows symlinks. A pull request template that is a
symlink (for example .github/pull_request_template.md pointing to a file
outside the repository) would therefore be read through to its target, and
that target's contents would be used as the PR body by `gh stack submit`
and `gh stack link`.

Reuse cli/cli's githubtemplate package (already a dependency) for template
discovery instead of the hand-rolled path list. It is the same code
`gh pr create` uses, and it ignores symlinked templates, so only regular
template files inside the repository are read. FindTemplate keeps the same
signature, so the submit and link callers are unchanged.

Two behavior changes come with the switch:
- YAML front-matter is stripped from the template, matching `gh pr create`.
- Template filename matching is slightly broader; hyphenated and non-.md
  variants are now recognized.

Add tests for FindTemplate and for the `gh stack submit --auto` and
`gh stack link` PR-creation flows to confirm symlinked templates are not
followed.
2026-07-15 12:07:42 -04:00
dependabot[bot] 514f5abf86 Bump the npm_and_yarn group across 1 directory with 2 updates (#159)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [esbuild](https://github.com/evanw/esbuild).


Updates `esbuild` from 0.27.4 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.4...v0.28.1)

Updates `vite` from 7.3.2 to 8.1.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.1.0/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 8.1.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>
2026-06-30 07:47:47 -04:00
dependabot[bot] 97c1b31284 Bump astro in /docs in the npm_and_yarn group across 1 directory (#138)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro).


Updates `astro` from 6.3.1 to 6.4.8
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/astro@6.4.8/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.4.8/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.4.8
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 21:20:28 -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.
v0.0.7
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 9b30b7f381 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.
2026-06-29 20:11:09 -04:00
Sameen Karim 797c62e9b4 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.
2026-06-29 20:11:09 -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 0efc764f8c 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.
2026-06-29 20:11:08 -04:00
Emily Wise 67d118fbf4 Unstack Updates: preflight PR eligibility before delete and improve API 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>
2026-06-19 11:07:51 -07:00
Francis Batac 4759126a51 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>
2026-06-17 14:00:46 -07:00
dependabot[bot] 84d160be10 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>
2026-06-15 18:22:49 -04:00
Sameen Karim e45a88c984 rebase without trunk (#129)
* flag for rebasing without trunk

* update docs with new rebase flag
v0.0.6
2026-06-15 13:54:20 -04:00
Sameen Karim 4c05e58b83 cache selected remote (#128)
* Save selected remote to gh-stack.remote git config

Users with multiple git remotes are prompted to choose a remote on
every gh stack operation, which is tedious. This adds the ability to
persist that choice so it only needs to be made once.

When a user interactively selects a remote (because multiple remotes
exist and none is configured as a push default), they are now shown a
Y/n follow-up prompt offering to save that remote for all future gh
stack operations. If accepted, the choice is written to the local git
config key `gh-stack.remote`, and instructions for changing or clearing
it are printed.

The saved remote is checked in `ResolveRemote` after the standard git
push config keys (branch.<name>.pushRemote, remote.pushDefault,
branch.<name>.remote) but before falling back to listing all remotes.
This means per-branch git push configuration still takes precedence,
and the --remote flag on individual commands continues to override
everything.

All commands that resolve a remote (push, submit, sync, rebase,
checkout, link, modify, trunk) go through the shared `pickRemote`
helper, so they all benefit automatically.

Changes:

- Add GetSavedRemote, SaveRemote, ClearRemote to the git Ops interface,
  defaultOps implementation, public wrappers, and MockOps
- Check gh-stack.remote in ResolveRemote's priority chain
- Move pickRemote from push.go to utils.go as a shared helper
- Add save-remote confirmation prompt after interactive remote selection
- Add unit tests for pickRemote save/decline/skip/override flows
- Add integration tests for ResolveRemote with saved remote and
  precedence, and for the SaveRemote/GetSavedRemote/ClearRemote
  lifecycle

* add error message for save failure

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-06-15 13:54:20 -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 f8c0100602 remove silent prefix detection in init (#126)
* Remove silent prefix detection from args path in init

When explicit branch names containing slashes were passed to `gh stack
init` (e.g. `gh stack init myprefix/branch`), detectPrefix would
silently extract the prefix and store it in the stack config. This
caused `gh stack add otherbranch` to unexpectedly produce
`myprefix/otherbranch` without the user ever opting in.

Remove the automatic prefix detection from the args path so that
explicit branch names are taken literally. Users who want a prefix
should use `--prefix`. The interactive path (no args) continues to
prompt for confirmation before setting a prefix.

* Remove dead detectPrefix function and its tests

After removing the silent prefix detection from the args path,
detectPrefix has no production callers. Remove the function and
its table-driven unit test to avoid maintaining unused code.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-15 13:54:19 -04:00
Sameen Karim 8c2d9e3da6 sync: skip trunk fast-forward silently when local branch doesn't exist (#125)
When the trunk branch (e.g. main) doesn't exist locally — only the
remote tracking ref (origin/main) exists — `fastForwardTrunk` called
`git rev-parse main origin/main` which failed, emitting:

  ⚠ Could not compare trunk main with remote — skipping trunk update

This also caused `stackNeedsRebase` to always return true (since
`IsAncestor("main", ...)` errors out), forcing an unnecessary rebase
and force-push on every sync.

Add a `BranchExists` check at the top of `fastForwardTrunk`. If the
local trunk doesn't exist, return silently — there's nothing to
fast-forward, and the remote tracking ref is sufficient for rebasing
via git's DWIM resolution.
2026-06-15 13:54:19 -04:00
Sameen Karim 7413e8889d Fix force-with-lease push for branches without tracking refs (#124)
gh stack sync could rebase a stack successfully then fail the final
force push with "stale info" when a branch lacked a local tracking ref
(refs/remotes/<remote>/<branch>). This happened because:

1. FetchBranches pre-filtered branches by existing tracking ref, so a
   branch with no tracking ref was never fetched and never gained one.
2. Push used a bare --force-with-lease flag, which has no lease basis
   for a branch without a tracking ref, causing git to reject the push.

FetchBranches now uses explicit refspecs for every branch:
  +refs/heads/<branch>:refs/remotes/<remote>/<branch>
This creates or updates tracking refs regardless of prior state. The
fast-path (single fetch) and per-branch fallback (for branches absent
on the remote) are preserved.

Push now builds explicit per-branch lease arguments when force=true:
  --force-with-lease=refs/heads/<branch>:<tracking-ref-sha>
for branches with a tracking ref, or:
  --force-with-lease=refs/heads/<branch>:
(empty expected value = "must not exist") for branches absent on the
remote. Explicit destination refspecs (<branch>:refs/heads/<branch>)
remove dependence on push.default and upstream configuration. The
non-force push path is unchanged.

Added 6 integration tests using real bare git remotes:
- Branch with current tracking ref: push succeeds
- Tracking ref deleted locally (regression test for #118): push succeeds
- Remote advanced by another client: push rejected (safety preserved)
- New branch absent on remote: created via empty-expect lease
- New branch race condition: rejected (safety preserved)
- Mixed stack (tracked + untracked branches): all succeed after fetch

Fixes #118
2026-06-15 13:54:18 -04:00