T-CREATOR

Git の依存取り込み比較:subtree vs submodule — 運用コストと安全性の実態

Git の依存取り込み比較:subtree vs submodule — 運用コストと安全性の実態

複数のリポジトリを管理するプロジェクトにおいて、外部の依存関係をどのように取り込むかは、開発チーム全体の生産性に大きく影響します。Git には、外部リポジトリを取り込むための代表的な手段として subtreesubmodule という 2 つのアプローチがあります。一見すると同じような機能に見えますが、実際の運用では全く異なる特性を持っており、選択を誤ると後々大きな負債となる可能性があります。

本記事では、実際のプロジェクト運用を想定しながら、subtree と submodule それぞれの特徴、運用コスト、安全性について深く比較します。どちらを選ぶべきか、その判断基準を明確にしていきましょう。

Git の依存管理とは

外部リポジトリ統合の必要性

モダンな開発現場では、単一のリポジトリだけで完結するプロジェクトは稀です。共通ライブラリ、設定ファイル、ドキュメント、テンプレートなど、複数のプロジェクトで共有したいコードが必ず存在するでしょう。

これらを効率的に管理するために、Git には外部リポジトリを現在のリポジトリに統合する仕組みが用意されています。

依存管理の 2 つのアプローチ

Git で外部リポジトリを取り込む方法として、主に以下の 2 つがあります。

  • git subtree:外部リポジトリの内容をメインリポジトリの一部として完全に取り込む方式
  • git submodule:外部リポジトリへの参照(ポインタ)だけを管理し、実体は別に保持する方式

どちらも「外部の Git リポジトリを利用する」という目的は同じですが、アーキテクチャが根本的に異なります。

以下の図で、両者の基本的な構造の違いを確認しましょう。

mermaidflowchart TB
    subgraph subtree["git subtree の構造"]
        main_repo1["メインリポジトリ"]
        merged["統合されたコード<br/>(完全にコピー)"]
        main_repo1 --> merged
    end

    subgraph submodule["git submodule の構造"]
        main_repo2["メインリポジトリ"]
        pointer["参照ポインタ<br/>(コミットハッシュ)"]
        external["外部リポジトリ<br/>(実体)"]
        main_repo2 --> pointer
        pointer -.-> external
    end

図で理解できる要点

  • subtree は外部コードを物理的にメインリポジトリに取り込む
  • submodule はあくまで参照(リンク)を保持し、実体は外部に残る
  • 両者の違いは「コードを持つか、参照だけを持つか」

この構造の違いが、後述する運用コストや安全性の違いに直結します。

Git subtree の特徴と運用実態

subtree の基本動作

git subtree は、外部リポジトリの内容を 完全にメインリポジトリの履歴に統合 します。外部リポジトリのコミット履歴もメインリポジトリに取り込まれ、あたかも最初から同じリポジトリで開発していたかのように扱えます。

subtree の追加

外部リポジトリを subtree として追加するには、以下のコマンドを使用します。

bash# リモートリポジトリを追加
git remote add library-repo https://github.com/example/library.git
bash# subtree として特定のディレクトリに追加
git subtree add --prefix=lib/library library-repo main --squash

--prefix オプションで、メインリポジトリ内のどのディレクトリに配置するかを指定します。--squash オプションを使うと、外部リポジトリの履歴を 1 つのコミットにまとめて取り込めます。

subtree の更新

外部リポジトリに更新があった場合、以下のコマンドで取り込みます。

bash# 外部リポジトリの変更を取得
git fetch library-repo main
bash# subtree に変更を反映
git subtree pull --prefix=lib/library library-repo main --squash

この操作により、外部リポジトリの最新の変更がメインリポジトリに統合されます。

subtree からの変更のプッシュバック

subtree 内で変更を加えた場合、その変更を外部リポジトリに反映することも可能です。

bash# subtree 内の変更を外部リポジトリにプッシュ
git subtree push --prefix=lib/library library-repo main

この機能により、共通ライブラリへの改善をメインプロジェクトから行い、他のプロジェクトにも共有できます。

subtree の運用コスト

導入時のコスト:★☆☆☆☆

subtree の導入は非常にシンプルです。git subtree add コマンド 1 つで外部リポジトリを取り込めますし、既存のメンバーは通常の Git 操作だけで開発を続けられます。

特別な初期化コマンドも不要で、clone したらすぐに全てのコードが揃っている状態になります。

日常運用のコスト:★★☆☆☆

日常的な開発では、subtree の存在を意識する必要がほとんどありません。通常の git pullgit commitgit push だけで作業できるため、チームメンバーへの教育コストも最小限です。

ただし、外部リポジトリの更新を取り込む際には、git subtree pull という専用コマンドを実行する必要があります。この操作は通常、特定のメンバー(リードエンジニアなど)が行うことが多いでしょう。

リポジトリサイズの増加:★★★★☆

subtree の最大のデメリットは、メインリポジトリのサイズが増大することです。外部リポジトリのコード、そして(--squash を使わない場合は)その履歴も全て含まれるため、clone や fetch の時間が長くなります。

大規模な外部ライブラリを複数取り込んでいる場合、リポジトリサイズが数 GB になることも珍しくありません。

