Git基礎 #5 取り消し — restore・reset・revertの使い分け

読了 7分

作業をしていれば、ミスは必ず起きます。ファイルを間違って編集し、意図しないファイルをステージングし、コミットメッセージにタイポを入れ、上げてはいけないコミットをプッシュします。Gitはこれらすべての状況を取り消せます。問題は、取り消しのコマンドがrestore、reset、revert、amendと複数あり、どの状況で何を使うべきか混乱しやすいという点です。この記事の目標は、この混乱を整理することです。基準はひとつです。何をどこまで取り消すのかを先に決めれば、コマンドは自動的に決まります。

全6回で構成します。

  • #1 Gitとは — スナップショットモデルと3つの領域
  • #2 add・commit・status — ステージングエリアの意味
  • #3 ブランチとマージ — fast-forwardと3-way
  • #4 リモートリポジトリ — clone・fetch・pull・pushとoriginの正体
  • #5 取り消し — restore・reset・revertの使い分け ← この記事
  • #6 GitHubと初めてのPull Request

この記事では、実務で最もよく出会う5つの状況をひとつずつ確認し、最後に状況とコマンドを1枚の表にまとめます。

取り消しが混乱しやすい本当の理由 #

第1回で整理した3つの領域モデルを思い出してみましょう。Gitには作業ディレクトリ(working directory)、ステージングエリア(staging area)、リポジトリ(repository)という3つの領域があり、変更はこの順に移動します。

取り消しコマンドが混乱しやすいのは、コマンドごとに触る領域が違うからです。restoreは作業ディレクトリとステージングエリアを扱い、resetはリポジトリのコミット位置まで動かし、revertはコミットを消す代わりに新しいコミットを積みます。だから「何を取り消したいのか」という質問に先に答える必要があります。まだコミットしていない編集なのか、ステージングしただけのファイルなのか、作ったばかりのコミットなのか、すでにプッシュしたコミットなのかで、答えが変わります。

状況1 — コミットしていない編集を破棄する: git restore #

ファイルを編集していて、方向が違ったと判断しました。最後のコミットの状態にファイルを戻すにはgit restoreを使います。

作業ディレクトリの編集を破棄
# ファイル1つを最後のコミットの状態に戻す
git restore app.py

# 作業ディレクトリ全体を戻す
git restore .

ここで必ず覚えておくべきことがあります。コミットしていない編集は、Gitがどこにも記録していません。つまりgit restoreで捨てた編集は復元する方法がありません。実行する前に、捨てても問題ない編集かをもう一度確認する習慣が必要です。

注記
git statusを実行すると、Gitが状況に合った取り消しコマンドをあわせて案内してくれます。use "git restore <file>..." to discard changesのようなヒントがそれです。どのコマンドを使えばよいか迷ったときは、まずgit statusを読むのが最も安全な出発点です。

状況2 — ステージングだけ解除する: git restore –staged #

git addを実行したものの、このファイルは今回のコミットに入れないことにしました。ファイルの内容はそのままに、ステージングエリアから下ろすだけで済みます。

ステージング解除
git restore --staged app.py

状況1と違い、このコマンドはファイルの内容にはまったく触れません。ステージングエリアに上がったスナップショットだけを解除し、作業ディレクトリの編集はそのまま残ります。失うものがない、安全なコマンドです。

状況3 — 最後のコミットを修正する: git commit –amend #

コミットを作ってから、メッセージにタイポがあったり、一緒に入れるべきファイルが漏れていたことに気づきました。コミットをもうひとつ積む代わりに、最後のコミット自体を作り直せます。

最後のコミットを作り直す
# メッセージだけ直す
git commit --amend -m "fix: ログイン検証エラーを修正"

# 漏れたファイルを含めて再コミット (メッセージはそのまま)
git add missed_file.py
git commit --amend --no-edit

