なぜこの記事を書いたのか
最近働き始めて、初めて集団開発でGitを触ることになりました。
で、早速履歴を壊しました。。。
既にpush済みの開発ブランチにmasterの更新を取り込むとき、以下の手順を踏んだのです。
git checkout master
git pull
git checkout feature
git rebase master
git push -f origin feature
はい、GitHubに表示される更新履歴がめちゃくちゃになりました。
あれこれ調べてみると、リモートにあげたブランチをrebaseするのは危険とあり、ではなぜ危険なのかということを調べることにしたのです。
一番さっくりわかったのは、公式のドキュメントでした。
Git のブランチ機能 - リベース の、本当は怖いリベースの項です。これで自分がpushしたコミットを取り込んだ他の人の履歴がめちゃくちゃになるということはわかりました。
でも、同時に不思議に思いました。
私がpushした開発ブランチは、私しか触っていません。他の人が取り込んだりはしていない。なのに、rebaseしてpushしたらその時点でGitHubの履歴がめちゃくちゃになった。この点についての説明にはなっていないと思ったのです。
なので、実際の開発を想定したrebaseについての実験をしてみようと思ったのでした。
実験の方針
git rebase して git push -f すると何が起こるか の手法を参考にしました。
まず、remoteリポジトリであるrepos.gitを用意します。
そしてそこからJaneとJohnという開発用リポジトリをクローンして作ります。
で、JaneとJohnそれぞれが開発ブランチをmasterから切り出し、それぞれにコミットを積み、途中であってもremoteにそれぞれのブランチをpushします。
今回はJaneの開発の方が早く終わった想定で、Janeが先にmasterにマージします。そしてJohnの方でそのmasterをrebaseで取り込み、自分の開発ブランチをmerge無しにremoteにpushしてしまった、という状況を再現するのです。
このとき、repos.gitのブランチはどうなっているのか?それを確認したいと思います。
実験の手順
詳細な手順は以下の通りです。
$ cd /tmp
$ (mkdir repos.git && cd repos.git && git init --bare)
$ git clone repos.git/ Jane
$ git clone repos.git/ John
# repos.gitは直接いじれないので、まずJaneでファーストコミットを作りremoteとJohnに同期させる
$ cd Jane/
$ git commit --allow-empty -m "init"
$ git push
$ cd ../John/
$ git pull
# 次にJaneで開発リポジトリbranchAにコミットを積み、remoteにpushする
$ cd ../Jane/
$ git checkout -b branchA
$ git commit --allow-empty -m "firstA"
$ git commit --allow-empty -m "secondA"
$ git push origin branchA
# 次にJohnで開発リポジトリbranchBにコミットを積み、remoteにpushする
$ cd ../John/
$ git checkout -b branchB
$ git commit --allow-empty -m "firstB"
$ git commit --allow-empty -m "secondB"
$ git commit --allow-empty -m "thirdB"
$ git push origin branchB
この時点でのremoteリポジトリは次の通りです。
$ cd ../repos.git/
$ git show-branch
! [branchA] secondA
! [branchB] thirdB
* [master] init
---
+ [branchB] thirdB
+ [branchB^] secondB
+ [branchB~2] firstB
+ [branchA] secondA
+ [branchA^] firstA
++* [master] init
続けます。
# Janeの開発ブランチをmasterにmergeしてremoteにpushします
$ cd ../Jane/
$ git checkout master
$ git merge branchA
$ git branch -d branchA
$ git push origin master
$ cd ../repos.git/
$ git branch -d branchA
# Johnの手元のmasterに先ほどmergeしたmasterを取り込みます
$ cd ../John/
$ git checkout master
$ git pull origin master
# そして開発ブランチに移動し、masterをrebaseします
$ git checkout branchB
$ git rebase --keep-empty master
この時点でJohnのリポジトリは以下の通りです。
$ git show-branch
* [branchB] thirdB
! [master] secondA
--
* [branchB] thirdB
* [branchB^] secondB
* [branchB~2] firstB
*+ [master] secondA
$ git show-branch --sha1-name
* [branchB] thirdB
! [master] secondA
--
* [e476307] thirdB
* [f529fc5] secondB
* [25159d5] firstB
*+ [d2ad4cc] secondA
ちゃんと混乱なくrebaseされています。
また、remoteリポジトリは次の通りです。
$ cd ../repos.git/
$ git show-branch --sha1-name
! [branchB] thirdB
* [master] secondA
--
+ [bf7bb97] thirdB
+ [4ab5a72] secondB
+ [165ca53] firstB
* [d2ad4cc] secondA
* [d0f004a] firstA
+* [207fe15] init
というわけで、push -f
します。
$ cd ../John/
# 念の為開発ブランチにいることを確認しておきます
$ git branch
* branchB
master
$ git push -f origin branchB
さて、どうなるでしょうか?
今度はremoteとlocalまとめて見ることにします。
$ git show-branch -a --sha1-name
* [branchB] thirdB
! [master] secondA
! [origin/branchB] thirdB
! [origin/master] secondA
----
* + [e476307] thirdB
* + [f529fc5] secondB
* + [25159d5] firstB
*+++ [d2ad4cc] secondA
これを見ても、remoteも混乱なくrebaseされているように見えます。
普通にgit log
を見てもコミット順に混乱はありません。
$ git log
commit e4763074f5a508cdc9470d406ce63c693db62858 (HEAD -> branchB, origin/branchB)
Author: rotelstift <rotelstift@gmail.com>
Date: Wed Sep 4 18:46:28 2019 +0900
thirdB
commit f529fc50f97cf57b776127bf44aaba2623df531a
Author: rotelstift <rotelstift@gmail.com>
Date: Wed Sep 4 18:46:20 2019 +0900
secondB
commit 25159d52ad933d8f3308e1240caf83461d79adf7
Author: rotelstift <rotelstift@gmail.com>
Date: Wed Sep 4 18:46:14 2019 +0900
firstB
commit d2ad4cccd5c9586d4ee73283f59343211ca9ca19 (origin/master, master)
Author: rotelstift <rotelstift@gmail.com>
Date: Wed Sep 4 18:44:53 2019 +0900
secondA
commit d0f004a2d17414005a0c4e786d0f31e1c028dd86
Author: rotelstift <rotelstift@gmail.com>
Date: Wed Sep 4 18:44:40 2019 +0900
firstA
commit 207fe1529b72bebc78e17160e89d369cdb3c4282
Author: rotelstift <rotelstift@gmail.com>
Date: Wed Sep 4 18:43:12 2019 +0900
init
ではGitHubの混乱はなんだったのか?
これですが、実に明瞭な答えがGitHubのヘルプページにありました。
コミットが間違った順番になっているのはなぜですか?
Git コミット履歴をリベースの実行中に書き換えると、時空連続体が変更されます。つまり、GitHub インターフェイスでは、コミットが期待どおりに表現されない可能性があります。
ということで、GitHubでのコミット順の混乱はそのままGitでも再現されるものではなかったのです。
まとめ
- 開発ブランチにmasterを取り込むときにrebaseを使うと、GitHubに表示される履歴がめちゃくちゃになる。
- 複数人で開発しているときに既にpush済みのコミットをrebaseしてしまうと、それをrebase前から取り込んでいた人の履歴がものすごく複雑になる。
- push済みのコミットをrebaseすること自体が履歴を壊すわけではない。
- しかし1,2のデメリットがすごく大きいのでpush済みのコミットをrebaseしてはいけない。