危なくないgitこと、うちのチームのgit戦略草案(ver. 2)

履歴

恥を忍んで記事を公開させていただいたおかげで、いろいろフィードバックいただきました。フィードバックを取り込んで更新を行なっています。

2012/11/16: cherry-pickしやすいように、というくだりのところは論理通ってないので削除しました。 1 pull req. 1 commitの原則をやめました。言いたいことであった「試行錯誤の過程を入れないで」を丸パクリしました! > id:kazuho その他表記修正、クリアコードさんの記事に説明丸投げなど。

まえがき

gitでトラブった!という話を何度か聞いたことがあります。なんでトラブッてるんだろう…と話を聞いたところ、同一のリモートブランチに対して複数人・複数環境から操作が行われているようです。極端な例を挙げると、masterブランチしか存在しておらず、コミットログをキレイにするためと称してgit pull –rebaseを常用しているような環境です。

gitは、以下のような操作をしなければ安全です。

  • mergeに相当する操作をしない
  • rebaseに相当する操作をしない

…悪い冗談。極端な話ですね。しかし、「merge/rebaseの回数を減らせば、トラブルが起こる確率を減らすことができる」というのは事実です。

そこで、GitHub(Enterprise)の利用を前提に、こういった運用ルールだといいんじゃね、という私案を公開します。ツッコミよろしくです!

なお、git-flowの用語を前提としていますが、developブランチとfeatureブランチしか説明に用いません。

想定する前提条件

比較的大規模なチーム開発において、大量に画像を使うようなWebサイトを制作する。本番デプロイは週3回程度。GitHubもしくはGitHub Enterpriseを開発に用いている。 チームメンバーには、非英語圏のオフショア開発拠点が含まれ、スキル・経験ともさまざまである。

ねらい

mergeとrebaseを減らしつつ、それらを行う際には意識的に行うようにする。結果、merge/rebaseミスによる意図しない破壊を防ぎ、コードレビューのタイミングを設け、コミットログもそこそこキレイになる。

mergeを減らす

developブランチでのmergeを減らすためには、developブランチに変更を行える人を制限し、一般の作業者が変更できないようにします。

git-flowを用いて、特定の環境でだけfeatureブランチを編集し、developへのマージはある特定の人だけが行う、といった運用は、上記の観点からトラブル防止に有効です。

developブランチへのfeatureブランチの取り込みは、すべてpull requestを用いて行います。developブランチに対する直接作業は禁止します。

pull requestのやり方は後述します。

pull requestに取り込みにおいて、相互レビューをするとよいです。自分がpull requestを受ける立場になれば、レビュワーにやさしいpull requestというものの重要性が実感として分かります。

同一ブランチに対する共同作業をどうするか

featureブランチについて、複数人で作業したい場合がある。たとえば、sotarok先生のgit-dailyは、そのような状況を想定したワークフロー・ツールである

feature/sugoiというfeatureブランチを開発するにあたって、サーバサイドロジック側エンジニアが2人、HTML/CSSコーダーが2人、JavaScript側エンジニアが2人、画像製作者が2人参加すると想定する。

画像製作者が制作する画像については、git管理外とする。詳しくは後述する。

それ以外の作業者については、feature/sugoiブランチ以下に個人ごとのサブブランチを立てる(ex. feature/sugoi/suenaga)。サブブランチはfeatureブランチを基に作成される。

あとは、個々人が自分の管理下のブランチを育てる。個人ごとのサブブランチは、リモートにpushはしない。リモートにpushする場合には、featureブランチにmergeを行い、featureブランチをpushする。ここでもpull requestを用いてもよいが、レビューが必要な場合などにとどめる。後述するように、どうせfeatureブランチの内容をdevelopブランチに入れる際にコミットログを加工するから、ここでコミットログを綺麗にしようとは意識しない。

featureブランチのサブブランチは個々人の管理なので、rebaseしようが何しようが問題なし。継続的コミットを採用し、1分ごとに自動コミットしてもらってもOK。本体のコミットログが英語onlyであっても、日本語のコミットログを残してもOK。自分がやりやすいように作業する。

画像ファイルなど、巨大なバイナリファイルの管理はどうするか。

gitには向いていません。Subversionや、Alienbrainなどの専用ソフトを使いましょう。

まず、バイナリファイルはmergeができません。よって、画像のバージョン管理の要件としては、任意のバージョンの画像が取得できれば十分となります。

次に、不自然だからです。例えば、Photoshopで作成したjpeg画像がプロジェクトに必要だったとします。Photoshopのpsdファイルはプログラムで言うソースコードにあたり、そこから生成されたjpeg画像はプログラムで言う実行バイナリにあたります。jpeg画像をgitリポジトリに入れることは、実行バイナリをgitリポジトリ管理していることに例えられます。確かに、デプロイの都合や、サードパーティー製ライブラリでソースコードがない場合など、実行バイナリのみをgit管理下にすることはありえます。しかし、一般的に実行バイナリをgit管理下に置くのは不自然だと考えます。

最後に、画像作成者の学習コストが減ります。例えばSubversionを用いた場合、TortoiseSVNなどの枯れたツールが採用できます。gitの概念を教育する必要もありません。Subversionを用いる場合には、ブランチなどは作らず、一本道のコミットを想定します。ブランチ的なことをやりたい場合には、パス・ファイル名を分けて作業します。例えば、特定の期間だけ、ある画像に「NEW」というレイヤーが入った画像を使いたいとします。そのような場合には、image.pngとimage_new.pngのように2ファイルで管理します。これで、参照元のgitのどのブランチからも、svn HEADを見ればよい状態を担保します。

