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プレフィックスに似ている部分)こそが、復旧操作の決め手になる部分です。上の場合は、deadbeef
1の部分が、(あなたが運悪くぶっ壊す直前の)壊れていない最新の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"という機能があります。
main
、develop
、stable
などの重要なブランチに"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
オプションの組み合わせは、何か月もご無沙汰していたプロジェクトに復帰したときに利用できます。スリルが欲しくなったら試してみるとよいでしょう。
訳注: 上はジョークですので本気にしないでください↓。
git push --force --all
— Joe Thompson (@caffeinepresent) February 5, 2021
🔗 Changelog
🔗 1.1.0(2024-07-25)
- リンクの更新、テキストの見直し、コマンドを最新のものに更新
🔗 1.0.0(2017-09-12)
- オリジナル記事を公開
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。