Git入門:バージョン管理のきほん
コミットを取り消す(reset の 3 モード)
このレッスンで分かること
git resetがコミット・ステージ・作業フォルダのどこまでを巻き戻すのか--soft/--mixed/--hardの違いを表で比較して選べるようになる--hardが何を消してしまうのか、そして使ってよい場面の境界
reset は「現在地」を動かすコマンド
前のレッスンの git restore は、コミットしていない編集やステージを片付けるものでした。ここで扱う git reset は、コミットそのものを取り消す コマンドです。
「取り消す」と書きましたが、実際に起きていることを正確に言うと、HEAD を別のコミットに付け替えているだけです。HEAD は「いま自分がどのコミットの上にいるか」を指す目印でした。この目印を 1 つ前のコミットに動かすと、そこから先のコミットは辿れなくなり、結果として「無かったこと」になります。
プレーンテキスト
reset 前 A --- B --- C ← HEAD
reset 後 A --- B ← HEAD
C (辿れなくなる)ここで大事なのは、C というコミット自体はまだ Git の内部に残っている、という点です。ただし通常の git log からは見えなくなります。誰からも指されなくなったコミットは、しばらく経つと本当に削除されるので、残っているうちに気づく必要があります。
そして目印を動かすとき、ステージと作業フォルダをどう扱うか を選べます。その選択肢が 3 つのモードです。
3 つのモードを比べる
| モード | コミット | ステージ | 作業フォルダ | ひとことで言うと |
|---|---|---|---|---|
--soft | 巻き戻す | そのまま | そのまま | コミットだけ取り消す |
--mixed(既定) | 巻き戻す | 巻き戻す | そのまま | add とコミットを取り消す |
--hard | 巻き戻す | 巻き戻す | 上書きする | 変更ごと全部消す |
上から順に、消す範囲が広がっていきます。--soft は最も影響が小さく、--hard は書いたものが手元から消えます。
モードを省略すると --mixed として動きます。git reset HEAD~1 と打ったときは --mixed だと覚えてください。
3 つとも HEAD を動かす点は共通です。違うのは、動かしたあとにステージと作業フォルダをどう扱うかだけです。
プレーンテキスト
--soft HEAD だけ動かす → ステージも編集もそのまま残る
--mixed HEAD とステージ → 編集した内容だけが残る
--hard HEAD もステージも編集も → 何も残らないこの 3 段階を「どこまで巻き込むか」の目盛りとして覚えると、選び間違いが減ります。目盛りを 1 つ上げるごとに、手元に残るものが減っていきます。
--soft でコミットをまとめ直す
--soft は、コミットの記録だけを取り消して、内容はステージに載ったまま残します。
ターミナル
$ git log --onelineプレーンテキスト
9f1c0a4 とりあえずコミット
3b7d21e トップページの見出しを修正「とりあえずコミット」という雑なメッセージを付けてしまったとします。やり直したいので 1 つ前に戻します。
ターミナル
$ git reset --soft HEAD~1
$ git statusプレーンテキスト
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/app.jsコミットは消えましたが、変更内容はステージに載ったままです。この状態で改めてコミットし直せます。
ターミナル
$ git commit -m "ログイン失敗時のエラーメッセージを追加"HEAD~1 は「1 つ前のコミット」を指す書き方です。HEAD~3 なら 3 つ前になります。細かく分けすぎたコミットを 1 つにまとめたいときは git reset --soft HEAD~3 としてから、まとめてコミットし直します。
このとき、ファイルの中身は 1 バイトも変わっていません。動いたのは HEAD の位置だけで、作業フォルダもステージもそのままです。だから --soft は 3 つのモードの中でいちばん安全に使えます。やり直しに失敗しても、内容が手元にある限り何度でもコミットし直せます。
直前の 1 コミットだけをやり直すなら、直前のコミットを直す(commit --amend) のほうが手軽です。
--softは複数コミットをまとめたいときに向いています。
--mixed でステージも解く
--mixed は、コミットに加えてステージも巻き戻します。書いたファイルの中身は残るので、「もう一度、何をコミットするか選び直す」状態になります。
ターミナル
$ git reset HEAD~1
$ 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: src/app.js
modified: src/style.cssChanges to be committed ではなく Changes not staged for commit に並んでいます。ここから必要なファイルだけ git add し直せば、コミットを分割できます。
3 つのモードのうち、いちばん使う場面が多いのがこれです。省略時の既定になっているのも、いちばん無難だからです。
コミット位置を指定する書き方
reset に渡せるのはコミットを指す表現なら何でも構いません。よく使うのは次の 3 通りです。
| 書き方 | 意味 |
|---|---|
HEAD~1 | いまの位置から 1 つ前 |
HEAD~3 | いまの位置から 3 つ前 |
3b7d21e | そのハッシュのコミットそのもの |
ハッシュで指定するときは、「そのコミットまで戻す」という意味になります。3b7d21e を渡すと、3b7d21e は残り、それより後ろが取り消されます。「そのコミットを消す」ではないので、1 つずれやすいところです。
不安なときは、実行前に git log --oneline で並びを確認してください。どこまで残るのかを目で見てから打つ習慣をつけると、数え間違いによる事故が減ります。
--hard は変更ごと消す
--hard は、コミット・ステージ・作業フォルダのすべてを、指定したコミットの状態に揃えます。
ターミナル
$ git reset --hard HEAD~1プレーンテキスト
HEAD is now at 3b7d21e トップページの見出しを修正たった 1 行だけ出て終わります。確認もされません。
このとき、取り消したコミットに入っていた変更は、手元のファイルからも消えます。 src/app.js を開いても、書いたコードは残っていません。git status は次のようになります。
ターミナル
$ git statusプレーンテキスト
On branch main
nothing to commit, working tree cleanclean という表示は、普段は「片付いている」という良い知らせですが、ここでは「書いたものが無くなった」という意味になります。
さらに危ないのが、コミットしていなかった編集も一緒に消える ことです。--hard は作業フォルダを問答無用で上書きするので、まだ git add すらしていない書きかけのコードも巻き添えになります。
ターミナル
$ git reset --hardこのように対象を省かずに打つと、HEAD の状態に作業フォルダを揃える、つまり いま編集中の内容を全部捨てる 動きになります。「なんか状態が変になったから reset --hard で綺麗にしよう」という使い方が、最も事故を起こします。
コミット済みだったものは、Git の内部にしばらく残っています。
git reflogを使うと辿れるので、多くの場合は取り戻せます。その手順は 消したはずの作業を取り戻す(reflog) で扱います。ただし 一度もコミットしていない編集は、reflog でも戻せません。
やりがちな失敗と、その避け方
よくあるのは、片付けのつもりで --hard を打ち、まだコミットしていない別の作業まで消してしまう失敗です。
防ぎ方は単純で、--hard を打つ前に必ず git status を見る ことです。
ターミナル
$ git statusプレーンテキスト
On branch main
Changes not staged for commit:
modified: src/other-feature.jsここに何か表示されていたら、それは --hard で消える対象です。関係ない作業が混ざっているなら、先に git stash で退避するか、別途コミットしてから実行してください。
もう 1 つの避け方は、そもそも --hard を使わないことです。目的が「コミットをやり直す」なら --soft か --mixed で足ります。作業フォルダまで消したい場面は実際にはあまりありません。
似た形で紛らわしいのが git reset ファイル名 という書き方です。これはコミットを巻き戻すのではなく、そのファイルをステージから降ろすだけの動きになります。
ターミナル
$ git reset src/app.js同じ reset という名前でも、コミットを渡したときとファイル名を渡したときで役割が変わります。紛らわしいので、いまはステージから降ろす操作には 変更を取り消す(restore) で学んだ git restore --staged を使ってください。役割ごとにコマンドが分かれていたほうが、取り違えが起きません。
--hardにファイル名は渡せません。git reset --hard src/app.jsはエラーになります。ファイル単位で編集を捨てたいならgit restoreの担当です。この境界も、コマンドが分かれている理由の 1 つです。
reset を使ってよい範囲
ここまで見てきたとおり、reset は履歴そのものを書き換えます。だから使ってよい範囲がはっきり決まっています。
git reset は、まだ誰とも共有していない、自分の手元だけの履歴に使うコマンドです。
git push して他の人と共有したコミットを reset で消すと、自分の履歴と相手の履歴が食い違います。次に push しようとすると拒否され、--force で無理やり上書きすることになり、相手の作業を壊します。
| 状況 | 使うもの |
|---|---|
| まだ push していない、手元だけの履歴 | git reset |
| すでに push して共有した履歴 | git revert |
この境界だけは覚えてください。revert は履歴を消さずに「打ち消すコミットを新しく積む」やり方で、共有済みでも安全です。詳しくは次の 公開済みの履歴を安全に戻す(revert) で扱います。
判断に迷ったら、git log --oneline で対象のコミットを確認し、それが push 済みかどうかを考えてください。push した覚えがあるなら reset は選ばない、という単純な基準で十分です。
もう少し正確に確かめたいときは、まだ送っていないコミットだけを一覧できます。
ターミナル
$ git log --oneline origin/main..HEADプレーンテキスト
9f1c0a4 とりあえずコミットorigin はリモートリポジトリに付けられる既定の名前で、origin/main はリモート側の main がどこまで進んでいるかを表す目印です。この一覧に出てくるコミットは、まだ自分の手元にしかありません。ここに入っているものだけが reset の対象だ、と考えてください。
まだリモートを使っていない段階なら、そもそも全部が手元の履歴なので reset を自由に使えます。リモートを扱い始めるのは リモートリポジトリと GitHub からです。それまでは、この章のコマンドは安心して練習できます。
git resetはHEADを別のコミットに動かして、そこから先を辿れなくする--softはコミットだけ、--mixed(既定)はコミットとステージ、--hardは作業フォルダまで巻き戻す--hardは取り消した変更が手元から消える。未コミットの編集も巻き添えになるので、実行前に必ずgit statusを見るresetは手元だけの履歴に使う。共有済みの履歴を戻すときはgit revertを使う