【Git】pushしたブランチをrebaseしてはいけない理由

git pushをした後やプルリクエストを出した後にはgit rebaseをしてはいけない」というのが、gitのリベース(rebase)の説明に良くでてきます。その理由について、この記事では解説します。

pushしたブランチをrebaseしてはいけない理由

pushしたブランチをrebaseしてはいけない理由を以下に示します(後ほど、具体例を用いて詳しく説明します)。

  • リベース後の再pushは強制pushが必要になるから
    • リベース(rebase)は、既存のコミット履歴を書き換える操作です。そのため、リベースを行うと、リベース前とリベース後で同じ内容のコミットであっても異なるコミットになります。その結果、ローカルリポジトリとリモートリポジトリで異なるコミット履歴を持つことになり、通常のpushではエラーが発生し、強制push(git push --force)が必要になります。
  • 強制pushするとリモートリポジトリの履歴を上書きして、他の開発者の作業に影響を与えるから
    • 強制pushを行うと、リモートリポジトリのコミット履歴がリベース後のコミット履歴に上書きされます。すなわち、リベース前のコミットがリモートリポジトリから削除されてしまいます。その結果、リベース前のコミットに依存していた他の開発者のローカルリポジトリや作業に影響を与えます。例えば、他の開発者がリベース前のコミットを基に新しいブランチを作成していた場合、その基になったコミットがリモートリポジトリから消えてしまうと、そのブランチのコミット履歴が不完全になったり、場合によっては作業の手戻りが発生する可能性があります。

では、実際に具体例を用いて上記の説明をします。まず、以下のような状況を想定してください。

  • mainブランチからfeatureブランチを作成して開発を始めている。
  • 他の人にコードレビューをしてもらうためにfeatureブランチをpushした。
  • その後、他のメンバーの作業によって、mainブランチのコミットが進んだ。
  • featureブランチをmainブランチの最新コミットにリベース(rebase)した。

この状況において、featureブランチを再度pushすると何が起きるのか説明します。

mainブランチからfeatureブランチを作成して開発を始めている

例えば、mainブランチからfeatureブランチを作成して、開発を進めたとします(下図ではmainブランチのコミットAからfeatureブランチを作成しています)。この場合、featureブランチの基点はコミットAになります。そして、featureブランチでの作業でコミットを2回(B, C)したとします。すると、ローカルリポジトリの状態は以下のようになります。

pushしたブランチをrebaseしてはいけない理由01

他の人にコードレビューをしてもらうためにfeatureブランチをpushした

他の人にコードレビューしてもらうために、以下のコマンドを実行して、featureブランチをGitHubやCodeCommit等にプッシュします。

git push origin feature

すると、ローカルリポジトリとリモートリポジトリの状態は以下のようになります。

pushしたブランチをrebaseしてはいけない理由02

その後、他のメンバーの作業によって、mainブランチのコミットが進んだ。

featureブランチをgit pushコマンドでGitHubやCodeCommit等にアップした後に、他のメンバーの作業によって、featureブランチの作業中にmainブランチが更新されることが多いです。例えば、mainブランチで2回更新(コミットD, コミットE)された場合、ローカルリポジトリとリモートリポジトリは以下のようになります(リモートリポジトリの内容をローカルリポジトリに反映するためにgit fetch済とします)。

pushしたブランチをrebaseしてはいけない理由03

featureブランチをmainブランチの最新コミットにリベース(rebase)する。

ここで、mainブランチの最新の状態をfeatureブランチに取り込みたい、しかも履歴を綺麗にした形で取り込みたいと思い、featureブランチをmainブランチの最新コミットにリベース(rebase)したとします。すると、ローカルリポジトリとリモートリポジトリの状態は以下のようになります。

pushしたブランチをrebaseしてはいけない理由04

すると、「コミットB」の親コミットが「コミットA」から「コミットE」に変更されます。また、リベース(rebase)は、実際にはコミットを移動しているのではなく、既存のコミットを破棄して、新しいコミットを作成しています。新しく作成されたコミットは見た目は元のコミットと似ていますが、実際には別のコミットです。そのため、「コミットB」と「コミットB'」は別のコミットになります。

