🌳

ZIP ファイル用の Git カスタムマージドライバを作ってみた

2024/04/05に公開

1. はじめに

世の中にはソースコード以外にも Git で管理できたらうれしいものがあります:

  • 画像ファイル
  • Microfot Word のファイル
  • Microsoft PowerPoint のファイル
  • Adobe のファイル
  • Affinity のファイル

しかしこれらはバイナリファイルなので,通常の Git ではうまくマージすることができません.マージできないのに Git で管理しようとすると悲惨な結果になります(経験談).

そういうわけで,バイナリファイルのマージ方法ってカスタムできないのかな?と調べてみました.すると,どうやらカスタムマージドライバというものが設定できるらしいということがわかりました.そこで,試しに ZIP ファイル用のカスタムマージドライバを作ってみることにしました.

https://www.pc-koubou.jp/magazine/29944

2. 方法

カスタムマージドライバを使うにはいろいろやることがあります:

  • Git リポジトリにカスタムマージドライバを設定する
  • カスタムマージドライバを実装する
  • .gitattributes で適用ファイルを指定する

なんでこんなにたくさん設定することがあるのかと思われるかもしれません.しかし,ひとつひとつの内容を見ていくと,どれも必要な設定であることがわかるはずです.

2.1. Git リポジトリへの設定

Git リポジトリへの設定は以下のようなコマンドで行います:

git config merge.zipmerge.name "Custom merge driver for zip files"
git config merge.zipmerge.driver "/workspaces/zip-merge-driver/src/main.sh %O %A %B %P"

zipmerge の部分に好きな識別名を入れて,namedriver を設定します.name はこの先とくに登場する機会もないので,たぶん適当に決めちゃえばいいです.driver には実行したいコマンドを設定すればよいみたいです.これがまさにカスタムマージドライバの本体ということになります.

2.2. カスタムマージドライバの実装

driver の引数の %O などは言わば Git の API のようなもので,以下のような意味らしいです:

  • %O : ブランチが分岐したときの状態のファイル名
  • %A : 現在のブランチのファイル名
  • %B : マージ対象のブランチのファイル名
  • %P : マージ結果のファイル名

言われてみれば大したことはない仕組みなのですが,%O, %A, %B の部分にマージするファイルのファイル名が渡されて,これ実際にリポジトリのルート直下に生成されます.ファイルのパスは path/to/repo/%O というような形になります.これらのファイルを参照してマージした結果を path/to/repo/%P に書き出すソースコードを作れば,Git がそれをマージ結果として処理してくれるというカラクリらしいです.

結局のところ %O, %A, %B を受け取り %P を返すプログラムを作ればいいということです.そして,この条件さえ満たせば実装はなんでも OK ということですね.

2.3. .gitattributes の設定

つぎに,.gitattributes で適用ファイルを指定します.今回は ZIP ファイル用のカスタムマージドライバですので,次のように記述します:

*.zip merge=zipmerge

zipmerge の部分は Git リポジトリに設定した識別名を入れればよいです.これにより,ファイルとマージドライバを紐づけます.

2.4. 処理の流れ

まとめると,カスタムマージドライバが利用されるまでの具体的な流れは次のようになります(想像を含みます):

  1. path/to/repo/%O, path/to/repo/%A, path/to/repo/%B にそれぞれマージするファイルを配置する
  2. .gitattributes を参照しファイルに対応するマージドライバを選択する
    • 特に指定がなければ text または binary で処理する
  3. マージドライバの識別名と対応するリポジトリで設定されたコマンド math/to/merge-driver %O %A %B %P を実行する
  4. path/to/repo/%P をマージ結果とする

.gitattributes で指定するのはあくまでも識別名だけで,カスタムマージドライバのパスをここで指定することは出来ないというのが,ひとつ複雑さを増している要因なのかなとは思います.

3. 実装

さてさて,仕組みが分かったところで,カスタムマージドライバを作ります.今回は ShellScript を使用しました.ここまで聞けば,カスタムマージドライバが ShellScript でも書けるというのも頷けるのかなと思います.

今回作るのは ZIP ファイル用のカスタムマージドライバです.ZIP ファイルは Git では画像ファイルと同じようにバイナリとして処理されます.そのため,コンフリクトした場合にはどちらかのブランチのものを採用する二者択一になってしまいます.そうではなく,ZIP ファイルの中身で差分をとってマージすると面白いのではないか.ということで,ZIP ファイルを展開しその中身をマージし再び圧縮したものをマージ結果とするカスタムマージドライバを作ることにしました.