以下の図で、subtree の運用フローを確認しましょう。

mermaidflowchart TD
    start["プロジェクト開始"] --> clone["git clone"]
    clone --> dev["通常の開発作業<br/>(git add/commit/push)"]
    dev --> need_update{"外部ライブラリの<br/>更新が必要?"}
    need_update -->|はい| subtree_pull["git subtree pull"]
    need_update -->|いいえ| dev
    subtree_pull --> conflict{"コンフリクト<br/>発生?"}
    conflict -->|はい| resolve["手動解決"]
    conflict -->|いいえ| dev
    resolve --> dev

図で理解できる要点

  • clone 後、すぐに全てのコードが揃っている
  • 通常の開発は標準的な Git コマンドだけで完結
  • 外部ライブラリの更新時のみ特別なコマンドが必要

subtree の安全性

データの独立性:★★★★★

subtree では外部リポジトリの内容が完全にメインリポジトリに統合されるため、外部リポジトリが削除されても全く影響を受けません。メインリポジトリだけで完全に自己完結しており、この点では最高レベルの安全性を持ちます。

バージョン固定の明確性:★★★☆☆

subtree では、どのコミットを取り込んだかの情報がコミットメッセージに記録されます。ただし、submodule のように明示的にコミットハッシュで管理されるわけではないため、やや可視性が低いと言えるでしょう。

意図しない変更のリスク:★★☆☆☆

subtree のコードはメインリポジトリの一部として扱われるため、開発者が気づかないうちに外部ライブラリのコードを変更してしまう可能性があります。適切なコードレビュー体制がないと、このリスクは高まります。

Git submodule の特徴と運用実態

submodule の基本動作

git submodule は、外部リポジトリへの 参照(コミットハッシュ)だけをメインリポジトリで管理 します。実際のコードは別のリポジトリに存在し、必要なときに取得する仕組みです。

submodule の追加

外部リポジトリを submodule として追加するには、以下のコマンドを使用します。

bash# submodule として追加
git submodule add https://github.com/example/library.git lib/library

このコマンドを実行すると、.gitmodules という設定ファイルが作成され、submodule の情報が記録されます。

.gitmodules ファイルの例

submodule を追加すると、以下のような .gitmodules ファイルが生成されます。

ini[submodule "lib/library"]
    path = lib/library
    url = https://github.com/example/library.git

このファイルには、submodule の配置場所と URL が記録されています。

submodule の初期化とクローン

他の開発者がリポジトリをクローンした場合、submodule は自動的にはダウンロードされません。以下のコマンドで初期化と取得を行う必要があります。

bash# リポジトリをクローン
git clone https://github.com/example/main-project.git
cd main-project
bash# submodule を初期化して取得
git submodule init
git submodule update

または、clone 時に --recurse-submodules オプションを使うことで、一度に全ての submodule を取得できます。

bash# submodule も含めてクローン
git clone --recurse-submodules https://github.com/example/main-project.git

submodule の更新

外部リポジトリに更新があった場合、以下の手順で取り込みます。

bash# submodule ディレクトリに移動
cd lib/library
bash# 外部リポジトリの最新を取得
git pull origin main
bash# メインリポジトリに戻って変更をコミット
cd ../..
git add lib/library
git commit -m "Update library submodule to latest version"

submodule の更新は、メインリポジトリから見ると「参照しているコミットハッシュの変更」として記録されます。

submodule 内での変更

submodule 内でコードを変更する場合、その submodule 自体を独立した Git リポジトリとして扱います。

bash# submodule ディレクトリに移動
cd lib/library
bash# 変更を加えてコミット
git add .
git commit -m "Fix bug in library"
git push origin main
bash# メインリポジトリで submodule の参照を更新
cd ../..
git add lib/library
git commit -m "Update library submodule with bug fix"

submodule の運用コスト

導入時のコスト:★★★☆☆

submodule の導入には、チームメンバー全員が git submodule コマンドの使い方を理解する必要があります。特に、初めて clone する際の手順が複雑で、initupdate の 2 段階コマンドを実行しなければならない点は、初心者には分かりにくいでしょう。

日常運用のコスト:★★★★☆

日常的な開発でも、submodule は常に注意を払う必要があります。以下のような場面で特別な操作が必要です。

  • ブランチを切り替えた際、submodule も適切なコミットに更新する
  • git pull 後に git submodule update を実行する
  • submodule 内で作業する際は、detached HEAD 状態に注意する

これらの操作を忘れると、ビルドエラーや予期しない動作の原因となります。

チーム全体での教育コスト:★★★★★

submodule の最大の課題は、チーム全員が正しい運用方法を理解していないと、すぐに問題が発生することです。特に、以下のようなトラブルが頻発します。

  • submodule の更新を忘れて古いコードを参照している
  • detached HEAD 状態で作業してコミットを失う
  • submodule のコンフリクト解決方法が分からない

以下の図で、submodule の運用フローを確認しましょう。

