元に戻すgitコマンド その5（過去の特定のコミットまで戻りたいと思った時 編）

## はじめに

Webエンジニアになって２年目。そろそろ、`add`、`copmmit`、`push`、`pull`、あと、ブランチ切ること以外も覚えたいなと思い、Gitでずっと気になってた各種**「元に戻す操作」**を勉強がてら記事を書こうと思いました。

Gitの操作をしていると、失敗して元に戻したい時は結構多いんじゃないかなーと思います。戻したい場面はいくつかあると思います。

今回は、**過去の特定のコミットまで戻りたいと思った時**の対処について書いていこうと思います。

## 過去のコミット時点でのファイル内容を取得

`practice.txt`に`追加分1`を記入し、コミットし、`追加分2`を記入してはコミットするを5回繰り返しました。

```practice.txt
練習用のテキストファイル

追加分1
追加分2
追加分3
追加分4
追加分5
```

ついでにリモートに`push`して、ログを見て見ました。

```shell
$ git log --oneline

136f0da (HEAD -> master, origin/master) add code 5
c372ce4 add code 4
e1caddd add code 3
f146df3 add code 2
d88d5ff add code
```

追加分1のコードだけを残したいと思います。`git show コミットID`で確認して見ましょう。

``` shell
$ git show d88d5ff
commit d88d5ff1283aa86432ab3d45436be149f25e25be
Author: name
Date:   Fri Jun 1 04:04:46 2018 +0000

    add code

diff --git a/practice.txt b/practice.txt
index ff49fb7..7d2e919 100644
--- a/practice.txt
+++ b/practice.txt
@@ -1 +1,3 @@
-練習用のテキストファイル
\ No newline at end of file
+練習用のテキストファイル
+
+追加分1
```

追加分2~5を記入する前の状態に戻りたい時は、`git reset コミットID`で戻れます。

```shell
$ git reset d88d5ff
Unstaged changes after reset:
M       practice.txt
```

下の図を見てください。

<img src="https://git-scm.com/images/about/index1@2x.png">

上の図は、コミットまでに必要な２段階手順の模式図[^1]です。

この図を見ながら、説明します。ワーキングディレクトリとステージングエリアの状態と、コミット履歴を確認します。

```shell
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   practice.txt

no changes added to commit (use "git add" and/or "git commit -a")

$ git log --oneline
d88d5ff (HEAD -> master) add code
...
```

ファイルの中身は、変わってません。

```practice.txt
練習用のテキストファイル

追加分1
追加分2
追加分3
追加分4
追加分5
```

つまり、先ほどの`$ git reset d88d5ff`を実行したことによって、コミットログは消えましたが、ワーキングディレクトリには、最新コミットまでの内容が入っていました。

```practice.txt
$ git diff
diff --git a/practice.txt b/practice.txt
index 7d2e919..d9c1c95 100644
--- a/practice.txt
+++ b/practice.txt
@@ -1,3 +1,7 @@
 練習用のテキストファイル
 
 追加分1
+追加分2
+追加分3
+追加分4
+追加分5
\ No newline at end of file
```

`git diff`したら、追加分1はコミットしてある状態とわかりました。

一度、コミットして`log`を見て見ましょう。

```shell
$ git add practice.txt
$ git commit -m "add code2~5"

$ git log --oneline
c2d276a (HEAD -> master) add code2~5
d88d5ff add code
```

ファイルの中身はまったく同じなのですが、最新のコミットIDが、`136f0da`だったのに、`c2d276a`に変わってしまいました。

なぜかというと、コミットIDがファイルの変更だけでなく、コミット時刻やAuthorなど、様々なパラメータの情報から計算された値だからです。そのため、コミット時刻やAuthorなどが違う今回のコミットと、前回のコミットは、全く違うコミットとして扱われます。

そこで以下のことに注意してください。

## 注意すること

`git`で元に戻す操作は、これまでのコミットの履歴を書き換えることになります。すでに取り込まれたコミットを書き換えるということは

- コミットメッセージ
- コミットした人
- リビジョン番号（コミットID）
- 変更日時
- 変更ファイル

などが書き換わります。ずっと一人開発のプトジェクトであれば、そこまで大きな問題にはならないと思いますが、チーム開発の場合は大きな問題となり得る行為なので、仮にやる際はメンバー合意の上、十分注意して行ってください。

## `git reset` その他のオプション

`git reset`には、以下のようなオプションがあります。

