Hatena::ブログ(Diary)

テクノロジーと広義のデザイン! このページをアンテナに追加 RSSフィード Twitter

2011-11-12 僕たちが行き着いた、シンプルで簡潔なGitの運用方法

僕たちが行き着いた、シンプルで簡潔なGitの運用方法 12:22

f:id:akihito_s:20111112123213g:image:w640

Gitを使い始めて1年以上たちます。最近、Gitを使ったプロジェクト運用方法が自分なりに固まってきたので公開します。

かなりシンプルなので、ある程度Gitに慣れていれば十分に運用可能だと思います。

僕たちが理想とするヒストリー

Gitを使った開発の成果物、それは開発のヒストリー(履歴)そのものです。

このヒストリーが論理的に正しく、かつ、簡潔で理解しやすいものを目指すというのが大方針となります。

その方針のなか、現時点で僕たちが理想だと考えているのが、以下の図のような履歴がリモートブランチに残ることです。

f:id:akihito_s:20111112104845j:image

このヒストリーには2タイプのブランチが存在します。

  • リリースブランチ … 次期リリースバージョンに向けて進んで行くブランチ。チケット1枚について1つのコミットが良い。
  • フィーチャーブランチ … チケット1枚について1つ切られるブランチ。機能を実装する際に小刻みにコミットしたログが残っているため、後から実装履歴を見返すことができる。

上の写真の例だとこうなります。

  • master, v0.1_devがリリースブランチ
  • origin/feature1, origin/feature2と書かれているブランチがフィーチャブランチ

勘違いしやすいのですが、リリースブランチの先頭は、リリースできる状態ではありません。どの状態をリリースしたの(またはすべきなの)?は、以下で述べるようにタグで管理します。

リリースのスナップショットにはタグを用いる

リリースのスナップショットにはタグを用います。

したがって、「いつでもリリースできる安全な状態」はタグで管理することになります。

上の写真の例だと、v0.1, v0.1.1, v0.1.2, v0.2とタグを打っています。

リリース用のタグは、一度打ったら絶対に変えてはいけません。これらがリリースのスナップショットとなるからです。

ブランチはコミットされることで日々進んでいく動的なものである一方、タグは一度打たれたら変わらない静的なものです。

開発者が意識すべき3か条

私はGitが大好きです。なぜならば、デベロッパーにあらゆる自由を与えてくれるからです。その自由は、開発する場所の自由、開発スタイルの自由です。あなたはどこでもGitを使えます。インターネットがつながらない太平洋上空でいくらでもコミットできます。また、あなたのローカルレポジトリ内であれば、どんな開発スタイルをとってもかまいません。あなたさえ快適であれば、ブランチを1万個つくって開発してもかまいませんし、1分に1回のペースでコミットしてもいいのです。

とはいえ、自由には責任がともないます。確かに自分の部屋(ローカルレポジトリ)では何をしてもかまいません。一方、公衆の面前(リモートレポジトリ)においては、守るべきルールが存在します。

Gitを使った開発では、開発者は以下の3か条を守らねばなりません。

その1. フィーチャブランチは、常にリリース用ブランチの先頭から始まっているべきである

その2. リリース用ブランチへフィーチャーブランチをpushするのは、フィーチャーの開発が完全に終わってからにすべきである

その3. ローカルのログは小刻みに、リモートのログは機能単位にすべきである

f:id:akihito_s:20111112104852j:image

これらによってコンフリクトを避け、履歴を必要十分な簡潔さに保ち、バージョンコントロールを用いた開発のパワーをいついかなる場所においても開発者に提供することができます。(理由は後日追記するかもしれません)

特にその1は重要です。これによりコンフリクトという危険な状態を最小限にしてくれるからです。また、履歴を理想的な簡潔さにしてくれるというオマケまでついてきます。

具体的にはrebaseコマンドを使うことでこれを実現できます。

さて、長い前置きはここまでです。ここからは実際の開発フローを見てみることにしましょう!

開発スタート!

今日は月曜日。先週末にv0.1をリリースした興奮さめやらぬなか、あなたはマネージャーから早くも次期バージョンの新機能の開発を依頼されました。開発厨のあなたは、早速フィーチャーブランチを作って開発を始めます。

$ git branch feature1 origin/master

$ git push origin feature1

ここまでわずか1秒足らず。これだけで実際にコーディングを始めてしまってよいのです。

f:id:akihito_s:20111112104901j:image

ローカルでは小刻みにコミットすべし!

上で書いた3か条の「その3」を思い出してください。

そう、ローカルでは小刻みにコミットすべきです。