mermaidflowchart TD
    start["プロジェクト開始"] --> clone["git clone<br/>--recurse-submodules"]
    clone --> check{"submodule<br/>取得済み?"}
    check -->|いいえ| init["git submodule init"]
    init --> update1["git submodule update"]
    check -->|はい| dev
    update1 --> dev["通常の開発作業"]
    dev --> pull["git pull"]
    pull --> update2["git submodule update<br/>(重要!)"]
    update2 --> switch{"ブランチ<br/>切り替え?"}
    switch -->|はい| update3["git submodule update<br/>(再度必要)"]
    switch -->|いいえ| dev
    update3 --> dev

図で理解できる要点

  • clone 後、必ず submodule の初期化と更新が必要
  • pull の度に submodule の更新を忘れてはならない
  • ブランチ切り替え時も submodule の更新が必要
  • 通常の Git 操作に加えて、常に追加のステップが発生

submodule の安全性

データの独立性:★★☆☆☆

submodule は外部リポジトリへの参照だけを保持するため、外部リポジトリが削除されたり、アクセス権限が変更されたりすると、プロジェクトが正常にビルドできなくなります。この依存関係は大きなリスクとなり得ます。

バージョン固定の明確性:★★★★★

submodule では、メインリポジトリが参照する外部リポジトリの「正確なコミットハッシュ」が明示的に記録されます。どのバージョンを使っているかが常に明確で、この点では最高レベルの透明性を持ちます。

bash# 現在参照している submodule のコミットを確認
git submodule status

実行結果の例:

text a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 lib/library (v1.2.3)

この出力から、具体的なコミットハッシュとタグが確認できます。

意図しない変更のリスク:★★★★★

submodule のコードは別のリポジトリとして管理されているため、誤って変更してしまうリスクが低いです。変更するには明示的に submodule のディレクトリに入る必要があり、この物理的な分離が安全性を高めています。

subtree と submodule の比較表

ここまでの内容を整理し、両者の違いを比較表でまとめます。

#比較項目subtreesubmodule
1統合方式外部リポジトリの内容を完全にメインリポジトリに統合外部リポジトリへの参照(コミットハッシュ)を保持
2リポジトリサイズ大きくなる(外部コードを全て含む)小さい(参照のみ)
3clone の簡易性簡単(通常の clone で完了)やや複雑(--recurse-submodules または init/update が必要)
4日常的な操作通常の Git コマンドだけで OK特別なコマンド(git submodule update など)が必須
5学習コスト低い(通常の Git 操作と同じ)高い(submodule 特有の概念と操作が必要)
6バージョン管理の明確性やや不明瞭(コミットメッセージに依存)非常に明確(コミットハッシュで管理)
7外部リポジトリ削除時の影響なし(完全に独立)大きい(ビルド不可になる)
8意図しない変更のリスクやや高い(同じリポジトリ内として扱われる)低い(明示的に移動しないと変更できない)
9複数プロジェクトでの共有やや難しい(push back が必要)容易(独立したリポジトリとして管理)
10ビルドツールとの統合容易(全て同じリポジトリ内)やや複雑(パス管理が必要)

この表から、subtree はシンプルで導入しやすい一方、submodule はバージョン管理の厳密性に優れていることが分かります。

運用コストの実態

subtree の運用シナリオ

subtree は、以下のようなシナリオで運用コストが低くなります。

シナリオ 1:共通ライブラリの取り込み

社内で開発している共通ライブラリを複数のプロジェクトで利用する場合、subtree を使えば簡単に取り込めます。

bash# 社内の共通ライブラリを subtree として追加
git remote add common-lib https://github.com/company/common-library.git
git subtree add --prefix=lib/common common-lib main --squash

この後、チームメンバーは通常通り開発を続けるだけで OK です。特別な操作は不要です。

シナリオ 2:ドキュメントの共有

複数のプロジェクトで共通のドキュメントやテンプレートを使う場合も、subtree が便利です。

bash# ドキュメントリポジトリを subtree として追加
git remote add docs https://github.com/company/documentation.git
git subtree add --prefix=docs docs main --squash

開発者は、プロジェクトのコードとドキュメントを同じリポジトリで管理でき、移動の手間がありません。

運用上の注意点

subtree で注意すべきは、「誰が外部リポジトリの更新を管理するか」を明確にすることです。

bash# 定期的に外部ライブラリを更新(担当者が実行)
git fetch common-lib main
git subtree pull --prefix=lib/common common-lib main --squash

更新作業は通常、リードエンジニアやテックリードが担当することが多いでしょう。チーム全員が実行する必要はありません。

submodule の運用シナリオ

submodule は、以下のようなシナリオで真価を発揮します。

シナリオ 1:厳密なバージョン管理が必要な場合

金融システムや医療システムなど、使用するライブラリのバージョンを厳密に管理する必要がある場合、submodule が適しています。

bash# 特定のバージョンを明示的に参照
git submodule add https://github.com/vendor/critical-library.git lib/critical
cd lib/critical
git checkout v2.3.1
cd ../..
git add lib/critical
git commit -m "Add critical library at version 2.3.1"

このコミットには、正確なコミットハッシュが記録され、監査時にも明確に証明できます。

シナリオ 2:複数のプロジェクトで同じライブラリを並行開発