| オプション | 結果 |
|-----------|------------|
| --soft       | 戻した分のコミット内容をインデックスに反映する        |
| --mixed     | オプション無しと同じ動作で、戻した分のコミット内容だけワーキングディレクトリに反映する      |
| --hard       | ワーキングディレクトリもコミット履歴も完全削除       |

### 試してみよう`git reset --hard`

このコマンドは、ワーキングディレクトリやコミット履歴まで消去してしまうとても恐ろしいgitコマンドです。

```shell
$ git reset --hard コミットID
```

これまでのを例に出すと、`追加分1`までのコードもコミット履歴もなくなりました。

```practice.txt
練習用のテキストファイル

追加分1
```

ちなみに、このままでは、リモートレポジトリに`push`できません。リモートレポジトリは、ローカルの`git reset`は行われていないので、削除された分を反映しべきかどうかがわからないいわゆる、Non Fast Forward 問題でpushできないとなります。

通常なら`git fetch`して`git merge`してコンフリクト消せば問題なく`push`できるとは思いますが、今のローカルの状態を強制的に反映させることもできます。それが、フォースプッシュです。

```shell
git push origin master -f
```

これで、ローカルで消えたコミット履歴までリモートに反映させてしまいました。このファイル上書き感は、FTP育ちのWEB制作な自分にとっては煩わしくなくて好きなのですが、危険性も考えた上で行ってください。少なくとも、チーム開発においては、勝手には絶対やらない方がいいと思います。

## `git reset`を元に戻す場合

間違って、リセットしちゃった場合も考えて、それの戻し方も説明していきます。`git reflog`というものを使うと、`reset --hard`とかも含めた復元をしてくれます。

```shell
$ git feflog

d88d5ff (HEAD -> master, origin/master) HEAD@{0}: reset: moving to d88d5ff
c349449 HEAD@{1}: pull origin master: Merge made by the 'recursive' strategy.
7a2ed7d HEAD@{2}: commit: add code2~5
d88d5ff (HEAD -> master, origin/master) HEAD@{3}: reset: moving to d88d5ff
c2d276a HEAD@{4}: commit: add code2~5
d88d5ff (HEAD -> master, origin/master) HEAD@{5}: reset: moving to d88d5ff
136f0da HEAD@{6}: commit: add code 5
c372ce4 HEAD@{7}: commit: add code 4
e1caddd HEAD@{8}: commit: add code 3
f146df3 HEAD@{9}: commit: add code 2
d88d5ff (HEAD -> master, origin/master) HEAD@{10}: commit: add code
```

ためしに、最初に`git reset`をした`136f0da`まで戻ってみましょう。そして、`log`を見て見ましょう。リモートは`d88d5ff`で止まっていることがわかります。

```shell
$ git reset --hard 136f0da
HEAD is now at 136f0da add code 5

$ git log --oneline
136f0da (HEAD -> master) add code 5
c372ce4 add code 4
e1caddd add code 3
f146df3 add code 2
d88d5ff (origin/master) add code
```

コミットログもファイルも元に戻っていることがわかります。

```practice.txt
練習用のテキストファイル

追加分1
追加分2
追加分3
追加分4
追加分5
```

なので、間違えて`git reset`をしてしまっても、焦らず、`git reflog`して`git reset --hard`して戻りましょう。
でも基本的には、間違えて`git reset`しないことが望ましいですね。

## ファイル内容は戻して、コミット履歴を消さない方法（`git revert`）

`git reset`はコミット履歴を消してしまうため、チーム開発では使わない方がいいと思います。

この場合には、`git revert コミットID`が有効なコマンドになります。

`revert`コマンドを実行すると、エディタが開き、`revert`コミットのコミットメッセージを編集することができます。

ワーキングディレクトリ内のコードの内容は指定したIDまでの内容に戻しましたが、コミット履歴までは消さないのでチーム開発には有効だと思います。

図解は、

https://www.slideshare.net/tamotsufuruya/git-14116631

このslide shareの13ページ目が参考になりました。

以上です。

## 親記事（まとめ記事）

この記事の[親記事（まとめ記事？）](https://qiita.com/ryosuketter/items/34e429ae555179293fc3)がありまして、そこでは、gitを使ったさまざまな**元に戻す**方法の記事をまとめています。ぜひ、見てください。

## 参考
- https://git-scm.com/about/staging-area
- https://www.slideshare.net/tamotsufuruya/git-14116631

[^1]: https://git-scm.com/about/staging-area