Mercurial Phases: Prevent Accidental History Rewrites with Draft, Public, and Secret
Mercurial's phases system prevents accidental history rewriting by classifying changesets as public, draft, or secret. This guide explains the mechanism with a worked example, configuration, and common mistakes.
01 May 2026, 15:25 UTC

The Core Problem: Accidental History Rewriting
When working with Mercurial, rewriting shared history can break collaborators' repositories. The phases system prevents this by classifying every changeset as public, draft, or secret. Mercurial blocks dangerous operations like rebase, amend, and strip on public changesets unless you explicitly force them. Phases have been enabled by default since Mercurial 2.1.
Understanding Phase States
Every changeset has one of three phases:
- Public: Immutable and already shared. Mercurial refuses to rebase, amend, or strip these without
--force. - Draft: Mutable and not yet pushed. Safe to rewrite, rebase, or edit.
- Secret: Never shared. Excluded from
hg pushandhg outgoingby default.
New commits start as draft. When you push to a publishing repository (the default for non-local remotes), the pushed changesets become public. Pulled changesets also become public.
Worked Example: Testing Phase Behavior
Run this in a scratch directory (no special permissions needed) to see phases in action. This is a verification exercise, not a state-changing operation on any real repository:
$ hg init test-repo
$ cd test-repo
$ echo "initial content" > file.txt
$ hg commit -A -m "Initial commit"
$ hg log -r . -T "{phase}\n"
draft
$ hg phase --force --public .
$ hg log -r . -T "{phase}\n"
public
$ hg rebase -s . -d 0
abort: cannot rebase public changesets
The rebase abort is the whole point: Mercurial refuses to touch a public changeset. The exact abort message wording may vary by version, but the refusal is the behavior to confirm.
Configuration for Safety
These settings in your .hgrc control phase behavior (both are defaults, shown for clarity):
[phases]
# New commits start as draft (default)
new-commit = draft
# Push makes changesets public (default for publishing repos)
publish = True
Publishing vs. Non-Publishing Repositories
A repository is publishing if it is meant to be shared. The publish flag is set per-remote in [paths] and defaults to True for non-local paths:
- Push to a publishing repo marks changesets as
public. - Pull from a publishing repo marks changesets as
public.
Check whether a remote is publishing:
$ hg showconfig paths.default.publish
A local clone configured with publish = False keeps pulled changesets as draft, letting you safely rebase or amend before pushing upstream. Be aware that a repository can be publishing for one remote and non-publishing for another, which can produce inconsistent phase states if not managed deliberately.
Secret Phase for Local-Only Work
Use the secret phase for experimental work or anything that must never leave your machine:
# Create a secret commit
$ hg commit --secret -m "Local experiment"
# Check phase
$ hg log -r . -T "{phase}\n"
secret
# Secret changesets are excluded from outgoing/push by default
$ hg outgoing
(no output)
To share a secret changeset later, demote it first: hg phase --draft <rev>. Moving draft to secret needs no force; moving public to draft requires hg phase --force --draft <rev>.
Common Mistakes and How to Avoid Them
Mistake 1: Treating push --force as a phase override
hg push --force does not change phases; it only bypasses certain checks (such as creating multiple heads). To rewrite public history you must first demote it, which is intentionally verbose:
$ hg phase --force --draft <revision>
$ hg rebase ...
Mistake 2: Assuming secret changesets are secure
Secret changesets are only hidden from push and outgoing. They are still visible in hg log and travel with a local clone. For truly sensitive data such as credentials, keep it out of the repository entirely.
Mistake 3: Expecting forced demotion to propagate
Phase changes made with --force are local metadata mutations stored in .hg/phaseroots. Collaborators who already pulled the changeset as public will still see it as public, so rewriting "shared" history this way does not actually unshare it.
Phase Transfers and Inspection
Phase state transfers on clone, pull, and push, so collaborators stay consistent under normal workflows. Inspect phase boundaries with:
$ hg debugphaseroots
Mercurial 6.0+ uses a phase cache for performance; if you suspect corruption, rebuild it with hg debugrebuildphasecache.
Limitations to Remember
- Phases only protect against local rewriting commands. They do not prevent
hg push -ffrom overwriting history on a non-publishing server. - They do not protect against
hg strip --forceon public changesets. - Extensions like
evolveandtopicinteract with phases; evolve can move changesets between draft and secret but still respects public immutability.
Verification Checklist
- Run
hg help phasesfor the built-in documentation matching your installed version. - Test transitions in a throwaway repository using the worked example above.
- Check publishing status with
hg showconfig paths.default.publish. - Confirm clone behavior:
hg clone test-repo test-clone && cd test-clone && hg log -r . -T "{phase}\n"should showpublic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.