GitLab flowから学ぶワークフローの実践

本エントリは翻訳リクエストより投稿いただきました。
ありがとうございます!リクエストまだまだお待ちしております!

Gitによるバージョン管理では、従来のSVNなどよりずっと簡単にブランチングやマージができます。さまざまなブランチ戦略やワークフローが可能であり、以前のシステムに比べるとほとんど全てが改善されたと言えるでしょう。しかしGitを利用する多くの組織はワークフローの問題に直面します。明確な定義がなく複雑で、Issue Tracking Systemと統合されていないからです。そこで、明確に定義された最良の実践的方法としてのGitLab flowを提案したいと思います。issue trackingにはfeature driven developmentfeature branchesを組み合わせます。

他のバージョン管理システムからGitに移行する際によく耳にすることは、効果的なワークフローの開発が難しいということです。この記事ではGitワークフローとIssue Tracking Systemを統合したGitLab flowを説明していきましょう。Gitを利用するための、簡潔で透過性のある効果的な方法をご紹介します。

Four stages (working copy, index, local repo, remote repo) and three steps between them

Gitに切り変える際にまず頭に置いておくことは、コミットして成果を他の開発者と共有する前に3つのステップがあるということです。ほとんどのバージョン管理システムでは、作業コピーをサーバーで共有するのはワンステップのみです。Gitではまず作業コピーのファイルをステージングエリアに置きます。次にローカルリポジトリへコミットします。そして最後に共有されるリモートリポジトリへプッシュします。この3ステップに慣れれば、次のチャレンジはブランチングです。

Multiple long running branches and merging in all directions

Gitを初めて使うチームの多くが、どのように利用するかという打ち合わせをしません。それで収拾がつかなくなるのです。一番大きな問題は部分的な変更を含んだ長期ブランチがたくさん作られてしまうことです。つまり、どのブランチの開発を進めて本番環境へデプロイするべきかを選ぶのに苦労することになります。この問題へのよくある対処法はGit-flowGitHub flowなどの標準パターンを採用することですが、それらにもまだ改善の余地があります。そこでGitLab flowという一連のプラクティスの詳細を紹介したいと思います。

Git-flowとその問題点

Git Flow timeline by Vincent Driessen, used with persmission

Git-flowはGitブランチを活用するために最初に提案されたフローの1つで、大変注目されました。masterブランチとは別にdevelopブランチがあり、その他にfeature、release、hotfixというブランチがあります。developブランチでの開発作業を進めた後に、releaseブランチを作成し、成果は最終的にmasterブランチへマージされます。Git-flowはよくできた標準モデルですが、複雑なために2つの問題があります。1つは開発者がmasterブランチではなく、developブランチを利用しなくてはいけないことです。masterブランチは製品としてリリースするソースコードを保持しています。従来の方法はmasterブランチをデフォルトとして、そこからブランチを作成したりマージしたりします。ほとんどのツールは自動的にmasterブランチをデフォルトとして、それをデフォルト表示しますので、いちいち他のブランチへ切り替えなくてはならないのは面倒です。2つ目の問題は、hotfixブランチとreleaseブランチがもたらす複雑さです。これらのブランチが有意義な場合もありますが、大多数にとっては余計なものです。最近では継続的デリバリが主流で、デフォルトのブランチがデプロイされます。つまりhotfixやreleaseブランチからの更新、例えば、releaseブランチに隠れた変更のマージなどは含まれなくなります。この問題を解決するための専用のツールもありますが、ドキュメンテーションが必要だったりしてさらに複雑になります。また、開発者が誤って変更をdevelopブランチではなくmasterブランチだけにマージするようなミスもよくあります。これらのエラーの原因はユーザーにとってGit-flowが複雑すぎることなのです。また、releaseとhotfixは自動的に連動していません。

よりシンプルな代用としてのGitHub flow

Master branch with feature branches merged in

