git reset
git reset
は変更を元に戻すための、複雑で汎用性のあるツールです。--soft、--mixed、--hard
のコマンドライン引数を使った、主に 3 つの形式で呼び出します。これら 3 つの引数はそれぞれ、Git にある 3 つの内部状態管理メカニズムであるコミットツリー (HEAD
)、ステージングインデックス、作業ディレクトリに対応しています。
git reset と 3 つの Git ツリー
git reset
の使い方を適切に理解するためには、まず Git の内部状態管理システムを理解しておく必要があります。これらのメカニズムは Git の「3 つのツリー」と呼ばれることがあります。従来のツリー型データ構造とは厳密には異なるため、ツリーという呼び方は必ずしも正確ではありません。しかし、編集のタイムラインを追跡する、ノードとポインターをベースにしたデータ構造になっているため、これらのメカニズムをわかりやすく説明するには、リポジトリにチェンジセットを作成して 3 つのツリーから辿っていくのが最適です。
まず初めに以下のコマンドで新しいリポジトリを作成します。
$ mkdir git_reset_test $ cd git_reset_test/ $ git init . Initialized empty Git repository in /git_reset_test/.git/ $ touch reset_lifecycle_file $ git add reset_lifecycle_file $ git commit -m"initial commit" [master (root-commit) d386d86] initial commit 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 reset_lifecycle_file
上のサンプルコードでは空のファイル reset_lifecycle_file
が 1 つ入った新しい Git リポジトリを作成しています。この時点で、サンプルリポジトリには reset_lifecycle_file
から追加した 1 つのコミット (d386d86
) があります。
作業ディレクトリ
最初に調べるツリーは「作業ディレクトリ」です。このツリーはローカルファイルシステムと同期しており、ファイルとディレクトリへの変更はこのツリーに即座に反映されます。
$ echo 'hello git reset' > reset_lifecycle_file
$ git status
On branch master
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git checkout -- ..." to discard changes in working directory)
modified: reset_lifecycle_file
デモリポジトリでは reset_lifecycle_file
を修正してコンテンツを追加しています。git status
を呼び出すと、ファイルへの変更を Git が認識していることがわかります。これらの変更は現在、1 つ目のツリーである「作業ディレクトリ」に属しています。git status
を使って作業ディレクトリへの変更を表示することができます。変更には「modified」のプレフィックスが付いた赤字で表示されます。
ステージングインデックス
次に「ステージングインデックス」ツリーを見ていきます。このツリーは、git add
でプロモートされて後続のコミットに保存される作業ディレクトリの変更を追跡します。ステージングインデックスは複雑な内部キャッシュメカニズムです。Git では通常、ステージングインデックスの実装の詳細をユーザーから隠そうとします。
ステージングインデックスの状態を正確に見るためには、あまり知られていない Git コマンド git ls-files
を使用する必要があります。git ls-files
は本来、ステージングインデックスツリーの状態を調べるデバッグユーティリティです。
git ls-files -s 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 reset_lifecycle_file
ここでは -s
または --stage
オプションを指定して git ls-files
を実行しました。-s
オプションを指定しない場合、git ls-files
出力にはインデックスに現在属しているファイル名とパスのリストだけが表示されます。-s
オプションではステージングインデックスのファイルの追加メタデータが表示されます。このメタデータはステージされたコンテンツのモードビット、オブジェクト名、ステージ番号です。ここでは 2 番目の値 (d7d77c1b04b5edd5acfc85de0b592449e5303770
) であるオブジェクト名に注目します。これは標準の Git オブジェクト SHA-1 ハッシュで、ファイルのコンテンツのハッシュです。コミット履歴にはコミットと ref へのポインターを識別するための独自のオブジェクト SHA ハッシュが保存されています。ステージングインデックスにはインデックス内のファイルのバージョンを追跡するための独自のオブジェクト SHA ハッシュがあります。
次に、修正した reset_lifecycle_file
をステージングインデックスにプロモートします。
$ git add reset_lifecycle_file
$ git status
On branch master Changes to be committed:
(use "git reset HEAD ..." to unstage)
modified: reset_lifecycle_file
ここではステージングインデックスにファイルを追加する git add reset_lifecycle_file
を呼び出しました。ここで git status
を呼び出すと「コミットされる変更」の下に緑で reset_lifecycle_file
が表示されます。git status
は実際にはステージングインデックスを表していないため、注意が必要です。git status
コマンドの出力には、コミット履歴とステージングインデックスの間の変更が表示されています。この時点のステージングインデックスのコンテンツを調べてみましょう。
$ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
reset_lifecycle_file
のオブジェクト SHAが e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
から d7d77c1b04b5edd5acfc85de0b592449e5303770
に更新されているのがわかります。
コミット履歴
最後のツリーはコミット履歴です。git commit
コマンドを実行すると、コミット履歴にある永続スナップショットに変更が追加されます。このスナップショットにはコミット時のステージングインデックスの状態も含まれています。
$ git commit -am"update content of reset_lifecycle_file" [master dc67808] update content of reset_lifecycle_file 1 file changed, 1 insertion(+) $ git status On branch master nothing to commit, working tree clean
ここでは「update content of resetlifecyclefile」
というメッセージを含む新しいコミットを作成しました。コミット履歴にチェンジセットが追加されました。この時点で git status
を呼び出すと、どのツリーにも保留中の変更がないことがわかります。git log
を実行するとコミット履歴が表示されます。これで 3 つのツリーからこのチェンジセットをたどったので、ここからは git reset
を使ってみましょう。
仕組み
表面上は、git reset
は git checkout
と挙動が似ています。git checkout
は HEAD
ref ポインターのみを操作しますが、git reset
は HEAD
ref ポインターと現在のブランチの ref ポインターを移動します。この挙動をわかりやすく説明するために、以下のサンプルを見てみてください。