複数のプロジェクトが同じライブラリを使いながら、それぞれのプロジェクトから改善を加えていくような開発スタイルでは、submodule が効果的です。

bash# プロジェクト A で submodule 内のライブラリを改善
cd lib/shared-library
git checkout -b feature/improvement
# 変更を加える
git commit -m "Improve performance"
git push origin feature/improvement
bash# プロジェクト B で同じ改善を取り込む
cd lib/shared-library
git fetch origin
git checkout feature/improvement
cd ../..
git add lib/shared-library
git commit -m "Use improved version of shared library"

このように、ライブラリを中心とした開発フローが構築できます。

運用上の注意点

submodule の最大の課題は、チーム全員が正しい手順を理解することです。以下のような運用ルールを定めると良いでしょう。

  1. clone 時のルール
bash# 必ず --recurse-submodules を使う
git clone --recurse-submodules <repository-url>
  1. pull 後のルール
bash# pull の後は必ず submodule を更新
git pull
git submodule update --init --recursive
  1. ブランチ切り替え時のルール
bash# ブランチを切り替えた後も submodule を更新
git checkout <branch-name>
git submodule update --init --recursive

これらをチームの標準手順として文書化し、新メンバーに教育することが重要です。

運用コストの総合評価

subtree の総合評価:★★☆☆☆(運用コスト低)

  • 導入が簡単で、チームへの教育コストが最小限
  • 日常的な開発では通常の Git 操作だけで完結
  • リポジトリサイズの増加が唯一の大きなデメリット

submodule の総合評価:★★★★☆(運用コスト高)

  • 導入時の学習コストが高い
  • 日常的な操作でも常に注意が必要
  • チーム全員が正しい手順を理解していないとトラブルが頻発
  • ただし、バージョン管理の厳密性では最高レベル

以下の図で、両者の運用コストの違いを視覚的に比較しましょう。

mermaidflowchart LR
    subgraph subtree_ops["subtree の日常操作"]
        st1["git clone"] --> st2["すぐに開発可能"]
        st2 --> st3["git add/commit/push<br/>(通常の Git 操作)"]
        st3 --> st4{"外部ライブラリ<br/>更新が必要?"}
        st4 -->|いいえ| st3
        st4 -->|はい| st5["git subtree pull<br/>(担当者のみ)"]
        st5 --> st3
    end

    subgraph submodule_ops["submodule の日常操作"]
        sm1["git clone<br/>--recurse-submodules"] --> sm2["git submodule init<br/>git submodule update"]
        sm2 --> sm3["開発可能"]
        sm3 --> sm4["git add/commit/push"]
        sm4 --> sm5["git pull"]
        sm5 --> sm6["git submodule update<br/>(必須!)"]
        sm6 --> sm7{"ブランチ<br/>切り替え?"}
        sm7 -->|はい| sm8["git submodule update<br/>(再度必須!)"]
        sm7 -->|いいえ| sm3
        sm8 --> sm3
    end

図で理解できる要点

  • subtree は clone 後すぐに開発でき、通常の Git 操作だけで完結
  • submodule は各ステップで追加の操作が必要で、忘れるとトラブルの原因に
  • 運用の複雑さが、チーム全体の生産性に影響する

安全性の実態

subtree の安全性評価

長期的な安定性:★★★★★

subtree は外部リポジトリの内容を完全にメインリポジトリに取り込むため、外部の変更に影響されません。以下のような状況でも、プロジェクトは問題なく動作し続けます。

  • 外部リポジトリが削除される
  • 外部リポジトリのアクセス権限が変更される
  • 外部リポジトリのホスティングサービスが停止する

この自己完結性は、長期的なプロジェクトの安定性において非常に重要です。

コード改変のリスク:★★☆☆☆

一方で、subtree で取り込んだコードはメインリポジトリの一部として扱われるため、開発者が意図せず変更してしまう可能性があります。

bash# 誤って subtree のファイルを変更してしまう例
# lib/common/ 配下が subtree だと気づかずに編集
vim lib/common/utils.js
git add lib/common/utils.js
git commit -m "Fix bug in utils"

このような誤った変更を防ぐには、以下の対策が有効です。

対策 1:CODEOWNERS ファイルの設定

GitHub では、CODEOWNERS ファイルを使って特定のディレクトリの変更に承認を必須にできます。

text# .github/CODEOWNERS
# subtree で管理しているディレクトリは特定のメンバーのみが変更可能
/lib/common/ @tech-lead @senior-dev

対策 2:pre-commit フックの活用

pre-commit フックを使って、subtree ディレクトリの変更を警告できます。

bash#!/bin/bash
# .git/hooks/pre-commit

# subtree で管理しているディレクトリのリスト
SUBTREE_DIRS=("lib/common" "docs")

# 変更されたファイルをチェック
for dir in "${SUBTREE_DIRS[@]}"; do
    if git diff --cached --name-only | grep -q "^$dir/"; then
        echo "Warning: You are modifying files in subtree directory: $dir"
        echo "Are you sure you want to proceed? (y/n)"
        read -r response
        if [[ ! "$response" =~ ^[Yy]$ ]]; then
            echo "Commit aborted."
            exit 1
        fi
    fi
done

