Edited at

detached HEAD から脱出する方法を git の内部構造から探る

More than 3 years have passed since last update.

この記事では、detached HEAD がどういう状態なのかを git の内部構造から読み解きます。

あなたは、この2つのコマンドがどう違うか答えられるでしょうか?


  • 問い: 下の 2 つのコマンドは同じでしょうか?違うでしょうか?


    • A: git checkout <ブランチ名>

    • B: git checkout <ブランチの指すSHA1>



正解は、違う です。

もし、答えに詰まってしまった場合は、ぜひこの記事を読んでみてください。

この解説の中で、下の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は、ブランチへの checkout ですから、HEADmaster → コミットという参照関係になります。

したがって、普通の 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 から脱出するコマンドは、 :point_down: のようになります:

$ 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> は何も結果を返しません。 :scream:

こういうときは、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 の内部構造を調べてみましょう!





  1. ちなみに、この記法は git の revision というものです。この revision 記法には、すごくいろいろな種類があって、一覧が https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html にありますので、読んでみるとおもしろいです。