gitはとても便利なツールですが、一人で使うのとチームで使うのでは、使い方が違ってきますよね。 今回は、ずっと一人でgitを使ってきた私が、チームでgitを使ってアプリケーションを開発するためにここ半年ほど気をつけていることを書いてみました。
0. 前提
Github等のサービスを使ったWIP-PR開発、あるいはredmine等のツールを使ったチケット駆動開発をしているチームのためのものです。 オープンソースのプロジェクトやライブラリの開発にあたっては必ずしも当てはまりませんので、ご注意ください。(特にオープンソースの開発で、プロジェクトの規約がある場合( 例えばSymfonyならこんな感じ )は、それに従いましょう)
1. コミットの単位
(1) 考え方
論理的な単位に分割しましょう
論理的な単位とは、関連するチケットやメール・ドキュメントを読まなくても、コミットメッセージとdiffを見れば、
- コードのどこを、
- どう変更したのか
わかる単位のことです。
つまり、「◯◯画面に☓☓を表示するようにした」「△△したとき◯◯ではなく□□するように変更した」のようなコミットが理想です。 逆に、「◯月☓日の作業分」「△△関連機能一式」といったコミットは避けましょう。 初めは面倒に感じるかもしれませんが、こうしておくと巻き戻す時に影響範囲を小さくすることができます。
唯一の例外は、コーディング規約上の問題を修正する場合です。特定のクラスや特定のディレクトリ配下のクラス群全体に対して括弧の位置や、改行コードや、インデントなどを修正した場合は「コーディング規約の修正」で1つにコミットにまとめても構いません。
小さくしましょう
できるかぎり小さく、1ファイル〜2ファイル分の変更(クラス+テストとか)のみが含まれているのが理想です。 論理的な単位を実現するためにも、フレームワークを使ったアプリケーション開発では、フロント画面など、設定ファイル・コントローラー・ビュー(テンプレート)…とファイル数を多くせざるを得ない場合がありますが、そのようなときでも1つのコミットで扱う単位をできるかぎり小さくしてください。
小さくすることで論理的な単位に分割しやすくなりますし、後の例のような場合にrevertすべきコミットがより見つけやすくなります。
(2) 例
たとえば、◯月☓日にA機能のfooをbarに変える改修とB機能のhogeをmageに変える改修をしたとして、後で□月◯日になってからA機能のbarをfooに戻すことになったとします。 「◯月☓日の作業分」でまとめてコミットされていると、こからどこまでA機能のdiffでどこからどこまでB機能のdiffなのか判別するのは困難です。仮にA機能とB機能が全く関係のない機能であれば、Symfony2のBundleが分かれていたり、diffのあるファイルが別々のディレクトリにあったりして、A機能のdiffとB機能のdiffが容易に判別できる場合もないわけではありません。が、往々にして、同じ日にやった作業は同じBundle、近接するディレクトリ(時には同じディレクトリかも)にあるファイルが多いものです。
悪い例
xxxxxx ◯月☓日作業分
→ xxxxxxのdiffの中からA機能のfooをbarに変えている部分を特定して手動でbarをfooに戻し、そのdiffを□月◯日作業分として他のdiffとまとめてコミット??
マシな例
xxxyyy A機能改修
yyyzzz B機能改修
→ 操作自体は git revert xxxyyy
で一発だが、このコミットの前にも後にも同じようなコミットメッセージ「A機能を改修」が並んでいたらどれが「fooをbarに変えた」コミットなのか探す手間が発生する。
良い例
xxxyyy A機能のfooをbarに変更
yyyzzz B機能のhogeをmageに変更
→ revertすべきコミットがxxxyyyだとすぐに特定でき、 git revert xxxyyy
で一発。
(3) 使える技
- 別コミットするつもりがうっかり–amをつけてしまった! → (コミットメッセージ編集に使っているエディタがvimの場合):cqでエディタをキャンセルするとgit addしただけの状態に戻ります。落ち着いて–amをつけずにgit commitし直しましょう。
- 分けるべきdiffをうっかりまとめてしまった! → 一旦git branch tmpで現状を保存しておいてgit reset HEAD^を実行するとそのコミットに含まれるdiffをgit addする前の状態に戻ります。落ち着いてgit addからやり直しましょう。(同じファイルへのdiffを分けたかった場合は後のdiffを一度なかったことにしてからgit addし直す必要があります)
- git rebase -i を使うとコミットの順番の入れ替え、あとから不要だと判明したコミットの破棄、間違えたり不適切だったコミットメッセージの書き換えもできます。(詳しい使い方については、ぐぐったら親切な説明があちこちに載ってます)
2. Pull Requestの単位
(1) 考え方
(再び)小さくしましょう
コミットの単位同様に、なるべく小さくしましょう。
「◯◯機能一式」のように1つのPRを大きくすると、ビジネスロジックを記述したクラスから画面の見た目まで、PRにすべてのコミットが積み上がることになります。すると、全体ができあがるまでリリースしないような新規開発や、他の機能と完全に独立した新機能開発なら、それほど影響はないですが、既存機能の改修ではPRがマージされたらすぐにユーザーの目に見えてしまうため、レビューOKとなってもマージを保留する場合が出てきます。そして、マージが保留されたままだと、リリース前に同じ機能に要件追加が発生した場合でも新しいPRを作りにくくなり(ブランチの分岐元をmasterにして新しいPRを作ることができない)、結果として、より多くのコミットを1つのPRに積み続けることになりかねません(悪循環ですよね)。 もちろんrebaseを活用すれば、保留されているPRのブランチから分岐した新しいブランチで別のPRを作ることもできます。しかし、元のPRの保留が長くなればなるほど、rebase時のコンフリクトは多くなり、更に、元のPRとは別のPRからmasterにマージされたクラスを使いたい場合が発生してしまったら……大混乱必至です。
副次的な効果として、PRを小さくしておくと、レビュワーの負担も軽くなり、マージしてもらいやすいです。
PRを設計しましょう
1つの機能を作るとき、「◯◯機能作成」でPRを作る前に、どのような構成にすれば小さなPRに分割できるかまず考えてみましょう。 その際、定型的でレビューの必要がほとんどなく、かつ、いくつかの機能で共通して使う部分(Symfony2ならバンドル作成、エンティティ作成など)は最初に小さなPRを作ってレビュー依頼を出してしまうと良いでしょう。
(2) 例
たとえば、C機能のために新しくバンドルを作り、必要なエンティティを作ったうえで、それぞれ独立した◯◯クラスと△△クラスを書く必要があるとします。◯◯と△△はどちらも、複雑でこみいったクラスになりそうです。
悪い例
PR #1234
xxxxxx C機能用のバンドル作成
yyyyyy C機能用のエンティティ作成
zzzzzz C機能の◯◯クラス作成
レビュー待ちの間にC機能の△△クラスを作りたい場合、C機能のバンドルとエンティティがまだマージされていないので△△クラス作成に取り掛かることができない。 ローカルの開発マシン上で、
xxxzzz C機能の△△クラス作成
のコミットを #1234 のブランチの続きとして作成したものの、#1234がなかなかマージされないので新しいPRを作れない。 あるいは、#1234のコミット(xxxxxx〜zzzzzz)をまるごと含めてPR #1235を作る? それとも、レビュワーにことわりを入れて△△クラスも#1234のPRで扱うことにする?
良い例
PR #1234
xxxxxx C機能用のバンドル作成
yyyyyy C機能用のエンティティ作成
→ 定型的なクラスだけなので、(よほど変なバンドル名だったり、仕様から外れたありえないDB設計でない限り)レビューが速く、既存機能に影響がないのでマージが保留されることもない。
PR #1235
zzzzzz C機能の◯◯クラス作成
→ #1234がすぐにレビューOKでマージされたのでmasterから分岐して新しいPRを作ることができた。
PR #1236
xxxzzz C機能の△△クラス作成
→ #1235のレビューには時間がかかりそうだが、#1234は既にマージされているので、masterから分岐した新しいPRで進行できる。
(3) 使える技
- ついうっかり別のPRに分けるべき内容を一つのブランチでコミットしてしまった! → git branch (新しい別のPR用のブランチ名)で現状を保存しておいて、git reset –hard HEAD^ や git rebase -i を駆使して元のPR用のブランチをPRに必要なコミットだけの状態に戻し、git checkout -b (別のPR用のブランチ名)で現状保存したブランチに移動、やはり git reset –hard HEAD^やgit rebase -i を駆使して新しい方のブランチから元のPRに含めたコミットを取り除けば、スムーズに別のブランチに分けることができます。
3.まとめ
いかがでしたか?
一人ひとりのほんのちょっとの習慣づけで、チーム開発でgitをよりよく活用できるようになりますよね。 コードを書く時(書く前)に少し気をつけてみてください。