Git-flowをよりシンプルにしたのがGitHub flowです。GitHub flowではfeatureブランチとmasterブランチしか使いません。シンプルでわかりやすいフローで、多くの開発チームが採用し成功を収めています。Atlassianブログでも類似した戦略が勧められていますが、それはfeatureブランチをリベースするやり方です。全てをmasterブランチにマージし、こまめにデプロイすることで、デプロイ待ちのコードの量を最小限にできます。これは無駄のない継続的デリバリのベストプラクティスにも則っています。しかしGitHub flowにも、デプロイ、環境、リリース、インテグレーションについて問題があります。GitLab flowでは、これらの問題について更なるガイダンスを提案します。

GitLab flowのproductionブランチ

Master branch and production branch with arrow that indicate deployments

GitHub flowでは、featureブランチをマージするたびに本番環境へデプロイ出来ることを想定します。しかしそれはSaaSアプリケーションでは可能ですが、大抵の場合はできません。リリース時間をコントロールできない例の1つはiOSアプリケーションで、AppStoreのバリデーションをパスする必要があります。また別の例としては、デプロイの時間枠(平日午前10時~午後4時、オペレーションチームがフル稼働の場合)があるのに、その時間外にコードをマージする場合です。そういう場合には、デプロイしたコードを反映させたproductionブランチを作成します。masterブランチからproductionブランチにマージして新しいバージョンをデプロイできます。本番環境のコードを知るためにはcheckoutでproductionブランチに切り替えればいいわけです。デプロイのおよその時間はバージョン管理システムのマージコミットと同様に容易にわかります。自動でproductionブランチをデプロイする場合はかなり正確に時間がわかります。さらに正確な時間が必要な場合は、それぞれのデプロイの度にタグを打ちます。このフローはGit-flowのリリースやタグ付け、マージの際にありがちなオーバーヘッドを防ぎます。

GitLab flowの環境ブランチ

Multiple branches with the code cascading from one to another

masterブランチを自動的に更新してくれる環境があると便利です。このケースに限って、環境の名称はブランチ名と異なります。例えば、ステージング、プリプロダクション、プロダクションという3つの環境があるとします。ステージングにmasterブランチがデプロイされたとします。もし誰かがプリプロダクションへデプロイしたい場合は、masterブランチからpre-productionブランチへのMerge Requestを作成します。そしてpre-productionブランチからproductionブランチへマージすることによってコードが稼働されます。コミットがダウンストリームへ流れるだけのワークフローは、テスト済みのものはすべての環境で全てテスト済みであることを保証します。もしhotfixのコミットをcherry-pickする必要がある場合は、それをfeatureブランチで開発しMerge Requestでmasterブランチにマージすることが一般です。この時にfeatureブランチを削除しないでください。もしmasterがOKであれば(継続的デリバリであれば問題ないはずです)それを他のブランチにもマージします。もし、さらに多くの手動テストが必要なためにそれができない場合は、featureブランチからダウンストリームのブランチへMerge Requestを送ることができます。Teatroにあるような、全てのfeatureブランチに環境ブランチを作成するような極端な例もあります。

GitLab flowのreleaseブランチ

Master and multiple release branches that vary in length with cherrypicks from master

ソフトウェアを世の中にリリースする場合のみ、releaseブランチが必要です。この例では、それぞれのブランチはマイナーバージョン(2-3-stable、2-4-stableなど)を含んでいます。stableブランチはできるだけ最新のmasterブランチから分岐します。そうすることにより、最小限のブランチへbugfixを適用するだけで済みます。またreleaseブランチが宣言された後は、重大なbugfixだけがreleaseブランチに取り込まれます。可能であればmasterブランチにマージしてからreleaseブランチへcherry-pickされることが好ましいです。そうすることでこれらをmasterへcherry-pickすることを忘れたり、つぎのreleaseで同じバグに出くわすことを避けられます。これは「アップストリームファースト」と呼ばれるポリシーで、GoogleRed Hatでも実践されています。bugfixがreleaseブランチに取り込まれる度に新しいタグが打たれ、パッチのバージョン番号(Semantic Versioningに準拠)が上がります。プロジェクトによっては最新のreleaseブランチと同じコミットを持つstableブランチがある場合もあります。このフローではproductionブランチ(Git-flowでのmaterブランチ)は通常ありません。