--no-editはメッセージをそのままにして内容だけ更新するオプションです。注意すべきなのは、amendが既存のコミットを修正するのではなく、新しいコミットに置き換えるという事実です。コミットハッシュが変わります。そのためamendは、まだプッシュしていないコミットにだけ使うべきです。プッシュ済みのコミットをamendすると、リモートと履歴が食い違います。

状況4 — コミットを取り消す: git reset #

最後のコミット自体をなかったことにしたいならgit resetです。resetはブランチが指すコミット位置を後ろに動かすコマンドで、オプションによって3つの領域のどこまで一緒に戻すかが変わります。

コミット取り消しの3段階
# コミットだけ取り消し、変更内容はステージングエリアに維持
git reset --soft HEAD~1

# コミットとステージングを取り消し、変更内容は作業ディレクトリに維持 (デフォルト)
git reset HEAD~1

# コミット、ステージング、作業ディレクトリの編集まですべて削除
git reset --hard HEAD~1

HEAD~1は現在のコミットのひとつ前のコミットを指す表記です。3つのオプションの違いを3つの領域モデルで整理すると次のとおりです。

オプションリポジトリ (コミット)ステージングエリア作業ディレクトリ
--soft取り消し維持維持
--mixed (デフォルト)取り消し解除維持
--hard取り消し解除編集を削除

--soft--mixedはコミットをほどくだけで変更内容自体は残るため、安全です。コミットを分割して作り直したいときや、メッセージを最初から書き直したいときに役立ちます。

注記
git reset --hardはこのシリーズで扱うコマンドの中で最も危険です。コミットしていない編集まで警告なしに削除し、その編集は復元できません。実行前にgit statusで失うものがないか必ず確認してください。なお、resetで取り消したコミット自体はreflogという記録を通じてしばらく復元できますが、この内容は別の記事で扱います。

状況5 — プッシュ済みのコミットを巻き戻す: git revert #

問題のあるコミットをすでにリモートにプッシュし、同僚がその履歴を取り込んでいるなら、resetを使ってはいけません。自分のリポジトリでコミットを消しても、リモートと同僚のリポジトリにはそのコミットが残っているため、強制プッシュなしには上げられなくなり、強制プッシュをすれば同僚の履歴まで一緒に崩れます。

このとき使うコマンドがgit revertです。revertはコミットを消しません。対象コミットの変更内容を正確に逆向きに適用する新しいコミットを上に積みます。

プッシュ済みコミットの巻き戻し
# 特定コミットの変更を逆に適用する新しいコミットを作成
git revert a1b2c3d

# 直前のコミットを巻き戻す
git revert HEAD

履歴は前にしか伸びず、すでに共有されたコミットはそのまま残るため、同僚は普段どおりpullするだけで済みます。コードが巻き戻されたという事実まで履歴に記録として残るのも利点です。

状況別まとめ #

5つの状況を1枚の表にまとめます。

状況コマンド危険度
コミットしていない編集を破棄git restore <ファイル>編集は復元不可
ステージングだけ解除git restore --staged <ファイル>安全
最後のコミットのメッセージや内容を修正git commit --amendプッシュ前のみ
ローカルのコミットを取り消すgit reset --soft/--mixed/--hard--hardは編集を削除
プッシュ済みのコミットを巻き戻すgit revert <コミット>安全

判断の順序は上から下です。まだコミット前ならrestore、コミット済みでプッシュ前ならamendかreset、プッシュまで済んでいるならrevertです。共有された履歴は消さずに新しいコミットで覆うという原則ひとつを守るだけでも、チームで起きるGit事故の大半を避けられます。

まとめ #

この記事の結論はひとつの文にまとまります。取り消しコマンドは暗記するものではなく、何をどこまで取り消すかを決めれば自然に決まります。コミット前はrestore、プッシュ前はamendとreset、プッシュ後はrevertです。

次の記事「Git基礎 #6 GitHubと初めてのPull Request」はシリーズの最終回です。ここまで学んだブランチ、コミット、プッシュをまとめて、コラボレーションの標準手順であるPull Requestを最初から最後まで作ってみます。

X