Git の変更を取り消す | git reset
git reset コマンド
git revert が変更を取り消す「安全な」方法だとすれば、git reset は「危険な」方法だといえる。git reset で変更を取り消し、さらに ref や reflog から元のコミットを参照できなくなると、元の状態へ復元する方法がなくなる場合がある。つまり、この「取り消し」操作自体を取り消せないことがある。git reset は Git で作業結果を失う可能性がある数少ないコマンドの 1 つなので、使うときは注意が必要である。
git checkout と同じく、git reset も複数のモードを持つ応用範囲の広いコマンドである。コミット済みのスナップショットを削除する目的にも使えるが、ステージング領域や作業ディレクトリの変更を取り消す目的で使われることの方が多い。いずれにしても、このコマンドはローカル変更の取り消しに限定して使うべきであり、他の開発者に公開済みのコミットを取り消すために使ってはならない。
使い方
git reset <file>
作業ディレクトリには何も変更を加えず、指定したファイルをステージング領域から取り除くコマンドである。変更内容を破棄せずに、そのファイルのステージングだけを解除する。
git reset
作業ディレクトリには何も変更を加えず、ステージング領域を直前のコミット時点の状態に戻すコマンドである。すべてのファイルのステージングを解除し、ステージ済みスナップショットを最初から組み直せる。
git reset --hard
ステージング領域と作業ディレクトリを、直前のコミット時点の状態に戻すコマンドである。--hard フラグは、変更のステージング解除に加えて、作業ディレクトリ内のすべての変更を破棄するよう Git に指示する。つまり、コミット前のすべての変更をなかったことにするコマンドなので、ローカルマシンで行った作業を本当に削除してよいか確認する必要がある。
git reset <commit>
現在のブランチの先端を <commit> の位置へ移動し、ステージング領域をその状態に合わせて戻す。ただし、作業ディレクトリはそのまま残す。このコマンドを実行すると、<commit> 以降の変更は作業ディレクトリに残るため、より小さく整理されたスナップショットを作り直して再コミットできる。
git reset --hard <commit>
現在のブランチの先端を <commit> の位置へ移動し、ステージング領域と作業ディレクトリをその状態に合わせて戻す。このコマンドを実行すると、コミット前の変更だけでなく、<commit> 以降に作られたすべてのコミットもなかったことになる。
オプション
--soft: index と作業ツリーを保持する。つまり両方とも保持する。--mixed: index を取り消し、作業ツリーだけを保持する。デフォルトのオプションである。--hard: index と作業ツリーの両方を取り消す。
補足説明
上で説明したコマンドは、いずれも何らかの形でリポジトリの状態から変更を取り除く。--hard フラグがない場合、git reset は変更のステージングを解除したり、一連のスナップショットコミットを取り消してリポジトリを整理したりした後、再構築するための手段である。--hard は、開発中の実験的な作業結果が望ましくなく、それを完全になかったことにしたい場合に便利である。
revert が公開済みコミットを履歴を保ったまま安全に取り消す機能を持つのに対し、git reset はローカル変更を取り消すための機能である。この 2 つのコマンドは目的が異なるため、動作も異なる。reset は変更履歴を削除する一方、revert は変更履歴を保持したうえで、内容を元に戻す新しいコミットを作成する。
Git チュートリアル: revert と reset
公開済みコミットの取り消しは厳禁
<commit> 以降のスナップショットを一度でも公開リポジトリへ push した場合は、git reset <commit> を使ってはならない。コミットを公開したら、他の開発者がそれを前提に作業していると考えるべきである。
他の開発者が作業中に使っているコミットを取り消すと、共同作業に深刻な問題が発生する可能性がある。彼らがあなたのリポジトリと同期しようとすると、プロジェクト履歴の一部が欠落したように見える。典型的な例では、origin/master ブランチをローカル master ブランチに対応する中央リポジトリのブランチとして扱う。
Git チュートリアル: 公開済みコミットの取り消し
reset 後に新しいコミットを作成すると、Git はローカル履歴が origin/master から分岐したものとして扱う。ローカルリポジトリを同期するために作られるマージコミットは、他の開発者を混乱させ、作業を妨げる可能性がある。
重要なのは、git reset <commit> はうまくいかなかったローカルの実験的な開発作業を破棄するためのものであり、公開済みの変更を取り消すためのものではないと認識することである。公開済みコミットを修正する必要がある場合は、その目的のために用意された git revert を使う。
使用例
ファイルのステージングを解除する
git reset は、ステージ済みスナップショットを作成するときによく使われる。次の例では、hello.py と main.py の 2 つのファイルがすでにリポジトリに追加されていると仮定する。
# Edit both hello.py and main.py
# Stage everything in the current directory
git add .
# Realize that the changes in hello.py and main.py
# should be committed in different snapshots
# Unstage main.py
git reset main.py
# Commit only hello.py
git commit -m "Make some changes to hello.py"
# Commit main.py in a separate snapshot
git add main.py
git commit -m "Edit main.py"
このように、git reset を使うと次のコミットに含めるべきでない変更のステージングを解除し、各コミットの目的を明確にできる。
ローカルコミットを削除する
次の例は、より高度なユースケースである。ここでは、しばらく実験的な開発を行い、いくつかのスナップショットをコミットした後、それらをすべて削除したい場合を想定する。
# Create a new file called `foo.py` and add some code to it
# Commit it to the project history
git add foo.py
git commit -m "Start developing a crazy feature"
# Edit `foo.py` again and change some other tracked files, too
# Commit another snapshot
git commit -a -m "Continue my crazy feature"
# Decide to scrap the feature and remove the associated commits
git reset --hard HEAD~2
git reset HEAD~2 は現在のブランチを 2 コミット分だけ前へ戻すコマンドであり、実質的には最近作成した 2 つのスナップショットをプロジェクト履歴から削除する。前述のとおり、このような reset は未公開コミットに限定して使うべきである。コミットを公開リポジトリへ push した場合は、決して上記の操作を行ってはならない。
さまざまな使用例
index への追加を取り消す
対象ファイルを index に追加したことを取り消す。作業ツリーの変更内容は保持される。
git reset [file]
git reset HEAD [file]
commit を取り消す
最後のコミットを取り消し、作業ツリーは保持する。コミットはしたが push していない場合に便利である。
git reset HEAD^
最後の 2 つのコミットを取り消し、作業ツリーは保持する。
git reset HEAD~2
最後の 2 つのコミットを取り消し、index と作業ツリーの両方を元に戻す。
git reset --hard HEAD~2
マージしたものをすでにコミットしている場合、そのコミットを取り消す。誤った merge をすでにコミットしてしまった場合に便利である。
git reset --hard ORIG_HEAD
HEAD で変更した内容を取り消す新しい commit を発行する。commit をすでに push してしまった場合に便利である。
git revert HEAD
作業ツリー全体を元に戻す
作業ツリー全体を最後のコミット状態に戻す。最後の commit 以降の作業ツリーと index の変更はすべて消える。変更をコミットしていない場合に便利である。
git reset --hard HEAD