Git を使っていると、こんなコマンドを見かけます。
git show HEAD~
git show HEAD^
git reset --hard HEAD~2
git diff HEAD^2
ここで疑問になるのが、
HEAD~とHEAD^は何が違うの?
という点です。
普通の一本道のコミット履歴では、HEAD~ と HEAD^ はほぼ同じように見えます。
しかし、merge commit が登場すると意味が大きく変わります。
結論から言うと、
-
HEAD^は「何番目の親を選ぶか」 -
HEAD~は「1番目の親を何世代さかのぼるか」
です。
この記事では、実際のコミットグラフを使って違いを整理します。
まず HEAD とは
HEAD は、基本的には
現在チェックアウトしているコミット
を指します。
たとえば、
A---B---C
^
HEAD
という状態なら、HEAD はコミット C を指しています。
HEAD^ の意味
^ は、
親コミットを指定する
ための記法です。
HEAD^
は、
HEAD^1
と同じ意味です。
つまり、
HEAD の「1番目の親」
です。
通常のコミットは親を1つしか持たないので、
A---B---C
^
HEAD
の場合、
HEAD^
は B を指します。
A---B---C
^ ^
HEAD^ HEAD
HEAD~ の意味
~ は、
1番目の親をたどって、何世代さかのぼるか
を表します。
HEAD~
は、
HEAD~1
と同じ意味です。
先ほどと同じ、
A---B---C
^
HEAD
なら、
HEAD~
も B を指します。
つまり、このケースでは
HEAD^
HEAD~
は同じ結果になります。
では何が違うのか
違いがわかるのは、2つ以上の親を持つ merge commit の場合です。
たとえば、次の履歴を考えます。
D---E
/ \
A---B---C-------M
^
HEAD
M は merge commit です。
merge commit M には、2つの親があります。
D---E
/ \
A---B---C-------M
↑ ↑
親? HEAD
もう少し正確に書くと、
D---E
/ \
A---B---C-------M
↑ ↑
│ HEAD
│
分岐元
たとえば main ブランチ上で別ブランチを merge した場合、一般的には、
-
M^1= merge 前のmain側 -
M^2= merge してきたブランチ側
になります。
図にすると、
D---E
/ \
A---B---C-------M
↑ ↙ ↑
M^1 M^2 M
実際には、
M^1 = C
M^2 = E
です。
HEAD^1 と HEAD^2
merge commit の場合、^ の後ろの数字が重要になります。
HEAD^1
は、
HEAD の1番目の親
です。
今回なら、
HEAD^1 = C
です。
一方、
HEAD^2
は、
HEAD の2番目の親
です。
今回なら、
HEAD^2 = E
です。
つまり、
D---E
/ \
A---B---C-------M
↑ ↑ ↑
HEAD^1 HEAD^2
HEAD
となります。
HEAD~ は必ず「1番目の親」をたどる
一方、~ は親を選択するための記法ではありません。
~ は、
1番目の親を繰り返したどる
という意味です。
したがって、
HEAD~
は、
HEAD~1
であり、
HEAD^1
と同じです。
今回なら、
HEAD~ = C
です。
HEAD~2 はどうなる?
ここが重要です。
HEAD~2
は、
1番目の親を2回たどる
という意味です。
今回のグラフでは、
D---E
/ \
A---B---C-------M
まず、
M
↓ 1番目の親
C
さらに C の1番目の親をたどって、
C
↓
B
となります。
したがって、
HEAD~2
は B です。
A---B---C-------M
↑ ↑ ↑
HEAD~2 HEAD~ HEAD
HEAD^2 と HEAD~2 は全然違う
ここを押さえると、両者の違いがかなりわかりやすくなります。
先ほどのグラフでは、
D---E
/ \
A---B---C-------M
^
HEAD
HEAD^2
HEAD^2
は、
HEAD の「2番目の親」
なので、
E
を指します。
HEAD~2
HEAD~2
は、
HEAD から「1番目の親」を2回たどる
ので、
B
を指します。
つまり、
D---E
↑ \
HEAD^2 \
A---B---C---------M
↑ ↑
HEAD~2 HEAD
です。
これが ^ と ~ の本質的な違いです。
表にすると
| 記法 | 意味 |
|---|---|
HEAD^ |
HEAD の1番目の親 |
HEAD^1 |
HEAD の1番目の親 |
HEAD^2 |
HEAD の2番目の親 |
HEAD~ |
1番目の親を1回たどる |
HEAD~1 |
1番目の親を1回たどる |
HEAD~2 |
1番目の親を2回たどる |
HEAD~3 |
1番目の親を3回たどる |
覚え方としては、
^ = どの親?
~ = 何世代前?
とするとわかりやすいです。
一本道なら HEAD^ と HEAD~ は同じ
merge がない、
A---B---C---D
^
HEAD
という単純な履歴なら、
HEAD^
と
HEAD~
はどちらも C です。
さらに、
HEAD^^
は、
HEAD^1^1
つまり、
1番目の親の、さらに1番目の親
なので B です。
これは、
HEAD~2
と同じになります。
つまり一本道では、
HEAD^^
と
HEAD~2
は同じです。
HEAD^^ と HEAD^2 は同じではない
ここも紛らわしいポイントです。
HEAD^^
と、
HEAD^2
は意味が違います。
HEAD^^
これは、
(HEAD^)^
という意味です。
つまり、
HEAD の1番目の親 → その1番目の親
です。
HEAD^2
一方、
HEAD^2
は、
HEAD 自身の2番目の親
です。
merge commit で考えると違いがはっきりします。
D---E
/ \
A---B---C-------M
^
HEAD
この場合、
HEAD^ = C
HEAD^^ = B
HEAD^2 = E
です。
D---E
↑ \
HEAD^2 \
A---B---C---------M
↑ ↑ ↑
HEAD^^ HEAD^ HEAD
^ と ~ は組み合わせられる
^ と ~ は組み合わせることもできます。
たとえば、
HEAD^2~
と書くと、
- HEAD の2番目の親へ移動
- そこから1番目の親を1回たどる
という意味になります。
先ほどの例なら、
D---E
/ \
A---B---C-------M
まず、
HEAD^2
で E。
そこから、
~
で1つ前の D。
したがって、
HEAD^2~
は D になります。
D---E
↑ ↑ \
HEAD^2~ HEAD^2 \
A---B---C-----------M
↑
HEAD
実際に Git で確認してみる
コミット履歴は、
git log --graph --oneline --all
で確認するとわかりやすいです。
たとえば、
* abc1234 Merge branch 'feature'
|\
| * def5678 feature commit 2
| * 123abcd feature commit 1
* | 456def0 main commit
|/
* 789abcd base commit
のような履歴だったとします。
現在の HEAD が merge commit abc1234 なら、
git show --oneline HEAD^1
で main 側の親、
git show --oneline HEAD^2
で feature 側の親を確認できます。
また、
git show --oneline HEAD~2
なら、
1番目の親を2回たどったコミット
を確認できます。
コミットIDだけ確認したいなら、
git rev-parse HEAD^1
git rev-parse HEAD^2
git rev-parse HEAD~2
も便利です。
実用例1: 1つ前のコミットを見る
単純に1つ前のコミットを確認するなら、
git show HEAD~
でOKです。
git show HEAD^
でも、通常は同じ結果になります。
実用例2: 3コミット前との差分を見る
git diff HEAD~3 HEAD
とすると、
現在から、first-parent を3回さかのぼったコミットとの差分
を確認できます。
単純な一本道の履歴では、直感的に「3コミット前」と考えて問題ありません。
実用例3: merge 前の状態を見る
merge commit が HEAD の場合、
git show HEAD^1
とすると、
merge する直前の、自分側のブランチ
を確認できます。
たとえば、
git diff HEAD^1 HEAD
とすると、
merge commit によって、1番目の親から見て何が変わったか
を確認できます。
実用例4: merge された側を見る
merge commit の2番目の親を見るなら、
git show HEAD^2
です。
たとえば、
git diff HEAD^1 HEAD^2
とすれば、merge 時点における2つの親の差を確認できます。
注意: 「HEAD~2 = 2コミット前」とは限らない
普段の一本道の履歴では、
HEAD~2
を、
2コミット前
と説明しても、だいたい問題ありません。
ただし厳密には、
1番目の親を2回たどったコミット
です。
merge がある場合、
D---E
/ \
A---B---C-------M
M から見れば、Git の履歴上には E や D も存在します。
しかし、
M~2
は E や D には進まず、
M → C → B
と、1番目の親だけをたどります。
そのため、
~ = Nコミット前
と丸暗記するより、
~ = first parent をN回たどる
と理解しておく方が正確です。
まとめ
HEAD^ と HEAD~ の違いは、次の一言に集約できます。
^ は「どの親か」
~ は「1番目の親を何回たどるか」
たとえば、
HEAD^2
は、
HEAD の2番目の親
です。
一方、
HEAD~2
は、
HEAD の1番目の親を2回たどったコミット
です。
最後に、これだけ覚えておけばOKです。
HEAD^ = HEAD^1
HEAD~ = HEAD~1
HEAD^2 = 2番目の親
HEAD~2 = 1番目の親を2回たどる
一本道の履歴では違いが見えにくいですが、merge commit が登場すると、
^ = 親を選ぶ
~ = first-parent をさかのぼる
という違いがはっきり現れます。
迷ったときは、
git log --graph --oneline --all
でコミットグラフを表示しながら、
git show HEAD^1
git show HEAD^2
git show HEAD~2
を実際に試してみると、一気に理解しやすくなります。