GitLab には備え付けのミラーリング機能があります。これを使えば GitLab から GitHub へ簡単にミラーリングできます。その際には対象のブランチを選択します。

Mirror all branches を選択した場合、すべてのブランチがミラーリングされます。このとき GitLab と GitHub の内容を完全に一致させるようにミラーリングします。具体的には、
- GitLab に my-branch があり、GitHub にはないとき:GitHub に my-branch が作成されます。
- GitLab に my-branch があり、GitHub にもあり、GitHub のコミットが GitLab よりも進んでいるとき:GitLab の内容で GitHub のブランチが上書きされます。
- GitLab に my-branch はなく、GitHub にはあるとき:GitHub のブランチは削除されます。
ときに2番目、3番目の動作は避けたい場面もあるはずです。要するに、
- 同名ブランチがあった場合は GitHub を優先したい
- GitHub にのみ存在するブランチはそのまま残したい
このような要件を満たしたい場合は、自作のスクリプトを作成してミラーリングしましょう。
ということでつくりました。
#!/usr/bin/env bash
set -euo pipefail
# How to use:
# 1. Create a new file `touch main.sh`.
# 2. Copy and paste this code to `main.sh`.
# 3. Make a file executable `chmod +x ./main.sh`
# 4. Run `./main.sh`
#
# Your access token requires following scopes:
# - GitLab: "read_api" scope
# - GitHub: "repo", "workflow" scope
# GitLab
GITLAB_HOST="gitlab.com"
GITLAB_REPOSITORY="my-group/my-project"
GITLAB_URL="https://${GITLAB_HOST}/${GITLAB_REPOSITORY}.git"
# GitHub
GITHUB_HOST="github.com"
GITHUB_REPOSITORY="my-owner/my-repository"
GITHUB_URL="https://${GITHUB_HOST}/${GITHUB_REPOSITORY}.git"
# Working directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BARE_REPO_DIR="${SCRIPT_DIR}/bare_repo"
# Period of GC
GARBAGE_COLLECTION_PERIOD="100"
require_command_exists() {
local command
for command in "$@"; do
if ! command -v "${command}" >/dev/null 2>&1; then
echo "ERROR: '${command}' is required but not found in PATH" >&2
exit 1
fi
done
}
list_remote_branches() {
local url="$1"
git ls-remote --heads "${url}" | awk '{print $2}' | sed 's#^refs/heads/##'
}
main() {
require_command_exists git sed awk sort comm wc tr
if [[ ! -d "${BARE_REPO_DIR}" ]]; then
echo "Creating bare repo directory: ${BARE_REPO_DIR}"
mkdir -p "${BARE_REPO_DIR}"
fi
if [[ ! -f "${BARE_REPO_DIR}/HEAD" ]]; then
echo "Initializing bare git repository in ${BARE_REPO_DIR}"
git init --bare "${BARE_REPO_DIR}" >/dev/null
fi
cd "${BARE_REPO_DIR}"
local git_remote_name_gitlab="gitlab"
local git_remote_name_github="github"
if git remote get-url "${git_remote_name_gitlab}" >/dev/null 2>&1; then
git remote set-url "${git_remote_name_gitlab}" "${GITLAB_URL}"
else
git remote add "${git_remote_name_gitlab}" "${GITLAB_URL}"
fi
if git remote get-url "${git_remote_name_github}" >/dev/null 2>&1; then
git remote set-url "${git_remote_name_github}" "${GITHUB_URL}"
else
git remote add "${git_remote_name_github}" "${GITHUB_URL}"
fi
local gitlab_file="${SCRIPT_DIR}/gitlab-branches"
local github_file="${SCRIPT_DIR}/github-branches"
rm -f "${gitlab_file}" "${github_file}"
list_remote_branches "${GITLAB_URL}" | sort -u > "${gitlab_file}"
list_remote_branches "${GITHUB_URL}" | sort -u > "${github_file}"
echo
echo "==== Branches ===="
echo "GitLab URL: ${GITLAB_URL}"
echo "GitHub URL: ${GITHUB_URL}"
echo
echo -n "All branches on GitLab : "; wc -l < "${gitlab_file}" | tr -d ' '
echo -n "All branches on GitHub : "; wc -l < "${github_file}" | tr -d ' '
echo -n "Branches only on GitLab: "; comm -23 "${gitlab_file}" "${github_file}" | wc -l | tr -d ' '
echo -n "Branches only on GitHub: "; comm -13 "${gitlab_file}" "${github_file}" | wc -l | tr -d ' '
local mirrored_branch_count=0
while IFS= read -r branch; do
echo "==> ${branch}"
# 1) Fetch full history for the branch from GitLab
git fetch --no-tags "${git_remote_name_gitlab}" "refs/heads/${branch}:refs/heads/${branch}"
# 2) Push the branch to GitHub
git push --no-verify "${git_remote_name_github}" "refs/heads/${branch}:refs/heads/${branch}"
# 3) Delete local ref to keep repo small
git update-ref -d "refs/heads/${branch}"
mirrored_branch_count=$((mirrored_branch_count + 1))
# 4) run GC to clean up git objects
if (( GARBAGE_COLLECTION_PERIOD > 0 && mirrored_branch_count % GARBAGE_COLLECTION_PERIOD == 0 )); then
echo "Run GC"
git reflog expire --expire=now --all
git gc --prune=now
fi
done < <(comm -23 "${gitlab_file}" "${github_file}") # Only on GitLab branches
echo
echo "==== SUCCESS ===="
echo "Mirrored branch count: ${mirrored_branch_count}"
}
main "$@"