「git push
をした後やプルリクエストを出した後にはgit rebase
をしてはいけない」というのが、gitのリベース(rebase)の説明に良くでてきます。その理由について、この記事では解説します。
pushしたブランチをrebaseしてはいけない理由
pushしたブランチをrebaseしてはいけない理由を以下に示します(後ほど、具体例を用いて詳しく説明します)。
- リベース後の再pushは強制pushが必要になるから
- リベース(rebase)は、既存のコミット履歴を書き換える操作です。そのため、リベースを行うと、リベース前とリベース後で同じ内容のコミットであっても異なるコミットになります。その結果、ローカルリポジトリとリモートリポジトリで異なるコミット履歴を持つことになり、通常のpushではエラーが発生し、強制push(
git push --force
)が必要になります。
- リベース(rebase)は、既存のコミット履歴を書き換える操作です。そのため、リベースを行うと、リベース前とリベース後で同じ内容のコミットであっても異なるコミットになります。その結果、ローカルリポジトリとリモートリポジトリで異なるコミット履歴を持つことになり、通常のpushではエラーが発生し、強制push(
- 強制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)したとします。すると、ローカルリポジトリの状態は以下のようになります。
他の人にコードレビューをしてもらうためにfeatureブランチをpushした
他の人にコードレビューしてもらうために、以下のコマンドを実行して、feature
ブランチをGitHubやCodeCommit等にプッシュします。
git push origin feature
すると、ローカルリポジトリとリモートリポジトリの状態は以下のようになります。
その後、他のメンバーの作業によって、mainブランチのコミットが進んだ。
feature
ブランチをgit push
コマンドでGitHubやCodeCommit等にアップした後に、他のメンバーの作業によって、feature
ブランチの作業中にmain
ブランチが更新されることが多いです。例えば、mainブランチで2回更新(コミットD, コミットE)された場合、ローカルリポジトリとリモートリポジトリは以下のようになります(リモートリポジトリの内容をローカルリポジトリに反映するためにgit fetch
済とします)。
featureブランチをmainブランチの最新コミットにリベース(rebase)する。
ここで、main
ブランチの最新の状態をfeature
ブランチに取り込みたい、しかも履歴を綺麗にした形で取り込みたいと思い、feature
ブランチをmain
ブランチの最新コミットにリベース(rebase)したとします。すると、ローカルリポジトリとリモートリポジトリの状態は以下のようになります。
すると、「コミット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すると、以下のエラーが発生します。
$ 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
ファイルにあるdenyNonFastforwards
をfalse
にすると修正すると、強制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済の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前のブランチがリモートリポジトリから削除される
お読み頂きありがとうございました。