画像作成者にgitを教えて使いこなせるようになったとしましょう。以下のようなメリット・デメリットが考えられます。

メリット

  • 全てのリソースを単一のgitリポジトリとして管理可能

デメリット

  • .gitディレクトリが膨らむ
  • 教育コストが甚大

僕はデメリットのほうが大きいと判断しています。

git-svnを用いてsubmoduleとしてSubversionを使う、というアイデアはありますが、調査できていません。

pull requestのやり方をどうするか

githubには、pull requestという機能があります。github上にpushされた変更点を、特定のリポジトリの特定のブランチに取り込むように依頼する機能です。

pull requestは、基となるリポジトリの位置によって2種類に分類されます。

  1.  github上でforkを行い、forkされたリポジトリからpull requestする方法
  2. forkを行う代わりにブランチを作成し、同一リポジトリ内別ブランチからpull requestする方法

1.については、fork元のリポジトリにpush権限がない場合に行います。fork元のリポジトリにpush権限を持つ場合は、1.でも2.でもかまいません。github上のforkは重いので、2.で十分。本稿では、今後2.を前提とします。

pull requestは基本1つのコミットから成るようにコミットを修正します。具体的には、git rebase -i developで1つを除いてpickをsquashに変えたりして、コミットメッセージを全体のコミットメッセージに変えます。

なぜ、pull requestを1つのコミットにまとめるか。

レビューをしやすくなるから

以下の2コミットがpull requestされたとします。

commit 2
+my $arg = 'test';
-use Data::Dumper;
-print Dumper($kikisan_peropero);
-$kikisan_peropero->func('test');
+$kikisan_peropero->func($arg);

commit 1
+use Data::Dumper;
+print Dumper($kikisan_peropero);
+$kikisan_peropero->func('test');

意図が掴みづらいですね。これは1例なので、まだ単純。しかし、このようなコミットが重なると、レビューが大変しづらくなります。ローカルのみに存在するブランチでは、このような履歴になることも厭わずどんどんコミットすべきです。しかし、それを人に見せるときには、整形したほうがよいです。

それぞれでは独立性が高いが、一体として取り込んで欲しい変更があったとします。そのような場合でも、1つのpull requestが複数のコミットで成り立つのではなく、複数のpull requestそれぞれが1つのコミットで成り立つように心がけます。なぜか。「独立性が高いが、一体として取り込むべきかどうか」ということも、pull requestを取り込む側が判断できるようにするためです。

もちろん、相互依存性が高い一連の変更だが、いくつかのまとまりに別れるようなものは、1つのpull requestに複数のコミットが含まれていてもかまいません。しかし、ルールを決めるという観点では、1pull request 1 commit というわかりやすいものに揃えたほうが運用しやすいです。全員がレビュワー・レビュイーとして成熟すれば、あとは「よかれ」に緩和してもよいところですね。

自らの作業の振り返りになるから

自らが行う振り返りレビューの際にも有用です。デバッグ用コードが残っていないか。コミットし忘れているファイルはないか。

ブランチのログが綺麗に保たれるから

綺麗というのは、美的な問題にとどまりません。例えば、リリースブランチにおいて、開発用ブランチにある変更のいくつかをcherry-pickしたいとします。

commit 4
Add files forgotten to commit n n

commit 3
This commit is not related and contains bugs

commit 2
oops, typo

commit 1
Add new big great feature

今回、commit 1, commit 3, commit 4を取り込みたいとします。コミット順が若い順にcherry-pickするのは面倒です。cherry-pickは複数のコミットを扱うことができますが、間にちょこちょこ取り込みたくないコミットがある場合にはちょっと面倒です。1 pull request 1 commitが徹底されていれば、機能単位でcherry-pickをすることができます。無論、機能数分だけcherry-pickをする必要はあります。しかし、総コミット数が少なく、それぞれが独立しているという場合には、cherry-pick総数を減らすことができるし、レンジでの指定も出来る可能性が高まります。

以下のようなコマンドで、派生元ブランチからの差分が1コミットであることを確認します。手癖でドット3つとかよく使ってます。

git log (派生元ブランチ)…(派生先ブランチ)

なお、コミットをまとめる前に、各種lintでソースコードの見た目を整えておくことも重要です。また、githubサイト上でdiffを見たりする際の利便性のためにも、タブキャラクターは使わないほうがよいです。

pull requestの後処理

取り込んだブランチは、まめに削除します。 git push origin :feature/sugoi

他の人が消したブランチがローカルではまだ表示される場合は git branch -rd origin/feature/sugoi や git remote prune origin すればよいです。

リモートで消えているブランチがgit branch -a/-rで表示されなくなる。

しないこと

公開された歴史の破壊

自らの手元にしかないブランチのコミットは、黒歴史として葬ることができます。 しかし、いったん外に出した歴史は、破壊すべきでないです。

具体的には、git push -fをしないように。必然的に、pushしたブランチに関して、git rebaseしてはいけません。

git rebaseは、1からコミット群を繋ぎ直す作業です。よって、リモートブランチにすでにpush済みであれば、git push -fが必要となります。いったん出したpull requestは、そのブランチに新たなコミットを重ねてpush、再度pull requestを行います。もしくは、pull requestを取り下げ、新たなpull requestを作成します。その際、ブランチは新しくする必要があります。

過度なコミットの綺麗さの追求

バージョン管理ツールは、人間のミスをリカバーすることも目的の一つです。過度なコミットの綺麗さの追求はしません。最終成果物の品質を上げることに注力しましょう。