取り消しは二つの問いで決まる
一言でいうと
元に戻す方法は、2つの問いで決まります。戻したいものがどこにあるか(ワーキングツリー / インデックス / コミット)、そして、そのコミットがすでに他の人に渡っているか、です。
なぜ必要なのか
Gitの取り消しコマンドが難しく感じられる理由は、コマンドが多いからではなく、コマンドを選ぶ基準を学んだことがないからです。1つ目の問いがコマンドの種類を決め、2つ目の問いが履歴を書き換えてよいかを決めます。
ここに、大きな安心材料を1つ加えておきます。コミットされたことのあるものは、ほぼ常に復元でき、コミットされたことのないものは、ほとんど復元できません。そのため、危険な瞬間は「コミットを消すとき」ではなく、「コミットしていない変更を上書きするとき」です。
どう動くのか
resetの3つのモードは、戻す範囲が違います。
| モード | 戻す範囲 | 失うもの | 使う場面 |
|---|---|---|---|
--soft |
ブランチの参照だけを移動 | なし | 複数のコミットを1つにまとめ直すとき |
--mixed(デフォルト) |
インデックスまで | ステージング状態 | ステージングを選び直したいとき |
--hard |
インデックスとワーキングツリーまで | コミットされていないすべての変更 | ローカルの実験をまるごと捨てるとき |
--keep |
インデックスまで。ただし、重なるローカルの変更があれば中断 | なし | 上書きの前に拒否してほしいとき |
危険度は、コマンド名ではなく、「このコマンドがコミットされていない変更を上書きするか」で判断します。
コミットがすでに他の人のリポジトリにあるなら、そのコミットをなくすあらゆる方法は、他の人の履歴を壊します。履歴を書き換える代わりに、履歴に付け足すことが答えです。そのため、revertを使います。強制プッシュで消しても、すでに受け取った人たちのローカルはそのままで、その人たちが次にプッシュすると、消したコミットが復活します。
マージのrevertには落とし穴が1つあります。マージをrevertしたあと、ブランチを修正して再度マージしても、元に戻した変更は戻ってきません。Gitは、マージを履歴の到達可能性だけで判断するからです。正解は、revertをrevertすることです。
現場での姿
git reset --hardでコミットを2つ飛ばしてしまい、顔が真っ青になる場面はよくあります。たいていは大丈夫です。git reflogを開いてHEAD@{1}に戻れば済みます。ただし、2つ知っておく必要があります。ブランチを削除すると、そのブランチの参照ログも一緒に削除されます(HEADのreflogには残ります)。そして、デフォルトの有効期限は、到達可能なもので90日、到達不能なもので30日です。
安全網をすぐになくすコマンドもあります。git reflog expire --expire=now --allとgit gc --prune=nowです。この2つを「整理」と呼んで習慣的に実行するチームは、事故が起きたときに復旧手段がありません。
本当に最後の手段は、git fsck --full --unreachable --no-reflogsで浮いているオブジェクトを探し、git cat-file -p <blob> > 파일で取り出すことです(プレースホルダーはファイル名です)。ステージングだけしておいた内容も、すでにオブジェクトなので、この方法で復活します。
状況別に何を使うか
前の2つの問いを実際の状況に当てはめると、次のような表になります。急いでいるときは、この表さえあれば、コマンドで悩まずに済みます。
| 状況 | 使うもの | 注意 |
|---|---|---|
| 今のコミットメッセージを間違えた | commit --amend |
すでにプッシュしていたら行いません |
| 今のコミットにファイルを1つ入れ忘れた | 追加してからcommit --amend --no-edit |
上と同じです |
| コミット3つを1つにまとめたい | reset --soft HEAD~3してから再コミット |
変更はそのまま残ります |
| ステージングだけを戻したい | restore --staged <파일> |
ファイルの内容には触れません |
| ファイル1つを最後のコミットの状態に | restore <파일> |
コミットしていない変更が消えます |
| すでにプッシュしたコミットを取り消す | revert <커밋> |
新しいコミットができ、履歴が残ります |
| ローカルの実験をまるごと捨てる | reset --hard |
元に戻せない唯一の場面です |
| 誤って消した | reflogで探してreset --hard <해시> |
30–90日以内ならたいてい復活します |
表のコードに含まれるプレースホルダーは、上から順に、ファイル名、ファイル名、コミット、ハッシュです。
表の中で危険な行は、ちょうど2つです。restore <파일>とreset --hardです(プレースホルダーはファイル名です)。どちらも、コミットされたことのない変更を上書きするためであり、その変更はどこにも記録されないので、reflogでも救えません。そこで、習慣を1つ勧めます。捨てるかどうか迷うときは、先にコミットするか、git stashで退避しておきます。どちらもオブジェクトを作るので、その瞬間から、取り戻す方法ができます。
stashについて、1つだけ補足します。デフォルトの動作では、追跡されていない新しいファイルは退避されないため、新しく作ったファイルがそのまま残り、次の作業に混ざり込みます。それも一緒に片付けるには-uを付ける必要があり、これを知らないと、「stashしたのに、なぜワーキングツリーがきれいにならないのか」と、しばらく迷うことになります。
次のラボですること
同じリポジトリを何個も作る必要があるため、まずリポジトリを量産するスクリプトを作ります。そのあと、amend、resetの3つのモード、revert、reflogによる復旧を、それぞれ別のリポジトリで実行し、結果の違いを目で比べ、最後に状況別のチートシートをまとめます。