torutkのブログ

ソフトウェア・エンジニアのブログ

ソースコード管理ツールをSubversionからGitへ変更して感じたこと

少人数チームでのソフトウェア開発でソースコードを管理するリポジトリにGitを適用して1,2ヶ月ほど経過しました。Gitを開発に使用するのは今回が始めてで、みなSubversionを使っていたメンバーです。

開発環境

OS Linux、たまにWindows
開発言語 Java
プログラミングツール NetBeans 7.4
Gitクライアント NetBeans標準搭載のGit機能、たまにコマンドラインWindowsではたまにTortoiseGit
Gitサーバー apacheでgit-http-backend、Redmineと認証統合

現在の使用状況

Gitの共有リポジトリを、開発サーバー上にapache(HTTP)でホストしています。

共有リポジトリはmasterブランチ一本で、各メンバーはローカルにcloneしたあとローカルのmasterで変更作業を実施し、適宜共有リポジトリのmasterへpushしています。

使い方をよく見ると、Subversionと同様コミットするとすぐpushしています。Gitをつかっているのですが使い方の概念はSubversionのままなので、Gitが「手間が増えたSubversion」と思われています。各メンバーが頻繁に共有リポジトリへpushしているので、大抵pushが成功せず、origin/masterからローカルのmasterへmergeをしてpushする操作となっています。
コンフリクトもよく発生して、コンフリクト解決が面倒!という状況です。

つまるところ、ツールは新しく(Git)したものの、リポジトリの運用が旧態依然(Subversion、集中リポジトリ)のままなので、ミスマッチがあるんだろうなと感じました。

そこで、Git(分散リポジトリ?)的な運用を確立したく、いろいろ調べることとしました。今回Gitを導入するにあたり、Gitの機能やコマンドの解説はいろいろ見ましたが、運用ノウハウはあまり検討していないままでした。

分散リポジトリは、開発者が便利とは思わないかも

Gitの最大の特徴は「分散リポジトリ」と思っていました。SubversionをGitに置き換えようと思ったいきさつも、現在開発場所と試験場所とがネットワークで結ばれていないとか、開発チーム(組織)が複数あってリポジトリを共有していないといった課題を解決するためです。

ところが、構成管理者にとっては分散リポジトリ方式はメリットがありますが、同じ共有リポジトリを常に利用できる環境でチーム開発をしている開発者にとっては一段手間が増えるだけでメリットを感じることが少ないです。

「構成管理上のメリットがあるので我慢して使おう」では、手間が増えた代償にはなりません。また、GitはSubversionより手間がかかるだけのツール、ではないはずです。

Gitの真髄はブランチにあり?

Gitの運用をいろいろ調べていくと、特にSubversionとの違いについて、集中か分散かではなく、ブランチおよびマージ機能に優れていることが実は重要なのだと思いました。

  • Subversionのブランチは、ディレクトリをコピーしているだけ(典型的にはtrunkの下をbranchesの下にブランチ名ディレクトリを作ってその下にコピー)
  • Subversionのマージは、ブランチが一本化するわけではなく、そのディレクトリ以下の修正を別のディレクトリに反映するだけ
    • 問題になるケース1)ブランチAとブランチBがあり、ブランチAでファイルaaaを追加、その後ブランチAからブランチBにマージし、しばらく後にブランチBからブランチAにマージすると、ファイルaaaに手を入れていなくてもコンフリクトが生じるそうです。また、ファイル追加のコンフリクト時はテキストをマージしてくれないようです。
    • 問題になるケース2)マージしてもマージ元の履歴をたどれない(どのブランチからマージしたかをコミットログに記録しておいて、後日必要があれば手動で辿る)

Gitは、共有リポジトリからローカルにクローンしたローカルリポジトリでいろいろ作業するので、気に入らなければ共有リポジトリに上げないということが簡単にできます。一方、Subversionは、個人的なブランチを切るにしても集中リポジトリ上に作成するので、開発者が多くなればそれだけリポジトリ上に個人ブランチが生成されます。

ブランチ運用の参考

ブランチの運用についてGitで一番参照されているのが、"A successful Git branching model"のようです。日本語訳が次のURLにありました(翻訳公開感謝です)。
http://keijinsonyaban.blogspot.jp/2010/10/successful-git-branching-model.html

このモデルは、共有リポジトリにmasterとdevelopmentの2つのブランチを設けます。これらをメインブランチと呼んでいます。
用途別にいくつかのサポート用ブランチを設けます。フィーチャーブランチ、リリースブランチ、Hotfixブランチなどです。
masterは、製品として出荷可能な状態を常に反映した(保守的な)ブランチです。
developは、次のリリースのための最新の開発作業を反映下ブランチです。developブランチに最新機能をpushし、安定させたらmasterへマージします。
フィーチャーブランチ(トピックブランチ)は、developからブランチし、通常開発者ローカルに作ってdevelopへマージします。
リリースブランチは、developからブランチし、リリース準備を行い結果をdevelopとmasterへマージします。
ホットフィックスブランチは、masterからブランチし、製品バージョンへの緊急的なバグを解決しdevelopとmasterにマージします。

使いかたのイメージ

この"A successful Git branching model"をベースにして、チケット駆動開発では次のような手順を考えました。

