TL;DR: git revertは古いコミットを打ち消す新しいコミットを追加します。履歴は保存され、他人がpull済みのブランチでも安全。git resetはブランチポインタを後ろへ動かします。履歴は書き換わり、pushしていないコミットにだけ安全。共有ブランチではrevert、ローカルの後片付けではreset。reset --hardはさらにコミット前の作業ツリーの変更も削除します。本物の仕事を食うのはこいつです。
git revertとgit resetの違いは何か?
どちらも「過去のコミットが無かったこと」にできます。違うのはその方法です:
git revert <sha>はコミットの逆パッチを計算してコミットします。履歴は「あれを取り消す」1コミットぶん伸びる。悪いコミットはログに残り、その後に取消しが続きます。他のすべてのSHAは無傷です。git reset <sha>は現在のブランチのポインタを<sha>へ動かします。それ以降のコミットは切り離されます。ガベージコレクションまではオブジェクトDBに残りますが、どのブランチからもgit logからも到達できません。
一方は何も書き換えず、もう一方は何事も無かったかのように振る舞います。この性質ひとつで、状況ごとにどちらを使うべきかが決まります:
# デモ:そのまま実行できる使い捨てリポジトリ
git init /tmp/revert-vs-reset && cd /tmp/revert-vs-reset
echo one > file.txt && git add . && git commit -m "one"
echo two > file.txt && git add . && git commit -m "two"
# Revert:履歴には両方のコミットが残り、"two"を打ち消す3つ目が追加される
git revert --no-edit HEAD
git log --oneline # 3コミット:revert、two、one
# Reset:ブランチポインタが戻り、"two"はログから消える
git reset --hard HEAD~1
git log --oneline # 1コミット:one 両方実行してgit logを見てください。この非対称性が答えのすべてです。
git resetの—soft、—mixed、—hardは実際何をするのか?
3つのモードは、ポインタが動いたあとに変更がどこに残るかを制御します:
| モード | ブランチポインタ | ステージング | 作業ツリー | 編集内容 |
|---|---|---|---|---|
--soft | 戻る | ステージ済みのまま | 無傷 | 完全に保持 |
--mixed(デフォルト) | 戻る | 未ステージ | 無傷 | 保持(未ステージ) |
--hard | 戻る | リセット | リセット | 削除 |
表の読み方を具体で:
git reset --soft HEAD~1— 「コミットが早すぎた」。すべてがステージ済みに戻り、再コミット待ち。追加の作業とまとめ直すのも自由です。git reset HEAD~1(mixed)— 「ステージするファイルを間違えた」。編集は作業ツリーに残り、ステージは空。行き場の無いファイルも消したいなら、git remove untracked filesと併用します。git reset --hard HEAD~1— 「このコミットもコミット前の編集もゴミだ」。gitは二度と尋ねません。stashするか先にコミットでもしない限り、作業ツリー部分を元に戻す手立ては存在しません。
revertにモードが無いのは、何も破壊しないからです。やることは相殺コミットの追加だけ。
git resetの代わりにgit revertを使うべきときは?
たった1つの質問です。このコミットを他人がpull済みか? Yesなら、resetは選択肢から消えます。
他人が作業の土台にしたブランチをresetすると、共有履歴が書き換わります。相手の次のpullは分岐したブランチに直面し、「修正」はたいていforce-pushになり、あなたのミスが全員の問題に変わります。revertは通常のコミットを追加するだけなので、素のgit pullが全員できれいにマージされます。mainやGitHubのプルリクエストでrevertがデフォルトの答えなのはこのためです。「Revert」ボタンが存在するのは、マージ済みPRの履歴を書き換える選択肢が無いからに他なりません。
resetは共有前の窓のための道具です。30秒前に作ったコミット、ステージのミス、ローカルの実験ブランチ。作品の唯一のコピーが自分のマシン上にあるなら、履歴を組み替えるのは自由。pre-pushの窓の存在意義はそれに尽きます。
中間ケースもあります。自分だけのフィーチャーブランチにpushしていて、誰もその上に作っていない場合。reset後のforce-pushはそこでは社会的に許容されますが、それでもrevertの方が調整コストは安い。force-pushは自分だけが所有するブランチに取っておきましょう。
共有ブランチではgit revertの方が安全なのか?
はい。慣習ではなく、構造的に安全です:
- revertは通常のコミットを1つ生むだけ。CIはその上で回り、diffはレビュー可能で、
git logは未来の自分に「なぜこの変更が消えたのか」を説明してくれます。 - resetは中間状態を黙って捨てます。あとからレビューする人は何がいつ取り消されたのか見えません。ログは、それが最初から無かったかのような一直線になるだけです。
実務上の危うい角が2つ:
- マージコミットのrevertには
git revert -m 1 <sha>が必要です(最初の親の系列を残す)。-m無しではgitは拒否し、あとはエラーメッセージを読むしかありません。 - revertはタイムマシンではありません。取り消すのは1コミットのdiffだけです。後続のコミットが同じ行に触れていれば、コンフリクトの解消が発生します。これは「取り消しが絡み合っている」とgitが教えてくれているのであり、有用な情報であって故障ではありません。
直前のコミット専用の取り消し(変更を残すか捨てるか)の選択肢は、git undo last commit: keep the changesにまとまっています。
git restoreはどこに収まるのか?
git restore(と対をなすgit switch)はgit 2.23で登場しました。名前の多義性でresetが下手にこなしていた仕事を、引き受けたのです:
| 作業 | 旧来の方法 | 明示的な方法 |
|---|---|---|
| ファイルの作業ツリー編集を破棄 | git checkout -- file / git reset --hard | git restore file |
| ファイルをアンステージ | git reset HEAD file | git restore --staged file |
| ブランチを別のコミットへ移動 | git reset <sha> | git reset <sha>(代替なし——これだけは残る) |
つまりモダンな役割分担はこうです。restoreはファイルを直し、resetはブランチを動かし、revertは公開済みコミットを取り消す。サジェストを見ると「git revert vs reset vs restore」が一緒に検索されていますが、もっともな話です。ひとつのメンタルモデルが3コマンドに分かれているだけなのですから。旧来のgit checkout形式はどこでもまだ動きます。新しい形式の効能は、ファイルを1つアンステージしたかっただけなのにブランチ丸ごとシュレッダーにかける事故を防ぐことにあります。
悪いコミットを消すのではなく良いコミットを前へコピーしたいなら、それはcherry-pickの仕事です。git cherry-pick: multiple commits, branches, conflictsを参照。
判断のまとめ
- コミットは公開済み(push済み、他人がpull済み)→
git revert <sha>。 - コミットはローカルのみ、やり直したい →
git reset --softか--mixedで再コミット。 - ローカルのみ、未コミットの汚れごと消したい →
git reset --hard。その前に、作業ツリーに他に何があるか正直に一度見る。 - 1ファイルだけ壊れた →
git restore <file>。ブランチは触れない。
このリストで本当に危険なのは--hardだけです。他はすべてgit reflogで口を割らせられます。gitでの恒久的な喪失は狭い範囲に限られ、たいてい明示的にタイプすることを要求します。身につけるべき習慣は、--hardの前に半秒止まること。gitの鋭い道具の使用を避けることではありません。
— mrsaynothing
Get the next one by email
One email per post. No spam, no algorithms.
what is this?記事を楽しんでいただけましたか?私の本業はこのような構築です。 採用のご相談