Migrating from Mercurial to Git Jump to heading
Mercurial and Git have the same core model β a graph of commits identified by hashes β so converting history is mechanical, but the details differ enough to need care. Mercurialβs named branches are recorded in every commit, while Git branches are movable pointers; bookmarks are the closer equivalent. Mercurial authors are free-form strings that often lack an email. Tags live in a versioned .hgtags file rather than as refs. Subrepositories have no direct Git counterpart. A good migration maps all of these deliberately, verifies that every converted commit has the same file contents as the original, and switches the team over on a planned date. This page does that with hg-fast-export, within repository migration and consolidation.
When to use this approach Jump to heading
- A Mercurial repository needs to move to Git with its history.
- Your hosting provider has ended Mercurial support.
- Several Mercurial repositories are being consolidated, perhaps alongside merging two repositories while keeping history.
- The same approach for Subversion is in migrating from Subversion to Git.
Step 1 β Inventory the Mercurial repository Jump to heading
List authors, named branches, bookmarks, tags and subrepositories before converting, so each can be mapped deliberately.
cd legacy-hg
hg log --template '{author}\n' | sort | uniq -c | sort -rn > ../authors-raw.txt
hg branches --closed
hg bookmarks
hg tags | head
[ -f .hgsub ] && cat .hgsub
hg log -r 'head() and not closed()' --template '{node|short} {branch}\n' # multiple heads per branch? Step 2 β Write an author map Jump to heading
Map every Mercurial author string to a Git name and email. Unmapped authors without emails produce commits that forges cannot link to accounts.
awk '{ $1=""; sub(/^ /,""); print }' ../authors-raw.txt > ../authors-list.txt
# Edit into the format hg-fast-export expects: "hg author"="Git Name <email>"
cat > ../authors.map <<'EOF'
"jdoe"="Jane Doe <[email protected]>"
"Jane Doe <[email protected]>"="Jane Doe <[email protected]>"
"build bot"="Build Bot <[email protected]>"
EOF Author rewriting in general is covered in rewriting author emails during a migration.
Step 3 β Convert with hg-fast-export Jump to heading
Create an empty Git repository and run the converter against the Mercurial repository. It converts named branches to Git branches, default to the branch you choose, and .hgtags entries to tags.
git init converted && cd converted
git config core.ignoreCase false
/opt/fast-export/hg-fast-export.sh -r ../legacy-hg -A ../authors.map -M main
git branch -a
git tag | head If the repository has several heads on one named branch, the converter stops and asks you to resolve them; merge or close the extra heads in Mercurial, or pass the option to name them separately.
Step 4 β Handle bookmarks and closed branches Jump to heading
Bookmarks are converted as branches when supported by the converter version; check. Closed Mercurial branches still become Git branches β delete them in Git or keep them under an archive prefix.
git for-each-ref --format='%(refname:short)' refs/heads/ | while read -r b; do
hg -R ../legacy-hg log -r "branch('$b') and closed()" -l 1 --template x 2>/dev/null | grep -q x && echo "closed: $b"
done
git branch -m old-feature archive/old-feature # keep, but out of the way Step 5 β Convert subrepositories Jump to heading
Subrepositories can become Git submodules, each converted separately, or be merged into the main tree if they were never really independent. Converting them to submodules preserves the boundary; merging simplifies day-to-day work.
# Option A: convert each subrepo separately, then add as a submodule at the recorded revision
/opt/fast-export/hg-fast-export.sh -r ../legacy-hg/libs/parser -A ../authors.map -M main # in its own empty repo
git submodule add https://git.example.com/org/parser.git libs/parser Step 6 β Verify contents match Jump to heading
Compare the file tree at every tag and branch head in both systems. Identical trees prove the conversion preserved content; history shape is checked by commit counts.
for t in $(git tag); do
hg -R ../legacy-hg archive -r "$t" -t files "/tmp/hg-$t" >/dev/null 2>&1
rm -f "/tmp/hg-$t/.hg_archival.txt" "/tmp/hg-$t/.hgtags"
mkdir -p "/tmp/git-$t" && git archive "$t" | tar -x -C "/tmp/git-$t"
diff -r -q "/tmp/hg-$t" "/tmp/git-$t" >/dev/null && echo "ok $t" || echo "DIFF $t"
done
echo "hg: $(hg -R ../legacy-hg log --template x | wc -c) git: $(git rev-list --all --count)" The .hgtags file appears in Mercurial trees but becomes refs in Git, so it is excluded from the comparison.
Step 7 β Cut over Jump to heading
Freeze the Mercurial repository, run a final incremental conversion, push to the Git host, and point everyone at the new remote. Keep Mercurial read-only for reference.
/opt/fast-export/hg-fast-export.sh -r ../legacy-hg -A ../authors.map -M main # incremental: picks up new commits
git remote add origin https://git.example.com/org/app.git
git push origin --all && git push origin --tags Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Are commit hashes preserved? Jump to heading
No. Git computes different hashes from different object formats. Record the mapping the converter produces, so references to Mercurial hashes in issues can be translated.
What about large files tracked with largefiles? Jump to heading
Convert them to Git LFS during or after conversion, as in migrating large binaries to Git LFS.
Can we keep working in Mercurial during trial runs? Jump to heading
Yes. The converter is incremental, so each run picks up new commits, and the final run at cutover is short.
Related Jump to heading
- Repository Migration & Consolidation β the parent topic.
- Migrating CI and Hooks with a Repository β moving the automation too.
- Archiving a Repository Without Losing History β retiring the old repository.
- Pinning and Updating Git Submodules Safely β converted subrepositories.