git による分散作業パターン

git による分散作業パターン

分散バージョン管理を華麗に扱いたい堀口です。
GREE Advent calendar 2013 の 14 日目として参加させていただきます。
お二人に続き Haskell の話をしようかと思ったのですが、急遽無難な開発の話に変更しました :o


Java や C++ には OOP の概念が必要であったように、分散作業の認識が薄いまま git や Mercurial を使うことは長期的に不幸をもたらします。

  • とあるプロジェクトにて、その一部を副産物のミドルウェアとして抽出すべく、アプリケーションと分離したい
  • 不具合があったので原因を探りたいが、依存関係が複雑すぎるのでコードを読む量を減らしたい
  • テストやレビュー、提案、リファクタの運用を強化したい
  • よそのプロジェクトに迷惑を掛けないように、そこのツールを改良して使いたい。

いままで何気なく「こんなもんだろう」と思って手間をかけていませんでしたか?その問題は分散作業パターンを使う事で効率よく解決できていたかもしれません。分散バージョン管理システムを華麗に扱う事で、分散作業のメリットを活かし、デメリットを減らしましょう!

このページではツールに git や Github を利用することを想定しております。


master - develop

master を常に出荷可能な安定状態に保ち、メンバーは develop に対してコミットを行います。

216758634d376a8dc3fd317c4fe925d1

最初は master から分岐させ、コミットを増やしていきます。
リリースの時期になったら master へマージしましょう。
git の場合とりあえず --no-ff オプションを指定するとマージの記録がコミットとして残ります。


develop - feature

develop には不完全な機能をコミットせず、メンバーは feature ブランチにコミットを行い、 develop へマージします。

1bdddc46e2b851bfa14258f16bce1686

こうすることで不完全な機能をコミットしても、他の開発者に影響を与えなくなり、比較的 develop が安定します。
別の新機能を開発する場合は、一旦収束させ、また develop から分岐させるのがポイントです。
収束させないパターンについては後述します。


hotfix

ときには master のバグが発見され、間髪入れずに修正しなければいけないことがあります。

すぐにパッチを書き、レビューを経て、マージしましょう。

7329fb278c335233ca4cb3d79618f42c

必要であれば develop へも rebasemerge します。特に merge する場合は起点に気を付けなければいけません。 develop から fix を作った後に master へマージなど愚かなことはしてはいけません。 develop から分岐させると意図しない開発中のコミットが master へ含まれる恐れがあります。


PullRequest (request-pull) "プルリク"

e604b8543f696eef7721976d89d0dc24

プルリクを活用することで開発者は直接マージ作業を行う必要が無くなります。
また、収束先ブランチへの操作が無くなるのでそのブランチを保持するリポジトリへの書き込みが不要になります。

この本の 5-1 章の中段より、リポジトリ配置は次にようになります。

215f1c60e9343199b433e4f289cae5ea

ここで重要なのは以下の三点です。

  • blessed repository は developer にとって共有の読み取り専用の聖域である。 絶対に汚してはならない。 聖域を侵す事はチーム全員へ迷惑をかける事に直結する。
  • developer public はメンバー全員それぞれ読み取り可能であり、自分のところは自由に push [-f] できる。
  • blessed repository に push できるのは integration manager である。多くの場合は熟練した developer のだれかが integration manager の役目を担う。

開発チームメンバーが明らかな場合、 blessed repository を origin とし、それぞれの developer public をその人のニックネームで git remote add しておくと良いでしょう。こうすることで開発チームは全員のプライバシーを守りつつも、それぞれの状況をいつでも把握でき、素早くアドバイスすることができます。
また、上記三点を満たせていない場合、なぜ重要なのかを良く考え、相談し、実施できると良いと思います。

もし Github を利用している場合は、 developer public の設置にかかる手間は Fork ボタンを押すだけです。さらに、 integration manager によるマージに関する作業はブラウザにてできてしまいます! Github すごい!

git 初心者の場合は便利すぎる Github を使わずに、自分で developer public を作るよう心掛けると理解が早いと思われます。


master - release - develop

master は常に安定動作が原則ですが develop は新機能が矢次に実装されてゆき、比較的不安定です。そのまま master へマージすることはストレスを伴います。

cc52425bbeecf96b8a06c3e085b2f733

そこで release ブランチを中間に用意し、出荷に向けてバグ修正など、安定化のための修正作業をこちらで行います。
この間、 develop は出荷の都合に関係なく成長することができ、 release が十分成長したところで master へマージし、 release の役目を終えます。
release/develop それぞれに必要だと考えられる修正は hot fix をそれぞれに行うか、 master だけでなく release を develop へもマージすることでプルリクの手間を軽減できます。


ここまでが基本形になります。おそらく git を利用する多くの人が、ここまでは意識せず使ってると思われます。


library - implement

Submodule にするまでもなく、バイナリでもないライブラリは、先にライブラリコードをマージさせ、あとから実装をマージさせようとすることで、コードレビューの見通しを良くしようする狙いがあります。

4ddc2c00adf7a4aac7c5e265a0a7c8ea

インデントの変更やリファクタなどにおいて、レビューの本質でない部分によって見通しが悪くなる恐れがある場合にも、このパターンは有効です。


