Blog
Jan 25, 2026 - 10 MIN READ

How I Migrated All My GitHub Repos to Bitbucket (Without Doing It One by One)

A practical guide to bulk migrating your GitHub repositories to Bitbucket using a simple bash script

Suprasanna Ojha

Suprasanna Ojha

So I had this situation where I wanted to have all my GitHub repositories backed up on Bitbucket. You know, the whole "don't put all your eggs in one basket" thing. Seemed simple enough, right?

I went online looking for a guide and... every single tutorial I found was about migrating one repository at a time. Click this, click that, import, wait, repeat. With over 50 repos, that was going to take forever. And honestly, who has time for that?

After digging around for a while, I couldn't find anything that did bulk migration properly. So I ended up writing my own script. And since I went through all that trouble, I figured I'd share it here in case someone else runs into the same problem.

What We're Going to Do

The idea is simple:

  1. Fetch all your repos from GitHub using their API
  2. Create matching repos on Bitbucket
  3. Mirror push everything over
  4. Keep track of what's done so you can resume if something breaks

Let's break it down piece by piece.

Before You Start

You'll need a few things set up first. Don't worry, most of these are probably already on your machine if you're a developer.

Prerequisites

Here's everything the script needs to run:

Git

You definitely need git installed. If you're reading this, you probably have it already. But just in case:

Mac:

# Usually comes with Xcode Command Line Tools
xcode-select --install

# Or via Homebrew
brew install git

Linux (Ubuntu/Debian):

sudo apt update && sudo apt install git

Windows: Download from git-scm.com or use winget install Git.Git

To check if you have it: git --version

curl

This is for making API calls to GitHub and Bitbucket. It comes pre-installed on most systems.

Mac: Already installed

Linux (Ubuntu/Debian):

sudo apt install curl

To check: curl --version

jq

This is a command-line JSON processor. We use it to parse the responses from the APIs. This one you might not have.

Mac:

brew install jq

Linux (Ubuntu/Debian):

sudo apt install jq

Windows:

winget install jqlang.jq

You can also download it directly from jqlang.github.io/jq/download

To check: jq --version

Python 3

We only use Python for one thing - URL encoding special characters in the API token. Nothing fancy.

Mac:

# Usually pre-installed, but if not:
brew install python3

Linux (Ubuntu/Debian):

sudo apt install python3

Windows: Download from python.org or use winget install Python.Python.3.12

To check: python3 --version

Quick Check

Before moving on, run these commands to make sure everything's installed:

git --version
curl --version
jq --version
python3 --version

If any of these fail, go back and install the missing tool.

GitHub Personal Access Token

You need a token to access your GitHub repos through the API.

  1. Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
  2. Generate a new token with repo scope (full control of private repositories)
  3. Copy it somewhere safe, you'll need it in a minute

Bitbucket API Token

Here's something that tripped me up initially - Bitbucket used to have something called "App Passwords" but they've replaced that with API tokens now.

To get your API token:

  1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
  2. Create a new API token
  3. Copy it and keep it safe

You'll also need to know your Bitbucket workspace name. That's the part that appears in your Bitbucket URLs like bitbucket.org/YOUR_WORKSPACE/repo-name.

Setting Up Environment Variables

The script uses environment variables for all the credentials. This keeps sensitive stuff out of the script itself.

Before running anything, you'll need to set these:

export GITHUB_USER="your_github_username"
export GITHUB_TOKEN="your_github_personal_access_token"
export BITBUCKET_WORKSPACE="your_bitbucket_workspace"
export BITBUCKET_EMAIL="your_atlassian_email"
export BITBUCKET_API_TOKEN="your_bitbucket_api_token"

The script checks if all of these are set and will stop with an error if any are missing:

for var in GITHUB_USER GITHUB_TOKEN BITBUCKET_WORKSPACE BITBUCKET_EMAIL BITBUCKET_API_TOKEN; do
  if [[ -z "${!var}" ]]; then
    echo "Error: $var environment variable is not set"
    exit 1
  fi
done

Nothing worse than running a script halfway through and realizing you forgot to set something.

Testing Bitbucket Authentication

Before doing anything else, the script makes sure your Bitbucket credentials actually work:

