git resetは学んだばかりのころは、「--hardをつけて間違った操作をすると実装が消えて戻らなくなる恐れがある」という印象が強く、実装を取り消す怖いコマンドのイメージでした。
使うのはたいてい以下のコマンドでしたが、どういうときに取り戻しがつかなくなるのかあまり分かっていなかったなと思います。
git reset --hard HEAD~
しかし別の使い方を見かけました。
例えば以下のようなツリーになっていて、branch1にいる時に、
git reset --hard branch2
とすると、以下のようにbranch2と同じcommitまで移動することができるということを知りました。
変更をリセットする(取り消す)コマンドだと思っていたので、git checkout branch2
と何が違うのか、「リセット」と「commitの移動」にどう関係しているのかが分かっていませんでした。
このあたりについてresetコマンドを知ることが理解につながるので、resetについて説明していきます。
resetについては以下のドキュメントが分かりやすかったので、こちらを参考にまとめてみました。
正確な情報は上記のドキュメントを読んでください
補足: Gitのオブジェクト等について
以下ではHEADやcommitなどといったGitが扱っている、Gitオブジェクトやリファレンスなどがでてきますが、この記事で説明しようとすると大変なので、省略してざっくり理解できるように進めていきます。
知りたい方は個人的には以下の記事がとてもわかりやすかったです!
https://qiita.com/marchin_1989/items/2ec01553e907f3a9e6bb
またドキュメントも図解しながら説明されているので理解しやすかったです。
3つのツリー
Gitで扱っているツリーという概念になぞらえて以下3つを挙げています。
端的に言うとこんな感じです。
- HEAD → 最新のcommit
- インデックス → git addしたステージングエリア
- 作業ディレクトリ → 編集する実際のファイル
画像を引用しますが、以下のような流れで作業をしていきます。
ファイルを変更 → add → commit |
---|
以下はfile.txtに3回変更を加えた後の状態を示しています。
--soft
はじめに--softオプションから理解を深めます。
softはHEADを指定したところまで移動させますが、インデックスと作業ディレクトリは変更しません。
以下のように、Changes to be committed
に緑色の文字でcommitした内容が存在しています。
そして、git log
をすると、HEADが移動したことにより最終commitがfile.txt v2
のコミットとなるので、3つ目のcommitが取り消されたように見えます。
$ git reset --soft HEAD~
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file.txt
インデックスと作業ディレクトリは変更されないため、編集したファイルの変更内容は取り消されずに残っています。
もとに戻す
さて、次のオプションも試したいので、file.txt v3
のコミットまで戻りたいのですが、これもgit reset --soft
を使って戻すことができます。
前提として、file.txt v3
のコミットハッシュを知っておく必要があるのですが、この例だと、以下のように実行することで、元に戻すことができます。
git reset --soft 38eb946
これも指定したコミットまでHEAD (とブランチ) を移動したことにより、元に戻った状態となります。
先程の、↓の画像の矢印が逆に動いたことになります。
--mixed (デフォルト)
さて、元に戻ったので、今度はmixedオプションを試します。
これはデフォルトなのでフラグがなくても同様の結果となります。
mixedオプションはHEADとインデックスの内容を変更します。
以下のように、Changes not staged for commit
に赤色の文字でcommitした内容が存在しています。
$ git reset HEAD~
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file.txt
作業ディレクトリは変更されないため、編集したファイルの変更内容は取り消されずに残っています。
最後のhardオプションを試したいので元に戻します。
git reset 38eb946
--hard
そして最後に今回の目的のhardです。
hardオプションは作業ディレクトリにも変更を加えます。
hardオプションを実行すると、作業ディレクトリも変更されるので、git statusをしても差分が表示されません。
そして手元のファイルを開いてみると、最後のコミットの変更内容がなかったことのように元に戻っています。
$ git reset --hard HEAD~
HEAD is now at 9e5e6a4 file.txt v2
$ git status
On branch main
nothing to commit, working tree clean
このように、変更された内容が消されてしまった状態になるので注意が必要です。
でも実際にはcommitが消されたわけではないので、今回の例だと、file.txt v3
はcommitされており、ハッシュ値も分かっているので、前の例のように元に戻すことができます。
git reset --hard 38eb946
これで作業ディレクトリの内容も復元されます。
もとに戻せないケース
ただし、commitされていない状態だと元に戻すことができなくなります。例えば、
$ touch hoge.txt
$ git add hoge.txt
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: hoge.txt
とcommitされていないと、
$ git reset --hard
$ git status
On branch main
nothing to commit, working tree clean
このように作業ディレクトリが上書きされて、変更自体がなくなってしまいます。
最初の例
git reset --hard branch2
最初の例の上記のコマンドに戻ります。以下の状態のように
branch1にいる時に 上記コマンドを実行すると、HEAD、インデックス、作業ディレクトリすべてがbranch2のコミットと同じものに書き換えられるため、git checkout branch2
をしたときのように移動したかに見えました。
git checkout branch2
と違う点
実はcheckoutとの違いとしては2点あります。
- 未保存の変更の扱い
- resetはチェックが行われずにすべてが上書きされる
- checkoutは未保存の変更をチェックして、作業ディレクトリを守ろうとする
- HEADの更新方法
- resetはブランチが移動
- checkoutはHEADが別ブランチに移動するだけで、branchは変更されない
このような違いを使い分ける必要があります。