l0w.dev

Git フォークの完全な同期


GitHub などにある Git リポジトリをフォークしたとき、 アップストリームでフォーク後に発生したブランチ・タグの追加や削除も含むすべての更新を、 フォーク先に同期する方法に悩んだことはないでしょうか。

このトピックでウェブを検索して見つかるのは、 GitHub の Syncing a fork の記事のようにコマンドラインで単一ブランチをフェッチ・マージ・プッシュして同期させる方法がほとんどです。 しかしながら、アップストリームの開発が活発で多くのブランチ・タグが作られたり消されたりする場合、 手作業ですべての変更を同期することは現実的ではありません。

最近 GitHub にある Redmine のように多数のブランチ・タグがあるプロジェクトをセルフホストな GitLab にフォークして独自機能をメンテナンスしたり CI でコンテナ化したりすることが増え、 この問題に悩むことが多くなりましたので、汎用的に使えるスクリプトを書いてみました。

スクリプト

次のシェルスクリプトは Linux や macOS、Windows の WSL や Git Bash などで動きます。 一部で Git 2.10 以降の機能を使っています (後述)。

#!/bin/sh

set -e

REMOTE_SRC=git@github.com:redmine/redmine
REMOTE_DST=git@gitlab.com:yaegashi/redmine
PREFIX=fork/
GIT_DIR=
PUSH_OPTIONS="--prune --push-option=ci.skip"

# Initialize bare repository in temporary $GIT_DIR
if test -z "$GIT_DIR"; then
        GIT_DIR=$(mktemp -d)
        trap "rm -rf $GIT_DIR" EXIT
fi

export GIT_DIR

git init --bare

# Fetch all branches/tags from src repository
git fetch $REMOTE_SRC +refs/heads/*:refs/src/heads/* --prune --no-tags
git fetch $REMOTE_SRC +refs/tags/*:refs/src/tags/* --prune --no-tags

# Fetch $PREFIX-ed branches/tags from dst to local src
git fetch $REMOTE_DST +refs/heads/${PREFIX}*:refs/src/heads/${PREFIX}* --no-tags
git fetch $REMOTE_DST +refs/tags/${PREFIX}*:refs/src/tags/${PREFIX}* --no-tags

# Push all branches/tags from local src to dst repository
git push $REMOTE_DST +refs/src/heads/*:refs/heads/* $PUSH_OPTIONS
git push $REMOTE_DST +refs/src/tags/*:refs/tags/* $PUSH_OPTIONS

最初にある次の変数によりスクリプトの動作を設定します。

変数説明
REMOTE_SRC同期元リポジトリのURL
REMOTE_DST同期先リポジトリのURL
PREFIX同期先で保持するブランチ・タグ名のプリフィクス
GIT_DIRローカルで使う Git リポジトリの場所
空の場合はテンポラリフォルダを作成して使います
PUSH_OPTIONSgit push に指定するオプション (後述)

このスクリプトは同期先で作られたブランチ・タグのうち $PREFIX が名前の先頭についたものは消さずに残します。 これにより、同期先でカスタマイズやプルリクエストのためのブランチを作成するときは fork/patch-1 のような名前を使用するといった取り決めの運用ができます。

$GIT_DIR は固定の場所に設定しておけば、繰り返し実行したときに無駄なダウンロードを避けることができます。

スクリプトの説明と注意事項

このスクリプトは残すべきブランチ・タグをすべてローカルにフェッチしてから、 最後のプッシュで --prune をつけて一括で反映しています。 この方法は最も効率的で、無駄なプッシュのイベントを発生させません。

ただし、めったにないことだとは思いますが、フェッチの段階でなにかバグや不具合が発生すると、 同期先にしかない $PREFIX つきブランチ・タグを消してしまう可能性があります。 心配な場合は $PUSH_OPTIONS から --prune を外したほうがよいかもしれません。 そうすると同期先のブランチ・タグが削除されることはなくなりますが、 同期元の削除は反映されず、増える一方となります。

--push-option=ci.skip は Git 2.10 で追加されたプッシュオプションの指定です。 Git 2.10 より前の場合は動きませんので $PUSH_OPTIONS から --push-option=ci.skip を削除してください。

GitLab にはプッシュオプションに ci.skip を指定すると CI の実行をスキップする機能がありますので、 同期元が他人のリポジトリでブランチに .gitlab-ci.yml が含まれている場合でも、安心して同期ができます。 もちろん安全なリポジトリを同期しており CI をやりたい場合は --push-option=ci.skip を削除してもかまいません。

現時点では避けることが難しい問題として、フェッチからプッシュまでの動作がアトミックでないため、 同時に作業中のユーザーによる $PREFIX つきブランチ・タグへの更新が失われることがあります。 昼間にこのスクリプトを頻繁に実行することは避けたほうがよいでしょう。

GitLab CI による定期実行

同期先が GitLab のリポジトリの場合 GitLab CI で前述のようなスクリプトを定期実行すると便利です。 そのための GitLab CI ブランチのサンプルを https://gitlab.com/yaegashi/gitlab-fork-sync に作りました。 以下、セットアップの手順を簡単に説明します。

まず同期先リポジトリを用意します。 これは GitLab の新規プロジェクト作成で同期元からの Import を選んでもよいですし、 空のリポジトリから始めてもかまいません。

次に gitlab-fork-syncfork/sync ブランチを同期先のリポジトリにコピーします。

git clone https://gitlab.com/yaegashi/gitlab-fork-sync
cd gitlab-fork-sync
git remote add dst git@gitlab.com:yaegashi/redmine.git
git push dst fork/sync

次にリポジトリのアクセスに使う SSH 鍵を作ります。

ssh-keygen -N '' -f ssh-key

このコマンドで ssh-key (秘密鍵) と ssh-key.pub (公開鍵) の 2つのテキストファイルができます。

同期先のリポジトリの deploy key に ssh-key.pub の中身を設定します。 その際にリポジトリ書き込み権限を与える必要があります (write access allowed)。

続いて同期先リポジトリで CI/CD schedule を作成します。

Scheduled pipeline ができたら、マニュアル実行して動作確認してみてください。

https://gitlab.com/yaegashi/redmine/pipelines より Redmine を使った実行例を見ることが出来ます。

なお GitLab CI で使用しているコンテナ registry.gitlab.com/yaegashi/gitlab-fork-syncfork/docker ブランチ で作成しています。 Alpine Linux で Git を使えるようにしただけのものです。

GitLab の有償版機能

実は GitLab には組み込みで リモートリポジトリをミラーする機能リモートリポジトリ用の GitLab CI といった機能があります。ただしこれらは有償版でしか使えない機能です。

gitlab-fork-sync を使うよりもセットアップが簡単かつ高機能なので、 利用可能であればこちらを利用するのがよいと思います。 なお https://gitlab.com では 2019/09/22 までこの機能は無料で使えるとのことです。