変更を取り消す
Git の取り消しには、スナップショットのコピー、参照の移動、逆変更を記録する新コミットがある。git restore、git reset、git revert は同じファイル内容を作れても、インデックス、履歴、オブジェクト数、復元用ポインターが異なる。
作業ツリー、インデックス、HEAD -> main のコミットは別の場所だ。restore は版をコピーし、reset はブランチを動かして必要なファイル層を戻し、revert は訂正記録を加える。同じ内容でも同じ履歴ではない。
三つの取り消しの仕組み
git restore app.txt は既定でインデックスから作業ツリーを上書きし、main、インデックス、オブジェクトを変えない。--staged を付けると既定の元は HEAD のツリー、先はインデックスになる。ファイル層の操作で、履歴は変えない。
git reset <target> は先に対象を解決し、旧先端を ORIG_HEAD に記録して main を動かす。モードが残りのコピーを決める。
| モード | main を移動 | 対象をインデックスへ | 対象を作業ツリーへ | 新コミット |
|---|---|---|---|---|
--soft | はい | いいえ | いいえ | なし |
--mixed | はい | はい | いいえ | なし |
--hard | はい | はい | はい | なし |
soft は元のインデックスと作業ファイルを保ってブランチを動かせる。mixed はインデックスも替え、hard は追跡済み作業ファイルも上書きする。ORIG_HEAD が持つのはコミット ID だけで、未ステージ・未コミット編集のバックアップではない。
git revert HEAD は選んだコミットと親から逆変更を計算し、インデックスと作業ツリーへ適用して、新コミットで main を進める。取り消したコミットは祖先に残る。後退せず、新しいスナップショットで訂正を記録する。
変更を取り消すステップ
既定は未誕生の main から始める。
edit app.txtが作業ツリーだけにmode=safeを書く。git add app.txtが blob を保存し、インデックスをそこへ向ける。git commit -m "base"がインデックスのツリーと根 C1 を作り、main を進める。三層は全てmode=safe。- 編集で作業ツリーだけを
mode=draftにする。 git restore app.txtは既定のインデックスからmode=safeをコピーする。draft は消え、main とインデックスは変わらない。- 作業ツリーを
mode=unsafeに編集する。 - add が unsafe blob を保存し、インデックスを更新する。
git commit -m "bad-change"が C1 を親に C2 を作り、main を進める。三層は unsafe になる。git reset --soft HEAD^が C1 を解決し、C2 を ORIG_HEAD に書いて main を C1 へ戻す。インデックスと作業ツリーは unsafe のままなので、新 HEAD に対して変更がステージされた状態になる。git reset --soft ORIG_HEADは保存済み C2 を先に解決し、その後 ORIG_HEAD を C1 に替え、main を C2 へ進める。両ファイル層は変えない。先に復元先を読まないと、上書き後の誤ったコミットを対象にしてしまう。git revert HEADは両層が C2 と一致することを確認し、C2 と C1 からmode=unsafe -> mode=safeを求めて適用する。C1 のツリーを再利用でき、blob やツリーを増やさず、コミット C3 だけを作る。main は C3 へ進み、C2 はその親として到達可能なまま。
最後のファイルは C1 と同じ safe。C1 への hard reset でも同じ内容にできるが、ブランチは後退し、コミットは二つだけになる。revert は三つ目へ前進し、悪いコミットを親に残す。内容だけでは経緯を判断できない。
各コマンドの書き込み先
| コマンド | 作業ツリー | インデックス | main | オブジェクト庫 |
|---|---|---|---|---|
restore app.txt | インデックスからコピー | 不変 | 不変 | 不変 |
restore --staged app.txt | 不変 | HEAD からコピー | 不変 | 不変 |
reset --soft | 不変 | 不変 | 移動 | 不変 |
reset --mixed | 不変 | 対象からコピー | 移動 | 不変 |
reset --hard | 対象からコピー | 対象からコピー | 移動 | 不変 |
revert HEAD | 逆変更の結果 | 逆変更の結果 | 前進 | この例では一コミット追加 |
一ファイルのエンジンでは B バイトのコピーやハッシュは O(B)。単一親の隣接対象 HEAD^ の解決は O(1)、main からの到達可能性の分類は O(V + E)。ブラウザモデルの計算量であり、Git のインデックス、pack、履歴走査、ファイルシステムの性能保証ではない。
境界ケース
- 未誕生 HEAD はツリーがなく、
restore --stagedの既定の元がない。 - 根には第一親がないので
HEAD^は失敗し、空ツリーには解決しない。 - 本モデルの ORIG_HEAD は最初の成功 reset まで未設定。未知または曖昧な短 ID も変更なしで失敗する。
- コピー先と元が同じ restore は何も変えず、参照やオブジェクトも書かない。
- インデックスか作業ツリーが HEAD と違えば、本モデルは revert を拒否し、未実装の競合手順を作らない。
- hard reset は Git がオブジェクトとして保存したことのない作業バイトを捨てうる。コミットの復元ポインターでその編集は戻せない。
よくある間違い
- restore がファイルを移動すると言う。版を先へコピーし、元は残る。
- reset がコミットを削除すると言う。参照を動かすだけで、旧コミットは ORIG_HEAD などに残りうる。掃除は別処理だ。
- ORIG_HEAD を reflog と同じに扱う。前者は一 ID、後者は独立した参照更新の履歴。
- revert がパッチオブジェクトを保存すると言う。逆変更を求め、その結果のツリーと親を新コミットとして記録する。
- コミットを復元できるから hard reset の全てを戻せると思う。上書きした未保存の作業編集は戻せない場合がある。
モデルの範囲
一つのローカルリポジトリ、接続した HEAD -> main、通常の追跡済み一ファイル、単一親履歴で、全ファイル restore、三種 reset、クリーンな revert HEAD を扱う。固定識別情報と実 Git 形式の SHA-1 ID を使う。
パス集合、パッチモード、削除、改名、モード、未追跡ファイルの妨害、分離 HEAD、複数ブランチ、マージ、revert -m、旧コミットの競合、revert シーケンサー、フック、署名、リモート、reflog 期限、ガベージコレクション、エディターやファイルシステムの復元は省く。実際の選択肢と上書き動作は git restore、git reset、git revert を参照できる。
続けて学ぶ
同じ手法につながるレッスンや、次の概念への入口です。
誤りや気になる点を見つけましたか? フィードバックを送る。