Gitリポジトリをミラーリングする

2つの Git リポジトリ間で、コードと Wiki をミラーリングする方法を紹介します。GitLab を使って動作確認をしていますが、GitHub でも同様だろうと思います。

また、例えば GitLab には、プロジェクトを新規作成するときに別の Git リポジトリから [インポートする機能](https://docs.gitlab.com/ee/user/project/import/) や、ずばり [ミラーリングする機能](https://docs.gitlab.com/ee/user/project/repository/mirror/) などがデフォルトで存在します。ここでは、ネットワークの問題や利用エディションの問題などで、これらの機能ではなく git コマンドだけで実現したいケースを想定しています。

### 確認環境

* Git 2.30.0
* GitLab 14.9.0-pre

### リポジトリ構成

* ミラーリング元のリポジトリ
    * `https://gitlab.com/oohira/awesome-source.git`
* ミラーリング先のリポジトリ
    * GitLab で空のプロジェクトを作成しておく
    * `https://gitlab.com/oohira/awesome-mirror.git`

:::note info
push 前に確認する
作業の性質上、間違っていないか不安になるので、git remote -v や git push --dry-run などを使って、push しようとしているミラーリング先リポジトリに間違いがないか、事前によく確認するとよいでしょう。
:::

### コードをミラーリングする方法1

後述しますが、GitLab の Merge Request や GitHub の Pull Request がない場合は、この方法が簡単です。

```shell
## 1. ミラーリング元リポジトリをローカルに取得
$ git clone --mirror https://gitlab.com/oohira/awesome-source.git awesome-mirror
$ cd awesome-mirror

## 2. push先だけミラーリング先リポジトリに差し替えて確認
$ git remote set-url --push origin https://gitlab.com/oohira/awesome-mirror.git
$ git remote -v
origin  https://gitlab.com/oohira/awesome-source.git (fetch)
origin  https://gitlab.com/oohira/awesome-mirror.git (push)

## 3. ミラーリング元リポジトリの変更をローカルに取得（初回は不要）
$ git remote update

## 4. ローカルをミラーリング先リポジトリにpush
$ git push --mirror
...
To https://gitlab.com/oohira/awesome-mirror.git
 * [new branch]      dev -> dev
 * [new branch]      main -> main
 * [new tag]         v1.0 -> v1.0
 * [new tag]         v2.0 -> v2.0
```

:::note warn
GitLab CI の注意
.gitlab-ci.yml が含まれていると、ミラーリング先でも意図せず GitLab CI がキックされてしまう可能性があります。ミラーリング先では CI/CD など使用予定のない機能は無効にしておくとよいでしょう。
:::

:::note warn
保護ブランチ（タグ）の注意
GitLab や GitHub には、特定のブランチやタグを保護する機能があります（protected branches/tags）。ミラーリング先のリポジトリでこの設定が有効になっている場合、git push で上書きしようとしたタイミングで失敗する可能性があるので、その場合は保護設定を見直してください。
:::

:::note warn
bare リポジトリの注意
--mirror オプションの指定によりワークツリーをもたない bare リポジトリが作成されるため、このディレクトリ内では git status など一部の Git コマンドが使えません。そのため、シェルのプロンプトに Git の情報を表示するようにしている場合は、何かコマンドを実行するたびに裏で git コマンドが実行され、`fatal: this operation must be run in a work tree` のメッセージが表示されることがあります。私の環境でも起きましたが、いったん無視してコマンド例からも除外しています。
:::

Merge Request や Pull Request がある場合、GitLab や GitHub が `refs/merge-requests/1` のような ref を追加で作っているようで、これを push しようとすると失敗します。無視して放っておいても実害はないのですが、気になる場合は「コードをミラーリングする方法2」をお試しください。

```shell
 ! [remote rejected] refs/merge-requests/1/head -> refs/merge-requests/1/head (deny updating a hidden ref)
 ! [remote rejected] refs/merge-requests/1/merge -> refs/merge-requests/1/merge (deny updating a hidden ref)
 ! [remote rejected] refs/merge-requests/14/head -> refs/merge-requests/14/head (deny updating a hidden ref)
 ! [remote rejected] refs/merge-requests/14/merge -> refs/merge-requests/14/merge (deny updating a hidden ref)
...
error: failed to push some refs to 'https://gitlab.com/oohira/awesome-mirror.git'
```

### コードをミラーリングする方法2

GitLab の Merge Request や GitHub の Pull Request がある環境下でも、不要な警告を出さずにミラーリングする方法です。初回 fetch する前に fetch 対象を調整する必要があるので、手動でリポジトリをセットアップする必要があり、若干面倒です。

```diff_shell
## 1. 空のリポジトリを作成（git clone --mirror は使わない）
$ git init --bare awesome-mirror
$ cd awesome-mirror/

## 2. fetch元にミラーリング元リポジトリを登録し、fetch対象をブランチ＆タグに限定
$ git remote add --mirror=fetch origin https://gitlab.com/oohira/awesome-source.git
$ vi config
 ...
 [remote "origin"]
         url = https://gitlab.com/oohira/awesome-source.git
-        fetch = +refs/*:refs/*
+        fetch = +refs/heads/*:refs/heads/*
+        fetch = +refs/tags/*:refs/tags/*

## 3. push先にミラーリング先リポジトリを登録して確認
$ git remote set-url --push origin https://gitlab.com/oohira/awesome-mirror.git
$ git remote -v
origin  https://gitlab.com/oohira/awesome-source.git (fetch)
origin  https://gitlab.com/oohira/awesome-mirror.git (push)

## 4. ミラーリング元リポジトリの変更をローカルに取得
$ git remote update

## 5. ローカルをミラーリング先リポジトリにpush
$ git push --mirror
...
To https://gitlab.com/oohira/awesome-mirror.git
 * [new branch]      dev -> dev
 * [new branch]      main -> main
 * [new tag]         v1.0 -> v1.0
 * [new tag]         v2.0 -> v2.0
```

### Wiki をミラーリングする方法

GitLab や GitHub では、Wiki も専用の Git リポジトリ（`〜.wiki.git`）で管理されているので、「コードをミラーリングする方法1」と同じ手法で Wiki のミラーリングを実現できます。リポジトリの URL を Wiki 用に変更する点だけ注意してください。

```shell
## 1. ミラーリング元リポジトリをローカルに取得
$ git clone --mirror https://gitlab.com/oohira/awesome-source.wiki.git awesome-mirror.wiki
$ cd awesome-mirror.wiki/

## 2. push先だけミラーリング先リポジトリに差し替えて確認
$ git remote set-url --push origin https://gitlab.com/oohira/awesome-mirror.wiki.git
$ git remote -v
origin  https://gitlab.com/oohira/awesome-source.wiki.git (fetch)
origin  https://gitlab.com/oohira/awesome-mirror.wiki.git (push)

## 3. ミラーリング元リポジトリの変更をローカルに取得（初回は不要）
$ git remote update

## 4. ローカルをミラーリング先リポジトリにpush
$ git push --mirror
```

:::note warn
Wiki リンクの注意
Wiki ページ間のリンクに Wiki 記法を使わず、絶対 URL で記載していると、ミラーリング先リポジトリ内でのリンクにならず、ミラーリング元と同じ URL へのリンクになってしまいます。ミラーリング先リポジトリ内で完結してリンクを機能させるためには、Wiki 記法を使った相対リンクでの記述を徹底しましょう。
:::

### 補足

* ミラーリング先リポジトリでの変更の扱い
    * やらないと思いますが、ミラーリング先のリポジトリで独自のコミットをしても、ミラーリング元の変更を再度 push するタイミングで上書きされます。ただし、force push されるので、保護ブランチが無効になっている必要があります
* 間違ってミラーリング元に push した場合の扱い
    * 万が一、ミラーリング元のコミット履歴の方が進んでいる状態で、ローカルを誤って（ミラーリング先ではなく）ミラーリング元に push すると、ミラーリング元のコミット履歴が上書きされて先祖返りします。恐ろしいので、ここで紹介した方法は push 先が常にミラーリング先を向くようにしています（＝ミラーリング元とミラーリング先で remote を分けるのではなく、1つの remote の fetch/push で分ける）
* 細かいことを気にせずもっと簡単にミラーリングする方法
    * git clone した後で origin とは別の remote を1つ登録して push すれば、ほぼ近しいことはできます。main ブランチだけミラーリングできればよい場合などは、これで十分かもしれません