Reversible operations & undo¶
Every mutating git-city command is built the same way: instead of firing Git commands as it goes, it first compiles your request into an explicit, ordered list of small steps. Each step is a tiny, well-understood operation — fetch a remote, move a ref, rebase one branch onto another, create or delete a branch, push. Nothing happens until that whole plan exists.
That single idea — commands are plans, not actions — is what makes the two safety features you rely on possible. --dry-run prints the plan and stops. undo reverses the plan that just ran. Neither is bolted on; both fall straight out of the same model.
One model, three behaviors
The plan is rendered as text for --dry-run, executed for a real run, and replayed in reverse for undo. Because all three read the same list of steps, what you see in --dry-run is exactly what runs, and what undo reverses is exactly what ran.
See the plan before it runs¶
Every mutating command accepts --dry-run. It prints the exact, ordered Git commands the plan would execute, then exits without touching anything.
git fetch origin --prune
git branch -f main origin/main
git checkout add-login
git rebase --onto main <base> add-login
git push --force-with-lease=add-login:<sha> origin add-login
Nothing executed (--dry-run).
This is not a paraphrase of what might happen — it is the plan itself, rendered to text. If a command is about to do something surprising, --dry-run is where you find out, before any ref moves.
Undo: reverse the last command¶
As git-city executes a plan, it records how to reverse each step before that step runs — capturing the pre-mutation state while it still exists. The result is an inverse program: a second plan that puts every ref back where it was.
Undo replays those captured inverses in reverse order. It can restore moved refs, recreate deleted branches, and even put back force-pushed or deleted remote branches — because it knows precisely what the previous command changed.
| You ran | Undo restores |
|---|---|
git city sync |
branches to their pre-sync shas, including the remote |
git city land |
the landed feature (local and remote) and its children's parents |
git city delete |
the deleted branch (its commits survive in the reflog) |
git city squash |
the original, un-squashed commits |
git city new / insert / reparent |
the prior branch layout and lineage metadata |
Single-level undo
Undo reverses exactly one command — the most recent one. Each git-city command overwrites the saved run-state, so there is no multi-step undo history. Undo, then re-inspect, then decide; you cannot peel back two operations in a row.
What undo refuses to do¶
Undo is deliberately conservative. It would rather stop with a clear message than risk destroying work, so it refuses in three situations:
- Dirty working tree. Undo will not run while you have uncommitted changes, because reversing refs could leave your files inconsistent. Commit or stash first.
- A branch carrying unique commits. If undoing would delete a branch whose commits exist on no other branch, undo refuses rather than orphan that work.
- A paused operation. If a sync is paused on a conflict, undo will not touch it. Finish the pause with
git city continueorgit city abortfirst.
Undo refuses; it does not force
These are hard stops, not warnings you can click through. When undo cannot proceed safely, it prints a clean git-city: <message> and exits without changing anything. The way forward is always to resolve the underlying state (clean the tree, finish the pause), then try again.
Pausing and resuming¶
The same plan-based model handles conflicts. If a rebase inside a sync hits a conflict, git-city does not abandon the run — it pauses. The rebase is left in progress, and the remaining steps (plus their inverses) are saved so the operation can pick up later.
git-city: sync add-login is paused.
stopped on: git rebase --onto main <base> add-login
resolve the conflict and `git add`, then:
git city continue resume the operation
git city abort undo everything and return to the start
git city info show this and the repo status
From a pause you have three choices: resolve and git city continue to run the rest of the plan, git city abort to replay the inverses captured so far and return to where you started, or git city info to re-print this message before the dashboard. Recall that undo itself refuses while paused — abort is the tool for unwinding a paused run.
Why this is trustworthy¶
Because reversal data is captured before each mutation, and because force-pushes always use --force-with-lease, undo never has to guess. It also never deletes a remote branch that git-city did not create — so it cannot clobber someone else's work in the name of cleaning up after itself.
If you want to see how plans, steps, and inverses are actually built and run — the functional core, the step types, the interpreter, and the run-state file — the internals page goes all the way down.