ここで、すでにpush済のfeatureブランチをrebaseして、再度pushしようとすると、以下のような問題があります。

  • リベース(rebase)後にpushしようとすると「強制pushでないとpushできない」というエラーになる
  • リベース(rebase)後に強制pushをするとコミットBとコミットCはリモートリポジトリから削除される

上記の問題点について順番に説明します。

rebaseしてpushしようとすると「強制pushでないとpushできない」というエラーになる

すでにpush済のfeatureブランチにおいて、リベース(rebase)後は再度pushすることができません。

pushしたブランチをrebaseしてはいけない理由05

再度pushすると、以下のエラーが発生します。

$ git push origin feature
To リモートリポジトリのパス
 ! [rejected]        feature -> feature (non-fast-forward)
error: failed to push some refs to 'リモートリポジトリのパス' 
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,  
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

このエラーは、同じfeatureブランチにもかかわらず、リモートリポジトリにあるものとローカルリポジトリにあるもので、情報が矛盾しているために生じています。ローカルリポジトリでは、コミットAから分岐していた「コミットB」が消えていますが、リモートリポジトリでは「コミットB」が存在していますね。

ただし、以下のように、強制push(git push -f)を使うと、リベース後の作業ブランチをリモートブランチに変更を反映することができるようになります。ただし、強制pushは既存のリモート履歴を上書きするため、慎重に行う必要があります。

git push -f origin feature

強制pushでもエラーが発生した場合

git push -f origin featureを実行しても以下のエラーが発生することがあります。

$ git push -f origin feature
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 478 bytes | 478.00 KiB/s, done.
Total 5 (delta 0), reused 5 (delta 0), pack-reused 0
remote: error: denying non-fast-forward refs/heads/feature (you should pull first)
To リモートリポジトリのパス
 ! [remote rejected] feature -> feature (non-fast-forward)
error: failed to push some refs to 'リモートリポジトリのパス'

このエラーはリモートリポジトリの設定が原因です。リモートリポジトリの.git/configファイルにあるdenyNonFastforwardsfalseにすると修正すると、強制pushができるようになります。

[core]
	repositoryformatversion = 0
	filemode = false
	bare = true
	symlinks = false
	ignorecase = true
	sharedrepository = 1
[receive]
	denyNonFastforwards = false  # falseにする。trueにすると、non-fast-forwardなコミットが拒否される

rebaseして強制pushをするとコミットBはリモートリポジトリから削除される

pushしたブランチをrebaseしてはいけない理由06

すでにpush済のfeatureブランチにおいて、リベース(rebase)後に強制pushすると、リモートリポジトリから古いコミットBとコミットCが削除され、このコミットに依存していた他の開発者の作業に影響を与える可能性があります。具体的には、他の開発者がコミットBやコミットCを基にした作業を行っていた場合、その作業の履歴が壊れる可能性があります。

そのため、git rebaseは自分専用のブランチのみで使用してください。複数人が使用するブランチですでにpush済のブランチをrebase後に再度強制pushすると、リポジトリの整合性が合わなくなります。

なお、基本的には、ひとつのブランチはひとりの開発者にアサインされることが多く、git pushをした作業ブランチが他の開発者の作業基盤になっていない場合がほとんどです。このように、「他の人がfeatureブランチを使っていない」と断言できるときは、git pushをした後にgit rebaseを行っても、他の開発者の作業に影響を与えません。

しかし、プルリクエスト後、レビューが進んでいる状態でリベースを行うと、レビューアーが混乱する可能性があります。リベースによってコミットIDが変更されることで、プルリクエストのレビューコメントが適切に表示されなくなる可能性があるからです。そのため、レビューが進んでいる場合は、rebaseでmainブランチの最新情報を取り入れるのではなく、mergeでmainブランチの最新情報を取り入れる方がよいと思います。mergeを使用するとコミットIDは変更されません。そのため、既存のレビューコメントやプルリクエストの履歴が保持され、レビュアーの混乱を防ぐことができます。

本記事のまとめ

この記事では『pushしたブランチをrebaseしてはいけない理由』について、以下の内容を説明しました。

  • リベース(rebase)後にpushしようとすると「強制pushでないとpushできない」というエラーになる
  • リベース(rebase)後に強制pushをするとrebase前のブランチがリモートリポジトリから削除される

お読み頂きありがとうございました。