* 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.
* 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
* 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>
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