Gitサブモジュールのコンフリクト解消ガイド
サブモジュールのコンフリクトはなぜ起きるのか
Section titled “サブモジュールのコンフリクトはなぜ起きるのか”サブモジュールは、親リポジトリ側で「サブモジュールがどのコミット(SHA)を指すか」だけを記録しています。
そのため、複数ブランチでサブモジュールの参照先SHAが別々に更新されると、親リポジトリ上ではコンフリクトになります。
%%{init: {'themeVariables': {'fontSize':'14px'}, 'flowchart': {'nodeSpacing': 28, 'rankSpacing': 35, 'padding': 10}}}%%
flowchart LR
A[main docs→A] --> C[マージ]
B[feature docs→B] --> C
C --> D{同じ参照?}
D -->|はい| E[自動マージ]
D -->|いいえ| F[コンフリクト]
具体例: 同じ親コミットから2本のブランチを切ったとき
Section titled “具体例: 同じ親コミットから2本のブランチを切ったとき”ここでは、親リポジトリ(メインプロジェクト)とサブモジュールの関係を、できるだけ具体的に追います。
- 親リポジトリの
mainはコミットP0にいる - サブモジュール
docs/はコミットS0を指している P0からfeature-aとfeature-bの2ブランチを切る(親の起点SHAは同じ)
%%{init: {'themeVariables': {'fontSize':'14px'}, 'flowchart': {'nodeSpacing': 28, 'rankSpacing': 35, 'padding': 10}}}%%
flowchart LR
P0[main docs→0] --> FA[feature-a docs→0]
P0 --> FB[feature-b docs→0]
この時点では、両ブランチともサブモジュール参照は S0 のままです。
ブランチを切っただけではコンフリクトは起きません。
コンフリクトの判定ルール(親リポジトリ視点)
Section titled “コンフリクトの判定ルール(親リポジトリ視点)”マージ時に Git が見ているのは、サブモジュール内のファイル差分ではなく 親が記録している SHA 1行 だけです。
| 状況 | 結果 |
|---|---|
| 片方のブランチだけがサブモジュール参照を変更した | コンフリクトしない(変更側の SHA が採用される) |
| 両方ともサブモジュール参照を変更し、同じ SHA を指している | コンフリクトしない |
| 両方ともサブモジュール参照を変更し、異なる SHA を指している | コンフリクトする |
| どちらのブランチもサブモジュール参照を変更していない | コンフリクトしない |
ケース1: コンフリクトしない(片方だけサブモジュール更新)
Section titled “ケース1: コンフリクトしない(片方だけサブモジュール更新)”feature-a だけがサブモジュールを S1 に更新し、親でその参照更新をコミットする。
feature-b は親の別ファイルだけ変更し、サブモジュール参照は S0 のまま。
%%{init: {'themeVariables': {'fontSize':'14px'}, 'flowchart': {'nodeSpacing': 28, 'rankSpacing': 35, 'padding': 10}}}%%
flowchart LR
P0[起点 docs→0] --> P1[feature-a docs→1]
P0 --> P2[feature-b docs→0]
P1 & P2 --> M[マージ] --> OK[競合なし →1]
feature-a と feature-b を main にマージすると、サブモジュール参照は S1 に更新された側がそのまま採用され、通常は競合しません。
ケース2: コンフリクトしない(両方とも同じ SHA に更新)
Section titled “ケース2: コンフリクトしない(両方とも同じ SHA に更新)”feature-a も feature-b も、サブモジュール側で同じコミット S1 を作り(または同じ既存コミットを checkout し)、親でもどちらも S1 を指すコミットを作った場合です。
%%{init: {'themeVariables': {'fontSize':'14px'}, 'flowchart': {'nodeSpacing': 28, 'rankSpacing': 35, 'padding': 10}}}%%
flowchart LR
P0[起点 docs→0] --> P1[feature-a docs→1]
P0 --> P2[feature-b docs→1]
P1 & P2 --> M[マージ] --> OK[競合なし]
親から見ると最終的にどちらも S1 なので、マージ時に参照の衝突は起きません。
ケース3: コンフリクトする(両方が異なる SHA に更新)
Section titled “ケース3: コンフリクトする(両方が異なる SHA に更新)”feature-a はサブモジュールを S1 に、feature-b は S2 に更新して親へコミットした場合です。
サブモジュール内の差分が似ていても、親が記録する SHA が違えば競合します。
%%{init: {'themeVariables': {'fontSize':'14px'}, 'flowchart': {'nodeSpacing': 28, 'rankSpacing': 35, 'padding': 10}}}%%
flowchart LR
P0[起点 docs→0] --> P1[feature-a docs→1]
P0 --> P2[feature-b docs→2]
P1 & P2 --> M[マージ] --> NG[競合 1か2?]
マージ時に both modified: docs のような表示になり、親側でどちらの SHA を採用するかを人が決める必要があります。
ケース4: コンフリクトする(内容は同じだが SHA が違う)
Section titled “ケース4: コンフリクトする(内容は同じだが SHA が違う)”feature-a と feature-b が、サブモジュール内で同じ修正を別々にコミットした場合です。
たとえば同じ1行の typo 修正でも、コミットが別なら SHA は別になります。
feature-a側: サブモジュールS1(abc1234)feature-b側: サブモジュールS2(def5678)
%%{init: {'themeVariables': {'fontSize':'14px'}, 'flowchart': {'nodeSpacing': 28, 'rankSpacing': 35, 'padding': 10}}}%%
flowchart LR
P0[親 docs→0] --> PA[feature-a docs→1]
P0 --> PB[feature-b docs→2]
PA & PB --> C[競合: 中身同じでも参照2つ]
git diff でサブモジュールの中身を比較すると差分ゼロでも、親の gitlink は abc1234 と def5678 の二択になるため競合します。
ケース5: コンフリクトしない(サブモジュールは触らず親だけ変更)
Section titled “ケース5: コンフリクトしない(サブモジュールは触らず親だけ変更)”両ブランチとも、親リポジトリの README や astro.config.mjs など、サブモジュール以外だけを変更した場合です。
このときは通常のファイルマージの話になり、サブモジュール参照は S0 のままなので競合しません。
%%{init: {'themeVariables': {'fontSize':'14px'}, 'flowchart': {'nodeSpacing': 28, 'rankSpacing': 35, 'padding': 10}}}%%
flowchart LR
P0[起点 docs→0] --> P1[feature-a 親のみ変更]
P0 --> P2[feature-b 親のみ変更]
P1 & P2 --> M[マージ] --> OK[競合なし]
ケース6: 順番にマージすれば済むこともある
Section titled “ケース6: 順番にマージすれば済むこともある”feature-a を先に main にマージしたあと、feature-b を最新 main にリベースしてからマージする運用では、状況によっては競合を回避できます。
ただし feature-b 側でもサブモジュール参照を別 SHA に更新している場合、リベース時点で競合が表面化することがあります。
%%{init: {'themeVariables': {'fontSize':'14px'}}}%%
sequenceDiagram
participant M as main
participant A as feature-a
participant B as feature-b
M->>A: 分岐
M->>B: 分岐
A->>M: docs→1 をマージ
B->>M: main を取り込み
Note over B,M: Bが docs→2 なら競合
解消の基本方針
Section titled “解消の基本方針”- まず親リポジトリ側でどのパスが衝突しているかを確認する
- サブモジュールに移動して、採用したいコミット(またはそのコミットを含むブランチ)を決める
- 親リポジトリに戻り、サブモジュールの参照先更新を1つに確定してコミットする
実際の解消手順
Section titled “実際の解消手順”1. 親リポジトリで競合ファイルを確認
Section titled “1. 親リポジトリで競合ファイルを確認”git statusboth modified: path/to/submodule のように表示されたら、サブモジュール参照の競合です。
2. サブモジュール側で採用コミットを決定
Section titled “2. サブモジュール側で採用コミットを決定”cd path/to/submodulegit log --oneline --decorate --graph -n 20必要であれば、サブモジュール内で merge や rebase を行って、最終的に採用したいコミットを作ります。
3. 親リポジトリで参照先を確定
Section titled “3. 親リポジトリで参照先を確定”cd ..git add path/to/submodulegit commit -m "Resolve submodule pointer conflict"git add path/to/submodule は、サブモジュール自体のファイル中身ではなく「どのSHAを指すか」をステージします。
よくある勘違い
Section titled “よくある勘違い”内容が同じならコンフリクトしない?
Section titled “内容が同じならコンフリクトしない?”いいえ、コンフリクトします。
内容が同じでも、コミットSHAが異なれば別コミットとして扱われるため、サブモジュール参照は競合します(上記ケース4)。
例えば次のようなケースです。
- チームA: 同じ修正をコミットしてSHAが
abc1234 - チームB: 同じ修正を別コミットとして作成してSHAが
def5678
コード差分は実質同じでも、親リポジトリから見ると abc1234 と def5678 は別参照です。
このとき、サブモジュール参照更新が同時に入ると競合します。
サブモジュール内でコンフリクトしなければ親も安全?
Section titled “サブモジュール内でコンフリクトしなければ親も安全?”そうとは限りません。
サブモジュール内のマージがきれいに終わっても、親リポジトリ側では「どのコミットSHAを指すか」が食い違っていれば競合します。
解消は親リポジトリで参照先SHAを1つに確定し、必要ならサブモジュール側で採用コミットの内容を確認します。
再発防止のコツ
Section titled “再発防止のコツ”- サブモジュール更新を含むPRは、できるだけ小さく保つ
- マージ前に親リポジトリとサブモジュールの両方で最新取り込みを行う
- 「サブモジュール更新PR」と「別機能PR」を分けてレビューしやすくする
- どのコミットを採用するかをPR説明に明記する