rebase

美しくないグラフに価値はありませんので、適度な大きさ、分岐点を選んで rebase しましょう。ある程度の古いコミットに --onto できるとコード間の依存が低いと考えられ、良いとされています。ある機能に限定したバグ修正であれば、プルリク前の feature ブランチの HEAD に対して --onto するのも良い手です。冗長なコミットコメントが続くような場合は squash しましょう。

9f829812d30ba9e76175cd381473e3ab

プルリクをお願いした後に「リヴェイスしてこい」と言われたり、恥ずかしい type を見つけた時はマージ前に push -f して上書きします。


pull - pull

このパターンはペアプロのように 二人で一つの機能を作りこむ ときに活用できます。よって、これは集中作業と捉えられます。

0fa62053f3203bb2b8c9d198d8b9f1fd

お互いの developer public から pull しあうことで素早く対応でき、二人で一つの機能を作りこんでいる事が明確になります。ごく短期間での運用に向き、最後にどちらかが develop へプルリクをし、収束させます。また push -f は控えましょう。 push -f をするケースの場合、先頭からいくつかの Hash が変わってしまうので、そのまま pull された場合 push -f 前の差分とマージされてしまいます。おそらく Conflict し手間を増やしてしまうので、 push -f をする場合は pull でなく rebase または reset が良いです。


PR - PR

プルリクに対して提案を行う場合、プルリクにプルリクをしましょう!

c91b9085f440eff9f9cce963698c6010

下らない指摘や議論をする前に、ぬくもりあるプルリクをすべきです。 Github には議論の場が設けられていますがコーダーならコードで語り合うほうが理解を得られやすいはずです。

pull - pull と違い、多くの developer からの提案を受け付けることが出来きます。長期運用も可能です。
ただしこちらも push -f は控えたほうが良いでしょう。


master branches

今までのパターンは master に収束させていくことが前提でしたが、そうでない運用も考えられます。たとえば製品を日本語版、英語版で永続的に分けたり、ミドルウェアを採用製品毎にカスタマイズする場合です。
おそらくここから先が git の利点を多く享受できるパターンだと思われます。

2f828a7c3449d3eacb4060a691c02ee7

収束させないので次第に乖離していきますが、同じ修正を行う場合、なるべくコミットやレビューの手間を減らすことがポイントになります。


それぞれに同じ修正が可能な場合、分岐点からコミットを成長させ、適応させるブランチの数だけプルリクします。
この成長は master - develop パターンと同じです。

a85763cb522ab949d2b63819e88d4102


乖離が進み同じ修正が出来ない場合、適度に古いポイントに対して rebase --onto を行い、 Conflict の量を減らします。

34e224e4093c64647632519435c0ab65


minor customize

ブランチは収束しないものの、違いが少ない場合は、違う部分だけ rebase させていく方法が考えられます。
コードのモジュール化が十分ではないが、自分専用にハードコードしたり、テストやリリースなどに向けてカスタマイズをする場合に活用できます。

4ebe7f6d585ada75580569f096ce0ea6

このパターンは手作業で HEADrebase し続けないといけないため、比較的運用が面倒です。しかし影響範囲を非常に限定できます。


submodule

submodule は大変強力な手段です。
ライブラリやツールなどを取り入れる事が考えられます。そのとき submodule 対象は master branches か minor customize パターンによる分岐がすでに出来ているか、もしくは使おうと考えているはずです。
親リポジトリのほうで、 submodule の Hash を minor customize しても良いかもしれません。夢がひろがります。
ただし、 submodule 内の作業ソースツリーは親の resetcheckout だけでは切り替わらないので、 developer が忘れていた場合は作業ソースツリーの一貫性が保たれなくなります。
チーム内で submodule を利用することについて合意し、どの戦略を利用するか決めましょう。


利用するコマンド

git コマンドラインの操作は比較的難しいものですが、実際に使うのはわずかです。殆どの作業は fetch,reset,rebase,commit,push だけで済むはずです。

  • fetch [--all] -- リモートリポジトリの内容を更新する
  • reset [--hard] -- 作業ブランチの内容を切り替える
  • rebase [-i,--onto] -- コミットを繋ぎ変える
  • commit [-am,--amend] -- コミットを成長させる
  • push repository src[:dst] [-f] -- コミットを公開する。 dst を指定してリネームがオススメ
  • merge [--no-ff]
  • checkout
  • add
  • config [-e,--global]
  • reflog
  • request-pull
  • cherry-pick

上記コマンド程度で alias をごちゃごちゃ設定すべきではありません。早く入力することよりも、柔軟に扱い、グラフの健康を保つ事が大切です。

せいぜい
git log --graph --color --pretty=format:'%Cgreen%h %cd %Cred%cn %Creset%s %Cred%d'
のように長く覚えられないものだけ alias に設定しましょう。これには --all-p を組み合わせると良いです。


まとめ

局所的に見ると重要ではなさそうですが、同じにみえてもそれぞれのグラフには大変重要な意味が込められてます。
グラフの意味を意識せずコミットを成長させるような愚かな事はしてはいけません。


明日は 何 川 さんです。国際派ゲーマー、プログラマ(と思っている)ので期待できます!