echo "Testing Bitbucket authentication..."
BB_TEST=$(curl -s -u "$BITBUCKET_EMAIL:$BITBUCKET_API_TOKEN" \
  "https://api.bitbucket.org/2.0/user")

if echo "$BB_TEST" | jq -e '.username' >/dev/null 2>&1; then
  echo "Bitbucket auth OK: $(echo "$BB_TEST" | jq -r '.username')"
else
  echo "Bitbucket authentication failed:"
  echo "$BB_TEST"
  exit 1
fi

This calls the Bitbucket API with your credentials and checks if it returns a valid username. If it doesn't, you'll know right away instead of finding out 30 repos into the migration.

Fetching Your GitHub Repositories

Now we grab the list of all your repos from GitHub:

repos=$(curl -s -u "$GITHUB_USER:$GITHUB_TOKEN" \
  "https://api.github.com/user/repos?per_page=200&affiliation=owner&visibility=all" \
  | jq -r '.[] | select(.fork == false) | .name')

A few things happening here:

  • per_page=200 gets up to 200 repos in one call (if you have more than 200, you'd need pagination but that's a rare case)
  • affiliation=owner means only repos you own, not ones you're just a collaborator on
  • visibility=all includes both public and private repos
  • The jq command filters out forked repos (since you probably don't need to back those up) and extracts just the repo names

The Main Migration Loop

Now we loop through each repo and do the actual migration:

for repo in $repos
do
  echo "Processing $repo"
  cd "$WORK_DIR"

  # Skip if already migrated
  if grep -q "^${repo}$" "$PROGRESS_FILE" 2>/dev/null; then
    echo "  Skipping $repo (already migrated)"
    continue
  fi

See that progress file check? This is really useful. If your internet drops or something fails halfway through, you can just run the script again and it'll pick up where it left off instead of starting over.

Creating the Bitbucket Repository

# Convert repo name to lowercase for Bitbucket
bb_repo=$(echo "$repo" | tr '[:upper:]' '[:lower:]')

# Create repo in Bitbucket
BB_RESPONSE=$(curl -s -X POST \
  -u "$BITBUCKET_EMAIL:$BITBUCKET_API_TOKEN" \
  -H "Content-Type: application/json" \
  "https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$bb_repo" \
  -d '{"scm":"git","is_private":true}')

One thing I learned: Bitbucket requires repository slugs (the URL-friendly name) to be lowercase. So if you have a repo called MyAwesomeProject on GitHub, it becomes myawesomeproject on Bitbucket. The script handles this conversion automatically.

Also, all repos are created as private by default. Change is_private to false if you want them public.

The Actual Migration

Here's where the magic happens:

if git clone --mirror "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$GITHUB_USER/$repo.git"; then
  cd "$repo.git"
  git push --mirror "https://x-bitbucket-api-token-auth:${ENCODED_BB_TOKEN}@bitbucket.org/$BITBUCKET_WORKSPACE/$bb_repo.git"
  cd "$WORK_DIR"
  rm -rf "$repo.git"
  echo "  Mirror push complete"

  # Mark as complete
  echo "$repo" >> "$PROGRESS_FILE"
fi

The --mirror flag is key here. A mirror clone gets everything - all branches, all tags, all refs. It's a complete copy of the repository, not just the main branch.

Then we push with --mirror to Bitbucket, which pushes everything we just cloned.

One weird thing about Bitbucket authentication in git URLs - you have to use x-bitbucket-api-token-auth as the username when using API tokens. Not your email, not your username. Just that exact string. Took me a while to figure that one out.

URL Encoding

If your API token has special characters in it (and it probably does), you need to URL encode it before putting it in a git URL:

urlencode() {
  python3 -c "import urllib.parse; print(urllib.parse.quote('$1', safe=''))"
}

ENCODED_BB_TOKEN=$(urlencode "$BITBUCKET_API_TOKEN")

This prevents characters like @ or & in your token from breaking the URL.

The Complete Script

Here's the full script all together. Save this as something like migrate-to-bitbucket.sh:

#!/bin/bash
set -e

# Validate required environment variables
for var in GITHUB_USER GITHUB_TOKEN BITBUCKET_WORKSPACE BITBUCKET_EMAIL BITBUCKET_API_TOKEN; do
  if [[ -z "${!var}" ]]; then
    echo "Error: $var environment variable is not set"
    exit 1
  fi
done

WORK_DIR="$(pwd)"
PROGRESS_FILE="$WORK_DIR/migrated_repos.txt"
touch "$PROGRESS_FILE"

# URL-encode function
urlencode() {
  python3 -c "import urllib.parse; print(urllib.parse.quote('$1', safe=''))"
}

ENCODED_BB_TOKEN=$(urlencode "$BITBUCKET_API_TOKEN")

# Test Bitbucket authentication
echo "Testing Bitbucket authentication..."
BB_TEST=$(curl -s -u "$BITBUCKET_EMAIL:$BITBUCKET_API_TOKEN" \
  "https://api.bitbucket.org/2.0/user")
if echo "$BB_TEST" | jq -e '.username' >/dev/null 2>&1; then
  echo "Bitbucket auth OK: $(echo "$BB_TEST" | jq -r '.username')"
else
  echo "Bitbucket authentication failed:"
  echo "$BB_TEST"
  exit 1
fi

# Fetch GitHub repositories
echo "Fetching GitHub repositories..."
repos=$(curl -s -u "$GITHUB_USER:$GITHUB_TOKEN" \
  "https://api.github.com/user/repos?per_page=200&affiliation=owner&visibility=all" \
  | jq -r '.[] | select(.fork == false) | .name')

for repo in $repos
do
  echo "Processing $repo"
  cd "$WORK_DIR"

  # Skip if already migrated
  if grep -q "^${repo}$" "$PROGRESS_FILE" 2>/dev/null; then
    echo "  Skipping $repo (already migrated)"
    continue
  fi

  bb_repo=$(echo "$repo" | tr '[:upper:]' '[:lower:]')
  rm -rf "$repo.git"

  # Create Bitbucket repo
  echo "  Creating Bitbucket repo..."
  curl -s -X POST \
    -u "$BITBUCKET_EMAIL:$BITBUCKET_API_TOKEN" \
    -H "Content-Type: application/json" \
    "https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$bb_repo" \
    -d '{"scm":"git","is_private":true}' > /dev/null

  # Mirror clone and push
  if git clone --mirror "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$GITHUB_USER/$repo.git"; then
    cd "$repo.git"
    git push --mirror "https://x-bitbucket-api-token-auth:${ENCODED_BB_TOKEN}@bitbucket.org/$BITBUCKET_WORKSPACE/$bb_repo.git"
    cd "$WORK_DIR"
    rm -rf "$repo.git"

    echo "$repo" >> "$PROGRESS_FILE"
    echo "  Done"
  else
    echo "  Failed to clone $repo, skipping..."
  fi
done

echo "Migration complete!"

Running It

  1. Save the script and make it executable:
chmod +x migrate-to-bitbucket.sh
  1. Set your environment variables:
export GITHUB_USER="yourusername"
export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
export BITBUCKET_WORKSPACE="yourworkspace"
export BITBUCKET_EMAIL="you@email.com"
export BITBUCKET_API_TOKEN="your_api_token"
  1. Run it:
./migrate-to-bitbucket.sh
  1. Sit back and watch it go through your repos one by one.

Things I Learned Along the Way

A few gotchas that might save you some time:

  • Bitbucket repo names must be lowercase. GitHub doesn't care, Bitbucket does. The script handles this but it's good to know.
  • API tokens replaced app passwords. If you're reading older tutorials about Bitbucket authentication, they might mention "app passwords." Those are gone now. Use API tokens from id.atlassian.com.
  • The weird username for git auth. When authenticating git operations with a Bitbucket API token, use x-bitbucket-api-token-auth as the username. Not your actual username or email.
  • Mirror clone is your friend. Don't just git clone - use git clone --mirror to get everything including all branches and tags.
  • Progress tracking saves headaches. When you're migrating 50+ repos, things can and will go wrong. Having a simple text file that tracks what's done means you can just re-run the script without starting over.

Wrapping Up

That's basically it. What would have taken me hours of clicking through web interfaces took about 20 minutes to run. And now I have all my repos backed up on Bitbucket without having to think about it.

If you run into any issues or have questions, feel free to reach out. And if you end up using this script, I'd love to hear about it!

Copyright © 2026 Suprasanna Ojha. All rights reserved.