このスクリプトは、subtree ディレクトリの変更を検出すると、開発者に確認を求めます。

submodule の安全性評価

外部依存のリスク:★★☆☆☆

submodule は外部リポジトリへの参照だけを保持するため、外部リポジトリが利用できなくなると、プロジェクトが正常に動作しなくなります。

bash# 外部リポジトリが削除されている場合
git submodule update --init --recursive
# エラー: fatal: repository 'https://github.com/example/library.git' not found

このリスクを軽減するには、以下の対策が有効です。

対策 1:重要な依存関係はミラーリング

社内の Git サーバーに外部リポジトリのミラーを作成し、そちらを参照するようにします。

bash# ミラーリポジトリを作成(GitLab/GitHub Enterprise などで)
# .gitmodules を編集して内部 URL を使用
[submodule "lib/library"]
    path = lib/library
    url = https://internal-git.company.com/mirrors/library.git

対策 2:定期的なバックアップ

CI/CD パイプラインの一部として、submodule が参照しているコミットをアーカイブします。

bash#!/bin/bash
# submodule をバックアップするスクリプト

# 全ての submodule をループ
git submodule foreach --recursive '
    COMMIT=$(git rev-parse HEAD)
    REPO_NAME=$(basename $(git rev-parse --show-toplevel))
    ARCHIVE_NAME="${REPO_NAME}-${COMMIT}.tar.gz"

    # アーカイブを作成
    git archive --format=tar.gz --output="/backup/${ARCHIVE_NAME}" HEAD
    echo "Backed up: ${ARCHIVE_NAME}"
'

このスクリプトを定期的に実行することで、外部リポジトリが削除されても復元できます。

バージョン固定の安全性:★★★★★

submodule は、使用している外部リポジトリの正確なコミットハッシュを記録します。この明確性は、以下のような場面で重要です。

  • セキュリティ監査:使用しているライブラリのバージョンを証明できる
  • 再現可能なビルド:過去のどの時点でも同じ状態を再現できる
  • 脆弱性管理:特定のバージョンに脆弱性が見つかった際、影響を即座に判断できる
bash# 現在使用している submodule のバージョンを確認
git submodule status

# 出力例:
# a1b2c3d4 lib/library (v1.2.3)
# e5f6g7h8 lib/utils (v0.8.1)

意図しない変更からの保護:★★★★★

submodule は物理的に別のディレクトリとして扱われ、さらに別の Git リポジトリとして管理されるため、誤って変更してしまうリスクが低いです。

bash# submodule 内で作業する際は、明示的に移動する必要がある
cd lib/library

# この時点で、別のリポジトリであることが明確
git status
# On branch main
# Your branch is up to date with 'origin/main'.

この物理的な分離が、コードの整合性を保つのに役立ちます。

安全性の総合評価

#安全性の観点subtreesubmodule
1外部リポジトリ削除時の耐性★★★★★(影響なし)★★☆☆☆(ビルド不可)
2バージョン管理の明確性★★★☆☆(やや不明瞭)★★★★★(完全に明確)
3意図しない変更からの保護★★☆☆☆(同じリポジトリ内)★★★★★(物理的に分離)
4長期的な安定性★★★★★(自己完結)★★★☆☆(外部依存あり)
5セキュリティ監査の容易性★★★☆☆(履歴から追跡)★★★★★(ハッシュで明確)

この表から、安全性の観点では一長一短があり、プロジェクトの性質によって適切な選択が異なることが分かります。

選択基準と推奨シナリオ

subtree を選ぶべきケース

以下の条件に当てはまる場合、subtree が適しています。

ケース 1:チームの Git スキルレベルが様々

新人からベテランまで幅広いスキルレベルのメンバーがいるチームでは、subtree のシンプルさが大きなメリットになります。

bash# 新人メンバーでも通常の Git 操作だけで開発できる
git clone https://github.com/company/project.git
cd project
# すぐに開発開始可能!
npm install
npm run dev

特別なコマンドを覚える必要がなく、学習コストが最小限です。

ケース 2:外部リポジトリの更新頻度が低い

外部ライブラリやドキュメントの更新が月に 1 回程度の場合、subtree の更新コストは問題になりません。

bash# 月次の定期メンテナンスで更新(リードエンジニアが実行)
git fetch common-lib main
git subtree pull --prefix=lib/common common-lib main --squash

更新作業を特定のメンバーに集中させることで、チーム全体の負担を減らせます。

ケース 3:長期的な安定性を重視

10 年後もプロジェクトが正常にビルドできる必要がある場合、外部依存のない subtree が安全です。

bash# 外部リポジトリが削除されても、このリポジトリだけで完結
git clone https://archive.company.com/legacy-project.git
cd legacy-project
# 10 年前のプロジェクトでも問題なくビルドできる
npm install
npm run build

特に、レガシーシステムの保守では、この自己完結性が重要になります。

ケース 4:リポジトリサイズが問題にならない

取り込む外部リポジトリのサイズが小さい場合(数 MB 程度)や、高速なネットワーク環境がある場合、subtree のデメリットは小さくなります。

