atlassian-cli is an independent, community open-source project, not affiliated with, endorsed by, or sponsored by Atlassian, and is not Atlassian's official CLI (acli). Product names are used only to identify compatibility.
Migrate Bitbucket to GitHub Without the Web Importer
To migrate Bitbucket to GitHub in bulk, script it: list every repository in your workspace with atlassian-cli, mirror-clone each one with git clone --mirror, and push it into a fresh GitHub repository with git push --mirror. GitHub's web importer is fine for a single repo, but it makes you click through a form for every repository, one at a time. That does not scale to ten repos, let alone the hundred a team accumulates over the years.
This is a recurring complaint on Reddit from people leaving Bitbucket: the point-and-click importer is slow, easy to lose track of, and gives you no clean way to re-run it if a repo fails halfway. A script fixes all three. It is idempotent-ish, loggable, and the exact same loop that migrates one repo migrates the whole workspace. This guide walks through that loop end to end.
How this differs from Atlassian's own CLI. atlassian-cli is an independent, community open-source project. It is not Atlassian's official CLI (acli) and is not affiliated with, endorsed by, or sponsored by Atlassian. Use the official acli if you want first-party vendor support. Use atlassian-cli if you want a single free, MIT-licensed Rust binary spanning Jira, Confluence, Bitbucket, and JSM. Here it does one job in the migration: it enumerates the source repositories so the rest of the script has a list to loop over.
What You Need Before You Start
The scripted migration leans on three free command-line tools, each doing the part it is best at:
- git does the actual data transfer. A mirror clone and a mirror push move every commit, branch, and tag.
- gh (the GitHub CLI) creates the destination repositories so you are not clicking "New repository" a hundred times.
- atlassian-cli lists the source repositories in your Bitbucket workspace, so the script has an authoritative inventory to iterate over rather than a list you typed by hand.
Authenticate each tool once. For atlassian-cli, log in with a Bitbucket app password or API token:
# Authenticate atlassian-cli against your Bitbucket workspace
atlassian-cli auth login \
--profile work \
--base-url https://api.bitbucket.org \
--email you@example.com \
--token "$BITBUCKET_APP_PASSWORD"
# Confirm it works
atlassian-cli bitbucket whoami --profile work
For gh, run gh auth login and pick your GitHub organization. For git, make sure your SSH keys are registered with both Bitbucket and GitHub (or be ready to supply app passwords over HTTPS). Once all three answer, you never touch a browser again.
Step 1: Inventory Every Repo with atlassian-cli
The migration is only as reliable as its list of repositories. Pull that list straight from Bitbucket in JSON so you are working from ground truth, not a stale spreadsheet:
# List every repo in the workspace as JSON
atlassian-cli bitbucket --workspace myteam repo list \
--profile work \
--limit 1000 \
--format json
Each entry is a repository object. Extract the field you need with jq. Inspect one repo first with repo get so you use the exact field name your version emits, then pull the slug for every repo into a plain text file:
# Peek at a single repo to see the JSON shape
atlassian-cli bitbucket --workspace myteam repo get api-service \
--profile work --format json | jq 'keys'
# Write one repo slug per line to repos.txt
atlassian-cli bitbucket --workspace myteam repo list \
--profile work --limit 1000 --format json \
| jq -r '.[].slug' > repos.txt
wc -l repos.txt # how many repos are we migrating?
Now repos.txt is your migration manifest. If you only want a subset, filter it: grep for a naming prefix, or narrow the list before writing the file. This inventory step is also worth keeping for the record. It is the same idea behind a full Bitbucket repo audit, where you snapshot the workspace before making changes.
Step 2: Mirror Each Repo to GitHub
With the manifest in hand, the migration itself is a loop. For each slug, mirror-clone from Bitbucket, create the empty GitHub repository, and mirror-push. A mirror clone is the important detail: it is a bare copy that includes every ref (all branches, all tags, notes), not just the default branch.
# The core migration loop
WORKSPACE="myteam" # Bitbucket workspace
GH_ORG="mycompany" # GitHub org or username
while read -r slug; do
echo "==> Migrating $slug"
# 1. Bare mirror clone from Bitbucket
git clone --mirror "git@bitbucket.org:$WORKSPACE/$slug.git"
# 2. Create the empty destination repo on GitHub
gh repo create "$GH_ORG/$slug" --private
# 3. Push every ref to GitHub
git -C "$slug.git" push --mirror "git@github.com:$GH_ORG/$slug.git"
# 4. Clean up the local mirror
rm -rf "$slug.git"
done < repos.txt
That is the whole migration for the git data. The --mirror flag on the clone gives you a complete bare copy; the --mirror flag on the push replays every ref exactly onto GitHub. Run it against one slug first to prove the pipe works, then let it churn through repos.txt.
Two practical notes. First, drop --private if you want public repositories, or make it a variable so you can decide per run. Second, if a large repo trips GitHub's push limits, migrate it on its own line rather than fighting the batch. The loop is deliberately simple so you can drop a failed slug back onto a retry list and re-run only that.
Web Importer vs. a Scripted Migration
Both approaches move your code. The difference is what happens when there is more than one repo, or when something goes wrong.
| Concern | GitHub web importer | Scripted migration |
|---|---|---|
| Repos per run | One at a time, form per repo | Whole workspace in one loop |
| Source list | Typed by hand | Pulled live from Bitbucket via atlassian-cli |
| Retry a failure | Redo the form manually | Re-run the script for the failed slug |
| Auditability | Clicks, no record | Logged commands, versioned manifest |
| Refs moved | Default branch plus branches | Every branch and tag via --mirror |
| Cost | Free | Free (git, gh, atlassian-cli) |
For a single repository, the web importer is genuinely quicker: no script to write. The scripted path wins the moment you have a handful of repos, a hard cutover date, or a need to prove afterward exactly what moved.
What a Mirror Moves (and What It Doesn't)
Be precise about scope so nobody is surprised after the cutover. A git mirror is a git operation, so it moves git things and nothing else.
Moves with the mirror:
- Complete commit history across all branches.
- Every branch, including release and long-lived feature branches.
- All tags and any git notes.
Does not move with the mirror:
- Open pull requests and their review comments. Recreate them on GitHub after the code lands, by hand or through the GitHub API.
- Branch permissions and merge restrictions. Re-apply them as GitHub branch protection rules.
- Bitbucket Pipelines. Your
bitbucket-pipelines.ymlhas to be rewritten as a GitHub Actions workflow. That is a topic on its own, covered in converting Bitbucket Pipelines to GitHub Actions.
For most teams the pragmatic order is: mirror the code first so history is safe, then handle open PRs and pipelines as follow-up work. Merged pull requests are already part of the git history and travel with the mirror. Only the open, in-flight PRs need a plan.
Step 3: Verify the Migration
Trust, then verify. After the loop finishes, confirm GitHub has every repository the source workspace had. atlassian-cli gives you the source count, gh gives you the destination count, and a diff catches anything the batch dropped:
# Source: repo slugs on Bitbucket
atlassian-cli bitbucket --workspace myteam repo list \
--profile work --limit 1000 --format json \
| jq -r '.[].slug' | sort > source.txt
# Destination: repo names on GitHub
gh repo list mycompany --limit 1000 --json name \
| jq -r '.[].name' | sort > dest.txt
# Anything only on the source side has NOT migrated
comm -23 source.txt dest.txt
An empty result from comm -23 means every source repo now exists on GitHub. For a spot check on history, compare a known tag or the latest commit on main between the two remotes. Once the diff is clean and a couple of repos pass a manual history check, the code migration is done and you can move on to PRs, permissions, and pipelines.
The Full Migration Script
Here is the whole thing in one place: inventory, mirror loop, and verification, with basic guardrails. Treat it as a starting point and adapt the workspace, org, and visibility to your situation.
#!/usr/bin/env bash
# Bulk migrate Bitbucket repos to GitHub
set -euo pipefail
WORKSPACE="myteam" # Bitbucket workspace
GH_ORG="mycompany" # GitHub org or username
PROFILE="work" # atlassian-cli profile
# 1. Inventory the source workspace
atlassian-cli bitbucket --workspace "$WORKSPACE" repo list \
--profile "$PROFILE" --limit 1000 --format json \
| jq -r '.[].slug' > repos.txt
echo "Found $(wc -l < repos.txt) repositories to migrate"
# 2. Mirror each repo across to GitHub
while read -r slug; do
echo "==> Migrating $slug"
git clone --mirror "git@bitbucket.org:$WORKSPACE/$slug.git"
gh repo create "$GH_ORG/$slug" --private || true
git -C "$slug.git" push --mirror "git@github.com:$GH_ORG/$slug.git"
rm -rf "$slug.git"
done < repos.txt
# 3. Verify nothing was dropped
sort repos.txt > source.txt
gh repo list "$GH_ORG" --limit 1000 --json name \
| jq -r '.[].name' | sort > dest.txt
echo "Repos still missing on GitHub:"
comm -23 source.txt dest.txt
echo "Migration pass complete."
The || true after gh repo create lets the loop continue if a destination repo already exists from an earlier run, which makes the script safe to re-run. Everything else is intentionally boring: read a line, clone, create, push, clean up. If you want to explore the source side more before migrating, the full command set lives in the Bitbucket CLI commands reference, and the Bitbucket hub covers repos, branches, and pull requests from the terminal.
Run your Bitbucket workspace from the terminal
atlassian-cli is a free, MIT-licensed binary for Jira, Confluence, Bitbucket, and JSM. Install it and inventory your repos in one command.
Try atlassian-cliFAQ
Can I migrate all my Bitbucket repos to GitHub at once?
Yes. Instead of clicking through GitHub's web importer one repo at a time, pull the full list of repositories from your workspace with atlassian-cli, then loop over it with git and the GitHub CLI. Each repo is mirror-cloned from Bitbucket and mirror-pushed to a new GitHub repository. A hundred repos migrate from the same script you use for one.
Does git clone --mirror bring over all my branches and tags?
Yes. A mirror clone copies every ref: all branches, all tags, and notes. Pushing it with git push --mirror recreates that complete ref layout on GitHub, so the destination repository has the same commit history, branches, and tags as the source. It moves the git data only, not Bitbucket metadata like pull requests or pipelines.
What about pull requests and Bitbucket Pipelines?
A git mirror moves commits, branches, and tags but not Bitbucket-specific metadata. Open pull requests, PR comments, and pipeline runs do not transfer with the git data. Recreate open PRs manually or via the GitHub API after the code lands, and rewrite bitbucket-pipelines.yml as a GitHub Actions workflow separately.
Do I need a paid tool to migrate Bitbucket to GitHub?
No. The scripted approach uses only free tools: git for the mirror clone and push, the GitHub CLI (gh) to create the destination repositories, and atlassian-cli (a free, MIT-licensed open-source binary) to enumerate the source repositories. There is no per-repo cost and nothing to install beyond those three.