Gitでやってはいけない操作と戻し方|push -f・reset --hard・reflog
コミットした内容は reflog で戻せることが多い一方、コミットしていない変更は戻せません。危ない Git 操作の境界線を整理します。
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-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~1checkout . と 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 # 対話的に選んで削除する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 # 現在のブランチをその時点に戻す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 <ハッシュ>危ない Git 操作の前のチェックリスト
- git status で未コミットの変更と、今いるブランチ名を確認した。
- push -f が必要な場合、対象が自分だけのブランチであり、--force-with-lease を使っている。
- reset のオプションが --soft・--mixed・--hard のどれかを読み上げて確認した。
- checkout・restore の対象が、意図したファイルだけになっている。
- clean は -n で削除対象を表示し、-x が付いていないことを確認した。
- 失敗に気づいたら、reflog を見て別ブランチで確保してから戻す。
Git の事故は、コマンドの意味を知っていても、急いでいるときやオプションを 1 文字打ち間違えたときに起きます。下の問題で、危ない操作とその戻し方を確かめてみてください。
問題で確かめる
この記事の内容を、制限時間つきの問題で確かめられます。
- push 済みで他の人も使っているブランチを rebase しても、共有に支障はない。Git / ○×
git reset --hard HEAD~1を実行すると何が失われる?Git / 4 択git reset --soft HEAD~1を実行した後の状態は?Git / 4 択- push 済みのコミット abc123 の変更を取り消したい。安全なのは?Git / 4 択
- push 済みコミットを amend した後
git pushが rejected された。理由は?Git / 4 択 - 共有ブランチへの
git push -fで起きうる最も深刻な問題は?Git / 4 択 - 共有の main ブランチへの
git push -fは、他の人のコミットを消すおそれがあるため指摘すべき操作である。指摘・レビュー / ○× - チャットで
git reset --softのつもりが --hard と書いてしまった。訂正として適切なのは?指摘・レビュー / 4 択