通常の機能開発
  • 機能開発のチケット(#1231)を作成し、着手する
  • 共有リポジトリから自分の開発マシンにクローンしてローカルリポジトリを作成する
    • 既に自分の開発マシンに共有リポジトリのクローンがあれば、fetchして最新にする
  • developブランチからフィーチャーブランチ(名前は"feature/#1231")を作成する
    • ブランチの命名規約を、ブランチ種類 + '/' + #チケット番号 とした
  • フィーチャーブランチ(feature/#1231)で変更を実施、適宜ローカルでコミットする
  • 機能開発が完了したら、共有リポジトリとローカルリポジトリを整合させる(fetch)
  • ローカルリポジトリのdevelopへフィーチャーブランチ(feature/#1231)をマージする
    • Fast-forwardマージにはしない(--no-ffを指定)
    • コンフリクトが出たら編集・解決する
  • ローカルのdevelopを共有リポジトリのdevelopへpushする
  • フィーチャーブランチ(feature/#1231)を削除する(残しておいてもいいですが・・・)
機能開発中にチームメートの成果を取り込み

上述のフィーチャーブランチ(feature/#1231)で変更実施中

  • 共有リポジトリからfetchする
  • ローカルのdevelopブランチをフィーチャーブランチへマージ
    • ここはrebaseの方がよいかも?
バグ修正(機能開発と同じく次のリリースに含めればよい場合)

緊急性はないので、次のリリースに向けた機能開発とほぼ同じワークフローで対処します。
ワークフローはフィーチャーブランチと一緒ですが、バグとして識別するため命名規約を変えています。

  • バグ修正チケット(#1272)を作成し、着手する
  • 共有リポジトリから自分の開発マシンにクローンしてローカルリポジトリを作成する
    • 既に自分の開発マシンに共有リポジトリのクローンがあれば、fetchして最新にする
  • developブランチからバグ修正ブランチ(名前は"bug/#1272")を作成する
    • ブランチの命名規約を、ブランチ種類 + '/' + #チケット番号 とした
  • バグ修正ブランチ(bug/#1272)で変更を実施、適宜ローカルでコミットする
  • バグ修正が完了したら、共有リポジトリとローカルリポジトリを整合させる(fetch)
  • ローカルリポジトリのdevelopへバグ修正ブランチ(bug/#1272)をマージする
    • Fast-forwardマージにはしない(--no-ffを指定)
    • コンフリクトが出たら編集・解決する
  • ローカルのdevelopを共有リポジトリのdevelopへpushする
  • バグ修正ブランチ(feature/#1272)を削除する(残しておいてもいいですが・・・)
バグ修正(緊急修正)

リリースした製品のバグを緊急に修正(バグ修正バージョンをリリース)する場合のワークフローです。

  • 共有リポジトリから自分の開発マシンにクローンしてローカルリポジトリを作成する
    • 既に自分の開発マシンに共有リポジトリのクローンがあれば、fetchして最新にする
  • masterブランチの対象リリースタグからホットフィックスブランチ(名前はhotfix/1.2.1)を作成する
    • 命名規約は、ブランチ種類 + '/' + このホットフィックスのリリース番号
  • ホットフィックスブランチ(名前はhotfix/1.2.1)で変更を実施、適宜ローカルでコミットする
  • バグ修正が完了したら、共有リポジトリとローカルリポジトリを整合させる(fetch)
  • ローカルリポジトリのmasterへホットフィックスブランチ(名前はhotfix/1.2.1)をマージする
    • Fast-forwardマージにはしない(--no-ffを指定)
    • コンフリクトが出たら編集・解決する
  • ローカルリポジトリのmasterでタグをつける(1.2.1)
  • ローカルのmasterを共有リポジトリのmasterへpushする
  • ローカルリポジトリのdevelopへホットフィックスブランチ(名前はhotfix/1.2.1)をマージする
    • Fast-forwardマージにはしない(--no-ffを指定)
    • コンフリクトが出たら編集・解決する
  • ローカルのdevelopを共有リポジトリのdevelopへpushする

SubversionとGitの履歴管理イメージ

Subversionは、リポジトリ全体をコミットごとにスナップショットを取るように見えます。スナップショットに連番を付けて履歴管理します。配列やJavaでいうArrayListな順番に依存する履歴管理ですね。スナップショット的なので、trunkもbranchesもひっくるめています。なので、過去の履歴を変更したり(たとえばコミット順番を入れ替えるとか)、なかったことにするとかはできないと。また、ブランチのマージも、マージ時に指定したレビジョン範囲でそのブランチ(ディレクトリ以下)に入った変更を取り出し対象ブランチに新たにその変更を当てる感じです。

Gitは、変更指示を積み重ねてリポジトリを構築しているように見えます。変更指示は違うリポジトリにも有効なので(分散ゆえに)、連番ではなく限りなく一意なID(ハッシュコード)を変更指示に付け、リンクリスト的に履歴管理します。リンクリスト的なので、履歴のつなぎ変え、削除、挿入が容易です。ブランチのマージは2つのリンクリストが合流するイメージです。

補足(2014-01-25)

上の文章は、SubversionとGitを内部構造を知らずブラックボックス的に使っているときに感じるイメージで、内部構造を言及しているものではありません。