git push --forceでコミットを上書きしたとき — 復旧と再発防止

読了 7分

git push --forceを実行した直後、リモートブランチから同僚のコミットが消えていることに気づきました。リモートの履歴が自分のローカルの状態に丸ごと置き換わり、その間に同僚が上げていた作業がブランチから見えなくなっています。結論から言うと、上書きされたコミットはまだどこかに残っている可能性が高いです。Gitはコミットを即座に削除せず、消えたコミットのコピーはたいてい複数の場所に散らばっています。ただし時間が経つほどコピーはひとつずつ消えていくので、速く動くほど復旧は簡単になります。

この記事では復旧経路を確実な順に3つ整理し、最後に同じ事故を構造的に防ぐ設定を扱います。

何が起きたのか #

force pushは、リモートブランチのポインタを自分のローカルブランチが指すコミットへ強制的に移動させる操作です。通常のpushが「リモートに自分の知らないコミットがあれば拒否する」という安全装置を持つのに対し、force pushはその確認を飛ばします。

force push前後のリモート履歴
[force push前 — リモート]
A --- B --- C --- D        ← main (Dは同僚のコミット)

[自分のローカル — Bから履歴を書き換え]
A --- B --- C'             ← main

[force push後 — リモート]
A --- B --- C'             ← main (CとDがブランチから消える)

ここで重要な事実がひとつあります。CとDは削除されたのではなく、どのブランチからも指されていない到達不能な状態になっただけです。サーバーのガベージコレクションが走るまでコミットのデータ自体は残っていて、ハッシュさえ分かれば再びブランチとして生かせます。そしてそのコミットの完全なコピーは、同僚のローカルリポジトリにも、場合によっては自分のリポジトリにも存在します。

経路1 — そのコミットを持つ同僚のローカル #

最も確実な復旧経路です。消えたコミットDを作った同僚(またはそのコミットをpullしていた誰でも)のローカルリポジトリには、Dがそのまま残っています。Gitのリポジトリは互いに対等なコピーなので、そのコピーでリモートを上書きし直せば終わりです。

今回は誤った状態がリモートにあるため、復旧する側が強制プッシュを実行することになります。実行者はそのコミットを持つ同僚です。

同僚のリポジトリで実行
git fetch                                  # リモートの現在の(誤った)状態を確認
git push --force-with-lease origin main    # 自分のローカルの状態でリモートを復旧

--force-with-leaseは、最後にfetchしたリモートの状態と実際のリモートが一致するときだけ上書きするので、復旧の最中に別のpushと交錯する二次事故を防いでくれます。

注記
この復旧は、自分がforce pushで上げた新しいコミットC’を再び上書きします。C’の変更も生かす必要があるなら、リモートを原状復旧したあとでC’をcherry-pickで乗せ直せば済みます。C’は自分のローカルにそのまま残っているので、失う心配はありません。復旧の優先順位は常に、消えたコミットの側にあります。

経路2 — 自分のリポジトリのremote-tracking reflog #

同僚が不在でも、消えたコミットを持つ人が見つからなくても、自分のリポジトリに手がかりが残っていることがあります。force pushの前にfetchやpullを実行したことがあれば、自分のorigin/mainが以前どこを指していたかがreflogに記録されています。

origin/mainの移動履歴を確認
$ git reflog show origin/main
f3d9a21 refs/remotes/origin/main@{0}: update by push: forced-update
8c4d2e7 refs/remotes/origin/main@{1}: fetch: fast-forward
1a2b3c4 refs/remotes/origin/main@{2}: fetch: fast-forward

forced-updateと記された項目が事故の時点です。その直前の項目である8c4d2e7が、force push前にリモートのmainが指していたコミット、つまり消えたコミットDです。fetchを通じてオブジェクトは自分のリポジトリに下りてきているので、このハッシュをそのままリモートブランチへ押し上げれば復旧できます。

以前のハッシュでリモートを復旧
git push --force-with-lease origin 8c4d2e7:main

