Gitでやってはいけない操作と戻し方|push -f・reset --hard・reflog

コミットした内容は reflog で戻せることが多い一方、コミットしていない変更は戻せません。危ない Git 操作の境界線を整理します。

Git操作で消えるもの・戻せるもの。clean の前は git clean -n で対象を表示する
Git操作で消えるもの・戻せるもの。clean の前は git clean -n で対象を表示する

Git は「ほとんどのことは元に戻せる」道具です。コミットした内容は、ブランチを消しても reset しても、しばらくはリポジトリの中に残っています。その一方で、コミットしていない変更や、他の人のリポジトリにあるコミットは、操作によっては取り戻せません。ベテランが Git のコマンドで手を止めるのは、その操作が「戻せる側」か「戻せない側」かを判断するためです。この記事では、事故の多い操作と、それぞれの戻し方を説明します。

消えるものと戻せるものの一覧

操作消えるもの戻せるか
git push -fリモートにあって手元に無いコミット他の人の手元に残っていれば、その人が push し直せる
git reset --hard未コミットの変更と、ブランチから外れたコミットコミットは reflog で戻せる。未コミットの変更は戻せない
git checkout -- . / git restore .作業ツリーの未ステージの変更戻せない
git clean -fd追跡されていないファイルとディレクトリ戻せない
git branch -Dブランチ名(コミットは残る)reflog でコミットを探して作り直せる
git rebase書き換え前のコミットへの参照reflog で書き換え前に戻せる

表から分かるとおり、Git の中に記録された「コミット」は戻せる場合が多く、記録される前の「作業中の変更」や「Git が管理していないファイル」は戻せません。迷ったときは、操作の前に git status で未コミットの変更が無いかを確かめ、必要なら一時的にコミットするか git stash で退避しておくと安全です。

push -f は他の人のコミットを上書きする

git push は通常、リモートのブランチに自分のコミットを「付け足す」ときだけ成功します。手元の履歴とリモートの履歴が分かれていると rejected(拒否)されます。-f(--force)を付けると、この確認を飛ばしてリモートのブランチを手元の状態で上書きします。自分が amend や rebase をした後に push が拒否されたとき、何も考えずに -f を付けると、その間に同僚が push したコミットまで消えます。

# 危ない例:リモートの状態を確かめずに上書きするgit push -f origin main# 安全な例:最後に fetch した時点からリモートが変わっていたら失敗するgit push --force-with-lease origin feature/login
force を使うなら --force-with-lease にする

--force-with-lease は、リモートのブランチが自分の手元にある origin/feature/login(リモート追跡ブランチ)と同じ位置にあるときだけ上書きします。ただし、エディタが裏で自動的に fetch している場合などは、他人のコミットを取得済みと見なされて上書きが通ってしまいます。Git 2.30 以降では --force-if-includes を併用すると、取得したリモートの変更を自分が取り込んでいるかも確認できます。そもそも main のような共有ブランチは、リポジトリの設定(ブランチ保護)で強制 push を禁止しておくのが一般的です。

共有ブランチの履歴を書き換えない

他の人が使っているブランチを rebase や amend で書き換えて push すると、他の人の手元の履歴と食い違い、マージのたびに混乱が起きます。履歴を書き換えてよいのは、基本的に自分だけが使っているブランチです。push 済みのコミットを取り消したいときは、git revert で打ち消すコミットを新しく作ります。

reset --hard は未コミットの変更を消す

git reset は、ブランチの指す先を別のコミットに動かします。--soft は変更をステージに残し、--mixed(既定)は作業ツリーに残しますが、--hard はステージと作業ツリーも指定したコミットの状態に合わせるため、コミットしていない変更は破棄されます。「直前のコミットを取り消して作り直したい」ときに --soft のつもりで --hard を打つと、編集中のファイルが消えます。

git statusgit stash push -m 'before reset'   # 未コミットの変更があれば退避するgit reset --hard HEAD~1
reset の前に未コミットの変更を確認する

checkout . と restore . と clean -fd

git checkout -- . と git restore . は、作業ツリーの変更を、ステージ(またはコミット)の内容で上書きして破棄します。「変更をいったん全部捨てたい」ときの操作ですが、ファイル名を 1 つ指定するつもりで . を付けると、すべてのファイルの変更が消えます。Git に記録されていない変更なので、reflog でも戻せません。

