Git Submodule vs Git Subtree:共有ライブラリをプロジェクトの「ブラックホール」にしないために

Git tutorial - IT technology blog
Git tutorial - IT technology blog

午前2時の電話と「コピペ」の罠

午前2時に鳴る電話は、決して良い知らせを運びません。電話の向こうでは、リードエンジニアが焦った声で言いました。「サーバーが落ちた!決済モジュールのロジックにエラーがある。至急確認してくれ!」

30分間の調査の後、私は苦い事実に直面しました。ある同僚が古いプロジェクトから payment-utils ライブラリを今のプロジェクトにコピペしていたのです。締め切りに間に合わせるために、彼はついでに10行ほどのロジックを修正していました。しかし、元のライブラリのバグが別のプロジェクトで修正されたとき、現在のプロジェクトは依然としてバグのあるバージョンを使用していました。環境間でのロジックの食い違いにより、システムはフリーズ。この「ついで」のコピペの代償は、パニックの中での徹夜のバグ修正でした。

これは、チームに再利用可能なコンポーネント(reusable components)の管理プロセスが欠けているときによくある典型的なシナリオです。手動のコピペの代わりに、Gitは2つの強力な武器を提供しています。それが Git SubmoduleGit Subtree です。それぞれのツールには一長一短があります。その本質を理解していないと、自分の足を撃つ(自業自得なミスをする)ことになりかねません。

1. Git Submodule:特定のコミットへの「ポインタ」

SubmoduleはWindowsのショートカットのようなものだと考えてください。メインのリポジトリ内にライブラリのコードを実際に保持するわけではありません。代わりに、使用している特定のコミットID(SHA)とパスのみを保存します。

実際の導入方法

例えば、web-app プロジェクトがあり、別のリポジトリから auth-service ライブラリを統合したいとします:

# プロジェクトにサブモジュールを追加
git submodule add https://github.com/user/auth-service.git lib/auth-service

# Gitは追跡用に .gitmodules ファイルを作成する
git status

この時点では、同僚が初めてリポジトリをクローンした場合、lib/auth-service フォルダは空になります。ライブラリのコードを取得するには、次のコマンドを実行する必要があります:

git submodule update --init --recursive

メリットとデメリット

  • メリット: ライブラリのコミット履歴を保存しないため、メインリポジトリが非常に軽量です。ライブラリを特定のバージョンに固定(pin)できるため、元のライブラリに変更があってもアプリの安定性を確保できます。
  • デメリット: チーム開発では非常に煩雑です。誰か一人がサブモジュールのコードを push し忘れたままメインリポジトリを push してしまうと、CI/CDパイプラインが即座にエラーを吐きます。

2. Git Subtree:「統合されるが、独立している」

Submoduleとは異なり、Subtreeはライブラリのコード全体とすべてのコミット履歴をメインリポジトリに直接取り込みます。これは、自分の別荘の敷地内に小さな家を建てるようなものです。

実行コマンド

Subtreeを使用してライブラリを追加する場合、通常は次のコマンドを使用します:

git subtree add --prefix lib/auth-service https://github.com/user/auth-service.git main --squash

ここでは --squash パラメータが鍵となります。これにより、ライブラリの何百ものコミットを1つのコミットにまとめることができます。これにより、メインプロジェクトの git log をクリーンで追跡しやすい状態に保てます。

なぜ Subtree が好まれるのか?

  • チームに優しい: メンバーが特別なGitコマンドを覚える必要はありません。リポジトリをクローンするだけで、すぐに実行可能なコードがすべて揃います。
  • 集中管理: メインリポジトリ内でライブラリのコードを直接修正できます。その後、修正を反映させたい場合は、元のリポジトリに push して戻すことができます。
  • デメリット: コミット履歴を抱え込むため、メインリポジトリの容量が増加します。また、subtree pull などのコマンドはかなり長く、覚えにくい傾向があります。

比較:剣と弓、どちらを選ぶべきか?

大規模プロジェクトでの実戦経験に基づいた、迅速な判断に役立つ比較表です:

項目 Git Submodule Git Subtree
リポジトリ構造 リンク(コミットSHA)のみ保存 コードと履歴を直接保存
チームの体験 複雑、「コミット漏れ」エラーが起きやすい シンプル、通常のフォルダと同じ
修正のしやすさ サブリポジトリに切り替える必要あり その場で直接修正可能
使用ケース サードパーティ製ライブラリ、修正が稀な場合 継続的な更新が必要な社内モジュール

コードが「消えた」経験から得た教訓

高度なGit操作をしていると、ミスは避けられません。かつて、サブモジュールのバグを急いで修正し、履歴を綺麗にするために git push --force を使ったことがありました。その結果、直前に同僚がプッシュした重要なコードをすべて上書きしてしまいました。それ以来、私は一つのルールを自分に課しています。「ディレクトリツリーの構造を完全に理解していない限り、絶対に --force を使わないこと」です。

もしプロジェクトで npmcomposerpip などを使用しているなら、サードパーティ製ライブラリにはこれらのパッケージマネージャーを優先してください。SubmoduleやSubtreeを使用するのは、自分たちのチームで開発し、複数のプロジェクト間で共有したいモジュールを管理する場合に限定すべきです。

社内モジュールに Subtree を使用する際の標準フロー:

  1. 追加: モジュールを /shared ディレクトリに配置する。
  2. 修正: バグを見つけたら、作業中のプロジェクト内で直接修正する。
  3. プッシュ: git subtree push を使用して、他のプロジェクトでも利用できるように修正版を元のリポジトリに反映させる。
# Subtreeから元のリポジトリにコードをプッシュする例
git subtree push --prefix=lib/auth-service https://github.com/user/auth-service.git main

まとめ

コード管理は単に addcommitpush を繰り返すことではありません。プロジェクトが大きくなるにつれ、コンポーネントを分割することでシステムのメンテナンス性が向上します。コピペされた混沌としたコードのせいで、午前2時に目を覚ます羽目になるまで待たないでください。今日からスマートな管理方法を選びましょう!

Share: