git reset と git revert: コミットを安全に取り消し、チームの履歴を壊さない方法
git reset はブランチの参照を別のコミットへ移し、履歴を書き換えるため、まだ送っていないローカルのコミットにだけ使います。git revert は逆向きの変更を新しいコミットとして追加し、履歴を残すので、サーバーにあるコミットを安全に取り消せます。
要点を言うと、git reset はブランチを後ろに戻し、余分なコミットがなかったかのように見せます。git revert は履歴をそのまま残し、古い変更を打ち消す新しいコミットを追加します。前者はローカルの作業に、後者はすでに共有リポジトリへ送ったすべてのものに使います。この1つのルールを覚えるだけで、Git の事故の大半は防げます。
以下では、両方のコマンドの仕組み、reset のモードの違い、push 後のコミットの取り消し方、「失われた」コミットの復元、そして git clean の役割を説明します。
取り消しの動作を変える3つの領域
違いを理解するには、Git の3つの領域を思い出してください。作業ディレクトリ(ディスク上のファイル)、インデックス(ステージングエリア。次のコミットに入るもの)、履歴(ブランチと HEAD が指すコミット)です。取り消しのコマンドは、それぞれ異なる領域の組み合わせを扱います。
| コマンド | 履歴 | インデックス | 作業ディレクトリ | 共有ブランチで安全か |
|---|---|---|---|---|
git restore ファイル | 変わらない | 変わらない | 変わる | はい |
git restore --staged ファイル | 変わらない | 変わる | 変わらない | はい |
git reset --soft | 変わる | 変わらない | 変わらない | いいえ |
git reset --mixed(既定) | 変わる | 変わる | 変わらない | いいえ |
git reset --hard | 変わる | 変わる | 変わる | いいえ |
git revert | コミットを追加 | — | — | はい |
git clean | 変わらない | 変わらない | 未追跡ファイルを削除 | はい。ただし取り消せない |
git reset の仕組み
git reset <コミット> は、現在のブランチのポインタ(HEAD も一緒に)を指定したコミットへ移します。その後にあったコミットは物理的には削除されませんが、ブランチからは見えなくなります。モードによって、インデックスと作業ディレクトリをどうするかが決まります。
git reset --soft: インデックスに変更を残してコミットを取り消す
ブランチだけが移動し、インデックスとファイルは変わりません。取り消したコミットの変更は、すべてコミットの準備ができた状態でインデックスに残ります。これは、コミットメッセージを書き直したり、直近の複数のコミットを1つにまとめたりするのに理想的な方法です。
git reset --soft HEAD~3
git commit -m "feat: форма регистрации"
直近の3つのコミットが1つになりました。ファイルの内容は変わっていません。
git reset --mixed: コミットとインデックスを取り消す
これが既定のモードです。git reset HEAD~1 は git reset --mixed HEAD~1 と同じです。ブランチが移動し、インデックスはリセットされますが、変更は作業ディレクトリに未ステージの状態で残ります。余計なコミットをしてしまい、改めてどのファイルをどの単位で追加するかを決め直したいときに便利です。
git reset HEAD~1
git status
# Changes not staged for commit:
# modified: app.py
# modified: config.yml
git reset --hard: 完全に巻き戻す
ブランチ、インデックス、作業ディレクトリのすべてが、指定したコミットの状態になります。追跡されているファイルのコミットされていない変更は、警告なしに失われます。reset のモードのうち、実際に作業を消し去ることができるのはこれだけです。
git reset --hard HEAD~1 # 直近のコミットと、すべての変更を捨てる
git reset --hard origin/main # ブランチをサーバーの状態に合わせる
--hard の前には、git status で確認し、大切なものがあれば git stash で退避してください。コミットされたものは後から戻せます(後述)。コミットされていない変更は戻せません。
ファイル単位の git reset
パスを指定すると、reset はブランチを動かさず、インデックスだけを変更します。git reset app.py はそのファイルをステージから外します。現在ではより分かりやすい git restore --staged app.py を使います。パスを指定した場合、--soft と --hard は使えません。
git revert の仕組み
git revert <コミット> は、指定したコミットの変更を計算し、その逆向きの変更を適用して、新しいコミットを作ります。追加された行は削除され、削除された行は戻ります。履歴は前へ伸びていくだけで、何も書き換えられないため、同僚は手元で何かを直す必要がありません。
git log --oneline
# a3f9c21 (HEAD -> main) docs: README
# 7be41d0 feat: кэширование ответов
# 1c02e8f fix: таймаут запросов
git revert 7be41d0
# [main 5d8e2aa] Revert "feat: кэширование ответов"
# 2 files changed, 3 insertions(+), 41 deletions(-)
Git はエディタを開き、Revert "..." という形式のメッセージと、This reverts commit 7be41d0... の行を表示します。なぜその変更を取り消したのかを追記しておくと良い習慣です。
revert の便利なオプション
--no-edit: エディタを開かず、標準のメッセージを使う。-n(--no-commit): 逆向きの変更をインデックスに適用するだけで、コミットは作らない。複数のコミットの取り消しを1つのコミットにまとめられる。git revert A..B: コミットの範囲を取り消す。各コミットに対して個別の revert コミットが、新しいものから古いものの順に作られる。-m 1: マージコミットには必須。どちらの親を本流とみなすかを指定する。
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert: 実験を含む3つのコミットを取り消し"
マージコミットの revert
マージには親が2つあります。Git は、どちらを基準に「逆向き」の変更を計算すべきか分からないため、-m がないと is a merge but no -m option was given というエラーになります。通常、本流は最初の親(統合先のブランチ)です。
git revert -m 1 9f1e2ab
注意点があります。後で同じブランチを再び統合しようとすると、Git はそのコミットがすでに統合済みだと判断し、変更を取り込みません。解決するには、まず revert コミット自体を revert してから、新しい変更を統合します。
git reset と git revert: どちらを選ぶか
| 比較項目 | git reset | git revert |
|---|---|---|
| 履歴への作用 | 書き換える(コミットがブランチから消える) | 新しいコミットを追加する |
| コミットがすでに送信されている場合 | push --force が必要。同僚の作業を壊す | 通常の git push でよい |
| 履歴の途中のコミットの取り消し | 以降のコミットもすべて巻き込む | ピンポイントで取り消せる |
| 履歴に残る記録 | 残らない | 残る。誰がいつ何を取り消したかが分かる |
| 典型的な場面 | push 前にローカルのコミットを整理する | 本番の main を壊したコミットを取り消す |
簡単なルールがあります。そのコミットを誰かがすでに見ていたら revert、見ていなければ reset。 例外は、誰も取得していない自分の feature ブランチです。そこでは履歴を書き換え、git push --force-with-lease で送ることが許されます。
共有ブランチで reset --hard を使うのが危険な理由
たとえば、main で git reset --hard HEAD~2 を実行し、git push --force をしたとします。同僚のリポジトリにはその2つのコミットがまだあります。同僚が次に git pull すると、Git は2つの履歴を統合しようとするため、「消えた」コミットが余計なマージコミットとともに戻ってきます。さらに悪いことに、同僚が先に別のコミットを push していた場合、その作業は強制 push で上書きされてしまいます。
そのため、保護されたブランチ(main、release)では、ホスティングの設定で強制 push を禁止するのが一般的です。どうしても書き換えが必要な場合は、git push --force-with-lease を使ってください。サーバーに自分の知らないコミットがあれば、それを拒否します。
典型的な取り消しの場面
push 前の直近のコミットを取り消す
git reset --soft HEAD~1 # 変更はインデックスに残る。もう一度コミットできる
git reset HEAD~1 # 変更はファイルに残る。インデックスは空
git reset --hard HEAD~1 # 変更も捨てる
メッセージを直したい、または忘れたファイルを追加したいだけなら、reset は必要ありません。git add 忘れたファイル と git commit --amend で十分です。
push 後のコミットを取り消す
git revert <ハッシュ>
git push
これが「git reverse commit」を探している人への答えです。Git に git reverse というコマンドはありません。逆向きのコミットを作るのが git revert です。
ブランチをサーバーの状態に戻す
git fetch origin
git reset --hard origin/main
ローカルのブランチが、失敗した rebase や実験の後で混乱したときに役立ちます。サーバーにないローカルのコミットは、すべてブランチから消えます。
誤った reset の後にコミットを復元する
Git は HEAD の移動をすべて reflog に記録します。reset で見えなくなったコミットは、少なくとも数週間はデータベースに残ります(既定では、到達できなくなった reflog の記録は30日間保持されます)。
git reflog
# 1c02e8f HEAD@{0}: reset: moving to HEAD~2
# a3f9c21 HEAD@{1}: commit: docs: README
# 7be41d0 HEAD@{2}: commit: feat: кэширование ответов
git reset --hard a3f9c21 # または HEAD@{1}
reflog はローカルのものです。自分のクローンにだけあり、サーバーには送られません。
git clean: 追跡されていないファイルを削除する
git reset --hard は、Git が追跡していないファイル(インデックスに入ったことのない新規ファイル、ビルド成果物、ログなど)には触れません。それらを消すには git clean を使います。このコマンドは取り消せません。削除されたファイルは reflog にもゴミ箱にも残りません。
git clean -n # ドライラン: 削除される対象を表示
git clean -f # 追跡されていないファイルを削除
git clean -fd # 追跡されていないフォルダも削除
git clean -fdx # .gitignore の対象(node_modules、dist など)も削除
git clean -i # 対話的に選ぶ
-f を付けないと、Git は既定で動作を拒否します(設定 clean.requireForce)。必ず -n から始めてください。特に -x は、ローカルの .env が無視リストに入っていれば、それも消してしまいます。
最後のコミットの状態に完全に戻したい場合は、次の組み合わせを使います: git reset --hard && git clean -fd。
よくある間違い
- --soft の代わりに --hard を使う。 コミットされたものは reflog で戻せますが、コミットされていないものは失われます。実験の前に
git stashをする習慣をつけると安心です。 - 中身を理解せずに revert する。 後のコミットが同じ行を変更していると、revert はコンフリクトになります。通常の手順で解決してください。ファイルを直し、
git add、git revert --continue。やめる場合はgit revert --abort。 - restore と reset を混同する。 1つのファイルの変更を取り消すために、ブランチ全体を reset する必要はありません。
git restore ファイルで十分です。 - main に強制 push する。 revert して通常の push をする方が、履歴が少し長くなっても安全です。
まとめ
git reset は、自分だけの、まだ公開していない履歴のための道具です。--soft は変更をインデックスに残し、--mixed はファイルに残し、--hard はすべてを捨てます。git revert は、共有ブランチを安全に取り消す方法で、履歴に確かな記録を残します。git clean は未追跡のファイルを整理しますが、取り消しはできません。restore、reset、revert を使った取り消しの手順は、Gitコースで実践的に扱っています。
よくある質問
git reset でコミットは完全に消えますか?
いいえ。コミットはデータベースに残り、git reflog で見つけられます。ガベージコレクションが削除するまでの間、到達できなくなった記録は既定で30日間保持されます。
git revert を取り消すことはできますか?
はい。revert のコミットも普通のコミットなので、もう一度 git revert で打ち消せます。まだ送っていなければ git reset で取り除くこともできます。
git reset と git checkout の違いは何ですか?
git reset は現在のブランチのポインタを動かします。git checkout(または git switch)は HEAD を別のブランチやコミットに切り替え、ブランチそのものは変えません。
HEAD~1 と HEAD^ の違いは何ですか?
どちらも現在のコミットの親を指します。HEAD~3 は最初の親をたどって3つ前のコミット、HEAD^2 はマージコミットの2番目の親です。
git clean を取り消すことはできますか?
Git の機能では不可能です。追跡されていないファイルはリポジトリに保存されていないため、削除前に必ず git clean -n で確認してください。 </content>