Skip to content

Instantly share code, notes, and snippets.

@mrcgrtz
Created March 30, 2026 17:13
Show Gist options
  • Select an option

  • Save mrcgrtz/e262c60346f72e5615b465ca4eb69aae to your computer and use it in GitHub Desktop.

Select an option

Save mrcgrtz/e262c60346f72e5615b465ca4eb69aae to your computer and use it in GitHub Desktop.
Migrate from Bitbucket to GitHub
#!/usr/bin/env bash
set -euo pipefail
# =============================================================================
# migrate-bitbucket-to-github.sh
#
# Batch-migrates all private Bitbucket repositories to GitHub.
# - Repos are created as private on GitHub
# - The default branch is renamed from `master` to `main` (if applicable)
# - Local clones are cleaned up immediately after each push
# - A temporary working directory is used and removed on exit
#
# Prerequisites:
# - `git`, `curl`, and `jq` must be installed
# - Bitbucket API Token with "Repository: Read" scope
# (create at bitbucket.org → Personal Bitbucket settings → API tokens)
# - Bitbucket account email address
# (find at bitbucket.org → Personal Bitbucket settings → Email aliases)
# - GitHub Personal Access Token (classic) with "repo" scope
# (create at github.com → Settings → Developer settings → Personal access tokens)
#
# Usage:
# chmod +x migrate-bitbucket-to-github.sh
# ./migrate-bitbucket-to-github.sh
# =============================================================================
# === Configuration ===
BITBUCKET_USER="" # Your Bitbucket username (not email)
BITBUCKET_EMAIL="" # Your Atlassian account email (for REST API auth)
BITBUCKET_API_TOKEN="" # Your Bitbucket API token
GITHUB_USER="" # Your GitHub username (not email)
GITHUB_TOKEN="" # GitHub Personal Access Token (classic) with "repo" scope
# === Temporary working directory ===
# All bare clones are created here and cleaned up after each repo.
# The trap ensures the directory is removed on exit, even on error.
WORKDIR=$(mktemp -d)
cd "$WORKDIR"
trap "rm -rf '$WORKDIR'" EXIT
# === Fetch all Bitbucket repos ===
echo "Fetching all Bitbucket repos for user: $BITBUCKET_USER ..."
# REST API authentication uses email + API token (Basic Auth).
# Note: If you have more than 100 repos, pagination needs to be implemented.
repos=$(curl -s -u "$BITBUCKET_EMAIL:$BITBUCKET_API_TOKEN" \
"https://api.bitbucket.org/2.0/repositories/$BITBUCKET_USER?pagelen=100" \
| jq -r '.values[].name')
echo "Found repos:"
echo "$repos"
# === For each repo: create on GitHub, mirror, rename branch, push ===
for repo in $repos; do
echo "--------------------------------------------"
echo "Processing repo: $repo"
# Create the repo on GitHub as private.
# HTTP 201 = created, 422 = already exists (both are fine).
response=$(curl -s -o /dev/null -w "%{http_code}" -X POST "https://api.github.com/user/repos" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d "{\"name\":\"$repo\",\"private\":true}")
if [[ "$response" == "201" ]]; then
echo "Created new repo on GitHub: $repo"
elif [[ "$response" == "422" ]]; then
echo "Repo already exists on GitHub: $repo"
else
echo "Failed to create repo on GitHub: HTTP $response — skipping."
continue
fi
# Clone a bare mirror from Bitbucket.
# Git authentication uses the static username x-bitbucket-api-token-auth + API token.
echo "Cloning mirror from Bitbucket..."
git clone --mirror \
"https://x-bitbucket-api-token-auth:$BITBUCKET_API_TOKEN@bitbucket.org/$BITBUCKET_USER/$repo.git" \
|| { echo "Clone failed, skipping $repo."; continue; }
cd "$repo.git"
# Rename master to main if master exists.
# git symbolic-ref updates HEAD so GitHub picks up main as the default branch.
if git show-ref --verify --quiet refs/heads/master; then
echo "Renaming master → main..."
git branch -m master main
git symbolic-ref HEAD refs/heads/main
echo "Renamed master to main."
else
echo "No master branch found, skipping rename."
fi
# Push all refs to GitHub.
echo "Pushing mirror to GitHub..."
git remote set-url origin \
"https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$GITHUB_USER/$repo.git"
git push --mirror
cd ..
# Remove the local bare clone immediately after pushing.
echo "Cleaning up local clone..."
rm -rf "$repo.git"
echo "Done: $repo"
done
echo "============================================"
echo "All repos migrated!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment