強まっていこう

あっちゃこっちゃへ強まっていくためのブログです。

git rebase は危険だ!悪だ!と言うガセネタに踊らされて rebase を怖がらないで!!!

rebase を恐れるなっ

rebase は危険だっ!push されてる場合は rebase するな!リポジトリを破壊するぞっ!絶対使うなっ!!

こんな脅し文句に踊らされ、人々は rebase に恐れおののき全力で逃げ出し理解すらしようとしなくなった結果、今日もログが荒れています。

私は悲しいです。

rebase は全く恐れなくて良いんです!むしろこんないい娘いません!!

本来一番自然な流れを作り出せるものですので、ぜひ理解して活用しましょう。

ってなわけで、訳の分からない変な呪縛から解き放つ、その手助けを今からいたします。

rebase は1つのブランチならば多人数でいじっている場合でも全く問題無いと言うか、merge の方が圧倒的に具合が悪いです。

rebase 実験

実験するとわかりやすいですからやってみましょう。

が、その前に以下を実行してください。

git config --global mergetool.keepBackup false

orig だの BACKUP だの作るのをヤメさせます。
(そもそも論 --abort だの reset だのがあるんだからこんなもんの必要性が全く無く鬱陶しいだけですし clean -f を毎回叩くのウザすぎです)

本サイトでは mergetool には vimdiff を使う事を強く推奨すると言うか、vim 使いましょう。それ以外認めません。

なので以下の設定を推奨です。

git config --global merge.tool vimdiff

[TODO]
rebase、marge でのコンフリクト解決の方法、vimdiff の使い方を機嫌が良い時に丁寧に教える記事を載せる。

その上で以下を実行してみてください。

[ Core ]
mkdir -p ~/git-test/remote/test
cd ~/git-test/remote/test
git init --bare

cd ~/git-test
git clone ~/git-test/remote/test a
git clone ~/git-test/remote/test b

echo 'base code' > a/code
cd a
git add code
git commit -m 'first commit'
git push
cd ../b
git pull
cd ..

cd a
echo 'bug fix by a' >> code
git add code
git commit -m 'bug fix by a'
git push
cd ..

command の詳細はまぁ rebase 使いこなそうとするぐらい気概のあるエンジニアなら見りゃわかるでしょって事で暴力的に説明を省きますが、ユーザー a と b が test をそれぞれ clone し、a が bug fix してpush している様を仮想的に行ってます。
a とb がそれぞれのユーザーの作業ディレクトリだと思ってください。

お次に、以下を実行してみましょう。

[ Bug fix by b ]
cd b
echo 'bug fix by b' >> code
git add code
git commit -m 'bug fix by b'

b がさらに bug fix しましたと。
ここで b が push してみると

git push // Error

a が push してて remote 側が新しくなってるから、pull しろっつって怒られます。
なので以下で rebase して、コンフリクト解消して push したとします。

(注: 以下の shell は貼り付けて一気に実行出ません。mergetool と rebase --con でエディタ開くので1行1行実行しましょう)

git pull -r
git mergetool
git rebase --con
git push

そうするとログはこんな感じになります。(ちなみにこのログは tig のログです。自分 tig しか勝たんと思ってるので)

o [master] {origin/master} bug fix by b
o bug fix by a
I first commit

a がやった修正の上に、自分の修正が乗っかってる自然なログですね。

そら rebase って自分の push してない commit を remote 側の最新引っ張ってきて乗っけなおす処理だから当然こうなります。
svn とか cvs の動作に近くて、全く不自然さを感じず理解しやすいですよね。

merge 実験

さぁ、今度は馬鹿みたいに merge してみましょうか。git の呪いを堪能です。

~/git-test 以下を全部消して実験コード[ Core ] と [ Bug fix by b ] を実行してください。

その後 b で以下を実行して merge します。

git pull
git mergetool
git merge --con
git push

ちなみに pull の際、エラーが出る場合は、

git config --global pull.rebase false

を実行してください。pull のデフォルト挙動が merge になります。以前までこんな設定せずともエラーにならなかったんですが、余計なお世話が入りました。

marge した結果のログが以下です。

M─┐ [master] {origin/master} Merge branch 'master' of /home/xxx/git-test/remote/test
│ o bug fix by a
o │ bug fix by b
I─┘ first commit

さっきは 3 行だったのが 4 行に増え、左側の部分も見づらくなりましたね。
さらに、時系列的に bug fix by a が先なのに、後に見えるようになり混乱出来ます。非常にありがたいですね。
まさにハッピーハッピーハーーッピーーーパピハピハピハピハーーッピーーーです。

まぁ、こりゃ remote の commit が先行したもんだから、そいつを自分ところの一番ケツにペシッとくっつけて、さらに merge で取り込みましたよ!とありがた迷惑な commit を作ってくれるからこうなるんです。

