ソースコード管理でのブランチの切り方を模索してきました。いったん落ち着いたので書き残しておきます。
前提条件は以下のとおりです。
iPhone アプリがアクセスするウェブAPIを開発する。
サーバサイドの開発者はふたり。大阪と東京。
他の案件とかけもち。
行き着いたところ
Mercurial
git-flow/git-daily に似たブランチ方針
機能追加、開発中機能のバグ修正: default ブランチから、XXX ブランチを切って機能追加/変更し、default へマージする。
リリース: default ブランチから、release/XXX ブランチを切って、ステージングでテスト/修正。終わったら、default と master へマージし、master の先頭を本番サーバにデプロイする。
リリース済不具合の修正: master ブランチから、hotfix/XXX ブランチを切って、ステージングでテスト/修正。おわったら、default と master へマージ。master からデプロイ。
XXX はチケット番号
タグづけしない
この方針がしっくりくるまで、紆余曲折がありました。
Phase 1: BP Mercurial Workflow
週末に何をしているか謎な ぁっぉ氏で有名な、ビープラウドで採用されている
BP Mercurial Workflow で始まりました。
すべての作業は default ブランチから、チケット番号に対応するブランチを作り、終わったら default ブランチに戻ります。
新機能追加や不具合修正のブランチは、テストが終わったり、リリースする直前に default ブランチへマージする。
リリースするときには必要に応じて、default ブランチからリリース用にブランチを作る。確認が終わったら default ブランチにマージする。
マスター・リポジトリとリリース・リポジトリを用意する。
マスター・リポジトリの default ブランチの先頭がリリース可能な状態になったら、リリース・リポジトリに push する。
リリース・リポジトリの default ブランチ先頭は、常にリリース可能である。
リポジトリをふたつ用意する、という運用を、実施したことはありません。最初、このリポジトリの意味を勘違いして、てっきりプロダクション環境にコピーしたリポジトリだと思っていました。
このブランチ戦略のいいところは、とにかくシンプルなことです。ブランチ名はチケットに対応づいているのでユニークですし、個々の作業が完了したら default に戻せばよいのです。
このやり方に馴染めなかったのは、自分がどの環境にデプロイされている機能を変更しているのか分からなくなるからでした。
頻繁なリリース、頻繁なバグフィックスが特徴の開発でした。バグがある状態でリリースすんなっていうのは、ごもっともです。iPhone アプリ側に不具合があったとき、すぐに修正が効かないし、大多数がアップデートするまで時間もかっかります。ですので、サーバサイドでなんとなく頑張るようなこともあります。
また、新機能を鋭意開発中に、別の仕様が飛び込んできたりもします。開発中のコードに手を入れることもあります。default ブランチと、そこから派生したチケットブランチだけでは、ツリーを見た時に「いま本番サーバでデプロイされているコードがどれか? ロールバックするとしたら、どのポイントなのか?」が、私には直感的に分かりませんでした。タグづけとかもしたんですけどね。
そんなわけで、開発中とデプロイ済のコードを、ブランチで分けてあるといいなぁと思いました。
Phase 2: git flow
次に試してみたのが、
A successful Git branching model です。これを支援する
git-flow という git 拡張があります。
開発用とリリース済用の、ふたつのブランチを使う方針です。
機能追加、開発中の不具合修正: develop ブランチから feature/name-of-feature ブランチを切って作業し、develope ブランチへマージ。
リリース: develop ブランチか release/x.y.z ブランチを切って作業し、準備ができたら default と master へマージ。x.y.z とタグをつけて、master からデプロイ。
リリース済の不具合修正: master ブランチから、hotfix/x.y.z ブランチを切って作業し、develop と master へマージ。x.y.z とタグをつけて master からデプロイ。
この方針では、master ブランチの先頭が、いまデプロイされているバージョンです。master ブランチのひとつ前のコミットが、その前にデプロイされたバージョンとなります。
develop ブランチは基本的に不安定で、release ブランチでの作業をへて安定し、master へマージする、というものです。これで遠慮無く devepopブランチに変更を反映できますし、master ブランチ(リリース済みのコード)の修正にも影響されずに、リリース用にコードを整理できます。
すばらしい! と思ったわけですが、1日に数回リリースすることもあって、バージョン番号とか面倒なのですね。また、master ブランチの各リビジョンが、基本的に本番サーバでのリビジョンなので、タグとか要らないんじゃないのか、と思ったりもしました。
Phase 3: git daily
@sotarok による
git-daily の紹介 を聞きました。git-daily はGREE での開発フローと、その支援をするツール。1日に複数回リリースすることだってある、というのが前提。これは、私の置かれている状況と似ています。
重要なのは gitflow というツールではない
tag とか切らない
リリースブランチ: release/yyyymmdd-hhMM
当時は hotfix と feature は未実装だった。今はhotfix が実装済。
これでかなり、状況に合った方針になりました。けど、ひとつだけ、ほんとひとつだけ馴染まないことがありました。
複数の案件に関わっていて、割り込みが多く hotfix の作業を1日で終わらせられるとは限らない。私があるバグの修正をしている間に、もう1人の開発者が別の機能を実装しおわって、リリースすることもあります。作業開始の日付と、実際にリリースされる時間の順番が一致しない。日付的なもの書かれていると、順番になっていない気になります。「5月7日にリリースした、20120501-1200 ブランチだけどさ」みたいな。気持ちの問題なんですけどね。桁数もおおいし。
前述のプレゼン資料を見てみると、こんなことが書かれています。
会社で使うならその会社での開発スタイルにあわせた Wrapper ツールは必要
gitflow をつかうもよし、git-dailyをつかうもよし、自作するもよし
Phase 4
そんなわけで行き着いたのが、日付の代わりにチケット番号を使えばよいではないか、と。
feature/XXX は作りません。元のブランチから分岐させて、変更し、もとのブランチに戻すというシンプルな作業だからです。SourceTree みたいなツールを使った時、 feasture/XXX みたいなブランチ名にしておくと、まとめてくれるんで便利かも、と最近思い始めています。
hotfix と release のBranch操作が手順が多かったので、hg のラッパスクリプトを書きました。超やっつけ。pelo っていうコマンド名は (ry
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf8
"""pelo
pelo branch management wrapper commands
"""
from datetime import datetime
from mercurial import commands
HELP_DOC = """COMMAND OPERATION [label]
pelo release start uXXXX .... start new release
pelo release finish ......... finish current release
pelo hotfix start uXXXX ..... start new hotfix
pelo hotfix finish .......... finish current hotfix
"""
def pelo_cmd(ui, repo, cmd, op, label=None, **opts):
if cmd == 'release':
return release(ui, repo, op, label, **opts)
elif cmd == 'hotfix':
return hotfix(ui, repo, op, label, **opts)
else:
_write(ui, HELP_DOC)
def release(ui, repo, op, label, **opts):
prefix = 'release'
start_at = 'default'
return operate_branch(ui, repo, op, label, prefix, start_at, **opts)
def hotfix(ui, repo, op, label, **opts):
prefix = 'hotfix'
start_at = 'master'
return operate_branch(ui, repo, op, label, prefix, start_at, **opts)
def operate_branch(ui, repo, op, label, prefix, start_at, **opts):
if op == 'start':
return start_branch(ui, repo, label, prefix, start_at, **opts)
elif op == 'finish':
return finish_branch(ui, repo, label, prefix)
else:
_write(ui, HELP_DOC)
def start_branch(ui, repo, label, prefix, start_at, **opts):
"""start prefix branch"""
# exit if label is not provided
if not label:
_write(ui,
'abort: You must specify ticket to start %s branch.' % prefix)
return
# exit if not in default branch
if _get_current_branch(repo) != start_at:
_write(ui,
'abort: Run "hg update default" before starting %s branch.' % start_at)
return
# branch <prefix>/<label>
commands.branch(ui, repo, '%s/%s' % (prefix, label))
# commit to mark start
commands.commit(ui, repo, message='started %s/%s' % (prefix, label))
def finish_branch(ui, repo, label, prefix='hotfix', **opts):
"""finish hotfix branch"""
# 変更が残っていたらエラー
status = repo.status()
for l in status:
if len(l) > 0:
_write(ui, 'abort: Your workspace has uncommit content.')
return
# label が指定されていたらエラー
if label:
ui.write('abort: You cannot specify branch name\n')
return
# <prefix> ブランチじゃなかったらエラー
if not _get_current_branch(repo).startswith('%s/' % prefix):
_write(ui,
'abort: you must be in %s branch' % prefix)
return
# determine current branch
current_branch = _get_current_branch(repo)
# merge into default branch
error = _merge(ui, repo, current_branch, 'default')
if error:
return
# merge into master branch
error = _merge(ui, repo, current_branch, 'master')
if error:
return
# notify
_write(ui, '%s was successfully finished.' % current_branch)
def _get_current_branch(repo):
ctx = repo[None]
current_branch = str(ctx.branch())
return current_branch
def _write(ui, message):
ui.status(message + '\n')
def _merge(ui, repo, from_, to):
commands.update(ui, repo, to)
error = commands.merge(ui, repo, from_)
if error:
return True
commands.commit(ui, repo, message='merged %s -> %s' % (from_, to))
cmdtable = {
'pelo': (pelo_cmd, [], HELP_DOC),
}
さいごに
念のために書いておくと、最後の方針が、ユニバーサルにベストな方法だとは思っていません。だいたい職場の開発方針や開発プロセスが変われば、ブランチ方針も変わるかも知れません。 開発スタイルに合わせた、ブランチ方針があると思っています。
「パターンによるソフトウェア構成管理」という本を参考にしました。想定しているツールが Subversion あたりなので、Git や Mercurial などにはバッチリこないこともあります。デスクトップアプリのようなのを想定しているので、メジャーバージョンごとに、リリースブランチがあったりとか。あくまでパターンの紹介なので、抱えている課題と合うものを採用するものだと思います。