Commit Graph

5 Commits

Author SHA1 Message Date
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 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 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
Sameen Karim b01754e4a9 Initial release 2026-04-10 03:32:08 -04:00