Gitでブランチに名前を付けずに（detached HEADのまま）作業する方法

## 概要
Gitでバージョン管理されたプロジェクトで簡単な作業をする際、私はローカルにブランチ名を付けることさえサボることがあります（後で消すのが面倒なので）。あまり勧められた方法ではないですが、この「detached HEAD」でも怖がらずに作業できるようになれば、Gitの仕組みの理解が進み、より応用も利くようになります。

本記事では、ローカルにブランチ名を付けずに新しく変更をコミットしてリモートへpushするまでのgitコマンドを纏めておきます。また念のため、見失ったコミットを探し出す方法も記します。

## 操作方法
checkoutとpushでの指定が少し複雑になるだけで、することは基本的にいつも通りです。

### fetch/pull
リモートの状態をローカルに反映します。これはdetachedかどうか関係なく、普通と同じです。

```bash
git fetch  # origin から取得
```

（個人的にpullはほとんど使っていません）

---

反映できたら、コミット履歴をグラフ表示して全体の様子を眺めます。**ここに表示されるコミットIDやリモートブランチなどは次で使います。**どのコミットから新しい変更を加えていくか想像しておくと作業しやすいです。

```bash
git log --all --graph --oneline
```

### checkout
detachedにするには、*ローカルブランチを指さないように*チェックアウト位置を指定します。例えば、

- `origin/xxx` のようにリモートブランチを指定する
- タグ名を指定する
- コミットIDを指定する（先頭4桁以上）
- 相対位置を追加して指定する

```bash
git checkout origin/${remote_branch}

git checkout ${tag}

git checkout ${commit_id}

git checkout HEAD~3  # 現在の3つ前のコミット
```

成功すれば「'detached HEAD'の状態になった」というお知らせが出ます（元々がdetachedでなかった場合）。とくに重要なことは書いてないので、意図的にdetachedにしたなら気にしなくて大丈夫です。

```
Note: switching to 'origin/master'.

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 7777777 Release 1.0.0
```

---

ちなみにコミットログを表示すると、detachedがどういうものか少し掴めるかもしれません。

```text:$&#x20;git&#x20;log&#x20;-1&#x20;--oneline
// git checkout master の場合
7777777 (HEAD -> master, origin/master, origin/HEAD) Release 1.0.0

// git checkout origin/master の場合
7777777 (HEAD, origin/master, origin/HEAD, master) Release 1.0.0
```

`HEAD` が普通はローカルブランチを指しているのに対し、detachedでは浮いています。

### commit
detachedでも普通にコミットできます。

```bash
git add .
git commit
```

### push
ローカルからリモートに反映します。通常はローカルブランチ名か `HEAD` のみ指定していると思いますが、detachedの状態では情報が足りないため、ここを `${local_ref}:${remote_ref}` のようにして情報を与えます。

ローカルの位置はcheckoutと同様に様々な指定ができますが、以下では簡単のため現在位置 `HEAD` に統一します。

もし既にリモートにブランチが存在するなら、その名前を指定すれば大丈夫です。（必要に応じて `--force-with-lease` など使う点は、普通の場合と同じです）

```bash
git push origin HEAD:${remote_branch}
```

**新しいブランチを作るなら、 `refs/heads/xxx` と指定します。**名前を書くだけでは、Gitはそれがブランチだと推測できません。

```bash
git push origin HEAD:refs/heads/${remote_branch}
```

同じようにして新しいタグを付けることもできます。

```bash
git push origin HEAD:refs/tags/${tag}
```

これらはリモートへ直接反映しているため、ローカルには反映されません。（fetchすれば反映されます）

## 消えた履歴の救出方法
detached HEADの怖いところは、**他のブランチをcheckoutした際に今までのコミット履歴が消える（ように見える）**ことです。「ように見える」と書いた通り実際には残っているので、探す方法を知っておけば問題ありません。

一番簡単な方法は、checkout時のメッセージを確認することです。detachedのままブランチを進めた状態から他をcheckoutすると、コンソール上に注意書きが出ます。

```text:$&#x20;git&#x20;checkout&#x20;master
Warning: you are leaving 2 commits behind, not connected to
any of your branches:

  4567def test-commit-2
  0123abc test-commit-1

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> 4567def

Switched to branch 'master'
Your branch is up to date with 'origin/master'.
```

ここに出ているコミット一覧が、このままだと見えなくなるものです。コミットIDを覚えておけばまだ参照できますし、メッセージ通りに `git branch ${new_branch_name} ${latest_commit_id}` と実行してブランチに名前を付ければ今後も表示されます。

メッセージを見逃してコミットIDがわからなくなってしまったら、 `git reflog` で履歴を出して探します。

```text:$&#x20;git&#x20;reflog
7777777 (HEAD -> master, origin/master, origin/HEAD) HEAD@{0}: checkout: moving from 0123abc... to master
4567def HEAD@{1}: commit: test-commit-2
0123abc HEAD@{2}: commit: test-commit-1
```

---

基本的にGitで作ったコミットは簡単には抹消できません。コミットへの参照を消すことでその存在を隠します。通常はブランチに名前が付いているためcheckoutでHEADを動かしても参照が残りますが、detached HEADだと他に誰も参照していない可能性があり、移動すると「履歴が消える」ということが起こり得ます。

なお、参照されていないコミットが何ヶ月も残るかというとそうではなく、ガベージコレクションで消されることがあるそうです。必要そうなものは、コミットIDをメモしておくのでなく、きちんとブランチ名などを付けておきましょう。
https://git-scm.com/book/ja/v2/Gitの内側-メンテナンスとデータリカバリ