Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

git push --forceで壊れたブランチの復旧方法と予防方法(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

git push --forceで壊れたブランチの復旧方法と予防方法(翻訳)

はじめに

gitコマンドを間違えたばかりにプロジェクトのリポジトリがカオスになってしまった経験はありませんか?場合によっては、チーム全体での復旧のために数時間を無駄にすることもあります。本記事では、運悪くgit push --forceでやらかしてしまったときに短時間で回復する方法を紹介します。

原文編集者注

本記事は2017年に公開されましたが、2024年に全面改訂されました。

「自分はそんなヘマしたことないから」とお思いの方、自信過剰は禁物です。この仕事をやっていれば、いつの日かやらかすときが来るものです。同一のgitリポジトリで複数のメンバーがリモートで作業していれば、最終的にいつかどこかでmainブランチ(あるいは絶対さわってはならない他の重要なブランチ)にgit push --forceを実行することになるのですから。

これはたとえば、Herokuのようにアプリケーションのビルドとデプロイで別々のgitリポジトリを使っているサービスでデプロイを行ったときに発生する可能性があります。せっかく丸一日かかって苦心して作り上げたのに、いつものようにgit push --force heroku mainを実行してデプロイすべきところで、うっかりgit push --forceを実行してしまう可能性はいくらでもありえます。

その瞬間、チームメイトたちの最新の成果は吹っ飛んでしまいます。皆の怒りの視線が痛すぎる...

しかし、かの有名な銀河ヒッチハイク・ガイドの表紙にいみじくも「"Don't PANIC"(パニクるな)」と書かれているではありませんか!とにもかくにもgitを使っているのですから、すべて修正可能なはずです。

最初は最もましなケースを考えてみましょう。
あなたがmainブランチをぶっ壊したその直前に、運良く同一のコードで作業している他の誰かがmainの最新バージョンをプルしていたとしましょう。この場合あなたがすべき作業は、チームのチャットでその人に最新の変更をプッシュするようお願いすることです。

コードの完全な履歴が運良くローカルリポジトリに残っていれば、ミスした部分を新しいコードで上書きしてきれいに回復できます(おそらく誰も気が付かないでしょう)。

しかし運が悪ければどうしたらよいのでしょうか?
続きをお読みください!

🔗 ケース1: ミスする前にmainブランチに最後にプッシュしたのが自分だった場合

グッドニュースです!ミスを元通りにするのに必要な材料はすべて揃っています。ただし慌ててターミナルを閉じたり表示をクリアしたりしないこと

以上を念頭に置いて、まずはチームのチャットで正直にお詫びしましょう。そしてあなたが問題を修正し終わるまで、1分ほどリポジトリを操作しないようチームに周知してください。

さて、git push --forceをやらかした直後には、ターミナルに以下のような感じの行が表示されているはずなので、それを見つけましょう。

 + deadbeef...f00f00ba main -> main (forced update)

この行の冒頭部分(コミットのSHAプレフィックスに似ている部分)こそが、復旧操作の決め手になる部分です。上の場合は、deadbeef1の部分が、(あなたが運悪くぶっ壊す直前の)壊れていない最新のmainコミットを表します。

つまり、ここで復旧に必要な作業は「炎で炎を吹き消すこと」2、つまりmainブランチの壊れたコミットに対して、壊れていないコミットをforce pushすることです。

$ git push --force origin deadbeef:main

復旧おめでとうございます🎉!ピンチを脱出できましたね。過ちから学ぶことをお忘れなく。

🔗 ケース2: mainブランチがぶっ壊れる直前に他の誰かがmainブランチを変更していた場合

次のケースは、あなたがgit push --forceをやらかす直前に、運悪く他の誰かが多数のプルリクをクローズし、mainブランチがローカルブランチのものから変わり果ててしまった場合を考えてみましょう。

この場合、自分のローカルに最新のmainブランチが存在しないので、git push --force sha1:mainでの復旧はもう無理です。
しかも、どのブランチにも属さなくなってしまったものは、もはやgit fetchで取り出しようがありません。

しかしここで慌てふためいてはいけません。深呼吸して落ち着きを取り戻したら、リモート作業中のチームメイトにしばらくリポジトリを操作しないよう周知しましょう。

GitHubは、到達不能になったコミットをすぐには削除しません。これが復旧に役立ちます。もちろんそのコミットはフェッチできませんが、実は回避方法があるのです。

ただしこの方法は、あなたが運良くそのリポジトリを「監視できている」(つまり、リポジトリ内で発生している内容がGitHubのフロントページ上のフィードに表示されている)場合でないと使えない点にご注意ください。

運良くそういう状態になっていれば、GitHubのフロントページ上で以下のようなフィードを探してください。

an hour ago
Username pushed to main at org/repo
 - deadbeef Implement foo
 - deadf00d Fix bar

この情報があれば、以下の操作が可能になります。

  • https://github.com/org/repo/tree/deadbeefというURLを組み立てる
    (ここでdeadbeefは、ぶっ壊したブランチにある、壊れていない最新コミットのハッシュだとします)

  • GitHubのWeb UIで、以下のブランチ/タグ切り替えドロップダウンを開く

GitHubのブランチ/タグ切り替え

  • 作業用の一時ブランチを適当に作ります(例: main-before-force-push
  • "Create branch"をクリックする

これで、失われたコミットをすべてフェッチできるようになります。

$ git fetch
From github.com:org/repo
 * [new branch]      main-before-force-push -> origin/main-before-force-push

ここまで来れば、問題の深刻さはケース1と同程度にまで軽減されます。

$ git push --force origin origin/main-before-force-push:main

自分が必要なものがmainブランチにまだ残っている場合は、以下のようにrebaseするだけで済みます。

$ git rebase origin/main

🔗 今後こういう事故を予防する方法

  • 1. GitHubやGitLabには"protected branches"という機能があります。
    maindevelopstableなどの重要なブランチに"protected"を指定しておけば、誰もそれらのブランチにforce pushできなくなります。履歴をどうしても上書きしなければならなくなった場合は、いつでも一時的に"protected"を解除できます。

詳しくはGitHubのドキュメントを参照してください。

  • 2. --forceオプションではなく、--force-with-leaseオプションを使うこと。
    --force-with-leaseオプションは、自分が作業しているブランチに誰かがプッシュした場合(かつブランチ変更を自分がプルしていない場合)、プッシュ操作を停止します。
    詳しくはAtlassianの以下の記事をどうぞ(しばらく更新されていませんが、内容は現在も有効です)。
    参考: '-force considered harmful; understanding git's -force-with-lease - Work Life by Atlassian

  • 3. 誤って破壊的操作を実行しないようにするため、git push --forceを利用するコマンドのエイリアスを作成しておきましょう。

# ~/.gitconfig
[alias]
  deploy = "!git push --force heroku \"$(git rev-parse --abbrev-ref HEAD):main\""

エイリアスについて詳しくは、ProGitの以下のドキュメントを参照してください。

参考: Git - Git エイリアス

  • 4. mainブランチでじかに実験するのは厳禁です!
    頼りにすべきはgit switch -c experimentsです。

プロ限定のヒント

git pushには多くのオプションが用意されています。
--forceオプションと--allオプションの組み合わせは、何か月もご無沙汰していたプロジェクトに復帰したときに利用できます。スリルが欲しくなったら試してみるとよいでしょう。

訳注: 上はジョークですので本気にしないでください↓。


🔗 Changelog

🔗 1.1.0(2024-07-25)

  • リンクの更新、テキストの見直し、コマンドを最新のものに更新

🔗 1.0.0(2017-09-12)

  • オリジナル記事を公開

関連記事

「巨大プルリク1件vs細かいプルリク100件」問題を考える(翻訳)

Git履歴をgit resetとgit rebaseで管理する(翻訳)


  1. 訳注: deadbeef(死んだ牛肉)は16進数のafだけで表せる単語なので、この種のサンプルでしゃれとして使われることがあります。 
  2. 訳注: 「炎で炎を吹き消す」(爆風消火)は、フランス映画『恐怖の報酬』における「油田火災をニトログリセリンの爆風で吹き飛ばして消す」が有名です。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。