【Git】リポジトリに接続できない状態で非リアルタイムに同期をとろうとした話（git format-patch）

# はじめに
[Git Advent Calendar 2023 4日目](https://qiita.com/advent-calendar/2023/git)の投稿です。よろしくおねがいします。
Gitではベアリポジトリと通信するために、いくつかのプロトコルをサポートしてます（[ここ](https://git-scm.com/book/ja/v2/Git%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC-%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB)）。
が、この前、↓の状況になりました。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526202/ca45d76c-ffcf-f97d-d76a-7f91e57c3714.png)

他社の環境（ここでは外とも言います）にベアリポジトリがあって自社（ここでは自分とも言います）からはリポジトリを操作、確認できない状況です。
この際に、他社のベアリポジトリで行われるコミットを追従して自社のリポジトリへ非リアルタイムで同期をとろうとして`git format-patch`を使おうとしたので備忘録的にメモしておきます。

無理が分かって結局やめてます（できないことはなさそうです）。

# やろうとしたこと概要
## 1.外のリポジトリをとりあえず再現する。
とりあえず全量リポジトリを送ってもらいます。この時点でダルい:innocent:

## 2.外で作業が行われる。
外でリポジトリに対して作業がしこしこ行われる。

## 3.外でパッチの生成をしてもらう。
`git format-patch`を使って指定された各コミットに対して1つずつパッチを生成する。

## 4.自分ちにパッチを取り込む。
`git am`でパッチを取り込む。


# 実際にやってみる
※読むのが面倒で結論を知りたい方は[問題点](#一応問題点を整理)まで飛んじゃっても読めると思います。


とりあえず外にあるベアリポジトリとして、以下のリポジトリを用意しました。
これが自分らの環境からは`git clone`も`git pull`もできないリポジトリとします。
```batch
~/Desktop/git/remote (master)
$ git log --oneline
30c185d (HEAD -> master) third commit
46072e4 second commit
c44c928 first commit
```
[1.](#1外のリポジトリをとりあえず再現する)に沿って、このリポジトリを物理コピーしてローカルに再現したとします。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git log --oneline
30c185d (HEAD -> master) third commit
46072e4 second commit
c44c928 first commit
```
ここで[2.](#2外で作業が行われる)として外のリポジトリで何かしら作業が行われたとします。`c8e94c6`のコミットを増やしました。
```batch
~/Desktop/git/remote (master)
$ git log --oneline
c8e94c6 (HEAD -> master) fourth commit
30c185d third commit
46072e4 second commit
c44c928 first commit
```
[3.](#3外でパッチの生成をしてもらう)として、外側のリポジトリでパッチを生成します。
パッチの生成は`git format-patch -n`です。
```batch
~/Desktop/git/remote (master)
$ git format-patch -1
0001-fourth-commit.patch
```
パッチファイルがリポジトリの直下にできました。
```batch
 ~/Desktop/git/remote (master)
$ ls
0001-fourth-commit.patch  sample.txt
```
[4.](#4自分ちにパッチを取り込む)として、このパッチを自分のリポジトリにあてます。
とりあえず自分のリポジトリの直下にファイルを持ってきました。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ ls
0001-fourth-commit.patch  sample.txt
```
パッチをあてるには`git am`です。

```batch
~/Desktop/git/自分側の環境/remote (master)
$ git am 0001-fourth-commit.patch
Applying: fourth commit
```
ログを見てみると自分のリポジトリが外のリポジトリと同期が取れたことが分かります。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git log --oneline
5501a99 (HEAD -> master) fourth commit
30c185d third commit
46072e4 second commit
c44c928 first commit
```

## 複数コミットがあったらどうなるか
今、外側のリポジトリで2つコミットを追加しました（`bd8b31a`と`e993c7d`）。

```batch
~/Desktop/git/remote (master)
$ git log --oneline
e993c7d (HEAD -> master) 6 commit
bd8b31a 5 commit
c8e94c6 fourth commit
30c185d third commit
46072e4 second commit
c44c928 first commit
```

外側のリポジトリでパッチを生成します。
```batch
~/Desktop/git/remote (master)
$ git format-patch -2
0001-5-commit.patch
0002-6-commit.patch
```
これを自分のリポジトリに持ってきます。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ ls
0001-5-commit.patch  0002-6-commit.patch  sample.txt
```
もってきたパッチを自分のリポジトリに取り込むために`git am`してみます。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git am *
Applying: 5 commit
Applying: 6 commit
Patch is empty.
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".

~/Desktop/git/自分側の環境/remote (master|AM 3/3)
$ git am --skip
```
`git log`して再現できたかどうか確認してみます。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git log --oneline
cf9face (HEAD -> master) 6 commit
d485670 5 commit
5501a99 fourth commit
30c185d third commit
46072e4 second commit
c44c928 first commit
```
コミットメッセージが同じなので無事再現できることを確認できました。
ブランチが1本の時はコミットが複数でも問題なく再現できそうです。

## コミットツリーが複雑だとどうなるか
もう少しコミットツリーを複雑にしてみました。
```batch
~/Desktop/git/remote (master)
$ git log --oneline --graph --all
* 8c6827d (HEAD -> master) master3 commit
*   6a7990b Merge branch 'another'
|\
| * 7c84e0a (another) another2 commit
| * 56f65c3 another1 commit
* | 5f935ea master2 commit
* | 56dae81 master1 commit
|/
* e993c7d 6 commit
* bd8b31a 5 commit
* c8e94c6 fourth commit
* 30c185d third commit
* 46072e4 second commit
* c44c928 first commit
```
今、自分のリポジトリでは`e993c7d`まで再現できている状態です。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git log --oneline --graph --all
* cf9face (HEAD -> master) 6 commit
* d485670 5 commit
* 5501a99 fourth commit
* 30c185d third commit
* 46072e4 second commit
* c44c928 first commit
```
とりあえず`e993c7d`以降のパッチを生成してみます。
```batch
~/Desktop/git/remote (master)
$ git format-patch master~4
0001-another1-commit.patch
0002-another2-commit.patch
0003-master1-commit.patch
0004-master2-commit.patch
0005-master3-commit.patch
```
パッチを自分のリポジトリに持ってきました。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ ls
0001-another1-commit.patch  0003-master1-commit.patch  0005-master3-commit.patch
0002-another2-commit.patch  0004-master2-commit.patch  sample.txt
```
このパッチをよしなに適用してくれるのか、`git am`で自分のリポジトリに適用してみます。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git am *
error: patch failed: sample.txt:1
error: sample.txt: patch does not apply
hint: Use 'git am --show-current-patch' to see the failed patch
Applying: another1 commit
Applying: another2 commit
Applying: master1 commit
Patch failed at 0003 master1 commit
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".
```
エラーがおこりました。
↓ログを見てみると、どうやら自動的に`another`ブランチは作ってくれず、`master`ブランチに`another`ブランチで行ったコミットを適用しているようです。これは外側のリポジトリの状況と違いますね。
```batch
~/Desktop/git/自分側の環境/remote (master|AM 3/6)
$ git log --oneline --graph --all
* 572a316 (HEAD -> master) another2 commit
* bc8c0e6 another1 commit
* cf9face 6 commit
* d485670 5 commit
* 5501a99 fourth commit
* 30c185d third commit
* 46072e4 second commit
* c44c928 first commit
```
もう少し調査してみます。
いま`master`ブランチに`another`ブランチのコミットを付け足してますね。
間違ってくっつけた最新コミットの↓`572a316`のファイルの状況です。「hogeanother2」と書いてあります。
```batch
~/Desktop/git/自分側の環境/remote (master|AM 3/6)
$ cat sample.txt
hogeanother2
```
適用しようとしているパッチファイルの中身をのぞいてみます。
適用しようとしているパッチの情報は「.git/rebase-apply/patch」ファイルにあります。
```batch
~/Desktop/git/自分側の環境/remote (master|AM 3/6)
$ cat .git/rebase-apply/patch
---
 sample.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sample.txt b/sample.txt
index 21f1c7e..862d3a0 100644
--- a/sample.txt
+++ b/sample.txt
@@ -1 +1 @@
-hogehoge6
+hogemaster1
--
```
パッチで適用しようとしているのは「hogehoge6」と書いてあるファイルに対して「hogemaster1」へ修正する内容なので、エラーが起こっています。
パッチの生成元のブランチの特性を知ることなしにパッチを適用することは難し（不可能？）そうです。

なんか手間がかかりそうですが、なんとか適用したいです。

そもそも今の状況だと適用が失敗しているので、とりあえずもとに戻します。
```batch
~/Desktop/git/自分側の環境/remote (master|AM 3/6)
$ git am --abort

~/Desktop/git/自分側の環境/remote (master)
$ git log --oneline --graph
* cf9face (HEAD -> master) 6 commit
* d485670 5 commit
* 5501a99 fourth commit
* 30c185d third commit
* 46072e4 second commit
* c44c928 first commit
```
再現したいコミットツリーはこうでした。
```batch
 ~/Desktop/git/remote (master)
$ git log --oneline --graph --all
* 8c6827d (HEAD -> master) master3 commit
*   6a7990b Merge branch 'another'
|\
| * 7c84e0a (another) another2 commit
| * 56f65c3 another1 commit
* | 5f935ea master2 commit
* | 56dae81 master1 commit
|/
* e993c7d 6 commit
...
```
それぞれのパッチの内容はこうです。
- 0001-another1-commit.patch：`e993c7d`と`56f65c3`の差分
- 0002-another2-commit.patch：`56f65c3`と`7c84e0a`の差分
- 0003-master1-commit.patch：`e993c7d`と`56dae81`の差分
- 0004-master2-commit.patch：`56dae81`と`5f935ea`の差分
- 0005-master3-commit.patch：`5f935ea`と`8c6827d`の差分
※パッチにはマージコミットに関する情報がないですね

まずはマージ前の`master`ブランチの内容を適用していきましょう。
```batch
 ~/Desktop/git/自分側の環境/remote (master)
$ git am 0003-master1-commit.patch
Applying: master1 commit

~/Desktop/git/自分側の環境/remote (master)
$ git am 0004-master2-commit.patch
Applying: master2 commit

~/Desktop/git/自分側の環境/remote (master)
$ git log --oneline
46259c7 (HEAD -> master) master2 commit
d560904 master1 commit
cf9face 6 commit
d485670 5 commit
5501a99 fourth commit
30c185d third commit
46072e4 second commit
c44c928 first commit
```
`git format-patch`のパッチファイルではブランチを再現できないので、ブランチを作成します。
外のリポジトリでは`e993c7d`（自分のリポジトリだと`cf9face`）から`another`ブランチが派生してるので、対応する自分のリポジトリのコミットからブランチを作ります。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git checkout -b another cf9face
Switched to a new branch 'another'
```

次に、自分側で作った`another`ブランチに対して、外側のリポジトリの`another`ブランチのパッチを適用していきます。
```batch
~/Desktop/git/自分側の環境/remote (another)
$ git am 0001-another1-commit.patch
Applying: another1 commit

~/Desktop/git/自分側の環境/remote (another)
$ git am 0002-another2-commit.patch
Applying: another2 commit
```
ここまで再現できました。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git log --oneline --graph --all
* 01c0879 (another) another2 commit
* 3a6d509 another1 commit
| * 46259c7 (HEAD -> master) master2 commit
| * d560904 master1 commit
|/
* cf9face 6 commit
* d485670 5 commit
* 5501a99 fourth commit
* 30c185d third commit
* 46072e4 second commit
* c44c928 first commit
```
外のリポジトリでは`another`と`master`をマージしているので、自分の方でもマージしてあげます。
マージがどうなったのか確認するために、パッチファイルを確認します。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ cat 0005-master3-commit.patch
From 8c6827dd19e21b57657676fe92cd1a64cd7d4c26 Mon Sep 17 00:00:00 2001
From: hogehoge <fugafuga>
Date: Tue, 14 Nov 2023 09:36:10 +0900
Subject: [PATCH 5/5] master3 commit

---
 sample.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sample.txt b/sample.txt
index cdfa144..352dcc4 100644
--- a/sample.txt
+++ b/sample.txt
@@ -1 +1 @@
-hogemaster2
+hogemaster3
--
```
マージ後は「hogemaster2」になってますね。
手動でマージして外のリポジトリと同じように修正して競合を解消してあげます。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git merge another --no-ff
Auto-merging sample.txt
CONFLICT (content): Merge conflict in sample.txt
Automatic merge failed; fix conflicts and then commit the result.

~/Desktop/git/自分側の環境/remote (master|MERGING)
$ cat sample.txt
<<<<<<< HEAD
hogemaster2
=======
hogeanother2
>>>>>>> another

~/Desktop/git/自分側の環境/remote (master|MERGING)
$ echo "hogemaster2" > sample.txt

~/Desktop/git/自分側の環境/remote (master|MERGING)
$ git add sample.txt

 ~/Desktop/git/自分側の環境/remote (master|MERGING)
$ git commit
[master 629622a] Merge branch 'another'
```
最後にマージコミットの後のパッチをあててあげます。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git am 0005-master3-commit.patch
Applying: master3 commit
```

再現できました。
```batch
~/Desktop/git/自分側の環境/remote (master)
$ git log --oneline --graph --all
*   23148e2 (HEAD -> master) master3 commit
*   629622a Merge branch 'another'
|\
| * 01c0879 (another) another2 commit
| * 3a6d509 another1 commit
* | 46259c7 master2 commit
* | d560904 master1 commit
|/
* cf9face 6 commit
* d485670 5 commit
* 5501a99 fourth commit
* 30c185d third commit
* 46072e4 second commit
* c44c928 first commit
```

ブランチが複雑な場合のやり方をまとめると、
1. とりあえずブランチ全部のログと自分のリポジトリにないパッチをもらう
1. 自分のリポジトリに足りないブランチを手動で作成する
1. パッチファイルの中身とログからどこのコミットに何のパッチをあてるか整理する
1. 各ブランチにパッチをあてる
1. 外のリポジトリでマージされてるときは手動でマージして競合があれば解決してあげる

正直やってられるか、って感じです。

## 一応問題点を整理

- ブランチ1本であれば問題なく再現できますが、ブランチが複数できた際にパッチではブランチを作成してくれないので自分で外側のリポジトリの状況を確認してブランチを作成したり競合解決するのがめちゃくちゃ手間です。
- あと、実際に使用する際は、外のリモートリポジトリ保持者に`git format-patch コミット範囲`でパッチを取得してもらって自分に送ってもらう必要があるのでそこもめんどくさいです。
- もはや`git format-patch`が今回の用途に合ってないことが分かった後に言うのもアレですが、[1.](#1外のリポジトリをとりあえず再現する)の手順でリポジトリを再現する方法として物理コピーしてますが、プロジェクトが大きくなった後にやろうとすると、コピーもめんどくさいです。

上のような問題点があり結局`git format-patch`を使いませんでした。
そもそもコマンドの目的が違うんでしょうね。勉強になりましたとさ。

- ドキュメント

https://git-scm.com/docs/git-format-patch