2010年、オランダのエンジニアVincent Driessenさんの一提案に過ぎなかったgitのブランチモデル A successful Git branching model はその後、世界中に広く浸透し今やgit運用モデルとしてデファクトスタンダードになりました。
今回、Vincent Driessenさんにご快諾いただき日本語訳を掲載することになりました。
Org:
http://nvie.com/posts/a-successful-git-branching-model/
Contact:
me@nvie.com
twitter.com/nvie
github.com/nvie
keybase.io/nvie
A successful Git branching model
はじめに
私がとあるプロジェクト(業務、プライベート双方)で1年ほど前に実際に運用してみて、とてもうまく運用できたと判った開発モデルに関して紹介してみたいと思います。とりあえず書き留めておこうと思ったものの、ちゃんと書き残しておく時間を確保できませんでした。ここでは、プロジェクトそのものの細かい話ではなく、単にブランチの運用方法とリリースの運用方法に関して紹介したいと思います。
Gitをソースコードのバージョン管理ツールとして注目してみましょう。
なぜGitを使うの?
中央管理型のソースコード管理ツールと比較して、Gitの⻑所/短所を議論したい場合こちらをご覧ください。
https://git.wiki.kernel.org/index.php/GitSvnComparsion
この件に関しては様々な議論がありますが、私はデベロッパーの一人として他のどのツールよりもGitを推したいです。(2010年当時の文書です) Gitはマージやブランチに対するデベロッパの考え方を大きく変えました。私は、CVSやSubversionを使っていたのでマージやブランチに少し不安がありました。(マージコンフリクトとかウンザリだし!)
でも、Gitになってからこれらの操作はとてもお手軽でシンプルになって、日常的なワークフローの一環で使用するごく普通の操作の一つになりました。例えば、CVSやSubversionの解説書などでは、ブランチやマージは上級者向けという扱いで後半の章にありますが、Gitの解説書だと基本の3章には登場してきます。
この結果、ブランチすることやマージすることはもはや別に怖いことではなくなりました。
ツールのお話はこのへんにしておいて、開発モデルに関して考えてみましょう。私がここで紹介しようとしている運用方法は、チームメンバー各個人がソフトウェアの開発進行管理をフォローするための一連の手続きの一つに過ぎません。
分散しているけど中央集権
ここで紹介するブランチ運用方法がうまく機能するための前準備として、本当の意味での中央リポジトリをセットアップします。注意点として、このリポジトリはただ一つの中央リポジトリとして解釈する点です。というのも、Gitそのもの DVCS(Distributed Version Control System)であって、中央リポジトリという概念はないためです。この中央リポジトリをoriginとして参照することにしましょう。このoriginという名称は、Gitユーザにとっても馴染みがあるためです。
デベロッパはみんな、pullやpushはoriginに対してします。でも、中央へのpush/ pullとは別に、各デベロッパは他のデベロッパと相互に変更点をpullすることができます。例えば、2人以上のデベロッパが協力して大きな新機能を取り扱う際に役立ちます。新機能が作業途中な状態でoriginにpushしてしまうことを回避することができるためです。
厳密には、これはAliceがbobという名のリモートリポジトリとして、Bobのリポジトリを参照するように定義している過ぎないのです。逆もまたしかりです。
メイン・ブランチ
基本的なところでは、一般に出回っている手法から大きく影響を受けています。中央リポジトリには恒久的に存在する2つのメインとなるブランチがあります。
originリポジトリのmasterブランチであればGitユーザであれば皆さんお馴染みですよね。そのmasterブランチと並んで、もう一方のブランチがdevelopと呼ばれるブランチが存在します。
私たちは、origin/masterブランチのHEADをいつでも出荷できる状態のmainブランチとして扱うことにします。
そして、origin/developブランチのHEADは常に次のリリースに向けた最新版となるように扱うことにします。これを統合ブランチと呼ぶ人もいます。夜間自動ビルドの対象となるのもこのブランチです。(2010年当時jenkinsは今ほど普及していなかった)
developブランチにあるソースコードが安定してリリースできる状態になった時点で、全ての変更点はmasterブランチへとどうにかしてマージして戻して、リリースナンバーのタグを打ち込みます。このやり方の細かいところは今も議論中です。
つまり、変更点がmasterブランチにマージして戻すたびに、製品が新しくリリースされることになります。これを厳守すれば、理論的には、Git hook scriptを使って masterブランチにcommitがあるたびに自動的にビルドして製品を出荷することが出来ます。
サポートブランチ
masterブランチやdevelopブランチとは別に、私たちの開発時の運用ではいろいろなサポート・ブランチを使っていました。これらのサポートブランチはチームメンバー内での並行開発を補助したり、機能のトラッキングを簡単にしたり、製品のリリース準備をしたり、製品にある実際の問題を手早く修正したりする用途に使います。メインブランチとは違って、これらサポートブランチは常に限られた寿命があり、最終的には削除されます。
使用するサポートブランチには
- 機能ブランチ (Feature branches)
- リリースブランチ(Release branches)
- Hotfixブランチ(Hotfix branches)
などがあります。それぞれのブランチには特定の目的があり、どのブランチが起源でどのブランチがマージ対象としなければならないという厳格なルールがあります。ちょっと、これらについて説明したいと思います。
Git的な概念としては、特別なブランチというわけではありません。これらのブランチをどのように運用するかでカテゴライズされます。もちろん、Git的にはごく普通のブランチに過ぎません。
機能ブランチ(Feature branches)
ブランチ元 | develop |
マージ先 | develop |
ブランチ名ルール | master, develop, release-?, or hotfix-? 以外 |
機能ブランチ(トピックブランチと呼ばれることもあります)は、近々発売される製品だとか、遠い先のリリースに 向けて新しい機能を実装する際に使用されます。機能ブランチの実装が開始された時点では、この機能ブランチがどのタイミングで統合されるのか確定していません。重要なのは、機能ブランチは実装が継続している間は存在し続けるのだけど、実装が終わった段階で出荷されるリリースに新機能としてdevelopブランチにマージされるか、あるいは残念な場合は切り捨てられます。大抵の場合、機能ブランチは開発メンバーのローカルのリポジトリのみに存在し、originには存在しません。
機能ブランチの作成
新しい機能ブランチを作成するには、developブランチからブランチします。
git checkout -b myfeature develop
“myfeature” という名の新しいブランチの作成と同時に、切り替えます
実装が完了した機能ブランチのdevelopへの統合
実装を終えた機能ブランチは、リリースに備えて確実にdevelopブランチへと統合しておきましょう。
$ git checkout develop
developブランチへ切り替えます
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557 (Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
myfeatureブランチは削除しておきます
$ git push origin develop
--no-ffオプションを指定しておくと、fastforwadが可能の場合であっても常に新しいCommitオブジェクトを作成します。こうしておくと、機能ブランチの履歴やグルー プにいるメンバーの全てのCommit記録が追加先のブランチに残ります。
後者(plain)のケースだと、Gitの履歴からどのCommitオブジェクトがある特定の機能実装に相当するのか分からなくなります。logメッセージから手動で読まなければなりません。複数のCommitから構成されているある特定の機能をrevertする場合、後者のケースだと本当に大変です。その一方で --no-ff オプションを付けておくけば簡単です。
確かに、--no-ffは少しだけ空のCommitオブジェクトを作成するけど、費用対効果はとても高いのです。
リリースブランチ
ブランチ元 | develop |
マージ先 | develop と master |
ブランチ名ルール | release-? |
リリースブランチは製品の新規リリースの準備をサポートします。最後の微調整をしても構いません。さらに、小さなバグ修正や、バージョン表記やビルド・タイムスタンプなどのメタデータの調整をしても構いません。これらの調整作業をリリースブランチに対して行うことで、developブランチは次の大きなリリースに向けた機能拡張に専念できます。
developからブランチさせて新規リリースブランチを作成するタイミングは、developブランチの状態が新しくリリースさせても(ほとんど)良い状態になった時です。少なくともこの時点で、この新規リリース対象に含めるべき全ての機能ブランチはdevelopブランチにマージされていなければなりません。次のリリースに向けた全ての機能ブランチは、この新規リリースブランチが作成されるまでdevelopブランチへのマージは待たなければなりません。
リリースブランチの作成
リリースブランチはdevelopからブランチすることで作成します。例えば現在の製品リリースのversionが1.1.5で、大きなリリースが近づいているとします。developブランチの状態は次のリリースに向けて準備が整っている状態で、これをversion 1.2 (1.1.6でもなく2.0でもなく)にすることを決めたとします。この場合、ブランチを切って新しいversion番号を反映させたブランチ名をつけてあげます。
$ git checkout -b release-1.2 develop
release-1.2ブランチへ切り替えます
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2 1 files changed, 1 insertions(+), 1 deletions(-)
新しくブランチを作成して、そのブランチに切り替えた後、そのVersionへと引き上がります。このbump-version.shというのはワークにあるFileをCopyすることで新しいversionを反映させる仮のスクリプトとします。(もちろん、これは手段による変更でも可能です。重要なのは、あるFileが変更されている点です)そして、引き上げられたversion番号はコミットされます。
この新しいブランチはしばらくの間、このリリースが完全に出荷されるまで存在し続けるます。この期間でのバグ修正などは(developブランチに対してではなく)このリリースブランチに対して適用されます。このリリースブランチに対して大きな新しい機能の追加は絶対に禁止です。リリースブランチはdevelopブランチにマージされるので、 次回の大きなリリースまで待ってください。
リリースブランチの終了
リリースブランチが本当にリリース可能な状態になった時、やっておくべきことがあります。まず、リリースブランチをmasterに対してマージする必要があります。(というのも、masterブランチに対する全てのコミットは原則的に新しいリリースだからです、覚えておきましょう)
次に、このmasterに対するマージコミットに対して、後で履歴を見やすくするためにもタグを打っておかなければなりません。最後に、リリースブランチに対して行った変更点はdevelopブランチにマージして戻しておく必要があります。
$ git checkout master
masterブランチへ切り替えます
$ git merge --no-ff release-1.2
Merge made by recursive. (Summary of changes)
$ git tag -a 1.2
リリースされたら、後のためにもタグを打っておきます。
リリースブランチに対する変更点を残しておきたい場合、developブランチにもマージして戻しておく必要があります。
$ git checkout develop
developブランチへ切り替えます
git merge --no-ff release-1.2
Merge made by recursive. (Summary of changes)
この段階では、マージコンフリクトが起きてしまうかもしれません。(多分、version番号を変えたところとか)その場合、修正してcommitします。
もう全て終えたのでリリースブランチを削除しましょう。というのも、リリースブランチはもう必要ないからです。
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).
Hotfixブランチ(Hotfix branches)
Hotfixブランチの作成
Hofixブランチはmasterブランチから作成されます。例えば、現在出荷されている製品Versionが1.2だったとして、その製品に深刻な不具合による障害が起きていたとします。でもdevelopブランチはまだ不安定だったとします。そんな時はHotfixブランチを作成してから問題の対応を始めましょう。
$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1 1 files changed, 1 insertions(+), 1 deletions(-)
ブランチ切った際に、versionを引き上げることを忘れずに!
そして問題となっているバグを修正して、1つか複数のCommitで修正をCommitします。
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem 5 files changed, 32 insertions(+), 17 deletions(-)
Hotfixブランチの終了
修正が終わったら、このBugFixはmaserブランチにマージで戻しておく必要がありますが、developブランチにもマージで戻しておく必要があります。次のリリースにもこのBugFixを適用させるためです。リリースブランチの終わらせ方と全く同じです。
まず、masterブランチのリリースタグを更新します。
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive. (Summary of changes)
$ git tag -a 1.2.1
次にbugfixをdevelopブランチにも入れ込んでおきます。
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive. (Summary of changes)
ここで、一つだけ例外ルールがあって、現在進行中のリリースブランチが存在している時は、このhotfixの変更はdevelopブランチの代わりに、そのリリースブランチに対してマージさせておく必要があるという点です。Bugfixをリリースブランチに戻しておけば、やがてそのBugfixは、そのリリースブランチの終了時にdevelopブランチにもマージされていきます。もし作業中のdevelopブランチで即座にそのbugfixを必要として、そのリリースブランチの終了まで待てない場合、そのbugfixをdevelopブランチに対して今までどおり間違わずにマージしても良いでしょう。
最後に、このテンポラリブランチを削除します。
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
まとめ
特にこれといった目新しいことは、このブランチ運用方法にはないのですが、この全体像は私たちのプロジェクトにおいて、とても実用的であることが判りました。この洗練された運用モデルを通して、チームメンバーがブランチとリリースの流れの理解を共有することが出来ました。