実務の失敗談から学ぶ！git reset,cherry-pick,rebaseを完璧に理解する

# はじめに
今回は実務経験半年の私が、実務での失敗談を通じて、チーム開発では必ずと言って良いほど使われるGitの使い方についてまとめていきます。なんとなくの理解で作業をすることの愚かさを身をもって体験しましたので、その後の学習の記録を共有いたします。ぜひ読者の方も何が間違いでどうすればよかったか考えながらお読みください！
間違いなどありましたら、コメントでご指摘ください！

# 前提
- ブランチの概念は理解している
- 作業をリモートに`push`することはできる
- コマンドの細かい仕様は理解していない

# 何をしたのか
※以下は**誤った手順**を含みます。**絶対にそのまま真似しないでください。**
1. `git switch -c feature-xxx`でブランチを切り作業をする
2. `git add xxx`で作業ファイルをステージング
3. `git commit -m'commit message'`でコミット
4. `git push origin feature-xxx`でリモートにプッシュ
5. 2~4繰り返し
6. `git add xxx`で誤ってコミットしたくないファイルをステージング
7. `git reset --hard HEAD~`
8. `git cherry-pick <リモートの最新のコミットID>`
9. `git add xxx`で目的のファイルをステージング
10. `git commit -m'commit message'`
11. `git push origin feature-xxx`←pushできない
12. `git pull origin feature-xxx`←pullできない
13. `git pull --rebase origin feature-xxx`
14. `git push origin feature-xxx`

# それぞれのコマンドの意味
## `git switch`
ブランチを切り替えるコマンド。  
`-c` オプションで新しいブランチを作成して同時に切り替える。

## `git reset`
`git reset` は、**HEAD（現在位置）を別のコミットに移動し、その際にステージ（index）や作業ツリー（working directory）もどう扱うかをオプションで指定**するコマンドです。

---

#### `git reset --soft`

- **HEAD だけを移動する**
- ステージ（index）と作業ツリーは **そのまま**
- 主な用途：**直前のコミットをなかったことにして、やり直したいとき**
#### **使いどころ：**
- `git commit --amend` したいとき
- 誤ってコミットしたけど、メッセージなどを直したいとき

---

#### `git reset --mixed`（デフォルト）
- HEAD を移動し、ステージ（index）をリセット
- 作業ツリーはそのまま（ファイルの変更内容は残る）
#### **使いどころ：**
- 一度コミットしたけど「やっぱり add するファイルを見直したい」とき
- ステージしたファイルを 一括で解除したいとき

---

#### `git reset --hard`
- HEAD・ステージ・作業ツリーすべてを指定コミットの状態に戻す
- 変更内容は 完全に破棄される（reflog以外で復元困難）
#### **使いどころ（慎重に！）：**
- 明らかに不要なコミットとファイル変更を完全に取り除きたいとき
- 作業中のごちゃごちゃを一旦すべて消したいとき（ただし注意）

#### **重要：**--hard はローカルでしか使わないようにしよう。
誤って共有ブランチで使うと他人の履歴を壊すことになります。

## `git cherry-pick`
指定したコミットだけを現在のブランチに取り込む。
- 内容が同じでも新しいコミット（別のコミットID）として追加される
- このため push 時に「リモートと履歴が違う」とされ、拒否される
## `git push`
ローカルの変更をリモートに反映する。
- リモートに同名ブランチがあり、かつ履歴が異なる場合は拒否される
- これは Git が「上書きしていいのか判断できない」ため
## `git pull`
リモートの変更をローカルに取り込むコマンド。
- デフォルトでは git fetch + git merge の動作（= マージ型pull）
- 履歴がローカルとリモートで分かれている場合、**明示的な統合方針が必要**
#### pull における merge と rebase の違い
| 比較項目 | git pull（マージ | git pull --rebase |
| --- | --- | --- |
| 履歴の構造 | 分岐が残る | 直線的に並ぶ |
| コミットの数 | そのまま（マージコミット含む）| コミットが付け直される（hashが変わる） |
| 履歴の見やすさ | ごちゃつくことがある | きれいで追いやすい |
| コンフリクト解消 | 1回で済むことが多い | 各コミットごとに必要なこともある |
| 推奨場面 | チーム開発で複数人が同時に作業するブランチ | 個人開発やfeatureブランチ、レビュー前の整理用 |

---
では今回の事例では何が起こったのか、次節でイメージ図とともに解説していきます。

# 何が起こったのか
## 前提の状態（正常な履歴）
まず、以下のようにローカルとリモートが同期していたとします。
```text
A---B---C   ← origin/feature-xxx
A---B---C   ← local HEAD（feature-xxx）
```
---

## ① git reset --hard HEAD~ を実行（Cを削除）
この操作で 1つ前（B）まで履歴を巻き戻し、コミットCもファイルの変更も **完全に消える**。
```text
A---B---C   ← origin/feature-xxx（リモートにはまだある）
A---B       ← local HEAD（Cが消えた）
```
※ この時点でローカルとリモートの履歴が **分岐(diverged)** してしまっています。