<ハッシュ>:mainという形式は、ローカルブランチを経由せずに特定のコミットをリモートのmainとして指定する文法です。復旧後はgit fetchでリモートの状態を受け取り直し、正常に戻ったことを確認します。

経路3 — ホスティングサービスに残った記録 #

ローカルのどこにも手がかりがなければ、ホスティングサービスの記録を調べます。GitHubの場合、3か所に痕跡が残ります。

  • Activityビュー — リポジトリのActivityページでForce pushesのフィルタをかけると、事故のイベントとともに上書き前のコミットへの比較リンクが表示されます。ここで以前のハッシュがすぐに手に入ります。
  • Events API — pushイベントには以前の状態がbeforeフィールドとして残ります。
Events APIで以前のハッシュを探す
curl -s https://api.github.com/repos/<owner>/<repo>/events \
  | grep -B2 -A2 '"before"'
  • PRページ — 消えたコミットがどこかのPRに含まれたことがあるなら、PRのコミット一覧にハッシュがそのまま残っています。

ハッシュが見つかったなら、サーバーにはまだオブジェクトが残っている可能性が高いので、自分のリポジトリへ取り込んでリモートブランチを復旧します。

ハッシュでオブジェクトを取得して復旧
git fetch origin 8c4d2e7
git push --force-with-lease origin 8c4d2e7:main

この経路まで塞がっていたら、最後の手段としてGitHub Supportに問い合わせます。サーバー側のガベージコレクションが走る前であれば、復旧を助けてもらえる余地が残っています。

復旧後の整理 #

リモートが原状復旧しても、チームの整理がもう一段残っています。事故と復旧の間にfetchやpullを実行した同僚のローカルには、誤った状態が残っている可能性があります。事故の時刻と復旧の完了をチームに知らせ、その間にリモートと同期した人にはgit fetchのあとで自分のブランチとorigin/mainがずれていないかをgit statusで確認してもらいます。ずれていた人は、ローカルの作業を保全したうえで(必要ならブランチに分離して)リモート基準に整理すれば済みます。

最後に事故の原因を確認します。大半は、rebase後にpushを拒否され、反射的に--forceを付けたケースです。次の節の設定で、同じ経路を構造的に断ちます。

再発防止 — 設定で防ぐ #

  • 強制プッシュは常に--force-with-leaseで行います。リモートに自分の知らないコミットがあればpushを中断してくれるので、今回のような事故の大半は実行前に弾かれます。--forceとの違いはGit実務ワークフロー #3 interactive rebase — squash・fixupでコミット整理で扱いました。
  • 保護ブランチでforce pushを遮断します。GitHubのbranch protectionでmainのような共有ブランチのAllow force pushesをオフにしておけば(デフォルト)、習慣に関係なくサーバーが強制プッシュを拒否します。人の注意力の代わりに設定が事故を防ぐ、最も確実な層です。
  • 共有ブランチはrebaseしません。force pushが必要になる状況自体を作らないための原則です。根拠はGit実務ワークフロー #2 rebase vs merge — 判断基準と禁則で整理しました。

まとめ — 対応の順序 #

force push事故に遭遇したときの順序を整理します。

  1. 追加のpushを止め、チームに事故を知らせます。コピーが消える前に動くことが最も重要です。
  2. 消えたコミットをローカルに持つ同僚を探します。いれば、その同僚が--force-with-leaseでリモートを復旧します。
  3. いなければ、自分のgit reflog show origin/mainで以前のハッシュを探し、<ハッシュ>:mainで押し上げます。
  4. それでもなければ、GitHubのActivityビュー、Events API、PRページから以前のハッシュを見つけ、fetchして復旧します。
  5. 復旧後はチームに知らせ、branch protectionと--force-with-leaseの習慣で再発を防ぎます。

コミットは思ったほど簡単には消えません。事故直後の数時間、落ち着いてコピーを追跡することが復旧のすべてです。

X