Undo any Git operation.
$ git rebase -i main # oops
$ git undo
Restored #41 (2 minutes ago): commit: Add the parser
refs/heads/feature: 9e1c3a0 -> 4b7d2f1
files: 3 changed
Undo this with `git undo redo`.
git-undo records a snapshot of your repository after every Git command:
where every ref points, HEAD, the index (conflicts included), the working
tree (untracked files included) and the stash. Each snapshot is ordinary Git
data, stored under refs/undo/ in the repository itself. git undo moves
the repository back to the previous snapshot, git undo redo forward again,
and git undo restore <n> to any of them. Refs move in one atomic
transaction; files that changed since the last snapshot are never
overwritten.
Git's reflog already lets you find where a branch used to point. It does not
know what was in your index, which untracked files existed, what a stash
contained before you dropped it, or how to put all of it back at once.
git-undo does.
Requires Git 2.31 or newer and, for automatic snapshots, 2.28 or newer.
cargo install git-undo
Or download a binary from the releases page and put git-undo on your PATH.
Git finds any git-<name> executable, so it runs as git undo.
Then, once per repository:
git undo init
This writes two hooks (reference-transaction and post-index-change,
keeping any hooks already there) that ask git-undo to record a snapshot once
the command has settled. Git never waits for it. git undo init -D removes
the hooks again; add --purge to delete the snapshots too.
| Command | What it does |
|---|---|
git undo |
Back to the state before the last operation. |
git undo 3 |
Back three operations. |
git undo redo |
Forward again. |
git undo list |
The snapshots, newest first; @ marks where you are. |
git undo show 41 |
What #41 recorded and how it differs from #40. |
git undo diff 41 |
Diff #41's files against the working tree now. |
git undo restore 41 |
Return to #41. |
git undo restore 41 --refs-only |
Move refs and HEAD only; leave files alone. |
git undo snapshot -m "before the big rebase" |
Record now, with a note. |
git undo watch |
Record as files change outside Git (editors, agents). |
git undo --dry-run |
Show what an undo would change. |
git undo gc |
Forget snapshots older than undo.expire (90 days). |
git undo status |
Hooks, snapshot count, settings. |
Consecutive undos walk further back, including through earlier undos, the
way an operation log does; redo walks forward. New work after an undo starts
a new line of history: git undo then undoes that work, and there is nothing
to redo.
A stopped rebase, merge or cherry-pick blocks undo until you finish or abort
it, or pass --force to discard it.
git undo list --format='%(seq)%09%(time)%09%(kind)%09%(head)%09%(summary)' -z
Placeholders follow git for-each-ref: %(seq), %(sha), %(short),
%(tree), %(time), %(date), %(relative), %(kind), %(summary),
%(head), %(restored), %(from), %(mark). -z terminates records with
NUL. Exit status is 0 on success and 1 on any failure, with the reason on
stderr prefixed fatal:.
Tools that already watch a repository (git-sim's
live mode, for one) can call git undo snapshot -q after each change they
observe and offer git undo restore <n> for any point on their timeline.
A snapshot is a commit whose tree shares every unchanged blob and subtree
with the previous one, so recording after each command costs about what
git add -A on the changed files costs, in a detached process. Snapshots
keep the commits they point at alive, so git gc will not prune a branch you
deleted until its snapshots are gone (undo.limit, default 1000 per
worktree, or git undo gc). Refs under refs/undo/ show up in
git log --all; no porcelain command otherwise sees them.
| Key | Default | Meaning |
|---|---|---|
undo.worktree |
true |
Record the working tree. false records refs and index only. |
undo.settle |
700 |
Milliseconds of quiet before a hook-triggered snapshot. |
undo.limit |
1000 |
Snapshots kept per worktree. |
undo.expire |
90d |
Age after which git undo gc forgets snapshots. |
The snapshot format and restore algorithm are specified in
docs/spec.md, precisely enough for another implementation to
read and write the same data. The tool is written in Rust and runs every
repository operation through the user's own git, so its semantics are
exactly those of the installed Git version. There are no bindings, no
database and no daemon: remove refs/undo/ and the hooks and nothing is
left.
- Objects pruned after their snapshot was forgotten cannot come back.
- Remotes are never touched; restoring
refs/remotes/*changes only what the local repository believes. - Configuration, hooks and submodule working trees are outside the state.
- A restore does not re-create a stopped rebase or merge; it records that one was in progress.
GPL-2.0, the same license as Git.