Preserving pull request history in a migration Jump to heading
Git history records what changed; pull request history records why. Review discussions, design decisions argued in comments, the reasons a change was approved or held back β all of that lives on the hosting platform, not in Git. A migration that pushes branches and tags moves the code and loses the reasoning. Months later, someone running git blame finds a merge commit that says βMerge pull request #2817β and a link to a host that no longer exists. Full fidelity migration between platforms is rarely possible, but most of the value can be kept: export the discussions, keep the pull request refs, publish an archive, and make every reference in history lead somewhere. This page does that, within repository migration and consolidation.
When to use this approach Jump to heading
- A repository with valuable review history is moving to another host.
- Commit messages and code comments reference pull request numbers.
- You need an audit trail of approvals for compliance after the move.
- You are also moving pipelines, as in migrating CI and hooks with a repository.
Step 1 β Decide what level of fidelity you need Jump to heading
Native import keeps pull requests as live objects on the new host, where an importer for the platform pair exists. An archive keeps them as read-only pages. Refs-only keeps the commits but not the discussion. Choose per repository.
Step 2 β Export pull requests with comments and reviews Jump to heading
Export every pull request with its description, comments, review comments and review decisions through the API, while the old host is still available.
mkdir -p pr-export
gh pr list --state all --limit 10000 --json number --jq '.[].number' | while read -r n; do
gh pr view "$n" --json number,title,body,author,createdAt,mergedAt,state,baseRefName,headRefName,mergeCommit,comments,reviews,url \
> "pr-export/$n.json"
gh api --paginate "repos/$OWNER/$REPO/pulls/$n/comments" > "pr-export/$n.review-comments.json"
done
ls pr-export | wc -l Respect API rate limits on large repositories by pausing between batches; the export can run over several days before cutover and be refreshed at the end.
Step 3 β Keep the pull request refs Jump to heading
Hosts keep each pull requestβs head under a special ref namespace. Fetch them and push them to the new host under a namespace of your own, so the exact commits of every pull request β including unmerged ones β remain available.
git fetch origin '+refs/pull/*/head:refs/pull-archive/*'
git for-each-ref refs/pull-archive/ | wc -l
git push new-origin 'refs/pull-archive/*:refs/pull-archive/*' Pushes into some reserved namespaces are refused by hosts; a custom prefix such as refs/pull-archive/ avoids that.
Step 4 β Publish a static archive Jump to heading
Render the exported JSON into simple read-only pages β one per pull request, with title, description, comments and review decisions β and host them at a stable URL.
mkdir -p pr-archive
for f in pr-export/[0-9]*.json; do
case "$f" in *review-comments*) continue ;; esac
n=$(basename "$f" .json)
jq -r '"# #\(.number) \(.title)\n\nAuthor: \(.author.login) Β· Created: \(.createdAt) Β· State: \(.state)\n\n\(.body // "")\n\n## Comments\n" +
([.comments[] | "**\(.author.login)** (\(.createdAt)):\n\n\(.body)\n"] | join("\n")) +
"\n## Reviews\n" + ([.reviews[] | "- \(.author.login): \(.state)"] | join("\n"))' "$f" > "pr-archive/$n.md"
done Step 5 β Link history to the archive Jump to heading
Merge commits say βMerge pull request #2817β. Make that number lead to the archive: add a note to each merge commit with the archive URL, and document the URL pattern in the repository.
git fetch new-origin 'refs/notes/*:refs/notes/*' 2>/dev/null || true
git log --first-parent --format='%H %s' main | sed -n 's/^\([0-9a-f]*\) Merge pull request #\([0-9]*\).*/\1 \2/p' |
while read -r sha n; do
git notes --ref=pr append -m "Archived discussion: https://pr-archive.example.com/app/$n" "$sha"
done
git push new-origin refs/notes/pr For squash-merged repositories, the pull request number is usually in the commit subject as (#2817); adjust the pattern.
Step 6 β Redirect old links Jump to heading
Links to the old hostβs pull request pages appear in issue trackers, chat, documentation and code comments. If you control the old domain, redirect pull request URLs to the archive. Otherwise, search and replace in the places you own.
git grep -n -E 'https://git\.old\.example/org/app/(pull|merge_requests)/[0-9]+' -- ':!pr-archive' | head
# Example redirect rule on the old domain (web server configuration):
# /org/app/pull/(\d+) β https://pr-archive.example.com/app/$1 Step 7 β Recreate open pull requests Jump to heading
Open pull requests need to stay workable. Merge or close as many as possible before cutover; recreate the rest on the new host from their branches, linking to the archived discussion.
gh pr list --state open --json number,headRefName,title --jq '.[] | "\(.number)\t\(.headRefName)\t\(.title)"' |
while IFS="$(printf '\t')" read -r n branch title; do
echo "recreate: $branch β $title (was #$n) β https://pr-archive.example.com/app/$n"
done Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Do imported pull requests keep their numbers? Jump to heading
Usually not: the new host assigns numbers in its own sequence, often shared with issues. That is why links in history should point to the archive, whose URLs use the original numbers.
What about authors who do not exist on the new host? Jump to heading
Archives show the original usernames as text. Native imports need a user mapping; unmapped users appear as a placeholder or the importing account.
Is the archive needed if we import natively? Jump to heading
It is cheap insurance. Imports sometimes drop review comments on outdated lines or attachments, and an archive keeps a faithful copy.
Related Jump to heading
- Repository Migration & Consolidation β the parent topic.
- Moving a Repository Between Hosting Providers β the Git data move.
- Archiving a Repository Without Losing History β retiring the old repository.
- Finding Who Changed a Line with log and blame β where archived discussions get used.