ぼくが実際に運用していたGitブランチモデルについて

ブランチモデル

オペレーションとかインフラ系のエンジニアリングからは少々離れそうなので、個人的な備忘録がてら、Gitのブランチモデルについて。淡々と書くよ。

基本的に、このA successful Git branching model(上記は翻訳記事)を参考にしています。ですが、完全ではありません。運用しながら都合よく省略していますし、悪く言えば曲解もしています。あくまで、わたしが都合良く解釈して取り回した結果と考えてください。

さて、このようなドッシリとしたブランチモデルが、あらゆる規模のプロジェクトに対して有効であるかといえば、もちろんそうではありません。コツコツ個人で開発しているライブラリなどは、ブランチを使わなくても良いケースがあるでしょうし、作ってもバージョン番号ブランチぐらいのケースだってザラにあります。

ここで述べるようなブランチモデルは、製品やサービスなど、現在顧客に提供している安定化されたビルド今後必要な機能をそなえた先進的なビルドを、区別して運用したいケースが主といえるでしょう。

この2つをハッキリと区別して管理できるのは重要で、目前の小さなfixを短期のイテレーションでデリバリーすることと、将来的に必要な機能を意欲的にディベロップすることの2つの両立するための運用負荷を下げることができます。

短期イテレーションで走り続けて、バグ発生の可能性が高いリスキーな機能追加まで短期の中に織り込まれると、(人員が潤沢ならともかく基本的には)相当しんどいです('A`)

基本的な役者

さて、A successful Git branching modelによるものですが、私の運用していたブランチ群には、以下のような役者がおりました。

これらのブランチには、絶対になくてはならないブランチと、省略しても平気なブランチに分けることができたりします。そのへんの、そのブランチを運用する意味的なところについても触れながら紹介していきましょう。

1. 特に重要で、なくてはならないブランチ

まずは無くてはならない彼ら。


master, develop

master, develop


masterブランチ

命名規則(固定)
master
  • 出荷されうる完全な品質を保証する
  • 特定のブランチからマージすることによってしか更新されない
  • 直接コミットしてはならないという制約をもつ
  • バージョンごとのtagはここから生まれる

当たり前のようにmasterブランチは基本ですが、このブランチモデルにおけるmasterは、開発の主軸ではありません。このmasterは、今顧客に提供している価値そのものを表すブランチです。

ようは、サービスなら現在本番サーバに鎮座しているもの、製品であれば現在配布しているもの、そういうことです

そのため開発者による直接的なコミットはされません。常にリリースしても良いと判断されたブランチ(基本的には後述のrelease)のみをマージすることによって発展していくブランチです。製品であればこのブランチをビルドしたものが配布パッケージですし、サービスであればこのブランチをデプロイしたものが本番運用でしょう。

また、各バージョンの完全な状態を残すためのtagはここから生まれます。

developブランチ

命名規則
develop/[バージョン番号など]
  • 開発中のコミットが行われる主軸
  • 後述のfeatureやreleaseブランチはここから派生
  • 次のバージョン、次の次のバージョンと並走することもある

そしてdevelopブランチ。小さいbug fixや、簡単なfeatureは、developブランチで直接コミットされます。開発者の時間の大半は、このブランチを指し示していることでしょう。次に説明、featureやreleaseと呼ばれる前段階的なブランチも、すべてここから生まれます。

developは開発ブランチですが、次期開発だけでなく、次々開発、次々々開発が並走する場合もあります。そのときは、develop/15xとdevelop/16xのように、異なるバージョンをもった複数のdevelopブランチが並走することもあります。

ここで15x、つまり近未来のブランチは特に気にせずコミットし続ければよいのですが、16xのようなひとつ先のブランチは15x側のブランチの開発の後に続かなければいけません。そのため、定期的にrebaseなりmergeなりを走らせて、15xの変更と同期します。これをサボると容易にconflict地獄にハマるので、注意が必要でしょう。

2. よく使う or 重要な意味をもつブランチ

developブランチから派生する補佐役。


feature, release

feature, release


featureブランチ(必須)

命名規則
feature/[派生元バージョン番号]/[機能名]
  • 中〜大規模な機能追加・改修などを行う作業ブランチ
  • やるぞー!ってなったらdevelopブランチから、featureブランチを切る
  • 完了後はdevelopブランチにマージされて、featureブランチは削除される

まとまった規模の機能追加・改修などを行う作業ブランチです。機能追加の紆余曲折を大事にして、このブランチにコミットログを残していくことになりますが、機能追加の只中であるときはどうしても、およそ完全でないバージョンをコミットしていまうことも少なくありません。

そんなときfeatureブランチを切っておけば、developブランチを共有する他の開発者に迷惑をかけることもありませんし、Gitの特性上、ブランチさえ切ってしまえばある程度手元で柔軟に対応することができます。

この開発ブランチは、役割の都合上、個人で完結するような機能追加であればローカルブランチとして作られて、そのまま手元でdevelopブランチによってmerge -> pushという流れになり、featureブランチそのものがpushされないケースも多いでしょう。反対に、他の開発者とリモート越しに共有したい機能追加の作業であれば、リモートにpushさえすれば済む話です。

releaseブランチ(必須でない)

命名規則
release/[バージョン番号]
  • developブランチから生まれる
  • 一通りの作業が終わればmasterブランチに取り込まれる

リリース準備に入った状態を示すブランチです。このブランチが、developブランチから生まれた瞬間、プロダクトは機能追加を止めてbugfixなどの調整に専念し、次バージョンをリリースするための準備に入ります。

とはいえ、ここまで紹介したブランチと比べると幾分優先度が低いブランチと言えます。リリース準備の仕上げと、先行開発がかぶらない(人的リソースが多くない)場合などは、releaseを切らずにdevelopのまま仕上げて、masterブランチにmergeしてしまえばよいでしょう。

そんなことを言いつつ、個人的にはrelaseブランチを切ることは推奨しています。なぜならreleaseブランチを切ることによって、マネジメントやプランニングの職能に「開発陣はリリース準備に入った!機能追加はSTOP!」という意思表明を行えるからです。実際、それはこのブランチの本来の存在意義に則しています。必要な機能が揃った上で、リリース準備に入るという宣言的な役割のブランチだからです。

releaseブランチを切る場合は、developブランチと並走することになりますが、developブランチはリリース後のmasterブランチをrebaseすることで、リリース作業分の変更を合流させるようにします。

fixブランチ(低優先)

命名規則
fix/[バージョン番号]/[バグ識別名]
  • あんまり作りませんでした

いきなり簡素な扱いで申し訳ないのですが、featureブランチと同じくして、まとまった規模のbugfixが発生するときに生まれることがあるブランチです。ただ、bugfixなんぞ単発でチラホラとレポートされるもので、それに対する修正作業もあまり大きい規模にはなりませんでした。

そのため自分の経験上、大半のケースではdevelopブランチに対する小さな問題に対する小さな1コミット、という状態です。個人的には、運用ルールとして必要なレベルの存在ではありませんでした。

3. なんだかんだで必要になるブランチ

緊急のbugfixや過去バージョンのサポートなど。


support, hotfix

support, hotfix


supportブランチ(条件つきで不要)

命名規則
support/[バージョン番号]
  • 過去バージョンをサポートするための復活ブランチ
  • 主にそのときのmasterから生まれたtagを元に作成する

過去のバージョンを、bugfixや次項のhotfixによって改修・修正する必要があるときに生まれるブランチです。

常に最新版をデプロイして提供しつづけるサービス形態であれば必要ではありません。反対に配布プロダクトスタイルであれば、そこそこの頻度で登場します。過去バージョンのバグなどを直さなければならないときに切るからです。

このsupportブランチに対するbugfixは、バグの影響が広範に渡るようであれば複数バージョンのsupportブランチに対して、ある修正コミットをcherry-pickで各々に適用することがあります。特定のバージョンに限った問題であれば、該当バージョンのsupportブランチに対する直接のコミットで修正されることもあります。

ちょっと難しいですが、リリースしたての最新版(master相当)に対するfixをdevelopで行うか、次のhotfixで行うかは悩み所です。あまり重要でなければdevelopへの修正commitでよいのですが、場合によってはhotfixとしてcommitしたのち、masterにmergeして修正をさっさとリリースし、developにはmasterをmergeすることで取り込むようなことがあります。

hotfixブランチ(悲しいけど必須)

命名規則
hotfix/[バージョン番号]/[バグ識別名] or hotfix/[バグ識別名]
  • masterブランチから派生
  • 配布プロダクト系であれば、該当バージョンのタグから生まれたsupportブランチから派生

一番切りたくないけれども、切るべきときはさっさと切るブランチです。

緊急に対応しなければならないような、致命的な脆弱性サービスにとって致命的な不具合などの対応時が出番です。配布型プロダクトで過去バージョンをサポートする場合は、前述のsupportブランチのような過去のバージョンから派生してブランチを切って修正することもあります。

もしもhotfixの対象が過去複数のバージョンに渡る場合は、なんとなく一番中間点にありそうなバージョンからhotfixを作成し、その他の周辺バージョンのsupportブランチに対しては、cherry-pickで該当の修正を適用して回るようにします。

過去2回、hotfixブランチをつくりました。

4. GitHubを利用したビルドとデプロイ

最後にリモートリポジトリとして利用していたGitHub周り。といってもクローズドプロダクトだったので、単にリモートリポジトリと思ってもらえばOK。


GitHub

GitHub


んで、最後に肝心のビルドとデプロイについて。基本的にはmasterさん最強なんですが、もちろんベータ版のリリースとかはありましたし、継続的な整合性の確認のため、developブランチを都度デプロイするような仕組みを用意していました。

リモートoriginはGitHubにプライベートリポジトリを作成して、運用していました。そして、pushするたびにPOST Hookで、ある環境にpushされたてホヤホヤの最新環境をデプロイするようにしていました。

また、単純に手元にビルドパッケージを用意したいときは、rubyで書かれた専用のビルドスクリプトにブランチ名を渡すことによって、いつでも該当ブランチの製品パッケージを作れるようにしていました。

開発者はローカルの開発環境で調整しながらローカルリポジトリにコミットを繰り返し、これでよし!となったらGitHubにあるリモートリポジトリにpushして、それらがビルドや継続的デプロイの対象になる感じです。

いかにリモートのブランチを美しく運用するか、そしてローカルのブランチをドロ臭くも柔軟に運用するか、Gitの重要なエッセンスはその辺りにあるんじゃないでしょうか。

おしまい

ということで、つらつらと述べてみましたが、汎用的なノウハウというよりは、使えるところを使ってもらえたら幸いな程度の個人的なナレッジです。自分としては、なかなかうまく行ったんではないかというモデルです。

アジャイル開発のプラクティスもそうですが、すべてを手本のままに実践しようとすると、精神的なコストが高くてうまくいかないことが多々あります。現場の現況にあわせて、柔軟に都合良く取り入れる度量が必要でしょう。

教科書通りにうまくいかないのはあたりまえとして、現状とすりあわせて運用できるレベルに消化し、徐々にワークフローに取り込むことが重要です。バージョン管理の運用も、目前のビジネスとすり合わせて、うまいこと回していけたらいいんじゃないかなと思います。

そんなかんじで〜。

参考

ちなみにぼくは、git-flowは使わず仕舞いでした。イテレーションが極端に短いスピーディなプロジェクトであれば、git-dailyのほうがいいのかもしれませんねー(未経験)