---

## ② git cherry-pick <CのコミットID> を実行
ローカルで、リモートの C を cherry-pick（適用）した状態
```text
A---B---C   ← origin/feature-xxx
A---B---C'  ← local HEAD（C' は cherry-pick された新しいコミット）
```
※ C と C' は内容が同じでも **コミットハッシュが異なる** 別履歴。

---

## ③ ここで git pull すると…？
Gitは以下のようなエラーを出します
```text
fatal: Need to specify how to reconcile divergent branches
```
git pull は内部的に git fetch + git mergeを実行しますが、ローカルとリモートの履歴が異なると、Gitは自動でどちらを優先すべきか決められません。
よって自分でどちらを使ってpullするかを指定しなければなりません。
この状態で git pull しようとした時の merge と rebase の違いを以下に図解します。

---

## git pull --rebase を実行した場合
```text
A---B---C---C''  ← local HEAD（C' を C の後ろに並べ直し）
A---B---C        ← origin/feature-xxx
```
- C' が C'' に置き換えられた
- 履歴が まっすぐに整い、push 可能に

---

## git pull --merge を実行した場合
```text
A---B---C-------M  ← local HEAD（mergeコミット）
         \     /
          C'---     ← ローカル履歴に分岐と統合あり
```
- M は Git による自動マージコミット
- 履歴が複雑化するが、push は可能になる

---

## 事の顛末
- `reset --hard` はコミットと変更内容を完全に消すため、履歴を壊すリスクが高い
- `cherry-pick` は内容を戻せても、履歴上は別コミット扱いになる
- 結果として `push/pull` 時に分岐とみなされ、エラーが出る
- 解決するには `git pull --rebase` または `--merge` を明示する

# どうすればよかったか

では今回の事例ではどうすればよかったのでしょうか？

## まず `git reset --hard` を使わない

今回のトラブルの根本原因は、`git reset --hard HEAD~` を実行して**ローカルの最新コミットと変更内容を完全に消してしまった**ことです。  
この操作は **履歴も作業内容も復元しづらくなるため、非常に危険**です。

---

## ステージングの取り消しは `git reset HEAD <ファイル名>`

もし `git add` を誤って実行してしまった場合は、以下のように**ステージ（add）だけを解除**すればよかったのです。

```bash
git reset HEAD ファイル名
```
- ファイルの内容はそのまま残る
- コミットもしない、履歴も壊れない
- 単に「ステージ解除」するだけなので、安全です

---

## 誤ってコミットした場合は --soft で巻き戻す
もし`commit`までしてしまった場合でも、`--soft`を使えば変更内容を保持したままコミットを取り消せます。
```bash
git reset --soft HEAD~
```
- 最新のコミットを取り消すが、内容はステージに残る
- 再度修正してコミットし直せばOK

---

## 内容やメッセージを直したいだけなら `git commit --amend`
```bash
git commit --amend
```
- メッセージの書き換え、ファイルの差し替えができる
- 履歴が1コミットで済むため、見た目がきれい

---

## `push`前にリモートの変更を取り込む（`pull --rebase`）
```bash
git pull --rebase origin feature-xxx
```
- ローカルとリモートの履歴がズレている場合、これでリモートの履歴を取り込んでから、自分の変更を後ろに付け直す
- 履歴が「まっすぐ」になり、`push`での衝突も起きづらくなる

---

## 消したコミットを復元したいなら`git reflog`
万が一`reset --hard`をしてしまった場合でも、Gitは内部で履歴を一時的に保存しています。
直後であれば`git reflog`でコミットの履歴を確認し、元に戻すことも可能です。
```bash
git reflog
git reset --hard <元のコミットID>
```

---

## push 後の履歴修正は原則避ける
リモートに`push`したコミットを`reset --hard`や`rebase`で書き換えた場合、履歴がズレて`push`できなくなることがあります。
- `push`してから履歴を壊すのは基本NG
- どうしても必要な場合は`--force`が必要だが、チーム開発では注意が必要

# まとめ

今回の失敗から学んだことは、**Gitの履歴操作は慎重に行うべき**ということです。特に `reset --hard` や `cherry-pick` は強力な分、使い方を誤ると履歴が壊れ、`push`や`pull`ができなくなる原因になります。

---

## 教訓

- `reset --hard` は履歴も変更も消える。**ほぼ最終手段**
- `cherry-pick` は見た目が同じでも別履歴になる
- `pull` できないのは履歴がズレているサイン。`--rebase` で整える
- 間違ったら `reset --soft` や `reflog` で安全に戻す

---

## 安全な対応まとめ

| 状況 | 正しい対応 |
|------|-------------|
| `add` だけ戻したい | `git reset HEAD <ファイル名>` |
| コミットをやり直したい | `git commit --amend` or `reset --soft` |
| push 前にズレを整えたい | `git pull --rebase` |
| 消えた履歴を戻したい | `git reflog` → `reset` |

---

Gitは「積み重ねる」ツールです。**焦らず、一歩ずつ修正する**のが一番の近道でした。

# 参考

https://git-scm.com/docs