Recover from a mistake
This page is a decision list. Find the situation you're in, run the command next to it, and read the section below for what it actually does and does not undo. Every claim here is verified against source, and every terminal block came from a real run in a throwaway repo, with /Users/you/demo standing in for the repo and a fictional hash standing in for the real one in cache paths.
| Situation | Command |
|---|---|
| I want to undo the last operation | gitq undo |
| A branch diverged from origin (someone force-pushed it) | gitq reset <branch> |
| I am stuck mid cascade | gitq abort |
| My store is wrong or gone | gitq import --replace |
| A lease is stuck | Finish or abort the cascade holding it |
| I just want to see what happened | gitq log |
See what happened first
gitq log lists every recorded operation for the current repo, most recent last, each with its timestamp and the branches it touched:
gitq log
2026-07-25T20:49:32.866Z sync (main, feat-a, feat-b)
2026-07-25T20:49:41.241Z sync (main, feat-a, feat-b)
It reads the same global operation-log.json that undo reads, filtered to this repo's common dir, so it's the fastest way to check "did that actually happen" and "what would undo even do" before you run it. It never modifies anything. See Where state lives.
Undo the last operation
gitq undo
undo takes the most recent operation-log entry that belongs to this repo: not a specific one you name, and not filtered by branch or stack, just "the last thing that happened here." If that entry isn't reversible, it says so and stops:
gitq: cannot undo "split" (not reversible)
Only four operation types are reversible: sync, cascade-rebase, reparent, and absorb. split, fold, and rename are logged but not reversible (they were deliberate one-way structural changes when you made them), and reset is never logged at all, so there's nothing for undo to find. See Restructure a stack for why.
absorb leaves undo pointing at the wrong operationabsorbCommand runs inside withOperationLog with no custom shouldLog, so the default rule applies: an entry is only written on a clean exit ((code) => code === 0). When the restack after a successful commit conflicts, absorb aborts the rebase, keeps the branch edits it already committed, and exits 1. No entry is written for that run.
gitq undo cannot tell any of this happened. It still just takes the most recent entry belonging to this repo, and after a failed absorb that entry belongs to whatever operation ran before it. undo does not fail, does not warn, and silently restores that earlier operation instead, as if the absorb had never run.
Check gitq log first if you are not sure what the last logged entry actually is. The real recovery for a failed absorb is gitq sync, not undo. See Absorb uncommitted changes for the full explanation.
A plain undo, restoring every branch the operation touched:
gitq sync --stack demo
gitq undo
undone: restored main, feat-a, feat-b
Exit 0. Every branch (including the stack's root) is reset to the head it had before the operation being undone, read from that log entry's branchSnapshots.
skippedBranches
Between the operation running and you undoing it, a snapshotted branch's git ref can stop existing: someone deleted it by hand, a script cleaned it up, whatever the reason. undo does not fail for that; it restores everything it still can and reports the rest separately:
git branch -D feat-b
gitq undo
undone: restored main, feat-a; dropped from stack (branch no longer exists): feat-b (Skipped deleted branches: feat-b)
echo $?
0
Exit 0, on a partial restore. success in the JSON result is about whether the restore mechanism itself worked, not about whether every branch came back. Check skippedBranches (also in the JSON, alongside restoredBranches) if you need to know exactly what didn't. The stack tree is updated to match: feat-b's node is dropped from the store (its children, if it had any, are reparented to its parent) rather than left pointing at a branch that no longer exists.
gitq stacks
demo (root main): feat-a
A branch diverged from origin
Someone force-pushed the branch (a rebase, an amend, whatever) and your local ref no longer matches. gitq reset <branch> snaps it back:
gitq reset feat-a
reset feat-a to origin/feat-a (757f53788a6be23a4f8fabb94a3e4de304339409)
Exit 0. Three things to know before you reach for it:
-
It does not fetch.
resetreadsorigin/<branch>as it already is; if your remote-tracking ref is stale,git fetchfirst or you'll reset to old data. -
It requires a clean launch worktree, even though it never checks anything out there:
bashgitq reset feat-agitq: Working tree has uncommitted changes. Commit or stash first.Exit
1, and nothing moves. Likerename, it also refuses up front, with the same message shape asrename's refusal, if the branch is checked out somewhere other than your launch worktree, dirty or not. When it does run, it compare-and-swaps the branch ref and leaves your checkout exactly where it was:gitq reset feat-afrommainleaves you onmain. -
It is never recorded in the operation log, so
gitq undohas nothing to give back afterward. Whatever the branch pointed at beforeresetran is gone unless you noted the sha yourself:bashgitq log2026-07-25T20:49:32.866Z sync (main, feat-a, feat-b)2026-07-25T20:49:41.241Z sync (main, feat-a, feat-b)No
resetentry, before or after.
Stuck mid cascade
You're not sure what state a paused sync left things in, you don't want to resolve it, or you just want out. gitq abort is the escape hatch:
gitq sync --stack demo
paused on feat-a in /Users/you/.mattstack/gitq/work/9f13ab2c7e0451dd/gitq-1 (commit ?/?):
UU README.md
resolve with git in that worktree, stage, then: gitq continue (or gitq abort)
gitq abort
aborted
Exit 0. It aborts the in-progress rebase where it actually lives (the leased slot, not your checkout), clears the pause file, re-detaches the slot, and releases the lease:
cat "$(git rev-parse --git-common-dir)/gitq/leases.json"
{ "leases": [] }
The branch that conflicted goes back to exactly where it was:
git log --oneline -1 feat-a
fb1414b feat-a touches README
It does not rewind branches the cascade already finished before it hit the conflict: those refs were already moved, and stay moved. A paused cascade is never written to the operation log either (a pause is resolved with continue/abort, not undo), so there's no log entry for gitq undo to act on afterward. If you need to walk back branches the cascade did finish before you aborted the rest, that's a manual git reset to whatever you remember, or re-running the cascade from a known-good state. See Resolve a conflict for the full walkthrough of what abort does and doesn't touch.
The store is wrong or gone
Deleted ~/.mattstack/gitq/stacks/<hash>.json by accident, or the tree in it just doesn't match reality anymore and hand-editing isn't worth it. If the forge still has the MRs, gitq import --replace rebuilds the store from there: the reverse direction of publish, forge state overwriting local state:
gitq import --replace
Two refusals guard it, both checked from source rather than a live run here since import hits a real forge API:
-
Without
--replace, over a non-empty store:gitq: import would discard 2 locally tracked stack(s) and re-mint stack ids; pass --replace to overwrite the local storeExit
1, checked before the token or network, so you find out what would be lost even offline. -
While any cascade is active anywhere in the repo,
--replaceor not:gitq: cascades are active; finish or abort them firstExit
1. Unlike every other mutating command's per-stack lease guard, this one fires on any lease, running or parked, for any stack:importis about to discard the store those leases are tracked against, not just one stack's.
Every stack import creates gets a brand-new random id (crypto.randomUUID() in StackManager.createStack), with no relationship to whatever id the same stack had before, even if you're importing back the exact MRs you just exported. The name you see in gitq stacks is a readable slug derived from the tip branch's MR title, so it can look stable across two imports, but the underlying id never is. Anything keyed to the old id, most importantly any gitq undo history for operations run before the import, stops resolving to a current stack the moment you replace it.
Treat it as a recovery tool for a broken store, not a routine refresh: gitq sync is what you want for a stack that's merely behind. See Publish a stack for the full command reference.
A lease is stuck
Something claims a stack is mid-cascade (stack has a parked sync lease on ... on every mutating command) and you don't believe it, or the process behind it is long dead. The lease registry is one file per repo:
<commonDir>/gitq/leases.json
<commonDir> is the repo's shared .git, the same one every worktree of the repo points at, so this is one file for the whole repo regardless of how many worktrees you have. While something is genuinely running or parked, it looks like this:
{
"leases": [
{
"slotPath": "/Users/you/.mattstack/gitq/work/9f13ab2c7e0451dd/gitq-1",
"stackId": "f75110fc-52cd-44a0-b26d-8cbe0095158f",
"action": "sync",
"pid": 66119,
"acquiredAt": 1785012631757,
"state": "parked"
}
]
}
running leases whose process (pid) is already dead are reaped automatically the next time any lease is acquired, so a killed cascade doesn't block the next one. parked leases are never auto-reaped, on purpose: a conflict waiting on you is legitimately long-lived, and clearing it out from under you would strand a real mid-rebase state in the slot.
The supported fix is finishing or aborting the cascade the lease belongs to: gitq continue or gitq abort, exactly as in the pause protocol. That's true even if you're certain the process is dead and the conflict is unrecoverable: gitq abort doesn't care whether anything is still running, it just aborts the rebase where it lives and releases the lease.
Editing or deleting leases.json by hand while a cascade is genuinely running or parked is how you actually lose track of things: the pause file is still on disk, the rebase is still half-done in the slot, and now neither gitq continue nor gitq abort can find it, since both start from a parked lease and answer nothing to continue (no parked cascade) without one. If you've done this, run git rebase --abort in the slot named by the pause file's worktreePath, then delete that gitq-pause.json. gitq abort does handle the opposite split, a parked lease whose pause file is missing. See Where state lives for the rest of what's safe and not safe to touch by hand.
Next
- The pause protocol and Resolve a conflict: the conflict loop these commands sit around.
- Work slots and leases: what a lease is and what it blocks.
- Where state lives: every file gitq writes, and what's safe to delete.
gitq undo: flags and JSON shape.