GitLab flowでのMerge/Pull Request

Merge request with line comments

Merge RequestまたはPull RequestはGitマネジメントアプリケーションで作成され、指定した担当者に2つのブランチをマージするよう依頼します。GitHubやBitbucketでは最初のアクションとしてfeatureブランチをプルするため、Pull Requestという名前を使用します。要求され、最終的に行うアクションという意味でGitLabやGitoriousではMerge Requestという言い回しを使用します。ここではどちらもMerge Requestと呼ぶことにします。

もしfeatureブランチで数時間以上にわたって作業をする場合は、中間成果をチームの他の人とシェアしたほうがいいでしょう。特定の人にアサインせずにMerge Requestを作成することができます。ディスクリプションやコメントに、見てもらう人の名前を入れましょう(/cc @mark @susan)。こう書くことでまだマージできる段階ではないけれどフィードバックは歓迎するよという意味になります。チームメンバーはMerge Request全般にコメントもできますし、特定の行に行コメントで書き込むこともできます。Merge Requestはコードレビューのツールとなりますので、Gerritやreviewboardといった別のツールを用意する必要がありません。もしレビューで欠陥が見つかった場合には、誰でもコミットや修正をプッシュすることができますが、通常はmerge/Pull Requestを作成した人が行います。merge/Pull Request時の差分は、新しいコミットがブランチにプッシュされると自動的にアップデートを行います。

このマージに慣れてきたら、変更しようとしているコードベースについて一番知識のある人にアサインし他の人にフィードバックを依頼することもできます。アサインされた人がマージされたブランチの結果に納得してから、フィードバックを受けることもできます。またアサインされた人はマージせずにMerge Requestをクローズすることもできます。

GitLabでは長期ブランチ(masterブランチなど)を保護するのが慣習です。開発者は保護されたブランチに変更を加えることはできません
もし保護されたブランチにマージした場合はmasterの管理権限を持つ人にアサインしなければなりません。

GitLab flowにおけるissue

Merge request with the branch name 15-require-a-password-to-change-it and assignee field shown

GitLab flowはコードとissue trackerの間をより透過的に関連付ける方法です。

コードに対するいかなる大幅な変更も、目標が述べられたissueで始められるべきです。コード変更の全てに対して理由があることは、その変更をチームの全員に知らせて、人々がfeatureブランチのスコープを小さく保ちやすくするために重要です。GitLabではコードベースへの各変更がIssue Tracking Systemにおいてissueとして始まります。もしissueがまだなく、かなりの作業が含まれる(1時間以上)なら、最初に作られなければなりません。多くの組織にとってissueが至急の要件として予想されるのでこれは当然でしょう。issueの表題はシステムの望む状態を言い表すべきです。例えば「管理者がユーザーを削除できない」の代わりに「エラーを受け取ることなく管理者としてユーザーを削除したい」などです。

コードを書く準備ができたらmasterブランチからそのissueのためのブランチを始めます。このブランチの名前はissue番号で始まるべきです。例えば「15-require-a-password-to-change-it」などです。

作業を終えたりコードについて議論したいときはMerge Requestを開きます。これはコードの変更やレビューを議論するためのオンライン環境です。プッシュする新たなブランチは長期の環境やreleaseブランチとなるかもしれませんが、マージしたいと常に望むわけではないためブランチの作成は手動です。Merge Requestを作成しても誰もアサインしないと、これは「work-in-process(処理作業中)」のMerge Requestになります。これらは提案された実装を議論するが、まだmasterブランチに含める準備ができていないときに使われます。