これにより、バージョン管理システムを使った開発の恩恵を十分に得られることになります。例えば、突然プログラムが動かなくなり、原因がわからないときは、ひとつ前のコミットと現在のコードを比較すればいいのです。

あなたは、30分〜1時間に1回程度のペースでコミットをしていきます。

$ git add <filiename>

$ git commit -m "誤字脱字: ワッフル→アップル"

$ git add <filiename>

$ git commit -m "<コメント>"

(以下、たくさんコミット)

f:id:akihito_s:20111112104931j:image

機能の実装は常にリリース用ブランチの先頭から始まっているべし!

あなたの開発はご機嫌に進み、ランチまでに10のコミットを行いました。

ここであなたは早くも他の開発者とのコンフリクトが心配になってきました。

まずはgit fetchして、リモートレポジトリの状態を取ってきましょう。

$ git fetch

ローカルのコードが書き換えられないか心配ですか?大丈夫です。fetchコマンドは、あなたの現在のローカルにあるソースコードを変更することは絶対にありません。

fetchしたら、実際に履歴を確認しましょう。

$ git log --graph --date-order -C -M --pretty=format:\"<%h> %ad [%an] %Cgreen%d%Creset %s\" --all --date=short

すると、あろうことかすでに3つのコミットがリリース用ブランチにされています!

つまり、「機能の実装は常にリリース用ブランチの先頭から始まっているべし」という規則に反している状態です。

この状態は、コンフリクトが起こりうるという意味で、とても居心地の悪い状態でもあります。

f:id:akihito_s:20111112104930j:image

このとき、あなたが取るべき方策は極めて簡単です。rebaseコマンドを使えばいいのです。

$ git rebase origin/master

rebaseはre-base、つまり、再びベースをつくりなおすということです。山を登るためのベースキャンプを頂上に近い所に変更するというイメージです。

このリベースコマンドにより、あなたのfeature1ブランチは、リリース用ブランチの先端から伸びている状態に復帰することができました。

f:id:akihito_s:20111112104928j:image

私たちが提案するGitの運用フローでコンフリクトが起こりうるのはこのrabaseの時です。したがって、rebaseを頻繁に行うことで、コンフリクトが起きても解決が容易になります。最低でも1日に1回はrebaseを試みるべきでしょう。

機能が完成!!

2週間、この作業を繰り返し、ついにあなたはfeature1の実装を完了させました。いよいよリリース用ブランチに取り込むときです。

逆に、この瞬間までは「絶対に」リリース用ブランチに取り込んではいけません!!リリース用ブランチのコミットは、機能単位のコミットであるべきだからです。

さて、取り込むために、まず最初にこのfeature1の開発における最後のリベースを行います。

$ git rebase origin/master

これで、origin/masterブランチとfeature1ブランチがコンフリクトすることは絶対に無くなりました。

f:id:akihito_s:20111112104927j:image

では、以下のコマンドでorigin/masterブランチにfeature1を取り込みましょう!

$ git checkout master

$ git pull origin/master # => ローカルのmasterブランチとリモートのmasterブランチを同期

$ git merge --squash feature1 # => feature1をローカルのマスターブランチにマージ!

$ git commit -m "#<ticket number> feature1"

ここでポイントになるのは mergeコマンドの--squashオプションです。

このオプションはマージの際にコミットを1つにまとめてくれる魔法のオプションです。これにより、あなたが2週間で作成した機能1が、たった1つのコミットとしてマスターブランチに取り込まれるのです。これで、リリース用ブランチのコミット1つは機能(またはバグフィックス)1つに対応している、という原則を守ることができます。

これで、ローカルのmasterブランチに機能1を正しく取り込めたので、最後にリモートのmasterブランチにpush してあげましょう!

f:id:akihito_s:20111112104926j:image

これで機能1の開発は完了です!!!

まとめ

この記事では、機能追加を例に私たちが採用しているGit運用フローを紹介しました。hotfixも機能追加と基本的に同じフローで、違いは、ブランチを切る場所と、リリース用ブランチにマージした後にただちにタグを打ってリリースするということです。

今回は主にプロジェクトとしての運用(≒リモートレポジトリの運用)について考えたため、「リベースは必ず頻繁に!』といった「規則」になりますが、ローカルで開発者がどうGitを使うか、というのことにはもはや規則は存在しません。あるのは「便利なTips」だけです。ネットの記事や知り合いの開発者からTipsを教えてもらいながら、ローカルの開発フローは「オレルール」で進めてくださいね。

Gitは本当に素敵ですね。ではでは。