bash# 小規模な設定ファイルのリポジトリを取り込む例
git remote add configs https://github.com/company/configs.git
git subtree add --prefix=config configs main --squash
# リポジトリサイズへの影響は軽微

submodule を選ぶべきケース

以下の条件に当てはまる場合、submodule が適しています。

ケース 1:厳密なバージョン管理が必須

金融、医療、航空宇宙など、使用するライブラリのバージョンを明確に記録する必要がある分野では、submodule の明確性が重要です。

bash# 特定のバージョンを明示的に固定
git submodule add https://github.com/vendor/certified-library.git lib/certified
cd lib/certified
git checkout v3.2.1  # 認証済みバージョン
cd ../..
git add lib/certified
git commit -m "Use certified library v3.2.1 (certification ID: CERT-2024-001)"

このコミット記録は、監査時に「どのバージョンを使用していたか」を証明する証拠となります。

ケース 2:複数プロジェクトで並行開発

共有ライブラリを複数のプロジェクトで使いながら、それぞれのプロジェクトから改善を加えていく開発スタイルでは、submodule が効率的です。

bash# プロジェクト A でライブラリを改善
cd shared-library
git checkout -b feature/optimization
# 最適化を実装
git commit -m "Optimize data processing"
git push origin feature/optimization
bash# プロジェクト B で即座に最新版を試せる
cd shared-library
git fetch origin
git checkout feature/optimization
cd ..
git add shared-library
git commit -m "Test optimization in project B"

このように、ライブラリの改善と各プロジェクトへの適用を柔軟に行えます。

ケース 3:大規模な外部リポジトリを参照

取り込む外部リポジトリのサイズが大きい(数百 MB ~数 GB)場合、submodule の参照方式がリポジトリサイズを抑えるのに有効です。

bash# 大規模なアセットリポジトリを参照
git submodule add https://github.com/company/large-assets.git assets
# メインリポジトリには参照だけが記録される
# 実際のアセットは必要なときだけダウンロード

特に、全てのメンバーが全てのアセットを必要としない場合、この方式が効率的です。

ケース 4:チームの Git スキルが高い

チーム全員が Git の仕組みを深く理解しており、submodule の運用に自信がある場合、その厳密性を活かせます。

bash# 高度な submodule 操作(例:特定のブランチを追跡)
git submodule add -b develop https://github.com/company/experimental.git lib/experimental
git submodule update --remote lib/experimental
# 常に develop ブランチの最新を追跡

このような高度な運用は、スキルの高いチームでこそ真価を発揮します。

推奨判断フローチャート

どちらを選ぶべきか迷った際は、以下のフローチャートを参考にしてください。

mermaidflowchart TD
    start["外部リポジトリの<br/>統合が必要"] --> q1{"チームの Git スキルは<br/>全員高いか?"}

    q1 -->|いいえ| q2{"長期的な安定性が<br/>最優先か?"}
    q1 -->|はい| q3{"厳密なバージョン管理が<br/>必須か?"}

    q2 -->|はい| subtree1["✓ subtree を推奨"]
    q2 -->|いいえ| q4{"外部リポジトリの<br/>サイズは大きいか?"}

    q4 -->|いいえ| subtree2["✓ subtree を推奨"]
    q4 -->|はい| q5{"複数プロジェクトで<br/>並行開発するか?"}

    q5 -->|いいえ| subtree3["✓ subtree を推奨<br/>(初回のみ時間がかかる)"]
    q5 -->|はい| submodule1["✓ submodule を推奨"]

    q3 -->|はい| submodule2["✓ submodule を推奨"]
    q3 -->|いいえ| q6{"複数プロジェクトで<br/>並行開発するか?"}

    q6 -->|はい| submodule3["✓ submodule を推奨"]
    q6 -->|いいえ| q7{"リポジトリサイズを<br/>抑えたいか?"}

    q7 -->|はい| submodule4["✓ submodule を推奨"]
    q7 -->|いいえ| subtree4["✓ subtree を推奨<br/>(シンプルで運用が楽)"]

図で理解できる要点

  • チームのスキルレベルが判断の最初の分岐点
  • 長期的な安定性を重視するなら subtree
  • 厳密なバージョン管理が必要なら submodule
  • 迷ったら subtree を選ぶのが無難

ハイブリッドアプローチ

実際のプロジェクトでは、subtree と submodule を併用することも可能です。

bash# 安定した共通ライブラリは subtree で取り込む
git subtree add --prefix=lib/stable stable-lib main --squash

# 開発中の実験的なライブラリは submodule で参照
git submodule add https://github.com/company/experimental.git lib/experimental

このように、用途に応じて使い分けることで、両者のメリットを活かせます。

移行と切り替えの実践

subtree から submodule への移行

subtree から submodule に移行する場合、以下の手順で行います。

ステップ 1:既存の subtree の削除

まず、subtree として取り込んだディレクトリを通常のファイルとして削除します。

bash# subtree のディレクトリを削除
git rm -r lib/library
git commit -m "Remove subtree directory before migration"

ステップ 2:submodule として再追加

同じ外部リポジトリを、今度は submodule として追加します。

bash# submodule として追加
git submodule add https://github.com/example/library.git lib/library
git commit -m "Add library as submodule instead of subtree"

