🎄

git subtree でリポジトリ in リポジトリを実現する

2020/12/25に公開

FORCIAアドベントカレンダー2020 25日目の記事です。

昨年に引き続きFORCIAアドベントカレンダー最終回を担当します、エンジニアの武田です。

今回は、私が担当しているプロジェクトでgit subtreeを利用することになったため、その紹介をしたいと思います。

git subtreeとは

gitリポジトリ内で複数のgitリポジトリの履歴を管理することができる、gitのサブコマンドです。その名前のとおり、メインリポジトリの履歴(main tree)と取り込んだリポジトリの履歴(sub tree)を管理することができます。

git subtreeを利用することになった背景

あるプロジェクトのプライベートなリポジトリで、特定のディレクトリをプロジェクト外の開発者も開発できるようにする必要がありました。一部のディレクトリを別リポジトリとして切り出し、プロジェクト側に取り込むには submodule を利用するケースが多いかと思います。フォルシアの各プロジェクトでも共通モジュールを submodule としてリポジトリに取り込むケースが多いです。

一方で、submodule を取り込んでいるプロジェクト側で submodule も併せて開発していくケースでは以下のような課題がありました。

  • 本体側と submodule を同時に修正する場合、それぞれのリポジトリで commit しなければならない
    • それぞれのリポジトリに push し、本体側では submodule のコミットハッシュを更新する必要があり、手順が多い
  • Merge Request/Pull Request を作成したときに submodule 内の差分を確認できない
    • コードレビュー時にリポジトリを行ったり来たりして確認する必要がある
  • submodule が更新された場合、各開発者が submodule update をしないと submodule が更新されず、アプリケーションが動かない、といったことが起きやすい

そこで git subtree を利用してみることにしました。

実際にgit subtreeを使ってみてどうか

コードレビューで subtree 側の差分についても確認できるというメリットが非常に大きいと感じています。
また、開発者は subtree である、ということを意識せずに開発を進められるため、取り込んだリポジトリに対して変更を加えていく場合は git subtree を利用した方が快適に開発を進めることができます。

一方、 git subtree に限った話ではありませんが、定期的にリモートの履歴を pull しておかないと競合が発生してつらいことになりそうです。git subtree 自体は素晴らしい仕組みですが、それをどのように利用し、運用していくか、といったところに難しさがあるような気がしました。
そちらについては今後運用を進めていく中で改善していければと考えています。

git subtreeコマンドについて

有名な話ですが、git subtree コマンドはシェルスクリプトで実装されています。

https://github.com/git/git/blob/master/contrib/subtree/git-subtree.sh

引数の処理、関数の呼び出し方、エラー処理、デバッグメッセージの出し方など非常に参考になる書き方が多いのでぜひ軽く眺めてみることをおすすめします。

各コマンドの詳細については git subtree --help で表示される説明が非常にわかりやすいため、詳細は省きますが git subtree add/split したときの動きについて簡単に説明します。

git subtree add/split

基本的に add/split で実現したいことは同じです。「本体リポジトリ」に別のコミット履歴を作り、

  • add は別のリポジトリをサブディレクトリとして取り込む
  • split はすでにあるリポジトリからサブディレクトリに関する履歴を別ブランチに分離する

という違いがあります。add は submodule add と似ていてイメージしやすいですが、split は submodule に対応するコマンドがなく、イメージしにくいかもしれません。split の名前のとおり「分離する」ということを意識すると理解しやすいです。

以下にそれぞれのコマンドを実行した場合のイメージ図を貼ります。

クリスマスということでコミットドットをオーナメントで表現してみました!(powered by gitgraph.js)

add/split した場合、完全に別の履歴となって管理されるようになります。add の場合は squash することで余計な履歴が発生しないようにすることも可能です。

# add する場合のコマンド例
$ git subtree add --prefix={addしたいサブディレクトリ} {addしたいリポジトリのURL} main
# split する場合のコマンド例
$ git subtree split --prefix={splitしたいサブディレクトリ} --annotate='(subtree) ' --branch subtree --rejoin

split の場合、該当のサブディレクトリに対して変更を加えたコミットのみ、別のコミット履歴として切り出して分離してくれます。 過去のすべてのコミットをチェックすることになるため、split コマンドの実行に時間がかかるのですが、--rejoin をつけることで subtree のコミットハッシュ、subtree のディレクトリがどこにあるか、といった情報がメインブランチに記録されます。
以降、git subtree split を実行する場合、このコミットより前の履歴を見なくする、といった工夫がされているため、コマンド実行時間が短縮されます。split に時間がかかる場合は定期的に --rejoin を実行すると良さそうです。

さいごに

非常に単純な仕組みを使ってこれだけ便利な機能が、1,000行に満たないシェルスクリプトで実装されていることに衝撃を受けました。外部のリポジトリを取り込みつつ、取り込んだリポジトリも開発していきたい、というケースでは git subtree の利用を検討してみてください。

FORCIAアドベントカレンダー2020も本日で終了となります。2021年が皆様にとって良い年になることを祈っています。メリークリスマス!良いお年を!

FORCIA Tech Blog

Discussion