このサンプルでは master
ブランチの連続するコミットを詳しく見ていきました。HEAD
ref と master
ブランチ ref はコミット d をポイントしています。それでは git checkout b
と git reset b.
を実行して結果を比較してみましょう。
git checkout b

git checkout
を実行しても、master
ref は d
をポイントしたままです。HEAD
ref が移動し、コミット b
をポイントしています。リポジトリは現在、「detached HEAD
」状態になっています。
git reset b

対照的に、git reset
は HEAD
とブランチ ref の両方を指定したコミットに移動します。
git reset
はコミットの ref ポインターを更新するだけでなく、3 つのツリーの状態を修正します。ref ポインターの修正は常に発生していて、3 番目のツリーであるコミットツリーが更新されます。コマンドライン引数 --soft、--mixed
、--hard
でステージングインデックスツリーと作業ディレクトリツリーの修正方法を指定します。
主なオプション
git reset
のデフォルトの呼び出しには --mixed
と HEAD
の暗黙の引数があります。つまり、git reset
を実行するいうことは、git reset --mixed HEAD
を実行しているのと同じことになります。このコマンドラインで、HEAD
は指定したコミットです。HEAD
の代わりに任意の Git SHA-1 コミットハッシュを使うこともできます。
--hard
これは最も直接的かつ "危険" で使用頻度の高いオプションです。--hard
を渡すと、コミット履歴の ref ポインターが指定したコミットに更新されます。その後、ステージングインデックスと作業ディレクトリは、指定したコミットの状態と同じになるようにリセットされます。ステージングインデックスと作業ディレクトリでこれまで保留されていたすべての変更は、コミットツリーの状態と同じになるようにリセットされます。つまり、ステージングインデックスと作業ディレクトリで確定していないすべての保留中の変更が失われることになります。
これをわかりやすく説明するために、前の手順で作成した、3 つのツリーがあるサンプルリポジトリを引き続き使っていきます。まずはリポジトリにいくつか修正を加えます。サンプルリポジトリで以下のコマンドを実行します。
$ echo 'new file content' > new_file $ git add new_file $ echo 'changed content' >> reset_lifecycle_file
これらのコマンドによって new_file
という名前の新しいファイルが作成され、リポジトリに追加されます。また reset_lifecycle_file
のコンテンツに修正が入ります。これらの変更を加えたところで、git status
を使ってリポジトリの状態を調べてみましょう。
$ git status On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) new file: new_file Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git checkout -- ..." to discard changes in working directory) modified: reset_lifecycle_file
リポジトリに保留中の変更があることがわかります。ステージングインデックスツリーには new_file
の追加に関する保留中の変更があり、作業ディレクトリには reset_lifecycle_file
の修正に関する保留中の変更があります。
次に進む前に、ステージングインデックスの状態を調べてみましょう。
$ git ls-files -s 100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
インデックスに new_file
が追加されたのがわかります。reset_lifecycle_file
を更新しましたが、ステージングインデックス SHA (d7d77c1b04b5edd5acfc85de0b592449e5303770
) に変化はありません。git add
を使ってこれらの変更をステージングインデックスにプロモートしていないため、これは想定していた挙動です。これらの変更は作業ディレクトリにあります。
では git reset --hard
を実行してリポジトリの新しい状態を調べてみましょう。
$ git reset --hard HEAD is now at dc67808 update content of reset_lifecycle_file $ git status On branch master nothing to commit, working tree clean $ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
ここでは --hard
オプションを使って「ハードリセット」を実行しました。Git には、HEAD
が最新のコミット dc67808
をポイントしていることを示す出力が表示されています。次は、git status
を実行してリポジトリの状態を確認してみましょう。Git には保留中の変更がないことがわかります。ステージングインデックスの状態も調べると、new_file
が追加される前の時点にリセットされていることがわかります。reset_lifecycle_file
に加えた変更と new_file
の追加は失われたことになります。このようなデータの消失は元に戻すことができないという点を忘れないでください。
--mixed
デフォルトの操作モードで、ref ポインターが更新されます。ステージングインデックスは、指定したコミットの状態にリセットされます。ステージングインデックスで元に戻したすべての変更は作業ディレクトリに移動します。それでは実際にやってみましょう。
$ echo 'new file content' > new_file $ git add new_file $ echo 'append content' >> reset_lifecycle_file $ git add reset_lifecycle_file $ git status On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) new file: new_file modified: reset_lifecycle_file $ git ls-files -s 100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file 100644 7ab362db063f9e9426901092c00a3394b4bec53d 0 reset_lifecycle_file
上のサンプルではリポジトリにいくつか修正を加えました。ここでも new_file
を追加して reset_lifecycle_file
のコンテンツを修正しています。これらの変更はその後、git add
を使ってステージングインデックスに適用されます。リポジトリがこの状態になったので、リセットを実行してみましょう。
$ git reset --mixed $ git status On branch master Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git checkout -- ..." to discard changes in working directory) modified: reset_lifecycle_file Untracked files: (use "git add ..." to include in what will be committed) new_file no changes added to commit (use "git add" and/or "git commit -a") $ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
ここでは「混合リセット」を実行しました。繰り返しになりますが、--mixed
はデフォルトのモードで、git reset
実行時と同じ結果が得られます。git status
と git ls-files
の出力を調べてみると、インデックス内にreset_lifecycle_file
のファイルしかない状態に、ステージングインデックスがリセットされていることがわかります。reset_lifecycle_file
のオブジェクト SHA が以前のバージョンにリセットされています。
ここで重要なのは、git status
によって、reset_lifecycle_file
に修正が入っていることと、追跡対象外ファイル new_file
があることがわかったことです。これは --mixed
の明示的な挙動です。ステージングインデックスがリセットされて、保留中の変更が作業ディレクトリに移動されました。対照的に、--hard
を使ったリセットのケースではステージングインデックスと作業ディレクトリの両方がリセットされて更新が失われていました。
--soft
--soft
引数が渡されると、ref ポインターが更新されてそこでリセットが停止します。ステージングインデックスと作業ディレクトリは変更されません。この挙動をわかりやすく説明するのは簡単ではありません。引き続きデモ用リポジトリを使ってソフトリセットの準備を行いましょう。
$ git add reset_lifecycle_file
$ git ls-files -s
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD ..." to unstage)
modified: reset_lifecycle_file
Untracked files:
(use "git add ..." to include in what will be committed)
new_file
ここでも git add
を使って、修正した reset_lifecycle_file
をステージングインデックスにプロモートしました。インデックスが git ls-files
の出力で更新されているのがわかります。git status
の出力には「コミットされる変更」が緑で表示されています。前のサンプルの new_file
が、追跡対象外ファイルとして作業ディレクトリで未確定状態になっています。以降のサンプルでは必要ないため、rm new_file
を実行してこのファイルを削除しましょう。
リポジトリがこの状態になったらソフトリセットを実行します。
$ git reset --soft $ git status On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) modified: reset_lifecycle_file $ git ls-files -s 100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
「ソフトリセット」を実行しました。git status
と git ls-files
を実行してリポジトリの状態を確認すると、何も変更されていないことがわかります。これは想定された挙動です。ソフトリセットではコミット履歴だけがリセットされます。デフォルトでは、git reset
は HEAD
とともにターゲットコミットとして呼び出されます。今回のコミット履歴はすでに HEAD
にあるため、HEAD
に暗黙的にリセットしても実際には何も起こりません。
--soft
の理解を深めてうまく使えるようになるために、HEAD
ではないターゲットコミットを用意する必要があります。ステージングインデックスには reset_lifecycle_file
があるので、新しいコミットを作成してみましょう。
$ git commit -m"prepend content to reset_lifecycle_file"
この時点で、リポジトリには 3 つのコミットがあるはずです。1 つ目のコミットまで時間をさかのぼりましょう。これを行うには 1 つ目のコミットの ID が必要です。git log
の出力を見ると ID がわかります。
$ git log commit 62e793f6941c7e0d4ad9a1345a175fe8f45cb9df Author: bitbucket Date: Fri Dec 1 15:03:07 2017 -0800 prepend content to reset_lifecycle_file commit dc67808a6da9f0dec51ed16d3d8823f28e1a72a Author: bitbucket Date: Fri Dec 1 10:21:57 2017 -0800 update content of reset_lifecycle_file commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 Author: bitbucket Date: Thu Nov 30 16:50:39 2017 -0800 initial commit
コミット履歴 ID は各システムで固有だということを覚えておきましょう。つまり、このサンプルのコミット ID と、ご自身のコンピューターに表示されているコミット ID は異なっているということです。このサンプルで使うコミット ID は 780411da3b47117270c0e3a8d5dcfd11d28d04a4
です。これが「最初のコミット」に対応する ID です。ID がわかったらそれを対象として指定してソフトリセットを実行します。
時間をさかのぼる前に、まずはリポジトリの現在の状態を確認しておきましょう。
$ git status && git ls-files -s On branch master nothing to commit, working tree clean 100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
ここでは git status
と git ls-files -s
の連携コマンドを実行しました。これで、リポジトリに保留中の変更があり、ステージングインデックスの reset_lifecycle_file
のバージョンが 67cc52710639e5da6b515416fd779d0741e3762e
であることがわかりました。これを忘れないようにしながら 1 つ目のコミットに戻ってソフトリセットを実行しましょう。
$git reset --soft 780411da3b47117270c0e3a8d5dcfd11d28d04a4 $ git status && git ls-files -s On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) modified: reset_lifecycle_file 100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
上のコードでは「ソフトリセット」を実行するとともに、リポジトリの状態を出力する git status
と git ls-files
の連携コマンドも呼び出しています。リポジトリの状態の出力を調べるといくつか注目すべき点があります。まず、git status
を実行したことで、reset_lifecycle_file
に修正が加えられて、それらの修正が後続のコミットにステージされた変更であることが強調表示されていることがわかります。次に git ls-files
の入力を見ると、ステージングインデックスは変更されておらず、前からあった SHA 67cc52710639e5da6b515416fd779d0741e3762e がそのままになっていることがわかります。
このリセットによって何が起こったのかをさらに詳しく見るために、git log
を調べてみましょう。
$ git log commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 Author: bitbucket Date: Thu Nov 30 16:50:39 2017 -0800 initial commit
ログの出力を見ると、コミット履歴に 1 つだけコミットがあることがわかります。これによって、--soft
で何が起きたかがわかりやすくなります。すべての git reset
呼び出し同様、リセットで行われる最初の処理はコミットツリーのリセットです。--hard
と --mixed
の前のサンプルでは両方とも HEAD
をポイントしておらず、コミットツリーを過去の状態に戻していませんでした。ソフトリセット中に起こるのはこれがすべてです。
そうなると、git status
の結果で修正されたファイルがあった理由がよくわからなくなります。 --soft
ではステージングインデックスを操作しないため、コミット履歴をたどってステージングインデックスが更新されたことになります。git ls-files -s
の出力を見ると reset_lifecycle_file
の SHA が更新されていないため、これは間違いありません。本来はツリー間の差分を表示するためのコマンドである git status
は「3 つのツリー」の状態を表示しない点をここで繰り返しておきます。このサンプルでは、ステージングインデックスがコミット履歴よりも先の変更になっていてあたかもすでにそれらをステージしたかのような状態になっています。
取消しと打消し
git revert
が変更を元に戻す「安全」な手段だとすると、git reset
は危険な手法という考え方もできます。git reset
には作業を失ってしまうという現実的なリスクがあります。git reset
でコミットが削除されることはありませんが、コミットが孤立して ref からそのコミットに直接アクセスするパスがなくなることになります。孤立したコミットは通常、git reflog
を使って特定、復元することができます。Git では内部ガベージコレクターが実行されると孤立したコミットが完全に削除されます。Git のデフォルトでは、30 日ごとにガベージコレクターが実行されるように設定されています。コミット履歴は「Git の 3 つのツリー」の 1 つです。残りの 2 つであるステージングインデックスと作業ディレクトリはコミットとは違って永続的なものではありません。作業を失ってしまう可能性のある唯一の Git コマンドであるため、git reset の使用には細心の注意が必要です。
一方、打消しは公開済みコミットを元に戻すための安全な方法です。git reset
ではステージングインデックスと作業ディレクトリへのローカルの変更を元に戻します。これら 2 つのコマンドの目的には明確な差があり、実装のされ方も異なっています。取消しではチェンジセットが完全に削除されるのに対して、打消しでは元のチェンジセットが保持されたまま、新しいコミットを作成して元に戻す操作が行われます。
公開済み履歴の取り消しは厳禁
git reset
コマンドを絶対に使用しないでください。コミットを公開したら、他の開発者がそれを前提として作業を行っていると考える必要があります。
他のチームメンバーが開発中に行ったコミットを取り消すとコラボレーション上の深刻な問題が生じます。彼らがあなたのリポジトリとの同期を行おうとすると、プロジェクト履歴の一部が欠落したように見えます。公開済みのコミットを取り消すと何が起こるかを以下の一連の図に示します。ここで origin/master
ブランチは、ローカルな master
ブランチに対応する中央リポジトリのブランチです。
取り消し後に新規コミットを行うと、Git ではローカルな履歴が origin/master
から分岐したと扱われ、ローカルリポジトリを同期するために作成されるマージコミットがチームの他の開発者に対する混乱と作業妨害を引き起こします。
重要なことは、git reset
コマンドの実行目的は、結果が思わしくなかったローカルの実験的開発作業の取り消しであって、公開済みの変更の取消しではないと認識することです。公開済みのコミットを訂正する必要がある場合は、その目的の専用コマンド git revert
が用意されています。
例
git reset
作業ディレクトリに何の変更も加えずに、指定したファイルをステージングエリアから削除するコマンドです。このコマンドを実行すると、変更を書き込むことなく指定したファイルをアンステージします。
git reset
作業ディレクトリに何の変更も加えることなくステージングエリアをリセットして直前のコミット時の状態と一致させるコマンドです。このコマンドを実行すると、変更を書き込むことなくすべてのファイルをアンステージし、一度ステージされたスナップショットを初めから再構築することができるようになります。
git reset --hard
ステージングエリアと作業ディレクトリをリセットして直前のコミット時の状態と一致させるコマンドです。--hard
は、変更をアンステージしたうえでさらに作業ディレクトリ内のすべての変更を元に戻すことを Git に指示するフラグです。言い換えると、これはコミット前のすべての変更をまったくなかったものとするコマンドであり、これを使用する場合はローカルマシーンで行った開発作業を本当に破棄していいのか否かを確認する必要があります。
git reset
現在のブランチの先端を commit
の位置に戻したうえでステージングエリアをその状態と一致するように元に戻しますが、作業ディレクトリのみそのままにしておきます。
の実行後に行われた変更はすべて作業ディレクトリに保存されており、より変更規模が小さくて整理されたスナップショットを使ってプロジェクト履歴への再コミットを行うことができます。
git reset --hard
現在のブランチの先端を
の位置に戻したうえでステージングエリアおよび作業ディレクトリをその状態と一致するように元に戻します。このコマンドを実行すると、コミット前の変更に加えて後に行われたすべてのコミットもまったくなかったものとなります。
ファイルのアンステージ
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 コマンドの使用対象は未公開のコミットに限ってください。コミットを公開リポジトリにプッシュしている場合には、決して上記の操作を行わないでください。
概要
git reset
は Git リポジトリの状態に対するローカルの変更を元に戻すための強力なコマンドであることがわかりました。git reset
を「Git の 3 つのツリー」で操作しました。これらのツリーはコミット履歴 (HEAD
)、ステージングインデックス、作業ディレクトリです。これら 3 つのツリーに対応する 3 つのコマンドラインオプションがあります。オプション --soft、--mixed、
--hard
は git reset
に渡すことができます。
この記事では、他にもいくつかの Git コマンドを活用して取消しプロセスをわかりやすく説明しました。各コマンドの詳細については、git status、git log、git add、git checkout、git reflog、
git revert
の各ページを参照してください。