atlassian-cli はコミュニティによる独立したオープンソースプロジェクトです。Atlassian と提携・関連しておらず、Atlassian による承認、推奨、後援のいずれも受けておらず、Atlassian が提供する公式 CLI(acli)でもありません。製品名は互換性を示す目的でのみ使用しています。
Web インポーターを使わずに Bitbucket から GitHub へ移行する
Bitbucket から GitHub へまとめて移行するなら、スクリプト化しましょう。atlassian-cli でワークスペース内のリポジトリをすべて一覧し、git clone --mirror で 1 つずつミラークローンして、git push --mirror で新しい GitHub のリポジトリへ push します。GitHub の Web インポーターはリポジトリが 1 つなら十分ですが、リポジトリごとにフォームを 1 件ずつクリックさせられます。10 件でも厳しく、チームが何年もかけてためた 100 件ともなればなおさらです。
これは Bitbucket から離れる人たちが Reddit で繰り返し漏らす不満でもあります。クリック操作のインポーターは遅く、進捗を見失いやすく、途中でリポジトリが失敗したときにきれいに再実行する手段がありません。スクリプトはこの 3 つをすべて解決します。ほぼ冪等で、ログを残せて、1 件を移行するのとまったく同じループがワークスペース全体を移行します。このガイドでは、そのループを最初から最後まで解説します。
Atlassian 自身の CLI との違い。 atlassian-cli はコミュニティによる独立したオープンソースプロジェクトです。Atlassian が提供する公式 CLI(acli)ではなく、Atlassian と提携・関連しておらず、Atlassian による承認、推奨、後援、保守のいずれも受けていません。ベンダーによる一次サポートが必要な場合は公式の acli を使ってください。Jira、Confluence、Bitbucket、JSM をカバーする MIT ライセンスの無料の Rust バイナリを 1 つで済ませたい場合は atlassian-cli を使ってください。この移行で atlassian-cli が担うのは 1 つの役割だけです。移行元のリポジトリを列挙し、スクリプトの残りの部分がループできる一覧を用意します。
始める前に必要なもの
スクリプトによる移行は、3 つの無料のコマンドラインツールに頼ります。それぞれが最も得意な部分を担当します。
- git が実際のデータ転送を行います。ミラークローンとミラープッシュで、すべてのコミット、ブランチ、タグが移ります。
- gh(GitHub CLI)が移行先のリポジトリを作成します。「New repository」を 100 回クリックせずに済みます。
- atlassian-cli が Bitbucket ワークスペースの移行元リポジトリを一覧します。手で入力した一覧ではなく、信頼できる棚卸し結果をスクリプトが順に処理できます。
各ツールで一度ずつ認証します。atlassian-cli では、Bitbucket のアプリパスワードか API トークンでログインします。
# 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
gh では gh auth login を実行し、GitHub の組織を選びます。git では、SSH キーが Bitbucket と GitHub の両方に登録されていることを確認してください(または HTTPS でアプリパスワードを渡せるようにしておきます)。3 つとも応答するようになれば、以降ブラウザーを開く必要はありません。
手順 1: atlassian-cli ですべてのリポジトリを棚卸しする
移行の信頼性は、リポジトリ一覧の正確さで決まります。古いスプレッドシートではなく実態そのものを扱えるよう、一覧は Bitbucket から直接 JSON で取得します。
# List every repo in the workspace as JSON
atlassian-cli bitbucket --workspace myteam repo list \
--profile work \
--limit 1000 \
--format json
各要素はリポジトリのオブジェクトです。必要なフィールドは jq で取り出します。まず repo get でリポジトリを 1 件確認し、お使いのバージョンが出力する正確なフィールド名を把握してから、全リポジトリの slug をテキストファイルに書き出します。
# 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?
これで repos.txt が移行のマニフェストになります。一部だけを対象にしたい場合は絞り込んでください。命名の接頭辞で grep するか、ファイルに書き出す前に一覧を狭めます。この棚卸しの手順は、記録として残しておく価値もあります。変更を加える前にワークスペースをスナップショットする Bitbucket リポジトリ監査と同じ考え方です。
手順 2: 各リポジトリを GitHub へミラーする
マニフェストがそろえば、移行そのものはループです。slug ごとに Bitbucket からミラークローンし、空の GitHub リポジトリを作成し、ミラープッシュします。重要なのはミラークローンである点です。既定のブランチだけでなく、すべての ref(全ブランチ、全タグ、notes)を含むベアコピーになります。
# 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
git のデータに関する移行はこれで完了です。clone 側の --mirror フラグは完全なベアコピーを作り、push 側の --mirror フラグはすべての ref をそのまま GitHub に再現します。まず slug を 1 つだけ指定して経路が通ることを確認し、その後 repos.txt をまとめて処理させてください。
実務上の注意が 2 つあります。1 つ目は、公開リポジトリにしたい場合は --private を外すか、実行ごとに切り替えられるよう変数にすることです。2 つ目は、大きなリポジトリが GitHub の push 制限に引っかかった場合、バッチと格闘せずにそのリポジトリだけを単独で移行することです。ループをあえて単純にしてあるのは、失敗した slug を再試行用の一覧に戻して、それだけを再実行できるようにするためです。
Web インポーターとスクリプトによる移行の比較
どちらの方法でもコードは移せます。違いが出るのは、リポジトリが複数あるときと、何かが失敗したときです。
| 観点 | GitHub の Web インポーター | スクリプトによる移行 |
|---|---|---|
| 1 回あたりのリポジトリ数 | 1 件ずつ。リポジトリごとにフォーム入力 | ワークスペース全体を 1 つのループで |
| 移行元の一覧 | 手入力 | atlassian-cli で Bitbucket から直接取得 |
| 失敗時の再試行 | フォームを手作業でやり直す | 失敗した slug だけスクリプトを再実行 |
| 監査のしやすさ | クリック操作のみで記録が残らない | コマンドのログとバージョン管理されたマニフェスト |
| 移動する ref | 既定のブランチと各ブランチ | --mirror で全ブランチと全タグ |
| 費用 | 無料 | 無料(git、gh、atlassian-cli) |
リポジトリが 1 つだけなら、Web インポーターのほうが実際に速いです。スクリプトを書く必要がありません。リポジトリが数件を超えたとき、切り替え期日が決まっているとき、後から何が移ったかを正確に示す必要があるときは、スクリプトによる方法に軍配が上がります。
ミラーで移るもの、移らないもの
切り替え後に驚く人が出ないよう、範囲をはっきりさせておきましょう。git のミラーは git の操作なので、移るのは git のものだけです。
ミラーで移るもの
- 全ブランチにわたる完全なコミット履歴。
- リリース用や長期運用の機能ブランチを含む、すべてのブランチ。
- すべてのタグと git notes。
ミラーで移らないもの
- オープンなプルリクエストとそのレビューコメント。コードを移した後に、手作業か GitHub API で GitHub 側に作り直します。
- ブランチの権限とマージ制限。GitHub のブランチ保護ルールとして設定し直します。
- Bitbucket Pipelines。
bitbucket-pipelines.ymlは GitHub Actions のワークフローとして書き直す必要があります。これはそれ自体が 1 つのテーマで、Bitbucket Pipelines から GitHub Actions への移行で解説しています。
多くのチームにとって現実的な順序はこうです。まずコードをミラーして履歴を安全な場所に移し、オープンな PR とパイプラインは後続の作業として扱います。マージ済みのプルリクエストはすでに git の履歴の一部なので、ミラーと一緒に移ります。計画が必要なのは、進行中のオープンな PR だけです。
手順 3: 移行を検証する
信頼したうえで、検証します。ループが終わったら、移行元のワークスペースにあったリポジトリがすべて GitHub 側にそろっているかを確認します。移行元の件数は atlassian-cli、移行先の件数は gh で取得し、差分を取ればバッチが取りこぼしたものが見つかります。
# 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
comm -23 の結果が空なら、移行元のリポジトリはすべて GitHub 上に存在します。履歴の抜き取り確認としては、既知のタグか main の最新コミットを 2 つのリモート間で比べてください。差分がなく、いくつかのリポジトリで手動の履歴確認も通れば、コードの移行は完了です。あとは PR、権限、パイプラインに進めます。
移行スクリプト全文
棚卸し、ミラーのループ、検証を、基本的な安全策とあわせて 1 か所にまとめました。出発点として扱い、ワークスペース、組織、公開範囲は自分の状況に合わせて調整してください。
#!/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."
gh repo create の後ろの || true は、以前の実行で移行先のリポジトリがすでに存在していてもループを続けるためのもので、スクリプトを安全に再実行できるようにします。それ以外はあえて退屈な作りです。1 行読み、クローンし、作成し、push し、後片付けするだけです。移行の前に移行元をもっと調べたい場合は、コマンドの全一覧を Bitbucket CLI コマンドリファレンスに、リポジトリ、ブランチ、プルリクエストをターミナルから扱う方法を Bitbucket ハブにまとめています。
Bitbucket ワークスペースをターミナルから運用する
atlassian-cli は、Jira、Confluence、Bitbucket、JSM に対応した MIT ライセンスの無料バイナリです。インストールすれば、コマンド 1 つでリポジトリを棚卸しできます。
atlassian-cli を試すよくある質問
Bitbucket のリポジトリをまとめて GitHub へ移行できますか?
はい。GitHub の Web インポーターを 1 件ずつクリックして進める代わりに、atlassian-cli でワークスペースのリポジトリ一覧をすべて取得し、git と GitHub CLI でループ処理します。各リポジトリは Bitbucket からミラークローンされ、新しい GitHub のリポジトリへミラープッシュされます。1 件で使うスクリプトがそのまま 100 件の移行にも使えます。
git clone --mirror はすべてのブランチとタグを移してくれますか?
はい。ミラークローンはすべての ref をコピーします。全ブランチ、全タグ、notes が対象です。これを git push --mirror でプッシュすると、その ref 構成がそのまま GitHub 側に再現されるため、移行先のリポジトリは移行元と同じコミット履歴、ブランチ、タグを持ちます。移るのは git のデータだけで、プルリクエストやパイプラインといった Bitbucket 側のメタデータは移りません。
プルリクエストや Bitbucket Pipelines はどうなりますか?
git のミラーはコミット、ブランチ、タグを移しますが、Bitbucket 固有のメタデータは移しません。オープンなプルリクエスト、PR のコメント、パイプラインの実行履歴は git のデータと一緒には移りません。コードを移した後で、オープンな PR は手動または GitHub API で作り直し、bitbucket-pipelines.yml は別途 GitHub Actions のワークフローとして書き直してください。
Bitbucket から GitHub への移行に有料ツールは必要ですか?
いいえ。スクリプトによる方法で使うのは無料のツールだけです。ミラークローンとプッシュには git、移行先のリポジトリ作成には GitHub CLI(gh)、移行元のリポジトリの列挙には atlassian-cli(MIT ライセンスの無料のオープンソースバイナリ)を使います。リポジトリごとの費用はかからず、この 3 つ以外にインストールするものもありません。