Last active
April 25, 2021 18:36
-
-
Save maghoff/2b86e4ca3208a5ce443b35c5860c8a4d to your computer and use it in GitHub Desktop.
Migrate all your repos (including wikis, _excluding_ issues) from Bitbucket to GitHub, converting hg to git in the process
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# Originally published on https://magnushoff.com/blog/kick-the-bitbucket/ | |
# Copyright (c) 2019 Magnus Hovland Hoff | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
set -eo pipefail | |
# Check for installed dependencies | |
for CMD in curl git hg git-remote-hg jq | |
do | |
if ! which "$CMD" > /dev/null | |
then | |
echo -e "\e[1m\e[31mError: Missing command $CMD\e[0m" | |
exit 1 | |
fi | |
done | |
# Specify -1 on the command line to only migrate one repository | |
ONE_REPO=false | |
if [ "x$1" == "x-1" ] | |
then | |
echo -e "\e[1m\e[32m-1 specified. Will only migrate one repository\e[0m" | |
ONE_REPO=true | |
fi | |
if [ -z "$BB_USERNAME" ] ; then read -p "Bitbucket username: " BB_USERNAME; fi | |
if [ -z "$BB_PASSWORD" ] ; then read -sp "Bitbucket password: " BB_PASSWORD; echo; fi | |
if [ -z "$GH_USERNAME" ] ; then read -p "GitHub username: " GH_USERNAME; fi | |
if [ -z "$GH_PASSWORD" ] ; then read -sp "GitHub password: " GH_PASSWORD; echo; fi | |
BB_CURL="curl -s --user $BB_USERNAME:$BB_PASSWORD" | |
GH_CURL="curl -s --user $GH_USERNAME:$GH_PASSWORD -H Accept:application/vnd.github.v3+json" | |
echo | |
echo -e "This script will migrate the \e[36msource\e[0m code and \e[36mwikis\e[0m from Bitbucket to GitHub." | |
echo -e "NOTE: Nothing else will be migrated! \e[33mIssues\e[0m, \e[33mdownloads\e[0m and more will be lost." | |
while true; do | |
read -p "Delete repos from Bitbucket after successful migration (yes/no)? " DELETE | |
case "$DELETE" in | |
yes) break;; | |
no) break;; | |
*) echo "Please answer yes or no";; | |
esac | |
done | |
TMP=$(mktemp -d "${TMPDIR:-/tmp/}$(basename $0).XXXXXXXXXXXX") | |
function cleanup { | |
rm -rf "$TMP" | |
} | |
trap cleanup EXIT | |
# Prefetch all the data from Bitbucket to avoid problems with deleting while iterating | |
echo -en "\e[1m\e[32mFetching metadata from Bitbucket" | |
ALL_REPOS="$TMP/all-repos.json" | |
echo > "$ALL_REPOS" | |
PAGE="https://api.bitbucket.org/2.0/repositories/$BB_USERNAME" | |
while [ "x$PAGE" != xnull ] | |
do | |
$BB_CURL "$PAGE" -o "$TMP/page.json" | |
echo -n "." | |
jq -c '.values[]' "$TMP/page.json" >> "$ALL_REPOS" | |
PAGE=$(jq -r .next "$TMP/page.json") | |
done | |
echo -e "\e[0m" | |
SIZE="$(jq -r '.size' "$TMP/page.json")" | |
I=0 | |
# jq iteration due to https://starkandwayne.com/blog/bash-for-loop-over-json-array-using-jq/ | |
for REPO in $(jq -r '. | @base64' "$ALL_REPOS") | |
do | |
echo "$REPO" | base64 --decode > "$TMP/repo" | |
I=$(( I + 1 )) | |
SELF="$(jq -r '.links.self.href' "$TMP/repo")" | |
CLONE_URL="$(jq -r '.links.clone[] | select(.name == "ssh") | .href' "$TMP/repo")" | |
SCM="$(jq -r '.scm' "$TMP/repo")" | |
SLUG="$(jq -r '.slug' "$TMP/repo")" | |
HAS_WIKI="$(jq -r '.has_wiki' "$TMP/repo")" | |
echo -e "\e[1m\e[32mMigrating \e[36m$SLUG\e[32m ($I/$SIZE)\e[0m" | |
case "$SCM" in | |
git) | |
CLONE_ARG="$CLONE_URL" | |
;; | |
hg) | |
CLONE_ARG="hg::$CLONE_URL" | |
;; | |
*) | |
echo -e " \e[1m\e[31mAborting due to unknown SCM: \e[36m$SCM\e[0m" >&2 | |
continue | |
esac | |
echo -e " \e[1m\e[32mCloning \e[36m$SCM\e[32m repository from Bitbucket\e[0m" | |
rm -rf "$TMP/cloned-repo.git" | |
git clone --mirror "$CLONE_ARG" "$TMP/cloned-repo.git" | |
echo -e " \e[1m\e[32mCreating new repository on GitHub\e[0m" | |
$GH_CURL https://api.github.com/user/repos -d "$( | |
jq -n -c \ | |
--slurpfile p "$TMP/repo" \ | |
'{ | |
name: $p[0].slug, | |
private: $p[0].is_private, | |
description: $p[0].description, | |
has_wiki: $p[0].has_wiki, | |
homepage: $p[0].website | |
}' | |
)" > "$TMP/to-repo.json" | |
if [ "$(jq -r .ssh_url $TMP/to-repo.json)" == null ] | |
then | |
echo -e " \e[1m\e[31mUnable to create repository \e[36m$SLUG\e[31m, does it already exist?\e[0m" >&2 | |
continue | |
fi | |
echo -e " \e[1m\e[32mPushing repository to GitHub\e[0m" | |
GH_WEB_URL="$(jq -r .html_url $TMP/to-repo.json)" | |
GH_REPO_URL="$(jq -r .ssh_url $TMP/to-repo.json)" | |
git -C "$TMP/cloned-repo.git" push --mirror "$GH_REPO_URL" | |
if [ "x$HAS_WIKI" == "xtrue" ] | |
then | |
echo -e " \e[1m\e[32mCloning wiki repository from Bitbucket\e[0m" | |
rm -rf "$TMP/cloned-wiki-repo.git" | |
git clone --mirror "$CLONE_ARG/wiki" "$TMP/cloned-wiki-repo.git" | |
echo -e " \e[1m\e[32mRenaming files for new naming convention\e[0m" | |
rm -rf "$TMP/cloned-wiki-repo" | |
git clone "$TMP/cloned-wiki-repo.git" "$TMP/cloned-wiki-repo" | |
export TMP | |
find "$TMP/cloned-wiki-repo" -depth -name "*.wiki" -exec sh -c 'git -C "$TMP/cloned-wiki-repo" mv "$1" "${1%.wiki}.creole"' _ '{}' \; | |
git -C "$TMP/cloned-wiki-repo" commit -m 'Update naming scheme from Bitbucket to GitHub' | |
git -C "$TMP/cloned-wiki-repo" push | |
echo -e " \e[1m\e[35mAction requred:" | |
echo -e " \e[32mVisit \e[36mhttps://github.com/$GH_USERNAME/$SLUG/wiki/_new" | |
echo -en " \e[32mCreate a wiki page (contents irrelevant), then press ENTER to continue\e[0m" | |
read | |
echo -e " \e[1m\e[32mPushing wiki repository to GitHub\e[0m" | |
GH_WIKI_URL="${GH_REPO_URL%.git}.wiki.git" | |
git -C "$TMP/cloned-wiki-repo.git" push --mirror "$GH_WIKI_URL" | |
fi | |
if [ "$DELETE" == "yes" ] | |
then | |
echo -e " \e[1m\e[32mDeleting Bitbucket repository\e[0m" | |
$BB_CURL -X DELETE "$SELF?redirect_to=$GH_WEB_URL" > /dev/null | |
fi | |
echo -e " \e[1m\e[32mSuccessfully migrated \e[36m$SLUG\e[32m ✔\e[0m" | |
if [ $ONE_REPO == true ] | |
then | |
break | |
fi | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment