この記事では、detached HEAD がどういう状態なのかを git の内部構造から読み解きます。
あなたは、この2つのコマンドがどう違うか答えられるでしょうか?
- 問い: 下の 2 つのコマンドは同じでしょうか?違うでしょうか?
- A:
git checkout <ブランチ名>
- B:
git checkout <ブランチの指すSHA1>
- A:
正解は、違う です。
もし、答えに詰まってしまった場合は、ぜひこの記事を読んでみてください。
この解説の中で、下の2つのコマンドがどう違うのかが見えてきます。
(ちょっとだけ宣伝: 学生向けに git challenge というイベントを開いています。いくつか git にまつわる問題を公開しているので、興味がある方はチャレンジしてみてください!: http://alpha.mixi.co.jp/entry/2015/11/24/083300 )
detached HEAD 状態のメッセージ
git で作業をしていると、ときたま次のようなメッセージに出くわします:
Note: checking out 'HEAD^'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at 123abc0... Hoge fuga moge
_人人人人人人人人人人人人_
> 突然の detached HEAD <
 ̄YYYYYYYYYYYY ̄
さて、この detached HEAD ですが、どのような状態なのでしょうか?
これを確認するために、これから detached HEAD とそうでない状態の git の内部構造を覗いていきます。
detached HEAD じゃないときの git の状態
まず、普通の HEAD (= detached HEAD じゃないときの HEAD)の git の状態を確認します。
さきほどは、 git checkout
で detached HEAD のメッセージが出たことを思い出してください。
しかし、ブランチへの checkout だとメッセージが表示されません。
git checkout master
を実行してみましょう:
$ git checkout master
メッセージは表示されません。どうやら detached HEAD じゃないようです。
このときの git の状態を確認してみましょう。
まず、現在のコミットをあらわす HEAD がどのような状態なのか確認します。
この HEAD は .git/HEAD
というファイルが実体です。
このファイルの内容をみてみましょう:
$ cat .git/HEAD
ref: refs/heads/master
ref:
という謎の文字列のあとに、ファイルパスらしき文字列 refs/heads/master
が並んでいます。
実は、この refs/heads/master
は、master
ブランチの実体のファイルを指しています。
この他にも、ブランチの実体は .git/refs/heads
以下に作成されます。
では、.git/refs/heads
というディレクトリにどのようなファイルがあるか確認してみましょう:
$ ls .git/refs/heads
fuga hoge master moge
master
以外にも hoge
fuga
moge
というブランチがあるようです。
git branch
の実行結果とも一致しますね:
$ git branch
fuga
hoge
master
* moge
では、このブランチの実体のファイル .git/refs/heads/master
には何が書いてあるのでしょうか?
ファイルの内容を見てみましょう:
$ cat .git/refs/heads/master
3cf6c642573a6834fdf872964749d106efbc03d0
このように、ブランチの先頭にあるコミットの SHA1 が記録されていることがわかります。
そして、コミットの実体は、 .git/objects
内にあります。
では、detached HEAD ではないときの git の参照関係を整理します:
HEAD --> master --> コミット (3cf6c64)
.git/HEAD --> .git/refs/heads/master --> .git/objects/3c/f6c642573a6834fdf872964749d106efbc03d0
detached HEAD のときの git の状態
次に detached HEAD のときの git の状態をみてみましょう。
先ほどの master が指すコミットへ checkout してみてください。
detached HEAD の状態にすることができます:
$ git checkout 3cf6c64
Note: checking out '3cf6c64'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at 3cf6c64... Initial commit
先ほどと同じように、HEAD がどのような状態なのかを確認します。
$ cat .git/HEAD
3cf6c642573a6834fdf872964749d106efbc03d0
おや?detached HEAD じゃないときの内容とは違う状態になっていますね。
この 3cf6c64...
は、さきほと checkout したコミットの SHA1 と一致しています。
この参照関係を整理してみましょう:
HEAD --> コミット (3cf6c64)
.git/HEAD --> .git/objects/3c/f6c642573a6834fdf872964749d106efbc03d0
detached HEAD とは何か
先ほどの参照関係の違いから、detached HEAD とそうでない状態には次のような違いがあることがわかりました:
- 普通の HEAD: ブランチを指している
- detached HEAD: コミットを指している
この違いを踏まえると、detached かどうかは、HEAD が何を参照するかによって決まることがわかります。
ここから、冒頭の問いの答えが見えてきます。
- 問い: 下の 2 つのコマンドは同じでしょうか?違うでしょうか?
- A:
git checkout <ブランチ名>
- B:
git checkout <ブランチの指すSHA1>
- A:
Aは、ブランチへの checkout ですから、HEAD
→ master
→ コミットという参照関係になります。
したがって、普通の HEAD になります。
Bは、コミットへの checkout ですから、HEAD
→ コミットという参照関係になります。
したがって、detached HEAD になります。
つまり、「2つのコマンドは違うもの」が答えでした。
detached HEAD から回復する方法
では最後に、どのようにしたら detached HEAD から回復できるかについてみていきましょう。
先ほど説明した通り、detached HEAD は直接コミットを参照している状態です。
これをブランチへの参照へと直す必要があります。
では、現在の HEAD のコミットから、元のブランチを探してみましょう。
その前に、現在のコミット HEAD
の SHA1 が必要なので、これを調べます。
先ほど説明した、 .git/HEAD
をみてもよいですが、ここでは git rev-parse
コマンドを使ってみましょう。
$ git rev-parse HEAD
3cf6c642573a6834fdf872964749d106efbc03d0
現在の HEAD が 3cf6c64...
であることがわかりました。
この SHA1 から、ブランチを探してみます。
.git/refs/heads
の中から、SHA1 が一致するファイルを探してくればいいのですが、ちょっと面倒ですよね。
そこで、 git name-rev
という便利コマンドを使います。
$ git name-rev 3cf6c642573a6834fdf872964749d106efbc03d0
3cf6c642573a6834fdf872964749d106efbc03d0 master
このように、 3cf6c64...
というコミットは master
が指すコミットであることがわかりました。
つまり、 detached HEAD が 3cf6c64...
であれば、 master
にチェックアウトすれば、detached じゃなくなるわけです。
この場合、 detached HEAD から脱出するコマンドは、 のようになります:
$ git checkout master
基本的には、このような手順で detached HEAD から脱出することができます。
ただし、この方法では脱出できないケースも稀にあります。
たとえば、checkout 後にブランチが進んでしまった/戻ってしまったパターンですね。
進んでしまった場合は、簡単です。
git name-rev
コマンドを実行すると、次のような表示になります(SHA1は適当です)
$ git name-rev 4b669c86f9c61635b13d0358b1b5a525dd9e6d67
4b669c86f9c61635b13d0358b1b5a525dd9e6d67 master~1
master~1
という表示がありますね。
さきほどは、SHA1 がピッタリ master と一致していたため、 単に master
と表示されていました。
実は、 ~1
は 1 つ前のコミット、という意味になっています1。
ブランチが進んでしまった場合は、 master~1
のように表示されるため、 ~数字
の前の部分がブランチ名ということがわかります。
難しいのは、ブランチが戻ってしまった場合です。
この場合、 git name-rev <SHA1>
は何も結果を返しません。
こういうときは、detached じゃない状態に戻るのが難しくなってきます。
具体的には git reflog
から戻すことになります。
ちょっと長くなるので、ここでは取り上げません。
では、detached HEAD から脱出する方法をまとめます。
$ git rev-parse HEAD
<detached HEAD の SHA1>
$ git name-rev <detached HEAD の SHA1>
<detached HEAD の SHA1> <ブランチ名>
$ git checkout <ブランチ名>
終わりに
どうだったでしょうか?
今回のように、 git の内部構造を知ると、 git の実力がメキメキあがっていきます。
ぜひ、git の内部構造を調べてみましょう!
-
ちなみに、この記法は git の revision というものです。この revision 記法には、すごくいろいろな種類があって、一覧が https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html にありますので、読んでみるとおもしろいです。 ↩