squash and mergeしか使ってないけど全く困ってない

こんな記事を見かけた。

こういうことはレポジトリ構成・ワークフローと密接に紐づいているので、そういう前提を抜きにはどれがいいとかはいうことはできない。が、自分はいわゆるsquash and mergeのみの環境しかほとんど経験がないし、それで困ったことが一度もない、という話をしておきたいので書いておきたい、ので書いておく。

squash and mergeのメリットは書いてある通りで、基本的にPR内の細かい修正というのはゴミみたいなコミットが多く、メッセージも雑なことが多いので、それをコミットログに残しておくのは嫌だということがある。それよりは意味のある単位のコミットを残しておきたいし、それの単位はPRで行うのが良い、ということだ。

“Google-style” workflow

デメリットの方は、いわゆるfeature branchというワークフローで顕在化する問題であると思う。で解決策はあり、それはワークフローを変えてfeature branchは作らない、ということになる。全ての作業は主にmain branchに対して行う。もちろんPRのためにはブランチを作るが、それは個々のPR単位で作り非常に短命になる。その昔、Googleが「全てmain branchで作業している」開発スタイルだという話を公表して当時は結構話題になったので私は”Google-style”のワークフローと呼んでるけど、ぶっちゃけ今となっては常識化してきたと思う。もっと一般的な名前があるのかどうかはよくわからない。(追記:trunk-based developmentという名前がちゃんとあったというのを指摘された。専用のドメインまであるのか。thanks for the info!)

このワークフロースタイルでは、機能を開発するのにブランチを作ることはしない。個々の開発者は個別にPRごとのブランチを作る(topic branchということが多いと思う)。このブランチでそれなりに意味のある変更が積み上がったら、それをPRする。PRでコードレビューされ、レビュー内容に応じてちょこちょこ変更したりするが、ともあれそれが終わってPRがマージされたらおしまい。そのブランチも消す。ブランチは短命で、平均寿命は1日以下とかみたいになる。そういうふうに短いサイクルでブランチを作っては消していく。なのでリンク先のような問題は起こりえない。

もちろん開発する機能というのは大規模なものもあり、そういうものは一つのPRで終わることもないし時間もかかる。でも全てのコミットはmainに積み上げていく。完成していない機能はフラグなどを使って実際には使われないようにしたりする。この方が全員が全てのことを把握できるし、意図せず他の機能のものを壊すこともないのでいいのだ、というのがjustificationであると思う。

cascading PRs

一つの作業をしていく中で、その作業がちょっと規模が大きくなってきたのでPRを分けた方がいいな、ということはある。そういう場合にはPR1のブランチがあって、それに依存したPR2のブランチがあり、さらにPR3のブランチもあって……みたいな構造をとる事もある。そうしてPR1がマージされた後でPR2/PR3はどうすればいいのか?という話はあると思う。

ただこれはfeature branchとは本質的に違う点があり、PR2/PR3というのは基本的には自分一人しか触らない。feature branchとして開発している場合にはチームみんながそれをいじっているわけなので、どうやってマージするか、整合性を取るかというのは大きな問題になるけど、自分の手元のローカルブランチなので好きなようにいじってしまえばいい。具体的には、普通PR1をマージしたら、PR1ブランチを削除し、mainをgit pullした後で、PR2はgit rebase mainすればそれで済む。PR1に由来するコンフリクトは自分にとっては自明だし、ほとんどの場合にはgit rebase –skipするだけだ。もしくはgit rebase -iして自分でPR1由来のコミットを消してもいいだろう。

release branches

場合によってはリリースごとにブランチを作るということはある(昨今のモダンなウェブ開発ではあんまりないかもしれないけど)。例えばChromeブラウザでは新しいバージョンごとにブランチが作られている。このブランチはリリーススケジュールにもよるがそこそこ長命であることが多い。Chromeは4週間ごとに新しいブランチが作られるが、stableチャンネルの他にbeta、devというチャンネルがあるため、8週間くらい生き延びる。

ただ、基本線は同じと思う。

まず、リリースブランチはリリースごとに新しいものを作る。例えばChromeの場合、一つの「stable channel用のブランチ」があるわけではない。Chromeは今バージョン119でbetaが120だが(あってるかな)、この場合には「バージョン120のブランチ」と「バージョン119のブランチ」が作られている。stableが120に移るというのは、stableは「バージョン120のブランチ」を使うように変更することを意味する。その時betaは新しく作られた「バージョン121のブランチ」を使うようにスイッチしている。「バージョン119のブランチ」はそのまま消されることはないが放置され、忘れ去られる。

そしてリリースブランチでは開発をしない。メインの開発とは別にセキュリティの修正など重要なパッチをリリースに含めたいことはとてもよくある。だがその時もリリースブランチに直接変更を加えることはしない。あくまでも修正はmainのブランチに対して作り、そちらにマージする。マージした後でその修正をリリースブランチにcherrypickする。

もちろんワークフローとか開発スタイルというのは大きく優劣があるようなものではない。例えばこういう開発スタイルは大規模分散的なオープンソースソフトウェア開発(例えばLinuxカーネルとか)には向いてないだろうと思う。文化的な面もある。

ただ、この「squash and merge」というのは基本的にはここで私のいう「Google-style workflow」と相性がよく、というかこのワークフローでないなら難しい面もあるということなのだと思う。どのマージスタイルを選ぶのかというのはちょっとした選択のようでいて、ワークフローも含んだ選択でもあるのだろう。

Android-style workflow

ところで、この記事の元ネタのような呟きを bluesky に書いていたところ「Androidは……」というツッコミというかなんというかをいただいた(笑)。AndroidはGoogleの一部でありながらこういう「Google-style」ワークフローでは全くなく、完全なるカオス。自分はちょっとだけ体験したけどマジでやばい。どうしてこうなってしまったのか、こうなる前にもっと別なところを改善すべきだったのでは、と個人的にいつも思っている。

あんまり詳しくは公表されていないのではっきり書かない方がいい気もするけど、例えばAndroidは社内にmainブランチ的なものがある一方でAOSP (Android OpenSource Project)として公開されているブランチもあり、どちらも活発に開発されている(新しいコミットがどんどんマージされている)。しかもwearとかはまた別の自前ブランチを持っていたりする。そういう中で、誰かの入れたコミットがそれぞれのブランチに適切にマージされていくような仕組みがある。だが(上記だけでも結構複雑そうに思えるが)実際にはさらに数段複雑な構造になっているため、「適切なマージ」の仕組みは複雑怪奇な仕組みになっている。

こういうことはやめましょうね、という反面教師としてしか存在価値はないような構成・ワークフローになってしまっていると思う。もっとシンプルでstraightforwardなもので充分です。AndroidやLinuxカーネルを開発するんじゃないんだからね、と、私としては思う次第。