Gitでstageしたファイルをunstageするには、git reset、git restore --staged、git rm --cachedの3つのコマンドがありますが、似て非なるものなので、違いを解説します。
目次
- ステージングエリアとstage/unstage
git resetgit restore --stagedgit rm --cached- コマンドの違いのまとめ
- 補足:unstageエイリアスコマンド作成
ステージングエリアとstage/unstage
Gitでは、ワーキングツリー(作業ディレクトリ)でのコード変更をいきなりレポジトリにコミットするのではなく、ステージングエリア(別名:インデックス)と呼ばれる中間地点に一度追加してからコミットします。

ワーキングツリーからステージングエリアへの追加はstageと呼ばれ、git addコマンド(もしくはgit addの公式エイリアスのgit stageコマンド)で行います。
例:READMEの変更内容をstageする
$ git add README
ステージングエリアにコンテンツが存在するかどうかは、git statusコマンドで確認できます。
$ git status
ブランチ master
Your branch is up to date with 'origin/master'.
コミット予定の変更点:
(use "git restore --staged <file>..." to unstage)
modified: README
ステージングエリアからレポジトリへのコミットはgit commitコマンドで行います。
$ git commit -m 'READMEの内容変更'
もし誤ってファイルをstageした場合、コミット前であればコミット履歴に残すことなくstage前の状態に戻すことが可能で、これをunstageと呼びます。このためステージングエリアは非常に便利なのですが、unstage用のコマンドが3つもあるため、違いを理解していないと思わぬミスをすることがあります。
git reset
ステージングエリアに追加・変更ファイルがある場合、git resetコマンドを以下のように使うことでunstageします。[<ファイルパス>]は任意であることを示しています。
$ git reset [<ファイルパス>]
例:ステージングエリアにあるREADMEをunstageする。
$ git reset README
Unstaged changes after reset:
M README
resetというコマンド名の通り、ステージングエリアをリセットします。ステージングエリアのリセットというのはつまりunstageと同義です。ファイルパスがある場合はそのファイルのみをステージングエリア上でリセットし、ない場合はステージングエリア全体をリセットします。ステージングエリアを何にリセットするのかというと、HEADです。リセットするコミットIDを明示的に指定することもできますが、ステージングエリアからファイルをunstageするという目的で使うことはほとんどないでしょう。コミットIDを指定しない限り、HEADがデフォルトで使われます。
なお、上記コマンドではモードオプションを省いていますが、その場合は–mixedモードと同義になります。–mixedモードはステージングエリアはリセットしますがワーキングツリーはリセットしないので、コンテンツの変更がファイルから失われるということはありません。
git restore –staged
ステージングエリアに追加・変更ファイルが存在する場合、以下のように使うことでunstageを行います。
$ git restore --staged <ファイルパス>
restoreというコマンド名は、ステージングエリア上のファイルをHEADにリストア(英語のリセットと同義)することに由来し、それはつまりunstageするということです。
例:READMEをgit restoreでunstage
$ git restore --staged README
--stagedオプションを付けないと、ステージングエリア上のファイルがリストアされないばかりでなく、ワーキングツリーのファイルがリストアされてしまうので注意が必要です。ちなみに、ワーキングツリーがリストアされるときは、ステージングエリアにリストアされます(ステージングエリア上のファイルと同じコンテンツになるということです)。
なお、git resetコマンドはファイルパスが任意であるのに対し、git restoreコマンドでは必須となっています。とは言え、ステージングエリア全体をunstageしたい時は.(ドット)で現在ディレクトリーをファイルパスに指定すれば同じことが可能ですので、ファイルを1つ1つ個別にリストアする必要はありません。
$ git restore --staged .
つまり、git restore --stagedとgit resetは、ステージングエリアからファイルをunstageするという点ではまったく同じ結果をもたらします。
ではなぜ別々のコマンドがあるかというと、git resetはステージングエリアからファイルをunstageするだけでなく、現在のブランチのHEADを変更してコミット履歴をリセットすることにも使えてしまうからです。そのようなうっかりミスを防ぐために、git restoreコマンドが追加されました。実際、昔はgit restoreコマンドがなかったので、私もunstageにはgit resetコマンドを使っていました。
うっかりミスを防ぐという意図は、git restoreではファイルパスが必須の引数となっていて、明示的に指定しなければならないことからも伺えます。
git rm --cached
rmの名前が示す通り(英語のremove、削除の略)、ファイルをステージングエリアから削除します。オプションなしのコマンドだとステージングエリアとワーキングツリー両方から削除しますが、--cachedオプションを付けることで削除対象がインデックスだけになり、ワーキングツリー上ではコンテンツ変更があろうがなかろうがファイルがそのまま残ります。
$ git rm --cached <ファイルパス>
ここで気をつけなければいけないのは、ステージングエリアの「リセット」ではなくステージングエリアのからの「削除」だということです。gitのステージングエリアは追加・変更があったファイルだけでなく、変更がなかったファイルやディレクトリ全てを含みますので、ステージングエリアからの削除というのは、次回コミット以降はそのファイルがgitのトラッキング対象外(untrackedのステータスと呼ばれます)になるということです。
stageされていたものがレポジトリにまだトラッキングされていない新規ファイルなら、git resetやgit restore --stagedと同じ結果になりますが、もしコミット履歴に既に含まれているファイルの変更がstageされていた場合、ただのunstageではなくなってしまうので注意が必要です。
例:READMEをgit rmでステージングエリアから削除
$ git rm --cached README
ちなみにgit rm --cachedを使うと、ファイルコンテンツの変更の有無やstageされたかどうかに関わらず、ステージングエリアから削除することができます。そういった意味では、このコマンドを使ってステージングエリアを操作することをunstageと呼ぶべきではないのかもしれません。
オプションなしのgit rmは不要になったファイルの削除でよく使いますが、わざわざgit addでインデックスに一度追加したものをgit rm --cachedを使ってトラッキング対象外にする必要が生じたことは、私個人の経験では未だかつてありません。
コマンドの違いのまとめ
3つのコマンドの違いをまとめると以下のようになります。
git reset: ファイルをステージングエリアからunstageするのに使います。但し、ファイルパスを指定しなかったりコミット履歴の変更などのうっかりミスには注意が必要です。git restore --staged: ファイルをステージングエリアからunstageするのに使います。git rm --cached: ファイルをステージングエリアから削除してトラッキング対象外にするのに使います。unstage用コマンドとして安全に使えるのは新規ファイルの場合のみです。
こうして整理してみると、git statusで表示されるヘルプメッセージ(use “git restore –staged …” to unstage)に記載されているように、ファイルをステージングエリアからunstageするにはgit restore --stagedを使うのがやはり最適だと再確認できますね。
なお、私が上記のようなGitコマンドの違いやGitの内部構造を学んだのはオライリーの実用Gitという本からです。初歩的なコマンドの使い方だけでなく、Gitをマスターしたい方は、ぜひ読んでみてください。
補足:unstageエイリアスコマンド作成
unstageにはgit restore --stagedを使いたいが毎回--stagedオプションをタイプするのが面倒な場合は、git設定ファイルでunstageというエイリアスコマンドを作成するのがおすすめです。
# ~/.gitconfig
[alias]
unstage = restore --staged
これで、unstageという直感的なコマンド名で作業を行えます。
例:READMEファイルをステージングエリアからunstage。
$ git unstage README