作成したソースコードはこちら:

https://github.com/fjktkm/zip-merge-driver/blob/main/src/main.sh

具体的な処理の流れを簡単に説明すると以下のとおりです:

  • path/to/repo/%O に配置された ZIP ファイルを展開し Git リポジトリとして初期化
  • path/to/repo/%A に配置された ZIP ファイルを展開し local ブランチにコミット
  • path/to/repo/%B に配置された ZIP ファイルを展開し main ブランチにコミット
  • local ブランチを main ブランチにマージ
  • マージ結果を ZIP ファイルに圧縮し path/to/repo/%P に保存

Git の処理の中で Git を使うのはなかなかおもしろいんじゃないかと思います.なお,コンフリクトした場合の対処については,ここではひとまず考慮しないことにしました.

4. 実行例

実際にカスタムマージドライバが上手く動作することを確認するため,テスト用のスクリプトを作成しました:

https://github.com/fjktkm/zip-merge-driver/blob/main/src/test.sh

わかりにくくて申し訳ないのですが,ひとつのテキストファイルを圧縮した ZIP ファイルで実際にマージしてみて結果を確認するというスクリプトになっています.

テスト用のテキストファイルの中身は次のようになっています:

testfile.txt (%O)
This is the base file.
testfile.txt (%A)
This is the base file.
This modification is from the local branch.
testfile.txt (%B)
This modification is from the remote branch.
This is the base file.

これらのテキストファイルを,テキストファイルとしてマージすれば次のようになります:

testfile.txt (%P)
This modification is from the remote branch.
This is the base file.
This modification is from the local branch.

しかし今回はこれを ZIP ファイルにしたうえでマージします.通常ならバイナリファイルとして扱われるためコンフリクトしてしまいますが,今回はカスタムマージドライバを使用します.カスタムマージドライバでマージした ZIP ファイルにおいて,中身のテキストファイルがこれと同じ結果になっていれば成功です.

実行結果は次のとおりです:

vscode ➜ /workspaces/zip-merge-driver (main) $ sh src/test.sh
Initialized empty Git repository in /workspaces/zip-merge-driver/example/.git/
[main (root-commit) 24525a8] Setup custom merge driver for zip files
 1 file changed, 1 insertion(+)
 create mode 100644 .gitattributes
  adding: testfile.txt (stored 0%)
[main b829d7a] Add base zip file content.zip
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 content.zip
Switched to a new branch 'branch_local'
updating: testfile.txt (deflated 10%)
[branch_local 687b7ba] Local changes in content.zip
 1 file changed, 0 insertions(+), 0 deletions(-)
Switched to branch 'main'
updating: testfile.txt (deflated 9%)
[main 10e1fa6] Remote changes in content.zip
 1 file changed, 0 insertions(+), 0 deletions(-)
Working in /tmp/tmp.q5328T4IWN
Switched to a new branch 'local'
Switched to branch 'main'
Auto-merging content.zip
Merge made by the 'ort' strategy.
Merge result:
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   content.zip

no changes added to commit (use "git add" and/or "git commit -a")
Archive:  content.zip
419f30a00a92fa0210ce649eef4a9c4a9f2c323c
  inflating: merged/testfile.txt
This modification is from the remote branch.
This is the base file.
This modification is from the local branch.
Test completed. You can review the merge result in example.

しっちゃかめっちゃかしていますが,最後の部分に注目してください.見事に ZIP ファイルのマージに成功しました.コミットグラフも次のとおりで,しっかりとマージできていることが確認できます.

5. 考察

カスタムマージドライバを使用してマージしたファイルは,どうやらマージ時点でコミットされないっぽいです.ここは少し不便な点ですね.もしかすると実装にすこし問題があるのかもしれません.

あと,カスタムマージドライバでコンフリクトした場合,どうすればいいのかはよくわかりません.Visual Studio Code だと GUI でいい感じにコンフリクトを解消することができますけど,カスタムマージドライバだとそうもいかないと思います.コンフリクトの解消が課題となりそうです.

6. 結論

ZIP ファイル用の Git カスタムマージドライバを作ってみました.仕組みがわかってみると単純なもので,思いのほか簡単に作成できることがわかりました.つぎは,画像のカスタムマージドライバとか作ってみると面白そうです.

7. 付録

作成したリポジトリはこちら:

https://github.com/fjktkm/zip-merge-driver

8. 参考文献

https://qiita.com/OmeletteCurry19/items/26b9bc6ef3706760a9cb

https://git-scm.com/docs/gitattributes

GitHubで編集を提案

Discussion