ステップ 3:適切なバージョンへの切り替え

subtree で使っていたバージョンと同じバージョンに submodule を切り替えます。

bash# 以前使っていたバージョンを確認(git log から)
# submodule を該当のコミットに切り替え
cd lib/library
git checkout <以前のコミットハッシュまたはタグ>
cd ../..
git add lib/library
git commit -m "Set submodule to previous version"

注意点

移行作業は、チーム全体に影響を与えるため、以下の点に注意してください。

  • 事前告知:チームメンバーに移行のスケジュールを共有する
  • ドキュメント更新:開発環境のセットアップ手順を更新する
  • CI/CD の更新:ビルドスクリプトに git submodule update を追加する

submodule から subtree への移行

submodule から subtree に移行する場合、以下の手順で行います。

ステップ 1:現在の submodule のバージョンを記録

移行前に、現在使用している submodule のバージョンを確認します。

bash# 現在の submodule のコミットハッシュを確認
git submodule status
# 出力例: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 lib/library (v1.2.3)

このコミットハッシュ(a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0)を記録しておきます。

ステップ 2:submodule の削除

既存の submodule を完全に削除します。

bash# submodule の登録を解除
git submodule deinit lib/library

# .gitmodules から削除
git rm lib/library

# コミット
git commit -m "Remove library submodule before migration"

ステップ 3:subtree として再追加

同じ外部リポジトリを、今度は subtree として追加します。

bash# リモートリポジトリを追加
git remote add library-repo https://github.com/example/library.git

# 以前使っていたコミットを指定して subtree として追加
git subtree add --prefix=lib/library library-repo <記録したコミットハッシュ> --squash

ステップ 4:チームへの通知

移行後、チームメンバーに以下の点を伝えます。

  • git submodule update コマンドが不要になったこと
  • 通常の git pull だけで最新のコードが取得できること
  • subtree ディレクトリの変更には注意が必要なこと

トラブルシューティング

subtree でよくあるエラーと解決方法

エラー 1:fatal: refusing to merge unrelated histories

subtree を pull する際に、このエラーが発生することがあります。

bash# エラーの例
git subtree pull --prefix=lib/library library-repo main --squash
# fatal: refusing to merge unrelated histories

原因:Git が 2 つの履歴を無関係なものとして認識している。

解決方法--allow-unrelated-histories オプションを追加します。

bash# 無関係な履歴のマージを許可
git subtree pull --prefix=lib/library library-repo main --squash --allow-unrelated-histories

エラー 2:subtree pull でコンフリクトが発生

外部リポジトリの変更とメインリポジトリの変更が衝突した場合、コンフリクトが発生します。

bash# コンフリクトの例
git subtree pull --prefix=lib/library library-repo main --squash
# Auto-merging lib/library/utils.js
# CONFLICT (content): Merge conflict in lib/library/utils.js

解決方法:通常の Git コンフリクトと同様に、手動で解決します。

bash# コンフリクトを解決
vim lib/library/utils.js
# コンフリクトマーカーを削除して修正

# 解決後、コミット
git add lib/library/utils.js
git commit -m "Resolve conflict in subtree pull"

エラー 3:subtree push が失敗する

subtree 内で行った変更を外部リポジトリにプッシュしようとした際、失敗することがあります。

bash# エラーの例
git subtree push --prefix=lib/library library-repo main
# error: failed to push some refs to 'https://github.com/example/library.git'

原因:外部リポジトリにプッシュする権限がない、またはブランチが保護されている。

解決方法

  1. 権限の確認:外部リポジトリへの書き込み権限があるか確認
  2. プルリクエストの作成:権限がない場合は、fork してプルリクエストを作成
bash# fork したリポジトリに push
git remote add library-fork https://github.com/your-username/library.git
git subtree push --prefix=lib/library library-fork feature-branch
# その後、プルリクエストを作成

submodule でよくあるエラーと解決方法

エラー 1:fatal: no submodule mapping found in .gitmodules

submodule の情報が正しく設定されていない場合、このエラーが発生します。

bash# エラーの例
git submodule update
# fatal: no submodule mapping found in .gitmodules for path 'lib/library'

原因.gitmodules ファイルに submodule の情報が記録されていない。

解決方法.gitmodules ファイルを確認し、必要に応じて再設定します。

bash# .gitmodules の内容を確認
cat .gitmodules
bash# submodule が正しく登録されていない場合、再度追加
git submodule add https://github.com/example/library.git lib/library

エラー 2:fatal: reference is not a tree

submodule が参照しているコミットが外部リポジトリに存在しない場合、このエラーが発生します。

bash# エラーの例
git submodule update
# fatal: reference is not a tree: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0

原因:外部リポジトリで、該当のコミットが削除されたか、force push で履歴が書き換えられた。

解決方法:submodule を最新のコミットに更新します。

bash# submodule ディレクトリに移動
cd lib/library

# 最新のコミットを取得
git fetch origin
git checkout origin/main

# メインリポジトリに戻って更新を記録
cd ../..
git add lib/library
git commit -m "Update submodule to latest commit"

エラー 3:detached HEAD 状態での作業

