Git HEAD~ と HEAD^ の違い

Git でコミットを指定するとき、HEADの一つ前ならHEAD^、二つ前ならHEAD^^のようにしていた。一方、Git の公式ドキュメントを見ていたら、HEAD~HEAD~2が使われていた。

ただ書き方が違うだけで同じものだと前にどこかで読んだので特に気にしていなかったのだが、ちょっと気になって調べてみたら実際には意味が違うということがわかった。

コミットを指定するときに、~(チルダ)と^(キャレット)を使ってあるコミットからの相対位置で指定することもできます。この時に、よく使われるのがHEADです。~(チルダ)を後ろに付け加えることで何世代前の親かを指定することができます。^(キャレット)は、ブランチのマージで親が複数ある場合に、何番目の親かを指定することができます。
ブランチの切り替え|サル先生のGit入門【プロジェクト管理ツールBacklog】

  • ~数字:○世代前の親(数字が1の場合は省略可)
  • ^数字:○番目の親(数字が1の場合は省略可)

HEAD^^だと「一番目の親の一番目の親」を指すので、結果としてはHEAD~2「二世代前の親」と同じ。

一方、HEAD^2は「二番目の親」を指すので、HEAD^^HEAD~2とは意味が異なる。

たとえば、以下のように3回コミットした状態だとする。

$ git log --oneline --all
904a3f4 (HEAD -> master) コミットC
be2e433 コミットB
b7851bc コミットA

ここで、git reset HEAD~2またはgit reset HEAD^^をすると、1つ目のコミットであるコミットAに戻せる。

一方、git reset HEAD^2とするとエラーになった。「二番目の親」が存在しないためだ。

$ git reset HEAD^2
fatal: ambiguous argument 'HEAD^2': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

これは今まで知らなかった。実際親が複数あったとき、どれが何番目の親だってわかるのだろうか?

自分の場合は、基本的に「○世代前」のつもりで使っていたので、~を使った方がよさそう。

ちなみにgit reset HEAD~~と、~チルダを2つ並べたらHEAD~2と同じように動いたけど、これも実は何か違ったりするのだろうか。調べた限りでは、git reset HEAD~~HEAD~2は一緒っぽい。ただ、^を勘違いしていただけに、ドキュメントとか “根拠” を見ないと不安が残る…。

~^を駆使したコミット指定については、以下の記事が詳しくわかりやすかった。

yu8mada.com

この記事内で紹介されていたman git-rev-parse で表示されるドキュメント。自分の Mac でも見てみた。もはや脳トレみたいになってる。ここまでくるとハッシュで指定した方が早いような。面白いけど。

man git-rev-parse で表示されるドキュメント

G   H   I   J
\ /     \ /
  D   E   F
   \  |  / \
    \ | /   |
     \|/    |
      B     C
       \   /
        \ /
         A

A =      = A^0
B = A^   = A^1     = A~1
C = A^2  = A^2
D = A^^  = A^1^1   = A~2
E = B^2  = A^^2
F = B^3  = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2  = B^^2    = A^^^2  = A~2^2
I = F^   = B^3^    = A^^3^
J = F^2  = B^3^2   = A^^3^2