Skip to content

Instantly share code, notes, and snippets.

@maghoff
Last active April 25, 2021 18:36
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save maghoff/2b86e4ca3208a5ce443b35c5860c8a4d to your computer and use it in GitHub Desktop.
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
#!/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