git clean は、Git が追跡していないファイルを削除します。-f が無いと既定では実行されず、-d でディレクトリも、-x で .gitignore で無視しているファイルも対象になります。-x を付けると、.env のような手元だけの設定ファイルやローカルのデータベースも消えます。-n(--dry-run)を付けると、何が消えるかだけを表示できます。

git clean -n -d     # 削除対象を表示するだけgit clean -f -d     # 確認した後で実行するgit clean -i -d     # 対話的に選んで削除する
clean は -n で確認してから実行する

reflog でコミットを取り戻す

reflog(参照ログ)は、HEAD やブランチが過去にどのコミットを指していたかを手元のリポジトリに記録したものです。reset --hard でコミットが見えなくなっても、rebase で書き換えても、ブランチを削除しても、reflog には移動の履歴が残っています。戻りたい時点のコミットを見つけて、そこにブランチを作るか reset で戻します。

git reflog# 3f2a1bc HEAD@{0}: reset: moving to HEAD~1# 9d4e7f0 HEAD@1: commit: ログイン画面のバリデーションを追加git branch rescue 9d4e7f0     # まず別ブランチとして確保するgit reset --hard HEAD@1     # 現在のブランチをその時点に戻す
reset --hard で消えたコミットを取り戻す

reflog はあくまで手元のリポジトリの記録で、push や clone では共有されません。また、記録は永遠には残らず、既定では到達できるコミットの記録が 90 日、どのブランチからも到達できないコミットの記録が 30 日を過ぎると、git gc の際に削除されます。消えたと気づいたら、早めに別のブランチを作って確保します。push -f でリモートから消えたコミットは、push した本人の reflog ではなく、そのコミットを持っていた人の手元に残っていることが多いので、関係者に確認します。

stash を消してしまったとき

git stash drop や git stash clear で退避した変更を消してしまった場合も、すぐであれば取り戻せることがあります。stash の中身は内部的にはコミットとして保存されており、消した直後はどこからも参照されないコミットとしてリポジトリに残っているためです。git fsck --unreachable で到達できないコミットを一覧し、該当するものを git stash apply に渡します。drop を実行したときに表示される「Dropped refs/stash@{0} (ハッシュ)」のハッシュが分かれば、それを直接指定できます。

git fsck --unreachable | grep commitgit show <ハッシュ>          # 中身を確認するgit stash apply <ハッシュ>
drop した stash を探して適用する

危ない Git 操作の前のチェックリスト

  • git status で未コミットの変更と、今いるブランチ名を確認した。
  • push -f が必要な場合、対象が自分だけのブランチであり、--force-with-lease を使っている。
  • reset のオプションが --soft・--mixed・--hard のどれかを読み上げて確認した。
  • checkout・restore の対象が、意図したファイルだけになっている。
  • clean は -n で削除対象を表示し、-x が付いていないことを確認した。
  • 失敗に気づいたら、reflog を見て別ブランチで確保してから戻す。

Git の事故は、コマンドの意味を知っていても、急いでいるときやオプションを 1 文字打ち間違えたときに起きます。下の問題で、危ない操作とその戻し方を確かめてみてください。

問題で確かめる

この記事の内容を、制限時間つきの問題で確かめられます。

参考

よくある質問

git push -f と --force-with-lease の違いは?

push -f はリモートの状態に関係なく上書きします。--force-with-lease は、リモートのブランチが最後に取得した状態から変わっていないときだけ上書きするため、他の人のコミットを誤って消す危険を減らせます。

git reset --hard を取り消せる?

コミットは git reflog で以前の位置を探し、git reset --hard HEAD@{1} などで戻せます。ただし reset 前にコミットしていなかった変更は Git に記録されていないため戻せません。

git reflog とは?

HEAD やブランチが過去に指していたコミットを記録した手元のログです。reset、rebase、ブランチ削除などで見えなくなったコミットを探すのに使います。

git clean -fd で消したファイルは戻せる?

Git が追跡していないファイルを削除する操作なので、Git の機能では戻せません。実行前に git clean -n -d で削除対象を確認します。

push 済みのコミットを取り消すには?

共有ブランチでは git revert で打ち消すコミットを作って push するのが安全です。reset と push -f で履歴を書き換えると、他の人の手元の履歴と食い違います。