これ、remote 側に、まだ local に取り込んでいない commit が存在する際に push しようとすると、毎度こうなります。

これでログが最高に汚くなってログを開くたび絶頂を迎えることになります。

マジ最高!!ありがとうリーナス!!

rebase 一択

merge はあまりに最高すぎて絶頂を迎えすぎるため基本的に rebase 一択です。

なので

git config --global pull.rebase true

しても良いです。すると

git pull

git pull -r 

とおなじになるので。この場合 merge したいとすると

git pull --no-rebase

とオプションが長くなりますが、まぁ merge する機会の方が少ないので良いかと思います。

が、自分はもう手癖で git pull -r とやっちゃうし、普段は tig でやってるんでこの設定はしてないです(照)。

merge の混乱

ちなみに remote 側が一切触られていない merge は rebase と同じ結果になります。

それが ff です。フレンドリーファイアーでありファイナルファンタジーであり、forfeit であり surrender で gg です。ま、ff とかこんな言葉くっそどうでも良いので気にしないでください。

つか merge の挙動が remote の様子で変わるのがどうだっつぅ話ですわ。これが混乱の元。激キモオヤジの嫌がらせです。

rebase 使えと言っても、他のブランチに対してコードの統合を行う場合は rebase -> merge と言う流れになるのでちょっと混乱するかもしれません。これは私のせいではありません。git の出来が悪いだけです。

rebase で統合先の最新コードを引っ張ってきといて、それをそのまま統合先に merge で追加って感じです。

簡単に、この悪質な merge の挙動をまとめると以下のようになります。

remote 側に新しいもんが無い

rebase と同じになります。remote の状態に自分の変更が乗っかる一番自然な代物です。

remote 側に新しいもんがある

remote の変更を今の自分の変更の上に余計なコミットとして乗っけてくる理解不能で劣悪な代物です。

他ブランチ取込 rebase 時の push --force に注意

話を戻して rebase が全く問題ないか?と言うと問題が発生する場合があります。

それは 他ブランチを取り込む場合、かつ自分が rebase の作業中に誰かがブランチに push した場合です。この push を消す可能性があるんです。

分かりづらいですかね。現実的なシチュで説明します。

master から新機能開発のために dev ブランチを切りましたと。

a が dev で新しい機能を追加しました。それを master に merge するために dev に対して現在の master で rebase します。

そら dev の元となった master に変更があったらそれを取り込むのは当たり前ですよね?

master の最新を取り込んだ dev を master に突っ込みます。終わり。

普通は特に問題は発生しません。

ただ、問題が発生するのが、rebase した後に、ブランチに対して変更をいれたやつがいる場合です。

もしその変更が push されていた場合、その commit が消えます。しかもおそらく気づかない。そっと消えるのでw。

まぁ消えたっつっても reset 使えば戻せるは戻せるから noob が騒ぎ散らすだけですがね。

んで、これは rebase が悪いのではなく、rebase 後の作業ブランチを強制的に push する

git push -f

がアブねぇって話です。(オプション長くて見るだけで憂鬱になる --force-with-lease を使えば安全って話でもないですよ)

回避策ともども説明しましょう。

他ブランチ取込 rebase 実験

さぁさっそく実験です。まずは、問題無い rebase です。以下を実行してください。

[ Core 2 ]
mkdir -p ~/git-test/remote/test
cd ~/git-test/remote/test
git init --bare

cd ~/git-test
git clone ~/git-test/remote/test a
git clone ~/git-test/remote/test b

echo 'base code' > a/code
cd a
git add code
git commit -m 'first commit'
git push
cd ../b
git pull
cd ..

cd a
git checkout -b dev
echo 'new func by a' >> code
git add code
git commit -m 'new func by a'
git push origin dev
git branch -u origin/dev

cd ../b
echo 'bug fix by b' >> code
git add code
git commit -m 'bug fix by b'
git push
cd ..

a が dev ブランチを作成し、new func を加えています。
その間、b が master に対して bug fix を入れました。

以下で、a が dev を master で rebase 後に dev を master に push します。

cd a
git pull -r origin master
git mergetool
git rebase --con

git checkout master
git pull
git merge dev
git push

この結果 master ログは以下のようになります。

o [master] {origin/master} new func by a
o bug fix by b
I first commit

非常に素直ですね。ただ、これ master で rebase した後、push してないんです。conflict の merge が大変だったりした場合、dev の今後の修正に備えて push しときたいですよね。

他ブランチ取込 rebase & push 実験

[ Core 2 ] 実行して以下を実行してください。

cd a
git pull -r origin master
git mergetool
git rebase --con
git push // Error

push で死にます。今の master の修正を取り込んだものが remote の dev と衝突してしまっているのでエラーになります。

なんだ、だったら rebase すりゃ良いじゃん?っつって rebase してみるとこんな風になります。

