0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Git の `HEAD~` と `HEAD^` の違いを、実例でわかりやすく理解する

0
Posted at

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^1HEAD^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^2HEAD~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~

と書くと、

  1. HEAD の2番目の親へ移動
  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 の履歴上には ED も存在します。

しかし、

M~2

ED には進まず、

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

を実際に試してみると、一気に理解しやすくなります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?