Commit Graph

15 Commits

Author SHA1 Message Date
Sameen Karim 6dca4c9cf5 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.
2026-07-28 02:26:11 -04:00
Sameen Karim ed2b46d646 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-27 17:49:38 -04:00
Sameen Karim dde62a516b 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.
2026-07-27 15:33:14 -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 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 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 b219e96fb5 rebase with preserve dates opt (#96)
* Add --committer-date-is-author-date flag to gh stack rebase

Introduce an opt-in `--committer-date-is-author-date` flag (with
`--preserve-dates` alias) for `gh stack rebase`. The flag is passed
through to every underlying `git rebase` invocation in the cascade,
keeping committer dates equal to author dates so that identical content
rebased onto an identical parent produces stable SHAs. This reduces
spurious force-push notifications and noisy review timelines, especially
in deep stacks where bottom branches get re-rebased on every merge.

Git layer changes:
- Add `RebaseOpts` struct with `CommitterDateIsAuthorDate` field to
  `internal/git/gitops.go`
- Update `Ops` interface, `defaultOps`, public wrappers, and
  `rebaseContinueOnce`/`tryAutoResolveRebase` helpers to accept and
  forward the flag
- Update `MockOps` to match the new signatures

Command layer changes:
- Register `--committer-date-is-author-date` and `--preserve-dates`
  flags on the cobra command in `cmd/rebase.go`
- Add `CommitterDateIsAuthorDate` to `cascadeRebaseOpts` and thread it
  to all `git.Rebase`/`git.RebaseOnto` calls in `cmd/utils.go`
- Persist the flag in `rebaseState` JSON so `--continue` resumes with
  the same behavior; pass it to `RebaseContinue` and subsequent cascade
  calls
- Update `internal/modify/apply.go` callers to pass zero-value
  `RebaseOpts{}`

Tests:
- Update all existing mock signatures in rebase, sync, and modify tests
- Add tests for flag passthrough, `--preserve-dates` alias, state
  round-trip, `--continue` flag restoration, and conflict state
  persistence

Docs:
- Update flag tables and examples in README.md and
  docs/src/content/docs/reference/cli.md

* clearer wording in docs and help text
2026-05-26 17:39:36 -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 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 f706944b63 fix --onto rebase for merged branches (#43)
* backfill ontoOldBase for deleted merged branches

* ensure upstack rebase checks immediate predecessor

* fix for stale ontoOldBase causing rebase conflicts
2026-04-20 11:15:32 -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 766d0e2ab9 fast forward active branches where local is behind remote (#40)
* fast forward active branches where local is behind remote
2026-04-20 11:10:55 -04:00
Sameen Karim b01754e4a9 Initial release 2026-04-10 03:32:08 -04:00