GitLab から GitHub にブランチをミラーリングするワンタイムスクリプト

GitLab から GitHub にブランチをミラーリングするワンタイムスクリプト

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

GitLab

Mirror all branches を選択した場合、すべてのブランチがミラーリングされます。このとき GitLab と GitHub の内容を完全に一致させるようにミラーリングします。具体的には、

  1. GitLab に my-branch があり、GitHub にはないとき:GitHub に my-branch が作成されます。
  2. GitLab に my-branch があり、GitHub にもあり、GitHub のコミットが GitLab よりも進んでいるとき:GitLab の内容で GitHub のブランチが上書きされます。
  3. 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 "$@"