コードの準備ができたと作成者が考えるとき、Merge Requestはレビュアにアサインされます。レビュアはコードをmasterブランチに含める準備ができたと考えるときmergeボタンを押します。この場合コードはマージされ、このイベントがのちに見やすいようマージコミットが生成されます。Merge Requestは誰からもコミットが加えられなくても常にマージコミットを作成します。このマージ戦略はGitで「no fast-forward(早送りなし)」と呼ばれます。マージのあとfeatureブランチは必要でなくなるので削除されます。GitLabではこの削除はマージするときのオプションです。

ブランチがマージされたが問題が生じてissueが再び開かれたとしましょう。この場合ブランチがマージされたときそのブランチは削除されているので、同じブランチ名を再利用することは問題ありません。どんなときにも各issueには多くて一つのブランチがあるだけです。一つのfeatureブランチが一つ以上のissueを解決することは可能です。

Merge Requestからのリンクとissueの完了

Merge request showing the linked issues that will be closed

issueへリンクすることはコミットメッセージ(修正#14、終了#67など)やMerge Requestの説明からメンションすることで起こり得ます。GitLabではMerge Requestがメンションするissueでコメントが作成されます。そしてMerge Requestはリンクされたissuesを表示します.これらのissueは一度コードがデフォルトのブランチへマージされると終了します。

issueを終了することなく参照を作りたいだけであれば、単に「ダック・タイピングが望ましい。#12」とメンションすることもできます。

複数のリポジトリにわたって広がるissueがあるなら、最良なのは各リポジトリに対してissueを作成して、全てのissueから親のissueへリンクすることです。

リベースによるコミットの圧縮

Vim screen showing the rebase view

Gitでは複数のコミットを一つに圧縮して整理するために対話的なリベース(rebase –i)を利用できます。この機能は、開発中に小変更のためのいくつかのコミットを行なったあと単一のコミットに置き換えたいときや、より論理的に整理したいときに有用です。しかしながらリモートサーバへプッシュしたコミットを決してリベースすべきではありません。誰かがそのコミットを参照していたりcherry-pickしているかもしれません。リベースするとコミットの識別子(SHA-1)が変わり、これは混乱を生じます。もしそうなると、同じ変更が複数の識別子として識別されて、さらなる混乱を引き起こすかもしれません。もし人々があなたのコードをすでにレビューしていたら、全てを一つのコミットへリベースすると、それ以前にあなたが行なった改善のみを彼らがレビューすることは困難でしょう。

皆しばしばコミットしてリモートリポジトリへ頻繁にプッシュすることを奨励されているので、他の人々はあらゆる人が取り組んでいる内容を知っています。これは変更ごとに多数のコミットをもたらして、履歴の理解をより困難させます。しかし安定した識別子を持つことの利点はこの欠点に勝ります。また文脈で変更を理解することで、コードがmasterブランチにマージされるとき、全てのコミットを一緒にまとめるマージコミットを常に探せます。

複数のコミットをfeatureブランチからmasterブランチへマージすると、元に戻すことはより困難になります。もし全てのコミットを一つへ圧縮していたら、このコミットを単に取り消せるだけです。しかしすでに述べたようにプッシュしたあとでコミットをリベースすべきではありません。幸運にもマージの取り消しでしばらく前へ戻すことはGitで可能です。しかしながら、取り消したいコミットのための特定のマージコミットを持つことを要求します。マージを取り消した後に、考え直して戻したい場合、Gitはコードを再びマージすることを許可しないので、再びマージする代わりに取り消しを取り消してください。

マージを取り消せることは、”–no-ff”オプションを使って手動でマージするときに、常にマージコミットを作成するよい理由です。Gitの管理ソフトウェアは、あなたがMerge Requestを受け入れるとき、常にマージコミットを作成します。

リベースでコミットを整理しない

List of sequential merge commits

masterブランチにコミットしたあとでそれらを整理するために、Gitでfeatureブランチのコミットをリベースすることもできます。これはmasterをまとめてfeatureブランチにして直線的で整った履歴を作成するときに、マージコミットの作成を防ぎます。しかしながら、ちょうど、コミットの圧縮のように、リモートサーバへプッシュしてきたコミットを決してリベースすべきではありません。そうすることでチームとすでに共有した進行中の作業はリベースできなくなります。これが私たちが推奨する方法です。featureブランチを更新し続けるためにリベースを使うとき、類似のコンフリクトを何度も解決する必要があります。ときとして記録された解決を再利用(rerere: reuse recorded resolutions)できますが、リベースをしなければ、一度のコンフリクトを解決するだけで準備は完了です。たくさんのマージコミットは避けるというのが良い方法であるべきでしょう。

たくさんのマージコミットの作成を避ける方法は、masterをfeatureブランチへ頻繁にマージしないことです。ここでmasterへマージするための3つの理由を議論したいと思います。コードの活用、マージのコンフリクトの解決、および長期のブランチです。もしfeatureブランチを作成したあとにmasterへ取り込まれたいくらかのコードを活用することが必要なら、ときとしてコミットをcherry-pickするだけでこれを解決できます。もしfeatureブランチにマージのコンフリクトがあるなら、マージコミットの作成はこれを解決する通常の方法です。マージのコンフリクトが起こりそうなときは、それを避けることを狙うべきです。一例はそのコードベースにおいて各バージョンごとに重要な変更が記録されるCHANGELOGファイルです。各々が(それぞれのブランチで)リストに機能の変更を追記していく代わりに、順不同でもそのバージョンの変更をリストに(まとめて)追記したほうが良いでしょう。そうすれば、コンフリクトすることなく複数のfeatureブランチをマージできそうです。マージコミットを作成するための最後の理由は、プロジェクトの最新の状態を保ちたい長期ブランチがあることです。マーティン・ファウラーはfeatureブランチに関する記事でこの継続的インテグレーション(CI)について述べています。GitLabにおいて私たちはブランチのテストでCIという概念を混乱させる罪を犯しています。マーティン・ファウラーの言葉を引用すると「人々が、たぶんCIサーバを使って、各コミットの各ブランチで、ビルドを実行しているからCIを行なっていると言うのを聞いてきました。それは継続的ビルドで、”よいこと”です。しかしインテグレーションがないので、CIではありません」 たくさんのマージコミットを避ける解決法は、featureブランチを短命にし続けることです。大部分が一日の仕事より短くあるべきです。もしfeatureブランチが一般に一日の仕事より長ければ、仕事のより小さな単位を作成する方法を調べるか、featureトグルを使ってください。一日以上続く長期ブランチに関しては2つの戦略があります。CI戦略では、遅れるほど手間のかかるマージを避けるために、毎日、一日のはじめにマージをします。同期ポイント戦略では、たとえばタグ付けされたreleaseのように、そのとき明確に定義されたポイントからマージするだけです。この戦略は、これらのポイントにおけるコードの状態がよく理解されているために、リーナス・トーバルズによって提唱されています。

結論として、マージコミットを避けるよう努めるべきですが、それらを撲滅すべきではないと言えます。コードベースはクリーンであるべきですが、履歴は実際に起こったことを説明すべきです。ソフトウェア開発は小さく面倒な段階があり、またこれを反映する履歴があっても構いません。コミットのネットワークグラフを見るためのツールを使うことができて、またコードが生み出した乱雑な履歴を理解できます。もしコードをリベースすれば履歴は正しくなくなり、また変化するコミット識別子に対処できないので、履歴を改善するツールのための道が閉ざされます。

Merge Requestへの投票

Voting slider in GitLab

+1や-1を意味する絵文字を使って賛成や反対を表明するのは、よくあることです。GitLabでは、それらをまとめてMerge Requestのページ上部に表示させています。ルールとして、賛成数が反対数の2倍にならなければ、マージできないことになっています。

ブランチのプッシュと削除

Remove checkbox for branch in Merge Requests

私たちは皆さんにfeatureブランチを頻繁にプッシュすることをお勧めしています。それがまだレビューしてもらえる段階でなくてもです。そうすることで、チームメンバーが誤って同じissueの処理を始めてしまうのを防ぐことができます。もちろん、issueを担当する者を予めissue tracking softwareで割り当てておくことで、この問題は防がれるべきです。しかしながら、まれにissue tracking softwareで割り当てをし忘れてしまうこともあります。ブランチがマージされた後、それはsource control softwareから削除させましょう。GitLabや同じようなシステムでは、マージするときのこの作業はオプションとなっています。そうすることで、repository management software内のブランチoverviewが進行中の作業のみを表示させてくれます。また、誰かがそのissueの作業を再開したとしても、同じ名前を新しいブランチとして問題なく使用することができます。Issueの作業を再開するときは、新しいMerge Requestを作成してください。

正しいメッセージで頻繁にコミットを行う

Good and bad commit message

私たちは早い段階で頻繁にコミットを実行することをお勧めします。コミットは、テストやコードの機能セットを変更するたびに実行することができます。これは、拡張やリファクタリングがうまくいかなかった場合でも作業を前の段階に戻すことができるという利点があります。今まで、作業を共有する準備が整った段階でコミットを実行していたことを考えると、SVNを使っていたプログラマにとって、これは大きな変更点と言えます。コツは、共有する準備が整った段階で、複数のコミットと一緒にmerge/Pull Requestを行うことです。コミットメッセージには、その内容ではなく、あなたがなぜそれを行ったかを記載しましょう。コミットの内容は簡単に見ることができるので、「なぜあなたがそのコミットを実行したか」を確認できるようにします。良いコミットメッセージはこんな感じです。“Combine templates to dry up the user views.” 情報が十分でない“change”“improve” “refactor”などは悪いコミットメッセージです。 また、“fix”や“fixes”なども文章や出典、issue番号の後に記述するのでなければ、良いコミットメッセージとは言えません。詳しいコミットメッセージの形式は、ティム・ポープのブログを参照してください。

マージする前のテスト

Merge requests showing the test states, red, yellow and green

継続的インテグレーション(CI)サーバーの古いワークフローでは、masterブランチでのみテストが実装されるのが一般的でした。この場合、開発者はコードがmasterブランチを壊さないよう注意する必要がありました。GitLab flowを使用する場合、開発者はmasterブランチからブランチを作成するので、(彼らのブランチ上でテストをして)テストが通ることが必要不可欠です。ですから、Merge Requestが承諾される前に必ずテストしなければなりません。TravisやGitLabのようなCIソフトウェアは、ビルド結果がMerge Request上に表示されるので便利です。一つ欠点をあげるとすれば、テストされるのはfeatureブランチであってマージ結果のブランチではないという点です。この欠点に対処するには、マージ結果のブランチをテストすることです。ただ問題は、マージ結果のブランチは、masterに何かがマージされるたびに変わってしまうということです。masterに対して全てのコミットを再テストするのは、計算コストが高く、また頻繁にテスト結果を待つという状況になります。もし、マージコンフリクトがなく、featureブランチが一時的なものであれば、リスクは許容範囲内です。しかし、masterブランチをfeatureブランチにマージしてマージのコンフリクトがあるのであれば、CIサーバーはテストを再実行してしまいます。もし、数日間続くようなfeatureブランチがあるのなら、issueを小さくするべきです。

他のコードとのマージ

Shell output showing git pull output

featureブランチを初期化する際、常に最新のmasterから分岐して行ってください。あなたが行っている作業が他のブランチに依存しているということが事前に分かっている場合はそのブランチから分岐してください。マージコミットへの理由付けを開始した後に他のブランチをマージする必要がある場合、まだ共有場所にコミットをプッシュしてない場合、masterもしくは他のfeatureブランチにリベースすることができます。もし、それをしなくてもコードがうまく機能し、きれいにマージされるのであれば、アップストリームとマージしないでください。リーナスもランダムポイントでアップストリームとマージをしてはいけません。マージできるのはメジャーリリースだけですと言っています。master履歴を最終的に破棄するfeatureブランチにマージコミットが作成されるのを防ぐ場合のみ、マージは行ってください。

参考資料