submodule 内で作業している際、detached HEAD 状態になり、コミットを失うリスクがあります。

bash# 警告の例
cd lib/library
git status
# HEAD detached at a1b2c3d

原因:submodule は特定のコミットを参照しているため、デフォルトで detached HEAD 状態になる。

解決方法:作業前に必ずブランチを作成します。

bash# 作業前にブランチを作成
cd lib/library
git checkout -b feature/my-changes

# 作業してコミット
git add .
git commit -m "Make changes"

# リモートにプッシュ
git push origin feature/my-changes

# メインリポジトリで更新を記録
cd ../..
git add lib/library
git commit -m "Update submodule with my changes"

エラー 4:submodule update を忘れてビルドエラー

git pull の後に git submodule update を忘れると、古いバージョンの submodule を参照してビルドエラーが発生します。

bash# エラーの例
npm run build
# Error: Cannot find module 'newFeature' in lib/library

原因:メインリポジトリは最新だが、submodule が古いバージョンのまま。

解決方法git pull の後は必ず git submodule update を実行します。

bash# pull 後に submodule を更新
git pull
git submodule update --init --recursive

# または、エイリアスを作成して自動化
git config --global alias.pullall '!git pull && git submodule update --init --recursive'

# 以降は git pullall で両方を実行
git pullall

トラブル予防のベストプラクティス

subtree のベストプラクティス

  1. 更新担当者を明確にする
bash# README.md に記載する例
# Subtree の更新

lib/common ディレクトリは subtree で管理されています。
更新は @tech-lead が月次で行います。

更新コマンド:
git fetch common-lib main
git subtree pull --prefix=lib/common common-lib main --squash
  1. subtree ディレクトリを明示する
bash# プロジェクトルートに SUBTREES.md を作成
echo "# Subtree Management

以下のディレクトリは subtree で管理されています:

- lib/common - https://github.com/company/common-library.git
- docs - https://github.com/company/documentation.git

これらのディレクトリの変更には注意してください。" > SUBTREES.md

submodule のベストプラクティス

  1. Git エイリアスの設定
bash# チームで共通の Git エイリアスを設定
git config --global alias.clone-recursive 'clone --recurse-submodules'
git config --global alias.pullall '!git pull && git submodule update --init --recursive'
git config --global alias.pushall '!git push && git submodule foreach git push'
  1. CI/CD での自動化
yaml# GitHub Actions の例
name: Build with Submodules

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout with submodules
        uses: actions/checkout@v3
        with:
          submodules: recursive

      - name: Build project
        run: npm run build
  1. 開発環境セットアップスクリプト
bash#!/bin/bash
# setup.sh - 新メンバー向けのセットアップスクリプト

echo "プロジェクトのセットアップを開始します..."

# submodule の初期化と更新
echo "Submodule を取得しています..."
git submodule update --init --recursive

# 依存パッケージのインストール
echo "依存パッケージをインストールしています..."
npm install

# submodule 内でも依存パッケージをインストール
echo "Submodule の依存パッケージをインストールしています..."
git submodule foreach 'npm install || true'

echo "セットアップが完了しました!"
echo "開発を始めるには: npm run dev"

まとめ

Git の subtree と submodule は、どちらも外部リポジトリを統合するための強力なツールですが、その特性は大きく異なります。

subtree の特徴

  • シンプルな運用:通常の Git 操作だけで開発できる
  • 自己完結性:外部リポジトリが削除されても影響を受けない
  • 大きなリポジトリサイズ:外部コードを全て含むため、サイズが増大する
  • 低い学習コスト:新メンバーでもすぐに使える

subtree は、チームの Git スキルが様々で、シンプルな運用を優先したい場合に適しています。

submodule の特徴

  • 厳密なバージョン管理:参照しているコミットハッシュが明確
  • 小さなリポジトリサイズ:参照だけを保持するため、サイズが抑えられる
  • 外部依存:外部リポジトリが削除されると影響を受ける
  • 高い学習コスト:特有のコマンドと概念を理解する必要がある

submodule は、バージョン管理の厳密性が求められる分野や、複数プロジェクトで並行開発する場合に適しています。

最終的な選択基準

どちらを選ぶべきかは、以下の要素で判断してください。

#判断要素subtree を選ぶsubmodule を選ぶ
1チームの Git スキル様々なレベル全員が高い
2運用の優先順位シンプルさ厳密性
3リポジトリサイズ小〜中規模の外部リポジトリ大規模な外部リポジトリ
4更新頻度低〜中程度高頻度
5長期的な安定性最優先ある程度妥協可能
6バージョン管理の厳密性ある程度で十分必須
7複数プロジェクトでの共有頻繁

迷った場合は、シンプルさを優先して subtree を選ぶのが無難です。後から submodule に移行することも可能ですし、運用の複雑さによるトラブルを避けられます。

一方、金融システムや医療システムなど、使用するライブラリのバージョンを厳密に記録する必要がある分野では、submodule の明確性が重要になります。

どちらの方式を選んでも、チーム全体で運用ルールを明確にし、ドキュメント化することが成功の鍵となります。この記事が、あなたのプロジェクトに最適な選択をする助けになれば幸いです。

関連リンク