git force pushの挙動

2023-01-01

なんで今更force pushか

私個人はgit push -fは危険コマンドと認識して、使うことを避けてきた。git push -fを使わないとできないことはほとんどなく、唯一必要なのが「PR作成済でPRをクローズせずにgit rebaseした状態をPRに反映する」ということをしたい場合だけである。それも、コミットログが汚くなるというデメリットを許容すれば、git rebaseの代わりにgit mergeを使えば解決できる。

というわけで、私はgit push -fを一切使わずgit mergeで切り抜けてきたし、squashがしたくなるようなコミットはそもそもしてこなかった。

ただ、現職ではgit rebasegit push -fしている人が多いようだったので、自分もそれに倣おうと思って調べてみたというわけである。

一人しか編集しないブランチでのforce push

基本的にgit push -fしても安全なはずである。

ただ、同僚から聞いた話だとGitHubでレビュー中にgit push -fするとコメントが消えたりすることがあるらしい。色々試してみたところ、Files changedタブではなくCommitsタブでコミットを指定してコメントをしている途中でそのコミットが消失すると、Refreshを促されサブミットできず、Refreshすると入力途中だったコメントが消えてしまうようだった。Files changedタブの場合だとオートセーブされているようで、当該行にコメントにコメント追加しようとすれば復帰する(たまに復帰しないことがあるがリロードしたら復帰した)。サブミット済のコメントはよしなに引き継いでくれるようである。

共有ブランチでのforce push

「複数人が編集するブランチにgit push -fするな! 以上!」で終わりなのだが、何が問題なのかはメモしておく。

  • 他の人がしたpush済コミットが消える
  • 他の人がpushしようとしてエラーになる
  • PRのマージコミットが消える
  • PRのマージ前にマージ先がforce pushされた場合、マージ前にrebaseをしなかったらログがぐちゃぐちゃになるかmergeコミットが作られないかの2択になる

前者二つについては非常に面倒だが、リカバリができる。

# 断っておくが、Aboutの免責に書いている通り、
# リカバリ手段としてこの手順を真似したことで起きる結果に対して私は一切責任を負わない
# (対象ブランチがfeature-xxxだとして)
git checkout feature-xxx # git push -fされる前のコミットのブランチ
git fetch
git checkout -b work-feature-xxx # 別名ブランチを作る
git branch -D feature-xxx # 旧ブランチを削除
git checkout feature-xxx # git push -fされた後のコミットのブランチ
git cherry-pick XXXXXXXXX # work-feature-xxxブランチのコミットログを一つ一つcherry-pickする
git push

もしかするとrebaseでうまいことやれたりするのかもしれない。ただ、cherry-pickを使った方法は確実なはずである。コミットしていない人はgit branch -D feature-xxxしてgit checkout feature-xxxするだけでよい。

共有ブランチ対してPRしていてマージ済にもかかわらずgit push -fした場合、PRのブランチをPR先(git push -fされたブランチ)にrebaseしてPRし直せば変更自体は取り込める。しかし、PR上でのやりとりは引き継がれないため、将来的に調査が必要になった時に困るはずである。

「PRのマージ前にマージ先がforce pushされた場合、マージ前にrebaseをしなかったらログがぐちゃぐちゃになるかmergeコミットが作られないかの2択になる」については、PR側のブランチではforce push前のコミットも含まれるため、PRをmergeするとコミットが重複したログとなってしまう。PRのマージ時にrebaseを選択すればコミットの重複はされないが、逆にmergeコミットも作成されなくなってしまう。対処としては、PRのブランチもgit rebasegit push -fすれば良いが、数が多ければ非常に面倒なことになる。

git push -fは刃物と一緒

git push -fは便利だが使い方を間違えれば大怪我をするという意味で刃物と同じである。ただし、上記の通り迷惑を被るのは常に同僚である。

なので、第一選択肢は一切使わないことである。最初に書いた通り、git rebasegit push -fではなくgit mergegit pushを使う。

もし使うのであれば、git push -fではなくgit push --force-with-lease --force-if-includesを使うべきである。--force-with-leaseはpush先にfetchしていないコミットがある場合はエラーにするオプションで、--force-if-includesはpush先にあってpushしようとしているブランチに含まれないコミットがある場合にエラーにしてくれるオプションである。

しかし、これらのオプションを指定したところで防げるのは「他の人がしたpush済コミットが消える」と「PRのマージコミットが消える」の2つで、「GitHubの入力中のコメントが消える」、「他の人がpushしようとしてエラーになる」、「PRのマージ先がforce pushされた場合、マージ前にrebaseしないと問題が起きる」の3つは何も解決されないことは理解しておく必要がある。

gitはコマンドにエイリアスを設定できるので、以下のように設定しておくのが良い。

git config --global alias.force-push 'push --force-with-lease --force-if-includes'

まとめ

  • 共同編集しているブランチにforce pushするな
  • git push --force-with-lease --force-if-includesでも共同編集しているブランチにするな
  • force pushするなら、自分一人しか編集しないブランチで、レビューされていないタイミングでやれ
  • 許されるならそもそもforce pushを一切使わないのが最も安全