Gitのdetached HEADで作ったコミットをブランチにする方法

​​Git​コマンドライン

どのブランチにもいない状態(detached HEAD)で作ったコミットは、その場で git switch -c <ブランチ名> を実行すればブランチになり、失わずに済みます。気付かずに別のブランチへ切り替えてしまったあとでも、切り替えたときの警告か git reflog に出るハッシュを git branch <ブランチ名> <ハッシュ> に渡せば取り戻せます。リリースのタグや昔のコミットに git checkout で切り替えて、そのまま修正をコミットしてしまったときに使う方法です。検証環境で git のコマンドを実際に実行し、ブランチにする前と後の出力をそのまま載せています。

補足

検証環境は Ubuntu 24.04、Git 2.43.0 です。作者の名前とメールアドレスは例示用の値(Neko <neko@example.com>)で、コミットの日時は環境変数で決めました。ページャーは使わない設定にしています。リモートのリポジトリへのプッシュ、git gc で実際にコミットが消えるまでの動き、Windows・macOS での動作は試していません。

タグに切り替えたまま、2件コミットした状態

main に3件コミットし、2件目の「料金表を追加」に v1.0 のタグを付けたリポジトリを用意しました。git checkout v1.0 でタグに切り替えると、detached HEAD になったことを知らせる長い案内が出ます。

console
$ git checkout v1.0
Note: switching to 'v1.0'.

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 switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at cf91611 料金表を追加

この状態のまま、price.txt を直して2件コミットしました(ファイルの編集と git add は省き、コミットの部分だけを載せています。以下も同じです)。コミットの出力の先頭が、ブランチ名ではなく [detached HEAD 28d2c50] になっています。

console
$ git commit -m "送料無料の条件を追記"
[detached HEAD 28d2c50] 送料無料の条件を追記
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git commit -m "代引き手数料を追記"
[detached HEAD 164991c] 代引き手数料を追記
 1 file changed, 1 insertion(+)

git status は、コミットする前の「HEAD detached at v1.0」から「HEAD detached from v1.0」に変わりました。コミットしたことで、HEAD が v1.0 から動いたことを示す表示です。git branch でも、* の付いた今いる場所はブランチ名ではなく (HEAD detached from v1.0) です。

console
$ git status
HEAD detached from v1.0
nothing to commit, working tree clean

$ git branch
* (HEAD detached from v1.0)
  main

履歴の図では、v1.0 のコミット(cf91611)から2件が枝分かれしています。ただし、この2件を指しているのは HEAD だけで、どのブランチにも入っていません。

console
$ git log --oneline --graph --all
* 164991c 代引き手数料を追記
* 28d2c50 送料無料の条件を追記
| * fbdf273 READMEに営業時間を追加
|/  
* cf91611 料金表を追加
* efa914a 最初のコミット

まだ切り替えていないなら、git switch -c でブランチにする

detached HEAD のまま git switch -c に新しいブランチ名を渡すと、今のコミットを指すブランチができ、そのブランチに切り替わりました。最初の案内にあった git switch -c <new-branch-name> のことです。

console
$ git switch -c fix/price
Switched to a new branch 'fix/price'

ブランチにした後は、git status と git branch に fix/price が出ます。履歴の図は前と同じで、コミットのハッシュも変わっていません。コミットを作り直したのではなく、そこを指すブランチを足しただけだからです。

console
$ git status
On branch fix/price
nothing to commit, working tree clean

$ git branch
* fix/price
  main

$ git log --oneline --graph --all
* 164991c 代引き手数料を追記
* 28d2c50 送料無料の条件を追記
| * fbdf273 READMEに営業時間を追加
|/  
* cf91611 料金表を追加
* efa914a 最初のコミット

git checkout の公式リファレンスの「DETACHED HEAD」の節では、git switch -c と同じ操作として git checkout -b が並べられ、ほかに git branch fix/price と git tag fix/price も挙げられています。この2つは、コミットを指すブランチやタグを作るだけで、HEAD は detached のまま残ります。

ブランチにしないまま main に戻ってしまったとき

もう一度 v1.0 に切り替えて(今度は git switch --detach v1.0)、README を直して1件コミットしました。そのまま git switch main で戻ると、どのブランチともつながっていないコミットを置いていく、という警告が出ました。警告には、置いていくコミットのハッシュ(7e09e2d)と、ブランチを作るためのコマンドが書かれています。

console
$ git commit -m "READMEに問い合わせ先を追加"
[detached HEAD 7e09e2d] READMEに問い合わせ先を追加
 1 file changed, 2 insertions(+)

$ git switch main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  7e09e2d READMEに問い合わせ先を追加

If you want to keep it by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> 7e09e2d

Switched to branch 'main'

切り替えは警告を出したうえで行われ、git log --all にも 7e09e2d は出なくなりました。git log の公式リファレンスによると、--all は、ブランチやタグなど refs/ の下にあるすべての参照と HEAD を起点にします。出るのは、そこからたどれるコミットだけです。

console
$ git log --oneline --graph --all
* 164991c 代引き手数料を追記
* 28d2c50 送料無料の条件を追記
| * fbdf273 READMEに営業時間を追加
|/  
* cf91611 料金表を追加
* efa914a 最初のコミット

警告を見逃したら、git reflog でハッシュを探す

警告が画面の上に流れてしまったときは、git reflog で HEAD が指してきたコミットの記録を見ます。HEAD@{1} の行が、main に戻る直前に作ったコミットです。1行目の「moving from」のあとにも 7e09e2d から始まるハッシュがあり、どのコミットから main に移ったかが分かります。

console
$ git reflog -3
fbdf273 HEAD@{0}: checkout: moving from 7e09e2d423d408baab1ae76c29d5c1c0b358504b to main
7e09e2d HEAD@{1}: commit: READMEに問い合わせ先を追加
cf91611 HEAD@{2}: checkout: moving from fix/price to v1.0

ハッシュを git branch に渡してブランチを作る

見つけたハッシュを git branch に渡すと、そのコミットを指すブランチができました。git switch -c と違って、今いる main からは切り替わりません。履歴の図に 7e09e2d が戻っています。

console
$ git branch fix/readme 7e09e2d

$ git branch
  fix/price
  fix/readme
* main

$ git log --oneline --graph --all
* 7e09e2d READMEに問い合わせ先を追加
| * 164991c 代引き手数料を追記
| * 28d2c50 送料無料の条件を追記
|/  
| * fbdf273 READMEに営業時間を追加
|/  
* cf91611 料金表を追加
* efa914a 最初のコミット

$ git status
On branch main
nothing to commit, working tree clean
注意

どこからも指されていないコミットは、いつまでも残るわけではありません。公式リファレンスによると、そうしたコミットはいずれ Git の定期的なガベージコレクションで消されます。HEAD の reflog の記録も、今の先端からたどれないものは、既定では30日を過ぎると git reflog expire で消されます(git config の公式リファレンスにある gc.reflogExpireUnreachable の説明)。置いていったコミットに気付いたら、早めにブランチを作ってください。

git reset --hard で消したコミットを reflog から取り戻す流れは、別の記事「git reflogで、resetで消したコミットを取り戻す方法」でも説明しています。

​Posted by 86