Mercurial Changeset Evolution: An Architecture Note on Safe History Mutation
How Mercurial's Changeset Evolution uses obsolescence markers and a filtered view to make history rewriting non-destructive, traceable, and safe to share across clones.
25 Mar 2026, 10:08 UTC

Mercurial's core promise is that history is append-only: once a changeset exists, it never changes. That guarantee makes rewriting history (amending, rebasing, folding) dangerous in a shared repository, because the "old" and "new" versions of a commit coexist with no record linking them. Changeset Evolution is the extension that solves this: it keeps every version of every commit and records, as shareable metadata, which commits replace which. The useful takeaway is that history mutation becomes recoverable and synchronizable across clones — but only if every participant runs the extension and exchanges the marker data.
Requirements the design must satisfy
The problem statement has four parts:
- Non-destructive mutation. Amending a commit must not delete the original. A mistaken
hg pruneor rebase must be recoverable. - Traceability. Given a rewritten commit, you must be able to find its predecessor, and vice versa.
- Distribution. If Alice amends a commit and Bob pulls, Bob's repository must learn that the old commit is obsolete — without manual coordination.
- Conflict detection. If two people independently rewrite the same commit, the system must surface the divergence rather than silently picking one.
Classic Mercurial (and Git, for comparison) handles none of these natively: hg strip destroys data, and a force-push leaves other clones holding dangling commits with no hint about their replacements.
The smallest suitable design: markers plus a filtered view
Evolution's design is deliberately minimal. It adds two things on top of the existing store:
1. Obsolescence markers. A marker is a small record of the form (predecessor node, successor nodes, metadata). When you run hg amend with the extension enabled, Mercurial writes the amended commit as a new changeset and appends a marker saying "old node X is succeeded by new node Y." Nothing is deleted. Markers live in a side store (the obsstore), separate from the changelog, so the physical history remains a complete, append-only log of everything that ever happened.
2. A filtered, "evolved" view. The working view of history hides commits that have living successors. Obsolete commits still exist on disk — you can update to them, diff them, and resurrect them — but hg log and revsets by default show the cleaned-up, final history. This is the key separation: physical storage is complete; the logical view is curated.
Everything else — hg evolve to rebase orphaned descendants onto their new parents, hg fold, hg prune — is tooling layered on these two primitives.
Trust and data boundaries
The obsstore is the trust boundary. Markers are exchanged between repositories during push and pull, like changesets, which is what makes distributed rewriting work: when Bob pulls Alice's amended history, he receives both the new changeset and the marker obsoleting the old one, and his view updates accordingly.
This creates a hard compatibility rule: the extension must be enabled on both ends. A client without evolve support ignores the obsstore. It will happily pull the "old" commits as if they were live history, and worse, it can push the obsolete commits back, resurrecting them for everyone. In mixed-version teams this is the most common operational failure. Treat evolve as an all-or-nothing adoption decision per repository, and enforce it server-side where possible (hosted Mercurial with publishing disabled, or hooks rejecting pushes from non-evolve clients).
Markers are also advisory metadata, not access control. Any client that can exchange markers can declare any visible commit obsolete. In practice this matches the trust model of pushing changesets at all, but it means a misused hg prune propagates: pruning a commit and pushing tells every other clone to hide it too.
A concrete walk-through
Enable the extension in ~/.hgrc or the repo's .hg/hgrc (no special permissions needed; this is per-user configuration):
[extensions]
evolve =
Then, in a clone where you have a draft commit you want to fix:
hg amend -m "parser: handle empty input correctly"
hg evolve --list
hg amend creates a successor changeset and a marker; it does not touch the original. hg evolve --list reports any "troubled" commits — typically orphans, descendants of the commit you just rewrote that still sit on the old parent. Running hg evolve rebases them onto the successor automatically.
To inspect the metadata directly:
hg debugobsstore
This dumps the raw markers (predecessor hash, successor hashes, date, user). It is read-only and safe to run anywhere. If it prints nothing after an amend, the extension is not active — that is your first verification check. A second check: hg log --hidden -r 'obsolete()' should show the pre-amend commit, proving it was retained rather than stripped.
Failure modes
- Divergence. Two clones rewrite the same commit differently. The commit now has two successors, and
hg evolveflags it as divergent rather than guessing. Resolution is manual: pick a winner withhg pruneon the losing successor, or merge the two lines. This is a feature — silent loss is the alternative — but it does interrupt workflows until resolved. - Ghost or resurrected commits. If markers are not exchanged (non-evolve client, or a transport that drops obsstore data), successors arrive without their markers. The old commits reappear as live history. The fix is to re-exchange markers from a repository that has them, but prevention (uniform client configuration) is far cheaper.
- Prune is the sharp edge.
hg prunemarks a commit obsolete with no successor, and pushing that marker hides the commit everywhere. The data still exists locally in each clone's store, so recovery (hg touchor updating to the hidden commit) is possible — but only while some clone retains it. Pruning is the closest thing evolution has to deletion and should be reserved for commits you genuinely intend to discard. - Extension dependency. Evolution is not part of Mercurial's legacy core; it ships as the bundled-but-explicitly-enabled
evolveextension, and behavior details vary across versions. Verify against your installed version withhg help evolvebefore relying on specific subcommand flags.
Conditions that would change the design
This architecture fits teams that rewrite draft history before review and share a server they control. It is the wrong fit when: clients cannot be standardized (the marker-exchange boundary breaks); the workflow forbids any history rewriting (plain Mercurial phases with everything public is simpler); or the team needs Git interoperability, since obsstore metadata has no Git equivalent and is lost in conversion. If your repository is a read-mostly archive where commits are never amended, enabling evolve adds a metadata channel and a failure surface for no benefit.
The design lesson generalizes: safe mutation in an append-only system comes from recording the relationship between versions as first-class, replicated data — not from trying to make deletion safe.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.