TT Lab
はじめる
学ぶ 学習パス コース

Git実戦

取り消しは二つの問いで決まる

TT Labで続きを見る

一言でいうと

元に戻す方法は、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による復旧を、それぞれ別のリポジトリで実行し、結果の違いを目で比べ、最後に状況別のチートシートをまとめます。