o [dev] new func by a
o bug fix by b
o {origin/dev} new func by a
I [master] first commit

あれ、同じ commit 増えました・・・しかも bug fix と new func が入れ替わりました。そらそうですね。dev で rebase しちゃいましたから。こりゃダメです。

じゃぁ merge してみましょうか。

M─┐ [dev] Merge branch 'dev' of /home/xxx/git-test/remote/test into dev
│ o {origin/dev} new func by a
o │ new func by a
o │ {origin/master} bug fix by b
I─┘ [master] first commit

終わってます。

他ブランチ取込 rebase & push --force で人に迷惑をかける実験

素直なままの君でいて欲しい。ってなわけで、remote 側を local で問答無用で書き換えるのが

git push -f

です。-f は --force です。フォースの力をうんちゃらかんちゃらでブォーンブォーンです。

よっしゃー!これで解決やでぇ!!

とはならず、さっき書いたように、commit ぶち消し問題が発生する可能性があります。

[ Core 2 ] を実行後に、以下を実行してみましょう。

cd a
git pull -r origin master
git mergetool
git rebase --con

cd ../b
git pull
git checkout dev
echo 'new func by b' >> code
git add code
git commit -m 'new func by b'
git push
cd ..

a が dev に master を rebase 後に b が dev に対して変更を追加しています。

この時、a のログは以下。

o [dev] new func by a
o {origin/master} bug fix by b
I [master] first commit

dev が push されていないので、{origin/dev} がいません。

b のログは以下のようになっています。

o [dev] {origin/dev} new func by b
o new func by a
I first commit

new func by b が追加されていますね。

さぁ、a で force な push をしてみましょう。

cd a
git push -f	
cd ..

b で pull してログを見てみてください。

o [dev] {origin/dev} new func by a
o [master] {origin/master} bug fix by b
I first commit

「はぁぁぁ!?吾輩の new func by b が消えたっ!!」と発狂ですね。

困った事にこんな感じで何も出ずツルっと消えるんですよね。なので消えた事に気づかない可能性がある。こりゃいかんです。

周りに迷惑をかけず 他ブランチ取込 rebase & push --force & merge するには?

push -f で --force-with-lease 使うと、remote 側に変更があった場合エラーになるので、rebase やりなおしって話なんですが、conflict 解決中に push されると無限ループの始まりです。

なので、チャットで

「おい!ぽまいら!dev を rebase するンゴ!手持ちの commit を全部 push すれ!」

と古の激キモ言語で大号令をかけ、全部 push してもろて、

「rebase するから、push すんなし!」

と push を封じれば良いです。つか、それしかないです。

ちゃんと b に push してもらった後、改めて rebase すると以下のようになります。

o [dev] {origin/dev} new func by b
o new func by a
o {origin/master} bug fix by b
I [master] first commit

んで、この dev を master に merge するわけですが、この時 --squash を付けましょう。master のログに開発のログがぐちゃぐちゃ入るのマジ勘弁なんで。

git checkout dev
git pull -r
git merge dev --squash
git commit

これで

o [master] {origin/master} Add new func !!
o bug fix by b
I first commit

こうなります。はい、きれい。

rebase は ユーザーが変わるとか、hash が変わるとかありますが、そんなもん普通どうでも良いので気にしないくて良いです。

どう考えてもログが汚くなる方が致命傷だし変わる前を追おうと思えばなんぼでも追えますし。

push --force で自分の commit が消されてしまった場合の対応例

最後に、もし push -f で消えてしまった場合の復活方法の一例を書いておきます。

new func by b を消された b は

o [dev] {origin/dev} new func by b
o new func by a
I first commit

だったのが、rebase すると

o [dev] {origin/dev} new func by a
o [master] {origin/master} bug fix by b
I first commit

と、自分の commit が消えちゃいます。が、焦らなくても OK です。

git reflog 

で、作業リポジトリの履歴を表示します。

aaa75d9 (HEAD -> dev, origin/dev) HEAD@{0}: rebase (finish): returning to refs/heads/dev
aaa75d9 (HEAD -> dev, origin/dev) HEAD@{1}: rebase (start): checkout refs/remotes/origin/dev
fb23a21 HEAD@{2}: commit: new func by b
8137020 HEAD@{3}: checkout: moving from master to dev
c9a59bd (origin/master, master) HEAD@{4}: commit: bug fix by b
71e6fc7 HEAD@{5}: initial pull

以下が、消えちゃった修正です。

fb23a21 HEAD@{2}: commit: new func by b

なので、hard reset を使って、ここまで完全に戻します。

git reset --hard fb23a21

ほいだら以下です。

git reset @^
git stash
git rebase
git pop
git commit

reset で履歴を1つ戻して commit を取り消し、修正したコードを stash に突っ込んで rebase して stash から戻して commit しなおしって感じです。

では良い git ライフを。