Git
Chapters ▾ 2nd Edition

9.1 Gitとその他のシステムの連携 - Git をクライアントとして使用する

世の中はそんなにうまくいくものではありません。 あなたが関わることになったプロジェクトで使うバージョン管理システムを、すぐさまGitに切り替えられることはほとんどないでしょう。 また、関わっているプロジェクトが他のVCSを使っていて、もしこれがGitだったらなぁと思うことも時々あると思います。 この章の前半では、作業中のプロジェクトが他のバージョン管理システムを使っていた場合に、Git をクライアントとして使用する方法を学びます。

どこかの時点で、既存のプロジェクトを Git へ変換したくなることもあるでしょう。 この章の後半では、いくつかの特定のシステムから Git へ、プロジェクトを移行する方法と、既存のインポート用ツールがない場合に使える手法について説明します。

Git をクライアントとして使用する

Git は開発者に対し、非常に優れたユーザ体験を提供してくれます。このユーザ体験は、多くの人々がこれまでに編み出した、自分の端末上で Git を使用する方法に基づいています。それは、同じチームの他のメンバーがまったく別の VCS を使用している場合でも同様です。 そのような場合には “ブリッジ” と呼ばれるアダプタが利用できます。 ここでは、その中でも遭遇する機会が多いであろうものを取り上げます。

Git と Subversion

オープンソース開発プロジェクトの大多数や、かなりの数の企業内プロジェクトが、ソースコードの管理に Subversion を利用しています。 Subversion は10年以上前から使われてきましたが、その間ほとんどの期間、オープンソースプロジェクトのVCSとしては デファクトスタンダード の地位にありました。 Subversion 以前は CVS がソースコード管理に広く用いられていたのですが、多くの点で両者はよく似ています。

Git の素晴しい機能のひとつに、Git と Subversion を双方向にブリッジする git svn があります。このツールを使うと、Subversion のクライアントとして Git を使うことができます。つまり、ローカルの作業では Git の機能を十分に活用することができて、あたかも Subversion を使っているかのように Subversion サーバーに変更をコミットすることができます。共同作業をしている人達が古き良き方法を使っているのと 同時に、ローカルでのブランチ作成やマージ、ステージング・エリア、リベース、チェリーピックなどの Git の機能を使うことができるということです。共同の作業環境に Git を忍び込ませておいて、仲間の開発者たちが Git より効率良く作業できるように手助けをしつつ、Git の全面的な採用のための根回しをしてゆく、というのが賢いやり方です。Subversion ブリッジは、分散VCS の素晴しい世界へのゲートウェイ・ドラッグといえるでしょう。

git svn

Git と Subversion の橋渡しをするコマンド群のベースとなるコマンドが git svn です。 この後に続くコマンドはかなりたくさんあるので、シンプルなワークフローを通してもっともよく使われるものから見ていきます。

注意すべきことは、git svn を使っているときは Subversion を相手にしているのだということです。これは、Git とはまったく異なる動きをします。 ローカルでのブランチ作成やマージは できることはできます が、作業内容をリベースするなどして歴史をできるだけ一直線に保つようにし、Git リモートリポジトリを相手にするときのように考えるのは避けましょう。

歴史を書き換えてもう一度プッシュしようなどとしてはいけません。また、他の開発者との共同作業のために複数の Git リポジトリに並行してプッシュするのもいけません。Subversion が扱えるのは一本の直線上の歴史だけで、ちょっとしたことですぐに混乱してしまいます。チームのメンバーの中に SVN を使う人と Git を使う人がいる場合は、全員が SVN サーバーを使って共同作業するようにしましょう。そうすれば、少しは生きやすくなります。

セットアップ

この機能を説明するには、書き込みアクセス権を持つ標準的な SVN リポジトリが必要です。 もしこのサンプルをコピーして試したいのなら、私のテスト用リポジトリの書き込み可能なコピーを作らなければなりません。 これを簡単に行うには、Subversion に付属の svnsync というツールを使います。 テスト用として、新しい Subversion リポジトリを Google Code 上に作りました。これは protobuf プロジェクトの一部で、protobuf は構造化されたデータを符号化してネットワーク上で転送するためのツールです。

まずはじめに、新しいローカル Subversion リポジトリを作ります。

$ mkdir /tmp/test-svn
$ svnadmin create /tmp/test-svn

そして、すべてのユーザーが revprop を変更できるようにします。簡単な方法は、常に 0 で終了する pre-revprop-change スクリプトを追加することです。

$ cat /tmp/test-svn/hooks/pre-revprop-change
#!/bin/sh
exit 0;
$ chmod +x /tmp/test-svn/hooks/pre-revprop-change

これで、ローカルマシンにこのプロジェクトを同期できるようになりました。同期元と同期先のリポジトリを指定して svnsync init を実行します。

$ svnsync init file:///tmp/test-svn \
  http://progit-example.googlecode.com/svn/

このコマンドは、同期を実行するためのプロパティを設定します。次に、このコマンドでコードをコピーします。

$ svnsync sync file:///tmp/test-svn
Committed revision 1.
Copied properties for revision 1.
Transmitting file data .............................[...]
Committed revision 2.
Copied properties for revision 2.
[…]

この操作は数分で終わりますが、もし元のリポジトリのコピー先がローカルではなく別のリモートリポジトリだった場合、総コミット数がたかだか 100 にも満たなかったとしても、この処理には約一時間かかります。 Subversion では、リビジョンごとにクローンを作ってコピー先のリポジトリに投入していかなければなりません。これはばかばかしいほど非効率的ですが、簡単に済ませるにはこの方法しかないのです。

使いはじめる

書き込み可能な Subversion リポジトリが手に入ったので、一般的なワークフローに沿って進めましょう。まずは git svn clone コマンドを実行します。このコマンドは、Subversion リポジトリ全体をローカルの Git リポジトリにインポートします。どこかにホストされている実際の Subversion リポジトリから取り込む場合は file:///tmp/test-svn の部分を Subversion リポジトリの URL に変更しましょう。

$ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags
Initialized empty Git repository in /private/tmp/progit/test-svn/.git/
r1 = dcbfb5891860124cc2e8cc616cded42624897125 (refs/remotes/origin/trunk)
    A	m4/acx_pthread.m4
    A	m4/stl_hash.m4
    A	java/src/test/java/com/google/protobuf/UnknownFieldSetTest.java
    A	java/src/test/java/com/google/protobuf/WireFormatTest.java
…
r75 = 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae (refs/remotes/origin/trunk)
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/my-calc-branch, 75
Found branch parent: (refs/remotes/origin/my-calc-branch) 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae
Following parent with do_switch
Successfully followed parent
r76 = 0fb585761df569eaecd8146c71e58d70147460a2 (refs/remotes/origin/my-calc-branch)
Checked out HEAD:
  file:///tmp/test-svn/trunk r75

これは、指定した URL に対して git svn init に続けて git svn fetch を実行するのと同じ意味です。 しばらく時間がかかります。 test プロジェクトには 75 のコミットしかなくてコードベースもそれほど大きくありませんが、Git は各バージョンをそれぞれチェックアウトしては個別にコミットしています。 もし数百数千のコミットがあるプロジェクトで試すと、終わるまでには数時間から下手をすると数日かかってしまうかもしれません。

-T trunk -b branches -t tags の部分は、この Subversion リポジトリが標準的なブランチとタグの規約に従っていることを表しています。trunk、branches、tags にもし別の名前をつけているのなら、この部分を変更します。この規約は一般に使われているものなので、単に -s とだけ指定することもできます。これは、先の 3 つのオプションを指定したのと同じ標準のレイアウトを表します。つまり、次のようにしても同じ意味になるということです。

$ git svn clone file:///tmp/test-svn -s

これで、ブランチやタグも取り込んだ Git リポジトリができあがりました。

$ git branch -a
* master
  remotes/origin/my-calc-branch
  remotes/origin/tags/2.0.2
  remotes/origin/tags/release-2.0.1
  remotes/origin/tags/release-2.0.2
  remotes/origin/tags/release-2.0.2rc1
  remotes/origin/trunk

このツールが Subversion のタグをリモート参照としてどのように管理しているかに注目してください。

Git の配管コマンド show-ref について、もう少し詳しく見ていきましょう。

$ git show-ref
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/heads/master
0fb585761df569eaecd8146c71e58d70147460a2 refs/remotes/origin/my-calc-branch
bfd2d79303166789fc73af4046651a4b35c12f0b refs/remotes/origin/tags/2.0.2
285c2b2e36e467dd4d91c8e3c0c0e1750b3fe8ca refs/remotes/origin/tags/release-2.0.1
cbda99cb45d9abcb9793db1d4f70ae562a969f1e refs/remotes/origin/tags/release-2.0.2
a9f074aa89e826d6f9d30808ce5ae3ffe711feda refs/remotes/origin/tags/release-2.0.2rc1
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/remotes/origin/trunk

Git が Git サーバからクローンを行う場合はこうはなりません。タグつきのリポジトリに対してクローンを行った直後は、このようになっています。

$ git show-ref
c3dcbe8488c6240392e8a5d7553bbffcb0f94ef0 refs/remotes/origin/master
32ef1d1c7cc8c603ab78416262cc421b80a8c2df refs/remotes/origin/branch-1
75f703a3580a9b81ead89fe1138e6da858c5ba18 refs/remotes/origin/branch-2
23f8588dde934e8f33c263c6d8359b2ae095f863 refs/tags/v0.1.0
7064938bd5e7ef47bfd79a685a62c1e2649e2ce7 refs/tags/v0.2.0
6dcb09b5b57875f334f61aebed695e2e4193db5e refs/tags/v1.0.0

Git は tags ディレクトリの内容をリモートブランチとして扱うのではなく、直接 refs/tags に格納しています。

Subversion へのコミットの書き戻し

作業リポジトリを手に入れたあなたはプロジェクト上で何らかの作業を終え、コミットを上流に書き戻すことになりました。Git を SVN クライアントとして使います。どれかひとつのファイルを変更してコミットした時点では、Git上でローカルに存在するそのコミットはSubversionサーバー上には存在しません。

$ git commit -am 'Adding git-svn instructions to the README'
[master 4af61fd] Adding git-svn instructions to the README
 1 file changed, 5 insertions(+)

次に、これをプッシュして上流を変更しなければなりません。この変更が Subversion に対してどのように作用するのかに注意しましょう。オフラインで行った複数のコミットを、すべて一度に Subversion サーバーにプッシュすることができます。Subversion サーバーにプッシュするには git svn dcommit コマンドを使います。

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r77
    M	README.txt
r77 = 95e0222ba6399739834380eb10afcd73e0670bc5 (refs/remotes/origin/trunk)
No changes between 4af61fd05045e07598c553167e0f31c84fd6ffe1 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

このコマンドは、Subversionサーバーからのコード上で行われたすべてのコミットに対して個別に Subversion 上にコミットし、ローカルの Git のコミットを書き換えて一意な識別子を含むようにします。ここで重要なのは、書き換えによってすべてのローカルコミットの SHA-1 チェックサムが変化するということです。この理由もあって、Git ベースのリモートリポジトリにあるプロジェクトと Subversion サーバーを同時に使うことはおすすめできません。直近のコミットを調べれば、新たに git-svn-id が追記されたことがわかります。

$ git log -1
commit 95e0222ba6399739834380eb10afcd73e0670bc5
Author: ben <ben@0b684db3-b064-4277-89d1-21af03df0a68>
Date:   Thu Jul 24 03:08:36 2014 +0000

    Adding git-svn instructions to the README

    git-svn-id: file:///tmp/test-svn/trunk@77 0b684db3-b064-4277-89d1-21af03df0a68

元のコミットの SHA-1 チェックサムが 4af61fd で始まっていたのに対して今は 95e0222 に変わっていることに注目しましょう。Git と Subversion の両方のサーバーにプッシュしたい場合は、まず Subversion サーバーにプッシュ (dcommit) してから Git のほうにプッシュしなければなりません。dcommit でコミットデータが書き換わるからです。

新しい変更の取り込み

複数の開発者と作業をしていると、遅かれ早かれ、誰かがプッシュしたあとに他の人がプッシュしようとして衝突を起こすということが発生します。他の人の作業をマージするまで、その変更は却下されます。git svn では、このようになります。

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: d5837c4b461b7c0e018b49d12398769d2bfc240a and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 f414c433af0fd6734428cf9d2a9fd8ba00ada145 c80b6127dd04f5fcda218730ddf3a2da4eb39138 M	README.txt
Current branch master is up to date.
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

この状態を解決するには git svn rebase を実行します。これは、サーバー上の変更のうちまだ取り込んでいない変更をすべて取り込んでから、自分の作業をリベースします。

$ git svn rebase
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: eaa029d99f87c5c822c5c29039d19111ff32ef46 and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 65536c6e30d263495c17d781962cfff12422693a b34372b25ccf4945fe5658fa381b075045e7702a M	README.txt
First, rewinding head to replay your work on top of it...
Applying: update foo
Using index info to reconstruct a base tree...
M	README.txt
Falling back to patching base and 3-way merge...
Auto-merging README.txt
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

これで手元の作業が Subversion サーバー上の最新状態の上でなされたことになったので、無事に dcommit することができます。

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r85
    M	README.txt
r85 = 9c29704cc0bbbed7bd58160cfb66cb9191835cd8 (refs/remotes/origin/trunk)
No changes between 5762f56732a958d6cfda681b661d2a239cc53ef5 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

ここで注意すべき点は、Git の場合は上流での変更をすべてマージしてからでなければプッシュできないけれど、git svn の場合は衝突さえしなければマージしなくてもプッシュできる(Subversion の挙動と同じように)ということです。 だれかがあるファイルを変更した後で自分が別のファイルを変更してプッシュしても、dcommit は正しく動作します。

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	configure.ac
Committed r87
    M	autogen.sh
r86 = d8450bab8a77228a644b7dc0e95977ffc61adff7 (refs/remotes/origin/trunk)
    M	configure.ac
r87 = f3653ea40cb4e26b6281cec102e35dcba1fe17c4 (refs/remotes/origin/trunk)
W: a0253d06732169107aa020390d9fefd2b1d92806 and refs/remotes/origin/trunk differ, using rebase:
:100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 e757b59a9439312d80d5d43bb65d4a7d0389ed6d M	autogen.sh
First, rewinding head to replay your work on top of it...

これは忘れずに覚えておきましょう。というのも、プッシュした後の結果はどの開発者の作業環境にも存在しない状態になっているからです。たまたま衝突しなかっただけで互換性のない変更をプッシュしてしまったときに、その問題を見つけるのが難しくなります。これが、Git サーバーを使う場合と異なる点です。Git の場合はクライアントの状態をチェックしてからでないと変更を公開できませんが、SVN の場合はコミットの直前とコミット後の状態が同等であるかどうかすら確かめられないのです。

もし自分のコミット準備がまだできていなくても、Subversion から変更を取り込むときにもこのコマンドを使わなければなりません。git svn fetch でも新しいデータを取得することはできますが、git svn rebase はデータを取得するだけでなくローカルのコミットの更新も行います。

$ git svn rebase
    M	autogen.sh
r88 = c9c5f83c64bd755368784b444bc7a0216cc1e17b (refs/remotes/origin/trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/origin/trunk.

git svn rebase をときどき実行しておけば、手元のコードを常に最新の状態に保っておけます。しかし、このコマンドを実行するときには作業ディレクトリがクリーンな状態であることを確認しておく必要があります。手元で変更をしている場合は、stash で作業を退避させるか一時的にコミットしてからでないと git svn rebase を実行してはいけません。さもないと、もしリベースの結果としてマージが衝突すればコマンドの実行が止まってしまいます。

Git でのブランチに関する問題

Git のワークフローに慣れてくると、トピックブランチを作ってそこで作業を行い、それをマージすることもあるでしょう。git svn を使って Subversion サーバーにプッシュする場合は、それらのブランチをまとめてプッシュするのではなく一つのブランチ上にリベースしてからプッシュしたくなるかもしれません。リベースしたほうがよい理由は、Subversion はリニアに歴史を管理していて Git のようなマージができないからです。git svn がスナップショットを Subversion のコミットに変換するときには、最初の親だけに続けます。

歴史が次のような状態になっているものとしましょう。experiment ブランチを作ってそこで 2 回のコミットを済ませ、それを master にマージしたところです。ここで dcommit すると、出力はこのようになります。

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	CHANGES.txt
Committed r89
    M	CHANGES.txt
r89 = 89d492c884ea7c834353563d5d913c6adf933981 (refs/remotes/origin/trunk)
    M	COPYING.txt
    M	INSTALL.txt
Committed r90
    M	INSTALL.txt
    M	COPYING.txt
r90 = cb522197870e61467473391799148f6721bcf9a0 (refs/remotes/origin/trunk)
No changes between 71af502c214ba13123992338569f4669877f55fd and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

歴史をマージしたブランチで dcommit を実行してもうまく動作します。ただし、Git プロジェクト上での歴史を見ると、experiment ブランチ上でのコミットは書き換えられていません。そこでのすべての変更は、SVN 上での単一のマージコミットとなっています。

他の人がその作業をクローンしたときには、 git merge --squash を実行したときのように、すべての作業をひとまとめにしたマージコミットしか見ることができません。そのコミットがどこから来たのか、そしていつコミットされたのかを知ることができないのです。

Subversion のブランチ

Subversion のブランチは Git のブランチとは異なります。可能ならば、Subversion のブランチは使わないようにするのがベストでしょう。 しかし、Subversion のブランチの作成やコミットも、git svn を使ってすることができます。

新しい SVN ブランチの作成

Subversion に新たなブランチを作るには git svn branch [ブランチ名] を実行します。

$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r90 to file:///tmp/test-svn/branches/opera...
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/opera, 90
Found branch parent: (refs/remotes/origin/opera) cb522197870e61467473391799148f6721bcf9a0
Following parent with do_switch
Successfully followed parent
r91 = f1b64a3855d3c8dd84ee0ef10fa89d27f1584302 (refs/remotes/origin/opera)

これは Subversion の svn copy trunk branches/opera コマンドと同じ意味で、Subversion サーバー上で実行されます。ここで注意すべき点は、このコマンドを実行しても新しいブランチに入ったことにはならないということです。この後コミットをすると、そのコミットはサーバーの trunk に対して行われます。opera ではありません。

アクティブなブランチの切り替え

Git が dcommit の行き先のブランチを決めるときには、あなたの手元の歴史上にある Subversion ブランチのいずれかのヒントを使います。手元にはひとつしかないはずで、それは現在のブランチの歴史上の直近のコミットにある git-svn-id です。

複数のブランチを同時に操作するときは、ローカルブランチを dcommit でその Subversion ブランチにコミットするのかを設定することができます。そのためには、Subversion のブランチをインポートしてローカルブランチを作ります。opera ブランチを個別に操作したい場合は、このようなコマンドを実行します。

$ git branch opera remotes/origin/opera

これで、opera ブランチを trunk (手元の master ブランチ) にマージするときに通常の git merge が使えるようになりました。しかし、そのときには適切なコミットメッセージを (-m で) 指定しなければなりません。さもないと、有用な情報ではなく単なる "Merge branch opera" というメッセージになってしまいます。

git merge を使ってこの操作を行ったとしても、そしてそれが Subversion でのマージよりもずっと簡単だったとしても (Git は自動的に適切なマージベースを検出してくれるからね)、これは通常の Git のマージコミットとは違うということを覚えておきましょう。このデータを Subversion に書き戻すことになりますが Subversion では複数の親を持つコミットは処理できません。そのため、プッシュした後は、別のブランチ上で行ったすべての操作をひとまとめにした単一のコミットに見えてしまいます。あるブランチを別のブランチにマージしたら、元のブランチに戻って作業を続けるのは困難です。Git なら簡単なのですが。dcommit コマンドを実行すると、どのブランチからマージしたのかという情報はすべて消えてしまいます。そのため、それ以降のマージ元の算出は間違ったものとなります。dcommit は、git merge の結果をまるで git merge --squash を実行したのと同じ状態にしてしまうのです。残念ながら、これを回避するよい方法はありません。Subversion 側にこの情報を保持する方法がないからです。Subversion をサーバーに使う以上は、常にこの制約に縛られることになります。問題を回避するには、trunk にマージしたらローカルブランチ (この場合は opera) を削除しなければなりません。

Subversion コマンド

git svn ツールセットには、Git への移行をしやすくするための多くのコマンドが用意されています。Subversion で使い慣れていたのと同等の機能を提供するコマンド群です。その中からいくつかを紹介します。

SVN 形式のログ

Subversion に慣れているので SVN が出力する形式で歴史を見たい、という場合は git svn log を実行しましょう。すると、コミットの歴史が SVN 形式で表示されます。

$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2014-05-02 16:07:37 -0700 (Sat, 02 May 2014) | 2 lines

autogen change

------------------------------------------------------------------------
r86 | schacon | 2014-05-02 16:00:21 -0700 (Sat, 02 May 2014) | 2 lines

Merge branch 'experiment'

------------------------------------------------------------------------
r85 | schacon | 2014-05-02 16:00:09 -0700 (Sat, 02 May 2014) | 2 lines

updated the changelog

git svn log に関して知っておくべき重要なことがふたつあります。まず。このコマンドはオフラインで動作します。実際の svn log コマンドのように Subversion サーバーにデータを問い合わせたりしません。次に、すでに Subversion サーバーにコミット済みのコミットしか表示されません。つまり、ローカルの Git へのコミットのうちまだ dcommit していないものは表示されないし、その間に他の人が Subversion サーバーにコミットした内容も表示されません。最後に Subversion サーバーの状態を調べたときのログが表示されると考えればよいでしょう。

SVN アノテーション

git svn log コマンドが svn log コマンドをオフラインでシミュレートしているのと同様に、svn annotate と同様のことを git svn blame [FILE] で実現できます。出力は、このようになります。

$ git svn blame README.txt
 2   temporal Protocol Buffers - Google's data interchange format
 2   temporal Copyright 2008 Google Inc.
 2   temporal http://code.google.com/apis/protocolbuffers/
 2   temporal
22   temporal C++ Installation - Unix
22   temporal =======================
 2   temporal
79    schacon Committing in git-svn.
78    schacon
 2   temporal To build and install the C++ Protocol Buffer runtime and the Protocol
 2   temporal Buffer compiler (protoc) execute the following:
 2   temporal

先ほどと同様、このコマンドも Git にローカルにコミットした内容や他から Subversion にプッシュされていたコミットは表示できません。

SVN サーバ情報

svn info と同様のサーバー情報を取得するには git svn info を実行します。

$ git svn info
Path: .
URL: https://schacon-test.googlecode.com/svn/trunk
Repository Root: https://schacon-test.googlecode.com/svn
Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029
Revision: 87
Node Kind: directory
Schedule: normal
Last Changed Author: schacon
Last Changed Rev: 87
Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)

blamelog と同様にこれもオフラインで動作し、最後に Subversion サーバーと通信した時点での情報しか表示されません。

Subversion が無視するものを無視する

どこかに svn:ignore プロパティが設定されている Subversion リポジトリをクローンした場合は、対応する .gitignore ファイルを用意したくなることでしょう。コミットすべきではないファイルを誤ってコミットしてしまうことを防ぐためにです。git svn には、この問題に対応するためのコマンドが二つ用意されています。まず最初が git svn create-ignore で、これは、対応する .gitignore ファイルを自動生成して次のコミットに含めます。

もうひとつは git svn show-ignore で、これは .gitignore に書き込む内容を標準出力に送ります。この出力を、プロジェクトの exclude ファイルにリダイレクトしましょう。

$ git svn show-ignore > .git/info/exclude

これで、プロジェクトに .gitignore ファイルを散らかさなくてもよくなります。Subversion 使いのチームの中で Git を使うのが自分だけだという場合、他のメンバーにとっては .gitignore ファイルは目障りでしょう。そのような場合はこの方法が使えます。

Git-Svn のまとめ

git svn ツール群は、Subversion サーバーに行き詰まっている場合や使っている開発環境が Subversion サーバー前提になっている場合などに便利です。Git のできそこないだと感じるかもしれません。また、他のメンバーとの間で混乱が起こるかもしれません。トラブルを避けるために、次のガイドラインに従いましょう。

  • Git の歴史をリニアに保ち続け、git merge によるマージコミットを含めないようにする。本流以外のブランチでの作業を書き戻すときは、マージではなくリベースすること。

  • Git サーバーを別途用意したりしないこと、新しい開発者がクローンするときのスピードをあげるためにサーバーを用意することはあるでしょうが、そこに git-svn-id エントリを持たないコミットをプッシュしてはいけません。pre-receive フックを追加してコミットメッセージをチェックし、git-svn-id がなければプッシュを拒否するようにしてもよいでしょう。

これらのガイドラインを守れば、Subversion サーバーでの作業にも耐えられることでしょう。しかし、もし本物の Git サーバーに移行できるのなら、そうしたほうがチームにとってずっと利益になります。

Git と Mercurial

DVCSの世界にあるのはGitだけではありません。 事実、Git以外にも様々なシステムが存在し、分散バージョン管理を正しく行う方法について、それぞれが独自の見方を持っています。 Gitを除くと、もっとも広く使われているのは Mercurial です。Git と Mercurialは多くの点で似通っています。

良いニュースとして、 Git のクライアントサイドの動作がお好みで、しかし作業中のプロジェクトでは Mercurial でソースを管理しているという場合、 Mercurial でホストされているリポジトリのクライアントに Git を使用するという方法があります。 Git はリモートを通してサーバリポジトリとやりとりしているので、このブリッジがリモートヘルパーとして実装されているのは驚くほどのことでもないと思います。 プロジェクト名は git-remote-hg で、 https://github.com/felipec/git-remote-hg から取得できます。

git-remote-hg

まず、 git-remote-hg をインストールする必要があります。 ここでは基本的に、そのファイルをどこかパスの通った場所に置く必要があります。以下のような感じです。

$ curl -o ~/bin/git-remote-hg \
  https://raw.githubusercontent.com/felipec/git-remote-hg/master/git-remote-hg
$ chmod +x ~/bin/git-remote-hg

…ここでは ~/bin$PATH に含まれていることを仮定しています。 git-remote-hg にはもう一つ依存先があります。 Python の mercurial ライブラリです。 Python をインストール済みなら、これは次のようにシンプルなコマンドで行えます。

$ pip install mercurial

(Python をインストールしていないなら、まず https://www.python.org/ からPython を入手してください。)

最後に必要なのは Mercurial のクライアントです。 インストール済みでないなら、 http://mercurial.selenic.com/ から入手してインストールしてください。

これで準備が整いました。 必要なのはプッシュが可能な Mercurial リポジトリだけです。 幸いなことに、 Mercurial リポジトリならどれでもこの操作が可能です。そのため、 Mercurial の使い方を学ぶときにみんなが使う "hello world" リポジトリを使用することにします。

$ hg clone http://selenic.com/repo/hello /tmp/hello

使いはじめる

これで、都合のいい “サーバサイド” のリポジトリができたので、以降では典型的なワークフローを見ていきます。 これから見ていくように、 Git と Mercurial はよく似ているため、食い違う箇所はそう多くありません。

Git でいつもやるように、まずクローンをします。

$ git clone hg::/tmp/hello /tmp/hello-git
$ cd /tmp/hello-git
$ git log --oneline --graph --decorate
* ac7955c (HEAD, origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master, master) Create a makefile
* 65bb417 Create a standard "hello, world" program

標準的な git clone コマンドを使用して Mercurial リポジトリを操作しているのが分かると思います。 これは git-remote-hg が非常に低いレベルで動作しているためです。 git-remote-hg は Git が HTTP/S プロトコルを実装しているのと同じようなメカニズム(リモートヘルパー)を使用しています。 Git と Mercurial は両方とも、すべてのクライアントがリポジトリの歴史の完全なコピーを持つように設計されています。そのためこのコマンドは、プロジェクトのすべての歴史を含む完全なクローンを作成します。また、この処理は非常に高速に行われます。

git logコマンドは2つのコミットを表示しています。最新のコミットは大量の参照から指されています。 実は、これらの中のいくつかは、実際には存在しません。 .git ディレクトリの中に実際には何が入っているか見てみましょう。

$ tree .git/refs
.git/refs
├── heads
│   └── master
├── hg
│   └── origin
│       ├── bookmarks
│       │   └── master
│       └── branches
│           └── default
├── notes
│   └── hg
├── remotes
│   └── origin
│       └── HEAD
└── tags

9 directories, 5 files

git-remote-hg は、物事をより Git 風にしようとしているわけですが、内部的には、2つの微妙に異なるシステムの間のマッピングを管理しています。 refs/hg ディレクトリには実際のリモート参照が格納されています。 例えば、 refs/hg/origin/branches/default は “ac7955c” で始まるSHA-1( master が指しているコミットを表している)を含むGitの参照ファイルです。 そのため、 refs/hg ディレクトリは refs/remotes/origin の模造品のようなものとも言えます。ただし、ブックマークとブランチの区別が追加されています。

notes/hg ファイルは、 git-remote-hg が Git のコミットハッシュと Mercurial のチェンジセットIDを対応付ける際の開始点となります。 ちょっと見てみましょう。

$ cat notes/hg
d4c10386...

$ git cat-file -p d4c10386...
tree 1781c96...
author remote-hg <> 1408066400 -0800
committer remote-hg <> 1408066400 -0800

Notes for master

$ git ls-tree 1781c96...
100644 blob ac9117f...	65bb417...
100644 blob 485e178...	ac7955c...

$ git cat-file -p ac9117f
0a04b987be5ae354b710cefeba0e2d9de7ad41a9

refs/notes/hg は Git オブジェクトデータベース中にあるツリーを指しており、その内容は他のオブジェクトの名前つきリストになっています。 git ls-tree はツリー中の要素のモード、タイプ、オブジェクトハッシュ、およびファイル名を出力します。 ツリー中の要素の一つについて掘り下げていくと、その実体は “ac9117f” ( master が指しているコミットの SHA-1 ハッシュ)という名前の blob で、内容は “0a04b98” ( default ブランチの先端の Mercurial チェンジセットのID)であることが分かります。

よいニュースとして、これらすべてのことについて、我々が気にする必要はほとんどありません。 典型的なワークフローは、 Git でリモートに対して作業をする場合と大差ありません。

以降の話をする前に、もう一つ注意しておかなければならないことがあります。 ignoreファイルです。 Mercurial と Git はこの点について非常に似通ったメカニズムを使用しています。ですが、おそらく実際に .gitignore ファイルを Mercurial リポジトリへコミットしたい、ということはないでしょう。 幸いなことに、 Git にはローカルからディスク上のリポジトリへファイルを登録する際に、指定したファイルを無視する方法があります。Mercurial のフォーマットは Git と互換性があるので、単にファイルをコピーするだけで済みます。

$ cp .hgignore .git/info/exclude

.git/info/exclude ファイルは .gitignore と同様の働きをしますが、コミットには含まれません。

ワークフロー

現在、何らかの作業をやり終え、 master ブランチにはコミットがいくつか作成されており、それをリモートリポジトリへプッシュできる状態にあるとしましょう。 現時点では、リポジトリは次のような内容になっています。

$ git log --oneline --graph --decorate
* ba04a2a (HEAD, master) Update makefile
* d25d16f Goodbye
* ac7955c (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Create a makefile
* 65bb417 Create a standard "hello, world" program

master ブランチは origin/master よりコミット2つぶん進んでいますが、これら2つのコミットはローカルのマシン上にしかありません。 ここで、誰か他の人が、何か重要な作業をこれと同時に行っていたらどうなるか見てみましょう。

$ git fetch
From hg::/tmp/hello
   ac7955c..df85e87  master     -> origin/master
   ac7955c..df85e87  branches/default -> origin/branches/default
$ git log --oneline --graph --decorate --all
* 7b07969 (refs/notes/hg) Notes for default
* d4c1038 Notes for master
* df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
| * ba04a2a (HEAD, master) Update makefile
| * d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard "hello, world" program

--all フラグを指定したため、 “notes” 参照が表示されていますが、これは git-remote-hg が内部的に使用しているものなので、無視して構いません。 残りが期待していた内容です。 origin/master はコミット1つぶん進んでおり、現在この歴史は枝分かれした状態にあります。 この章で扱っている他のシステムと異なり、 Mercurial にはマージをハンドリングする機能が備わっているので、ややこしいことをする必要は何もありません。

$ git merge origin/master
Auto-merging hello.c
Merge made by the 'recursive' strategy.
 hello.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --decorate
*   0c64627 (HEAD, master) Merge remote-tracking branch 'origin/master'
|\
| * df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
* | ba04a2a Update makefile
* | d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard "hello, world" program

完璧です。 テストを実行して、結果はすべて正常でした。これで、成果物をチームの他のメンバーと共有できる状態になりました。

$ git push
To hg::/tmp/hello
   df85e87..0c64627  master -> master

これで完了です! Mercurial リポジトリを見てみれば、期待していた内容が分かるはずです。

$ hg log -G --style compact
o    5[tip]:4,2   dc8fa4f932b8   2014-08-14 19:33 -0700   ben
|\     Merge remote-tracking branch 'origin/master'
| |
| o  4   64f27bcefc35   2014-08-14 19:27 -0700   ben
| |    Update makefile
| |
| o  3:1   4256fc29598f   2014-08-14 19:27 -0700   ben
| |    Goodbye
| |
@ |  2   7db0b4848b3c   2014-08-14 19:30 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

番号 2 のチェンジセットは Mercurial が作成したもので、番号 3 および 4 のチェンジセットは Git で作成したコミットを git-remote-hg がプッシュして作成したものです。

ブランチとブックマーク

Git のブランチは1種類しかありません。コミットに合わせて移動する参照です。 Mercurial では、この種の参照は “ブックマーク” と呼ばれており、 Git のブランチとほぼ同じように振る舞います。

Mercurial の言う “ブランチ” は Git のそれよりもっと重量級の概念です。 ブランチの上でチェンジセットが作成された場合、ブランチは チェンジセットと一緒に 記録されます。つまり、常にリポジトリの歴史に残ります。 develop ブランチの上で作成されたコミットの例を次に示します。

$ hg log -l 1
changeset:   6:8f65e5e02793
branch:      develop
tag:         tip
user:        Ben Straub <ben@straub.cc>
date:        Thu Aug 14 20:06:38 2014 -0700
summary:     More documentation

“branch” で始まる行に注目してください。 Git はこれを完全に複製することはできません(また、する必要もありません。いずれのタイプのブランチも Git では参照として表現されるため)が、 Mercurial にとってはこの違いが問題となるため、 git-remote-hg はこの違いを理解している必要があります。

Mercurial のブックマークを作成するのは、 Git のブランチを作成するのと同様に簡単です。 Git 側では、

$ git checkout -b featureA
Switched to a new branch 'featureA'
$ git push origin featureA
To hg::/tmp/hello
 * [new branch]      featureA -> featureA

これだけです。 Mercurial 側では、これは次のように見えます。

$ hg bookmarks
   featureA                  5:bd5ac26f11f9
$ hg log --style compact -G
@  6[tip]   8f65e5e02793   2014-08-14 20:06 -0700   ben
|    More documentation
|
o    5[featureA]:4,2   bd5ac26f11f9   2014-08-14 20:02 -0700   ben
|\     Merge remote-tracking branch 'origin/master'
| |
| o  4   0434aaa6b91f   2014-08-14 20:01 -0700   ben
| |    update makefile
| |
| o  3:1   318914536c86   2014-08-14 20:00 -0700   ben
| |    goodbye
| |
o |  2   f098c7f45c4f   2014-08-14 20:01 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

リビジョン5に付いている新しいタグ [featureA] に注目してください。 これらの挙動は Git 側から見ると Git のブランチと非常によく似ていますが、一つ例外があります。 Git の側からブックマークを削除することはできません(これはリモートヘルパーの制限によります)。

“重量級” の Mercurial のブランチ上で作業をすることもできます。 branches 名前空間にブランチを追加します。

$ git checkout -b branches/permanent
Switched to a new branch 'branches/permanent'
$ vi Makefile
$ git commit -am 'A permanent change'
$ git push origin branches/permanent
To hg::/tmp/hello
 * [new branch]      branches/permanent -> branches/permanent

Mercurial 側ではこれは次のように見えます。

$ hg branches
permanent                      7:a4529d07aad4
develop                        6:8f65e5e02793
default                        5:bd5ac26f11f9 (inactive)
$ hg log -G
o  changeset:   7:a4529d07aad4
|  branch:      permanent
|  tag:         tip
|  parent:      5:bd5ac26f11f9
|  user:        Ben Straub <ben@straub.cc>
|  date:        Thu Aug 14 20:21:09 2014 -0700
|  summary:     A permanent change
|
| @  changeset:   6:8f65e5e02793
|/   branch:      develop
|    user:        Ben Straub <ben@straub.cc>
|    date:        Thu Aug 14 20:06:38 2014 -0700
|    summary:     More documentation
|
o    changeset:   5:bd5ac26f11f9
|\   bookmark:    featureA
| |  parent:      4:0434aaa6b91f
| |  parent:      2:f098c7f45c4f
| |  user:        Ben Straub <ben@straub.cc>
| |  date:        Thu Aug 14 20:02:21 2014 -0700
| |  summary:     Merge remote-tracking branch 'origin/master'
[...]

“permanent” という名前のブランチが、 7 とマークされたチェンジセットと一緒に記録されています。

Git 側では、いずれのタイプのブランチで作業をするのも変わりません。普段と同じように、チェックアウト、コミット、フェッチ、マージ、プル、プッシュが行えます。 一つ知っておくべきこととして、 Mercurial は歴史の書き換えをサポートしておらず、追記しか行えません。 対話的リベースと強制プッシュを行うと、 Mercurial リポジトリは次のような内容になります。

$ hg log --style compact -G
o  10[tip]   99611176cbc9   2014-08-14 20:21 -0700   ben
|    A permanent change
|
o  9   f23e12f939c3   2014-08-14 20:01 -0700   ben
|    Add some documentation
|
o  8:1   c16971d33922   2014-08-14 20:00 -0700   ben
|    goodbye
|
| o  7:5   a4529d07aad4   2014-08-14 20:21 -0700   ben
| |    A permanent change
| |
| | @  6   8f65e5e02793   2014-08-14 20:06 -0700   ben
| |/     More documentation
| |
| o    5[featureA]:4,2   bd5ac26f11f9   2014-08-14 20:02 -0700   ben
| |\     Merge remote-tracking branch 'origin/master'
| | |
| | o  4   0434aaa6b91f   2014-08-14 20:01 -0700   ben
| | |    update makefile
| | |
+---o  3:1   318914536c86   2014-08-14 20:00 -0700   ben
| |      goodbye
| |
| o  2   f098c7f45c4f   2014-08-14 20:01 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

チェンジセット 8910 が作成され、 permanent ブランチに紐付けられていますが、古いチェンジセットも残っています。 これは Mercurial を使用している他のチームメンバーを かなり 混乱させるので、できる限り避けましょう。

Mercurial のまとめ

Git と Mercurial は非常に似通っており、相互に作業してもほとんど苦になりません。 (一般的に推奨されているように)あなたのマシン上にある歴史を変更しないようにさえしていれば、相手側にあるのが Mercurial であることを意識する必要もありません。

Git と Perforce

Perforce は企業内では非常によく使われているバージョン管理システムです。 Perforce が登場したのは 1995 年で、この章で扱う中ではもっとも古いシステムです。 そしてその言葉通り、Perforce は当時の制約に合わせて設計されています。単一の中央サーバへの常時接続を仮定しており、またローカルディスクに保存しておけるバージョンは一つだけです。 確かに、Perforce の機能と制約は、ある特定の問題にはうまく適合します。しかし、実際には Git の方が上手くいくにも関わらず、 Perforce を使用しているというプロジェクトも数多くあります。

Perforce と Git を混在して使用したい場合、2通りの選択肢があります。 1つ目に取り上げるのは Perforce の開発元から出ている “Git Fusion” ブリッジです。これは、 Perforce ディポのサブツリーを読み書き可能な Git リポジトリとして公開させるものです。 2つ目はクライアントサイドのブリッジである git-p4 です。これは Git を Perforce のクライアントとして使用できるようにするもので、 Perforce サーバの設定を変更することなく使用できます。

Git Fusion

Perforce は Git Fusion という製品を提供しています( http://www.perforce.com/git-fusion から入手可能)。これは、サーバサイドで Perforce サーバと Git リポジトリとを同期させます。

セットアップ

本書の例では、 Git Fusion のもっとも簡単なインストール法として、仮想マシンをダウンロードし、 Perforce デーモンと Git Fusion をその上で実行する方法をとります。 仮想マシンイメージは http://www.perforce.com/downloads/Perforce/20-User から入手できます。ダウンロードが完了したら、お好みの仮想ソフトへインポートします(本書では VirtualBox を使用します)。

仮想マシンの初回起動時には、3つの Linux ユーザ( rootperforce 、および git )のパスワードを設定するよう要求されます。また、同じネットワーク上の他の仮想マシンとの区別のため、インスタンス名を決めるよう要求されます。 これらすべてが完了したら、次の画面が表示されるはずです。

Git Fusion 仮想マシンのブート画面
Figure 145. Git Fusion 仮想マシンのブート画面

ここで表示されるIPアドレスは、後で使用するので、メモしておいてください。 次に、 Perforce ユーザを作成します。 下部にある “Login” オプションを選択肢、Enterキーを押下し(または仮想マシンへSSHで接続し)、 root としてログインします。 続けて、次のコマンドでユーザを作成します。

$ p4 -p localhost:1666 -u super user -f john
$ p4 -p localhost:1666 -u john passwd
$ exit

1つめのコマンドは、ユーザのカスタマイズのため VI エディタを起動しますが、 :wq に続けて Enter を入力すれば、デフォルト設定のまま利用することもできます。 2つめのコマンドは、パスワードを2度入力するようプロンプトを表示します。 シェルプロンプトで行う必要のある作業はこれで全部ですので、セッションを終了します。

次に手順に従って行う必要があるのは、Git が SSL 証明書を検証しないようにすることです。 Git Fusion の仮想マシンイメージには証明書が同梱されていますが、これはあなたの仮想マシンのIPアドレスとは合わないであろうドメインのものなので、 Git は HTTPS 接続を拒否してしまいます。 今回インストールした環境を今後も使い続けるつもりなら、 Perforce Git Fusion マニュアルを参考に、個別の証明書をインストールしてください。本書で例を示すだけの用途なら、以下で十分です。

$ export GIT_SSL_NO_VERIFY=true

これで、すべてが動作しているかテストできるようになりました。

$ git clone https://10.0.1.254/Talkhouse
Cloning into 'Talkhouse'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 630, done.
remote: Compressing objects: 100% (581/581), done.
remote: Total 630 (delta 172), reused 0 (delta 0)
Receiving objects: 100% (630/630), 1.22 MiB | 0 bytes/s, done.
Resolving deltas: 100% (172/172), done.
Checking connectivity... done.

仮想マシンイメージには、クローンできるサンプルプロジェクトが同梱されています。 ここでは、上で作成したユーザ john を使用し、 HTTPS 経由でクローンしています。今回の接続時には認証情報を要求されますが、以降のリクエストでは Git の認証情報キャッシュが働くので、このステップは省略できます。

Fusion の設定

Git Fusion をインストールし終わったら、設定を調整したいことと思います。 設定の変更は、お好きな Perforce クライアントを使用して、実際非常に簡単に行えます。Perforce サーバの //.git-fusion ディレクトリをワークスペースにマップするだけです。 ファイル構造は次のようになっています。

$ tree
.
├── objects
│   ├── repos
│   │   └── [...]
│   └── trees
│       └── [...]
│
├── p4gf_config
├── repos
│   └── Talkhouse
│       └── p4gf_config
└── users
    └── p4gf_usermap

498 directories, 287 files

objects ディレクトリは Perforce のオブジェクトを Git へ対応付ける(逆も同様)ために Git Fusion が内部的に使用しているもので、この内容に触れる必要はありません。 このディレクトリにはグローバルな p4gf_config 設定ファイルがあります。また、このファイルは各リポジトリにも一つずつあります – これらは、 Git Fusion の動作を決定する設定ファイルです。 ルートディレクトリにあるファイルを見てみましょう。

[repo-creation]
charset = utf8

[git-to-perforce]
change-owner = author
enable-git-branch-creation = yes
enable-swarm-reviews = yes
enable-git-merge-commits = yes
enable-git-submodules = yes
preflight-commit = none
ignore-author-permissions = no
read-permission-check = none
git-merge-avoidance-after-change-num = 12107

[perforce-to-git]
http-url = none
ssh-url = none

[@features]
imports = False
chunked-push = False
matrix2 = False
parallel-push = False

[authentication]
email-case-sensitivity = no

ここでは各フラグの意味については説明しませんが、このファイルが、 Git の設定ファイル同様、単なる INI ファイル形式のテキストファイルであるという点は明記しておきます。 このファイルではグローバルなオプションを設定しています。これらの設定は repos/Talkhouse/p4gf_config などのリポジトリ固有の設定で上書きできます。 このファイルを開くと、 [@repo] セクションに、グローバルなデフォルト値とは異なる設定がされているのが分かると思います。 また、次のようなセクションがあると思います。

[Talkhouse-master]
git-branch-name = master
view = //depot/Talkhouse/main-dev/... ...

これは Perforce のブランチと Git のブランチのマッピングです。 このセクションの名前は好きなように決められるので、一意になるように長い名前も付けられます。 git-branch-name を使えば、Git にとってはとても長いディポのパスを、より扱いやすい名前に変換できます。 view では、 Perforce のファイルが Git のリポジトリへどう対応するかを、通常のビュー・マッピング用のシンタックスで設定します。 複数のマッピングを指定することもできます。次に例を示します。

[multi-project-mapping]
git-branch-name = master
view = //depot/project1/main/... project1/...
       //depot/project2/mainline/... project2/...

通常のワークスペースのマッピングが、ディレクトリの構造の変更を含む場合、この方法では、それも含めてGitリポジトリを複製することができます。

最後に取り上げるのは users/p4gf_usermap で、これは Perforce のユーザを Git のユーザにマッピングするファイルですが、必要ないかもしれません。 Perforce のチェンジセットを Git のコミットへ変換する際、 Git Fusion のデフォルトの動作では、 Perforce ユーザを探して、そのメールアドレスとフルネームを Git の作成者/コミッターフィールドに使用します。 逆の方向に変換する場合、デフォルトでは Git の作成者フィールドに格納されているメールアドレスで Perforce ユーザを検索し、そのユーザとしてチェンジセットを送信します(パーミッションも適用されます)。 ほとんどの場合、この動作で上手くいきます。ただし、次のマッピングファイルについても考慮しておいてください。

john john@example.com "John Doe"
john johnny@appleseed.net "John Doe"
bob employeeX@example.com "Anon X. Mouse"
joe employeeY@example.com "Anon Y. Mouse"

各行のフォーマットは <ユーザ名> <メールアドレス> "<氏名>" となっています。一行ごとに一つ、ユーザの対応づけを定義しています。 最初の2行は、2つの異なるメールアドレスを同一の Perforce ユーザアカウントへ対応づけています。 これは、 Git のコミットを複数のメールアドレスで作成していた(または、メールアドレスを変更した)際に、それらを同じ Perforce へ対応づけたい場合に便利です。 Perforce のチェンジセットから Git のコミットを作成する際には、 Perforce のユーザとマッチした最初の行が Git の作成者情報として使用されます。

最後の2行は、Git のコミットから Bob と Joe の氏名とメールアドレスが分からないようにします。 これは、社内のプロジェクトをオープンソース化したいが、社員名簿を全世界へ晒したくはない、というときに役立ちます。 注意すべき点として、すべての Git コミットが実際には存在しない1人のユーザによるものである、としたい場合を除き、メールアドレスと氏名は一意になるよう設定してください。

ワークフロー

Perforce Git Fusion は Perforce と Git の間の双方向ブリッジです。 Gitの側から作業するとどんな感じなのかを見てみましょう。 ここでは、前述した設定ファイルを使用して “Jam” プロジェクトをマッピングしたと仮定しましょう。次のようにクローンが行えます。

$ git clone https://10.0.1.254/Jam
Cloning into 'Jam'...
Username for 'https://10.0.1.254': john
Password for 'https://ben@10.0.1.254':
remote: Counting objects: 2070, done.
remote: Compressing objects: 100% (1704/1704), done.
Receiving objects: 100% (2070/2070), 1.21 MiB | 0 bytes/s, done.
remote: Total 2070 (delta 1242), reused 0 (delta 0)
Resolving deltas: 100% (1242/1242), done.
Checking connectivity... done.
$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/rel2.1
$ git log --oneline --decorate --graph --all
* 0a38c33 (origin/rel2.1) Create Jam 2.1 release branch.
| * d254865 (HEAD, origin/master, origin/HEAD, master) Upgrade to latest metrowerks on Beos -- the Intel one.
| * bd2f54a Put in fix for jam's NT handle leak.
| * c0f29e7 Fix URL in a jam doc
| * cc644ac Radstone's lynx port.
[...]

最初にこれを実行した際には、ちょっと時間がかかるかもしれません。 ここで何が行われているかというと、 Git Fusion が、 Perforce の歴史中にある、適用可能なチェンジセットをすべて Git のコミットへ変換しています。 この処理はサーバ内部で行われるので、比較的高速ですが、大量の歴史がある場合には、ちょっと時間がかかります。 以降のフェッチでは増分だけを変換するので、体感的に Git 本来のスピードにより近づきます。

見て分かるとおり、このリポジトリは普段作業している Git リポジトリと見た目上まったく変わりません。 3つのブランチがあり、 Git は親切なことに origin/master ブランチを追跡するローカルの master ブランチまで作成してくれています。 ちょっと作業をして、新しいコミットを2つほど作成してみましょう。

# ...
$ git log --oneline --decorate --graph --all
* cfd46ab (HEAD, master) Add documentation for new feature
* a730d77 Whitespace
* d254865 (origin/master, origin/HEAD) Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]

新しいコミットが2つできました。 今度は、他の人が作業していなかったか確認してみましょう。

$ git fetch
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://10.0.1.254/Jam
   d254865..6afeb15  master     -> origin/master
$ git log --oneline --decorate --graph --all
* 6afeb15 (origin/master, origin/HEAD) Update copyright
| * cfd46ab (HEAD, master) Add documentation for new feature
| * a730d77 Whitespace
|/
* d254865 Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]

誰かいたみたいですよ! このビューからは分からなかったかも知れませんが、コミット 6afeb15 は Perforce クライアントを使用して実際に作成されたものです。 Git の視点から見ると、他のコミットと変わりませんが、 そこがポイントです。 Perforce サーバがマージコミットをどのように処理するのかを見てみましょう。

$ git merge origin/master
Auto-merging README
Merge made by the 'recursive' strategy.
 README | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Perforce: 100% (3/3) Loading commit tree into memory...
remote: Perforce: 100% (5/5) Finding child commits...
remote: Perforce: Running git fast-export...
remote: Perforce: 100% (3/3) Checking commits...
remote: Processing will continue even if connection is closed.
remote: Perforce: 100% (3/3) Copying changelists...
remote: Perforce: Submitting new Git commit objects to Perforce: 4
To https://10.0.1.254/Jam
   6afeb15..89cba2b  master -> master

Git からは、うまくいったように見えているようです。 p4v のリビジョングラフ機能を使用して、 README ファイルの歴史を Perforce の視点から見てみましょう。

Git でのプッシュの結果作成される Perforce のリビジョングラフ
Figure 146. Git でのプッシュの結果作成される Perforce のリビジョングラフ

この画面を見たことがないと、混乱するかもしれませんが、 Git の歴史をグラフィカルに表示するビューアと同じ概念を示しています。 ここでは README ファイルの歴史を見ています。そのため、左上のディレクトリツリーでは、様々なブランチなかからそのファイルだけを取り上げて表示しています。 右上には、そのファイルの様々なリビジョンがどのように関連しているか、ビジュアライズされたグラフが表示されます。このグラフの全体像は右下にあります。 ビューの残りの部分では、選択したリビジョン(この場合は 2 )の詳細を表示しています。

ひとつ注目してもらいたいのは、このグラフが Git の歴史のグラフとそっくりだということです。 Perforce にはコミット 1 および 2 を格納する名前つきのブランチがありません。代わりに “anonymous” ブランチを .git-fusion ディレクトリに作成し、そこに保管しています。 同様のことは、名前つきの Git のブランチに、対応する名前つきの Perforce のブランチがない場合にも起こります(設定ファイルを使えば、後でそのようなブランチを Perforce のブランチへ対応づけることも可能です)。

これのほとんどは舞台裏で行われますが、最終的には、ひとつのチームの中で、ある人は Git を使用し、またある人は Perforce を使用するということができ、その双方とも他の人が何を使用しているのかを知ることはない、という結果になりました。

Git-Fusion のまとめ

Perforce サーバへのアクセス権がある(または取得できる)なら、 Git Fusion は Git と Perforce が互いにやりとりできるようにする素晴らしい方法です。 多少の設定は必要ですが、学習曲線は急ではありません。 本節は、この章において、 Git の全機能を使用することに関して注意事項のない、数少ない節の一つです。 Perforce は指定した処理すべてを喜んでこなす、とは言いません – すでにプッシュ済みの歴史を書き換えようとしたら、 Git Fusion はそれをリジェクトします – ですが、 Git Fusion は Git そのままの感じになるよう非常に苦心しています。 また、Git のサブモジュールを使用したり(Perforce のユーザには変な風にみえますが)、ブランチのマージをしたり(Perforce 側では統合として記録されます)することも可能です。

Git Fusion をセットアップするようサーバの管理者を説得できなかったとしても、Git と Perforce を一緒に使用する方法は他にもあります。

Git-p4

git-p4 は、 Git と Perforce の間の双方向ブリッジです。 git-p4 は Git リポジトリの中だけで動作するので、 Perforce サーバにおけるいかなるアクセス権も必要としません(もちろん、ユーザ権限は必要ですが)。 git-p4 には Git Fusion ほどの柔軟性や完全性はありません。ですが、やりたいであろうことの大半を、サーバ環境に対して侵襲的になることなく実施できます。

Note

git-p4 で作業を行う場合、 p4 ツールに PATH が通っている必要があります。 これを書いている時点では、 p4http://www.perforce.com/downloads/Perforce/20-User から無料で入手できます。

セットアップ

例のため、前に見てきたとおり Perforce サーバを Git Fusion OVA で実行しますが、ここでは Git Fusion サーバをバイパスして、 Perforce のバージョン管理機能を直接使用します。

p4 コマンドラインクライアント( git-p4 がこれに依存している)を使用するには、環境変数を2つ設定する必要があります。

$ export P4PORT=10.0.1.254:1666
$ export P4USER=john

使いはじめる

Git でやるのと同様、最初にすることはクローンです。

$ git p4 clone //depot/www/live www-shallow
Importing from //depot/www/live into www-shallow
Initialized empty Git repository in /private/tmp/www-shallow/.git/
Doing initial import of //depot/www/live/ from revision #head into refs/remotes/p4/master

これで、 Git の用語で言う “シャロー” クローンが作成されます(Perforce の最新のリビジョンだけが Git へインポートされます)。覚えておいて欲しいのですが、 Perforce はすべてのリビジョンをすべてのユーザに渡すようデザインされてはいません。 Git を Perforce のクライアントとして使用するにはこれで十分ですが、それ以外の用途には不十分といえます。

クローンが完了したら、十分な機能を備えた Git リポジトリの出来上がりです。

$ cd myproject
$ git log --oneline --all --graph --decorate
* 70eaf78 (HEAD, p4/master, p4/HEAD, master) Initial import of //depot/www/live/ from the state at revision #head

Perforceを表す “p4” リモートがあることに注意が必要ですが、それ以外はすべて、通常のクローンと同じように見えます。 実際、これは少し誤解をまねきやすいのですが、実際にはリモートがあるわけではありません。

$ git remote -v

このリポジトリにはリモートはひとつもありません。 git-p4 は、サーバの状態を表すために参照をいくつか作成します。これが git log からはリモート参照のように見えます。ですが、これらの参照は Git 自身が管理しているものではなく、またそこへプッシュすることもできません。

ワークフロー

オーケー、それでは作業を始めましょう。 ここでは、ある非常に重要な機能に関して進捗があり、その成果をチームの他のメンバーに見せられる状態になっているとしましょう。

$ git log --oneline --all --graph --decorate
* 018467c (HEAD, master) Change page title
* c0fb617 Update link
* 70eaf78 (p4/master, p4/HEAD) Initial import of //depot/www/live/ from the state at revision #head

すでに2つのコミットを作成しており、Perforce サーバへ送信する準備もできています。 今日、他の誰かが作業をしていなかったか確認してみましょう。

$ git p4 sync
git p4 sync
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12142 (100%)
$ git log --oneline --all --graph --decorate
* 75cd059 (p4/master, p4/HEAD) Update copyright
| * 018467c (HEAD, master) Change page title
| * c0fb617 Update link
|/
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

誰かが作業をしていたようです。また、 masterp4/master が分岐しています。 Perforce のブランチのシステムは Git とは まったく 異なり、マージコミットを送信しても意味をなしません。 git-p4 では、コミットをリベースすることを推奨しており、そのためのショートカットも用意されています。

$ git p4 rebase
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
No changes to import!
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
Applying: Update link
Applying: Change page title
 index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

出力から分かったかと思いますが、 git p4 rebasegit p4 sync の後に git rebase p4/master を実行するショートカットです。 実際にはもう少し賢いのですが(特に複数のブランチで作業をしている場合には)、これはよい近似と言えます。

これで歴史がリニアになり、変更を Perforce へ提供できる状態になりました。 git p4 submit を実行すると、 Git の p4/master から master の間の各コミットに対して Perforce のリビジョンを作成しようとします。 実行するとお好みのエディタが開かれます。ファイルの内容は次のような感じです。

# A Perforce Change Specification.
#
#  Change:      The change number. 'new' on a new changelist.
#  Date:        The date this specification was last modified.
#  Client:      The client on which the changelist was created.  Read-only.
#  User:        The user who created the changelist.
#  Status:      Either 'pending' or 'submitted'. Read-only.
#  Type:        Either 'public' or 'restricted'. Default is 'public'.
#  Description: Comments about the changelist.  Required.
#  Jobs:        What opened jobs are to be closed by this changelist.
#               You may delete jobs from this list.  (New changelists only.)
#  Files:       What opened files from the default changelist are to be added
#               to this changelist.  You may delete files from this list.
#               (New changelists only.)

Change:  new

Client:  john_bens-mbp_8487

User: john

Status:  new

Description:
   Update link

Files:
   //depot/www/live/index.html   # edit


######## git author ben@straub.cc does not match your p4 account.
######## Use option --preserve-user to modify authorship.
######## Variable git-p4.skipUserNameCheck hides this message.
######## everything below this line is just the diff #######
--- //depot/www/live/index.html  2014-08-31 18:26:05.000000000 0000
+++ /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/index.html   2014-08-31 18:26:05.000000000 0000
@@ -60,7 +60,7 @@
 </td>
 <td valign=top>
 Source and documentation for
-<a href="http://www.perforce.com/jam/jam.html">
+<a href="jam.html">
 Jam/MR</a>,
 a software build tool.
 </td>

これは、 git-p4 が気を利かせて末尾に追加している内容を除けば、 p4 submit を実行した場合とほぼ同じ内容です。 git-p4 は、コミットやチェンジセットに対して氏名を指定する必要がある場合、 Git と Perforce で設定をそれぞれ個別に行えるようにしていますが、その設定を上書きしたい場合もあります。 例えば、 Git のコミットをインポートしていて、そのコミットの作成者が Perforce のユーザアカウントを持っていない場合を考えます。この場合、最終的にできるチェンジセットは(あなたではなく)そのコミットの作成者が書いたように見えるようにしたいだろうと思います。

git-p4 は気が利いていて、Git のコミットからメッセージをインポートして、この Perforce のチェンジセットの内容にセットしてくれます。そのため、ここではファイルの保存とエディタの終了を2回(コミット1つにつき1回)行うだけで済みます。 結果として、シェルへの出力は次のような感じになります。

$ git p4 submit
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Synchronizing p4 checkout...
... - file(s) up-to-date.
Applying dbac45b Update link
//depot/www/live/index.html#4 - opened for edit
Change 12143 created with 1 open file(s).
Submitting change 12143.
Locking 1 files ...
edit //depot/www/live/index.html#5
Change 12143 submitted.
Applying 905ec6a Change page title
//depot/www/live/index.html#5 - opened for edit
Change 12144 created with 1 open file(s).
Submitting change 12144.
Locking 1 files ...
edit //depot/www/live/index.html#6
Change 12144 submitted.
All commits applied!
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12144 (100%)
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
$ git log --oneline --all --graph --decorate
* 775a46f (HEAD, p4/master, p4/HEAD, master) Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

出力は、実際に行われている処理にもっとも近いアナロジーとして、単に git push を実行したかのような内容になっています。

注意事項として、この処理では Git の各コミットが個別の Perforce のチェンジセットに変換されます。複数のコミットを1つのチェンジセットへスカッシュしたい場合は、 git p4 submit の前に対話的リベースを行ってください。 また、チェンジセットとして送信された全コミットの SHA-1 ハッシュが変更される点にも注意してください。これは、 git-p4 が変換した各コミットの末尾に行を追加するためです。

$ git log -1
commit 775a46f630d8b46535fc9983cf3ebe6b9aa53145
Author: John Doe <john@example.com>
Date:   Sun Aug 31 10:31:44 2014 -0800

    Change page title

    [git-p4: depot-paths = "//depot/www/live/": change = 12144]

マージコミットを送信しようとした場合、何が起こるでしょうか? やってみましょう。 現在の状況は次のようになっています。

$ git log --oneline --all --graph --decorate
* 3be6fd8 (HEAD, master) Correct email address
*   1dcbf21 Merge remote-tracking branch 'p4/master'
|\
| * c4689fc (p4/master, p4/HEAD) Grammar fix
* | cbacd0a Table borders: yes please
* | b4959b6 Trademark
|/
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

Git と Perforce の歴史が 775a46f の後で分岐しています。 Git 側に2つのコミットがあり、次に Perforce 側の HEAD とのマージコミット、さらにその次にまた別のコミットがあります。 ここでは、これらすべてのコミットを Perforce 側のチェンジセットの一番上に追加してみます。 現時点で送信した場合に何が起こるか見てみましょう。

$ git p4 submit -n
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would synchronize p4 checkout in /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would apply
  b4959b6 Trademark
  cbacd0a Table borders: yes please
  3be6fd8 Correct email address

-n フラグは --dry-run の省略形で、送信コマンドを本当に実行したら何が起こるかを表示してくれます。 この場合、 Perforce サーバに存在しない3つの非マージコミットに対応するように、 Perforce のチェンジセットを3つ作成するように見えます。 これは、望む結果そのもののように見えますので、結果どうなったか見てみましょう。

$ git p4 submit
[…]
$ git log --oneline --all --graph --decorate
* dadbd89 (HEAD, p4/master, p4/HEAD, master) Correct email address
* 1b79a80 Table borders: yes please
* 0097235 Trademark
* c4689fc Grammar fix
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

送信の前にリベースを実行したかのように(実際、それが行われているのですが)、歴史がリニアになりました。 これはつまり、Git 側ではブランチを作成したり、ブランチ上で作業したり、ブランチを放棄したり、ブランチをマージしたりといった作業を自由に行えること、その際に歴史が何らかの形で Perforce と非互換になってしまう心配もないということです。 リベースが行えるなら、その内容を Perforce サーバへ送信できます。

ブランチ

Perforce プロジェクトに複数のブランチがある場合でも、運の尽きというわけではありません。 git-p4 はそのようなプロジェクトを、 Git と同じように扱えます。 まず、 Perforce のディポが次のような内容になっているとしましょう。

//depot
  └── project
      ├── main
      └── dev

さらに、次のようなビュー・スペックを持った dev ブランチがあるとしましょう。

//depot/project/main/... //depot/project/dev/...

git-p4 はこのような状況を自動的に検出し、正しく処理を行います。

$ git p4 clone --detect-branches //depot/project@all
Importing from //depot/project@all into project
Initialized empty Git repository in /private/tmp/project/.git/
Importing revision 20 (50%)
    Importing new branch project/dev

    Resuming with change 20
Importing revision 22 (100%)
Updated branches: main dev
$ cd project; git log --oneline --all --graph --decorate
* eae77ae (HEAD, p4/master, p4/HEAD, master) main
| * 10d55fb (p4/project/dev) dev
| * a43cfae Populate //depot/project/main/... //depot/project/dev/....
|/
* 2b83451 Project init

ディポのパスに “@all” という指示子がついていることに注意してください。これは git-p4 に対して、パスの示すサブツリーの最新のチェンジセットだけでなく、そのパスにあったことのあるすべてのチェンジセットをクローンするように指示しています。 これは Git のクローンの考え方に近いですが、作業中のプロジェクトに長い歴史がある場合、ちょっと時間がかかるかもしれません。

--detect-branches フラグは、 git-p4 に対して、Perforce のブランチを Git の参照へマッピングする際に、 Perforce のブランチ仕様を使用するように指示しています。 そのようなマッピングが Perforce サーバになかった場合(これは Perforce を使う分にはまったく問題ないやり方ですが)でも、git-p4 に対してブランチのマッピングがどのようになっているかを指示でき、同じ結果が得られます。

$ git init project
Initialized empty Git repository in /tmp/project/.git/
$ cd project
$ git config git-p4.branchList main:dev
$ git clone --detect-branches //depot/project@all .

設定値 git-p4.branchListmain:dev を設定すると、 “main” と “dev” がいずれもブランチであること、2つめのブランチは1つめのブランチの子であることを git-p4 へ示します。

ここで git checkout -b dev p4/project/dev を実行してからコミットを作成した場合でも、 git-p4 は充分に賢いので、 git p4 submit を実行した際には正しいブランチを対象にしてくれます。 なお、残念なことに、 git-p4 ではシャロークローンと複数ブランチを混ぜて使うことができません。巨大なプロジェクトにおいて複数のブランチで作業したい場合、ブランチごとに git p4 clone を実行する必要があります。

また、ブランチの作成や統合には、 Perforce クライアントを使用する必要があります。 git-p4 にできるのは既存のブランチに対する同期と送信だけで、それも対象にできるのはリニアなチェンジセットを一度にひとつだけです。 Git で2つのブランチをマージして新しいチェンジセットを送信しても、一塊りのファイルの変更として記録されるだけです。マージの対象となったブランチはどれかといったメタデータは失われてしまいます。

Git と Perforce のまとめ

git-p4 は、 Git のワークフローを Perforce サーバ上で使用できるようにします。また、それを非常にうまいやり方で可能にします。 ですが、大元を管理しているのはあくまで Perforce であり、 Git はローカルでの作業にのみ使用しているということは忘れないでください。 Git のコミットの共有については特に気をつけてください。他のメンバーが使用しているリモートがある場合、 Perforce サーバに送信していないコミットをプッシュしないよう気をつけてください。

ソース管理のクライアントに Perforce と Git を自由に混ぜて使いたい場合、さらにサーバの管理者を説得してインストールの許可を貰える場合、Git Fusion は Git を Perforce サーバ用の第一級のバージョン管理クライアントにしてくれます。

Git と TFS

Git は Windows を利用する開発者の間でもよく使われるようになってきています。 Windows 上でコードを書いているのなら、 Microsoft の Team Foundation Server (TFS) を使用することもあるでしょう。 TFS はコラボレーションスイートで、不具合および作業成果物に対するトラッキング、 Scrum やその他の開発プロセスのサポート、コードレビュー、そしてバージョン管理といった機能が含まれています。 ここがちょっとややこしいところなのですが、 TFS 自体はサーバで、ソースコード管理には、 Git や TFS 専用のバージョン管理システム( TFVC (Team Foundation Version Control) とも呼ばれる)をサポートしています。 TFS の Git サポートは幾分新しい機能(バージョン2013から搭載)なので、それ以前からあったツールはどれも、実際にはほぼ TFVC だけを使用している場合であっても、バージョン管理部分のことを “TFS” と呼んでいます。

所属しているチームは TFVC を使用しているけれど、あなた自身はバージョン管理のクライアントに Git を使用したいという場合には、そのためのプロジェクトがあります。

どちらのツールを使うか

実際には、ツールは2つあります。 git-tf と git-tfs です。

git-tfs ( https://github.com/git-tfs/git-tfs から入手できます)は .NET プロジェクトで、(これを書いている時点では)Windows でのみ動作します。 git-tfs は、Git リポジトリに対する操作に libgit2 の .NET バインディングを使用しています。libgit2 はライブラリ指向の Git の実装で、処理性能が高く、また Git の内部に対して柔軟な操作が行えるようになっています。 libgit2 は Git の機能を網羅的に実装してはいないため、その差を埋めるために、 git-tfs の一部の操作では実際にはコマンドライン版の Git クライアントが呼び出されています。そのため、Git リポジトリに対する操作に関して、git-tfs の設計に起因した制約は特にありません。 git-tfs は、 TFS サーバの操作に Visual Studio のアセンブリを使用しているため、TFVC 向け機能は非常に成熟しています。 また、これは Visual Studio のアセンブリにアクセスできる必要があるということでもあります。そのため、比較的新しいバージョンの Visual Studio (Visual Studio 2010以降の任意のエディション。バージョン2012以降の Visual Studio Express でもよい)か、Visual Studio SDK のインストールが必要です。

git-tf (ホームページは https://gittf.codeplex.com )は Java プロジェクトで、Java実行環境のあるあらゆるコンピュータで実行できます。 git-tf は Git リポジトリに対する操作に JGit (JVM 用の Git の実装)を使用しているため、 Git の機能という観点においては事実上制約はありません。 しかし、 TFVC に対するサポートは git-tfs と比較すると限定的です – 例えば、ブランチをサポートしていません。

どちらのツールにも長所と短所があり、また一方よりもう一方が向いている状況というのはいくらでもあります。 本書では、2つのツールの基本的な使用法について取り上げます。

Note

以降の手順に従って操作を行うには、 TFVC ベースのリポジトリへのアクセス権が必要です。 そのようなリポジトリは Git や Subversion のリポジトリほど世の中にありふれたものではないので、自前で作成する必要があるかもしれません。 この場合、 Codeplex (https://www.codeplex.com) や Visual Studio Online (http://www.visualstudio.com) を利用するのがよいでしょう。

使いはじめる: git-tf の場合

最初に行うことは、あらゆる Git プロジェクトと同様、クローンです。 git-tf は次のような感じです。

$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main project_git

最初の引数は TFVC コレクションの URL で、2番目の引数は $/プロジェクト/ブランチ の形式になっており、3番目の引数はローカルに作成する Git リポジトリのパスです(3番目の引数はオプショナルです)。 git-tf は一度にひとつのブランチしか扱えません。別の TFVC ブランチへチェックインしたい場合は、対象のブランチから新しくクローンを作成する必要があります。

次のコマンドで、フル機能の Git リポジトリが作成できます。

$ cd project_git
$ git log --all --oneline --decorate
512e75a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Checkin message

これは シャロー クローンと言われるもので、最新のチェンジセットだけがダウンロードされます。 TFVC は、各クライアントが歴史の完全なコピーを持つようには設計されていません。そのため git-tf は、幾分高速な、最新のバージョンだけを取得する方法をデフォルトとしています。

時間があるなら、プロジェクトの歴史全体をクローンしてみるといいでしょう。 --deep オプションを使用します。

$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main \
  project_git --deep
Username: domain\user
Password:
Connecting to TFS...
Cloning $/myproject into /tmp/project_git: 100%, done.
Cloned 4 changesets. Cloned last changeset 35190 as d44b17a
$ cd project_git
$ git log --all --oneline --decorate
d44b17a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Goodbye
126aa7b (tag: TFS_C35189)
8f77431 (tag: TFS_C35178) FIRST
0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
        Team Project Creation Wizard

TFS_C35189 のような名前のタグに注目してください。これは、Git のどのコミットが、 TFVC のどのチェンジセットに対応しているかを分かりやすくするための機能です。 タグで表現するというのは上手い方法です。簡単な log コマンドだけで、どのコミットが TFVC 中のどのスナップショットに対応しているか確認できます。 なお、このタグ付けは必須ではありません(実際、 git config git-tf.tag false で無効にできます) – git-tf では、コミットとチェンジセットとのマッピングは .git/git-tf に保存されています。

使いはじめる: git-tfs の場合

git-tfs のクローン処理は、 git-tf とは少し異なります。 見てみましょう。

PS> git tfs clone --with-branches \
    https://username.visualstudio.com/DefaultCollection \
    $/project/Trunk project_git
Initialized empty Git repository in C:/Users/ben/project_git/.git/
C15 = b75da1aba1ffb359d00e85c52acb261e4586b0c9
C16 = c403405f4989d73a2c3c119e79021cb2104ce44a
Tfs branches found:
- $/tfvc-test/featureA
The name of the local branch will be : featureA
C17 = d202b53f67bde32171d5078968c644e562f1c439
C18 = 44cd729d8df868a8be20438fdeeefb961958b674

--with-branches フラグに注意してください。 git-tfs では TFVC のブランチを Git のブランチへマッピングできます。ここで --with-branches フラグは、 git-tfs に対し、すべての TFVC ブランチについて、対応するブランチをローカルの Git に作成するよう指示しています。 TFS 上で一度でもブランチの作成やマージを行っている場合、このオプションを指定することを強くお勧めします。ただし、このオプションは TFS 2010 より古いバージョンのサーバでは動作しません – それ以前のリリースでは “ブランチ” はただのフォルダだったためです。 git-tfs は単なるフォルダからはそのような指示は行えません。

結果の Git リポジトリを見てみましょう。

PS> git log --oneline --graph --decorate --all
* 44cd729 (tfs/featureA, featureA) Goodbye
* d202b53 Branched from $/tfvc-test/Trunk
* c403405 (HEAD, tfs/default, master) Hello
* b75da1a New project
PS> git log -1
commit c403405f4989d73a2c3c119e79021cb2104ce44a
Author: Ben Straub <ben@straub.cc>
Date:   Fri Aug 1 03:41:59 2014 +0000

    Hello

    git-tfs-id: [https://username.visualstudio.com/DefaultCollection]$/myproject/Trunk;C16

2つのローカルブランチ masterfeatureA があり、それぞれ最初のクローンの開始位置( TFVC の Trunk )と、子のブランチ( TFVC の featureA )を表しています。 また “リモート” tfs にも defaultfeatureA の2つの参照があり、これは TFVC のブランチを表現しています。 git-tfs はクローン元のブランチを tfs/default へマッピングし、それ以外のブランチにはそれぞれのブランチ名を付与します。

もうひとつ注意すべき点として、コミットメッセージにある git-tfs-id: という行があります。 タグとは異なり、このマーカーは git-tfs が TFVC のチェンジセットを Git のコミットへ対応づけるのに使用しています。 これは、TFVC にプッシュする前と後とで Git のコミットの SHA-1 ハッシュが異なるということを暗黙的に意味しています。

git-tf[s] のワークフロー

Note

どちらのツールを使用するにせよ、問題を避けるため、次の2つの Git の設定値をセットする必要があります。

$ git config set --local core.ignorecase=true
$ git config set --local core.autocrlf=false

当たり前ですが、次はプロジェクトに対して作業を行いたいことと思います。 TFVC および TFS には、ワークフローをややこしくする機能がいくつかあります。

  1. TFVC 上に現れないフィーチャーブランチがあると、ややこしさが増します。 これには、TFVC と Git とでブランチを表現する方法が まったく 異なることが関係しています。

  2. TFVC では、ユーザがサーバからファイルを “チェックアウト” して、他の誰も編集できないようにロックを掛けられることを認識しておいてください。 ローカルリポジトリ上でファイルを編集する妨げには当然なりませんが、変更を TFVC サーバへプッシュする段になって邪魔になるかもしれません。

  3. TFS には “ゲート” チェックインという概念があります。これは、チェックインが許可されるには、 TFS のビルドとテストのサイクルが正常に終了する必要がある、というものです。 これは TFVC の “シェルブ” 機能を使用していますが、これについてはここでは深入りしません。 手作業でなら、git-tf でもこの方式をまねることはできます。 git-tfs はこれを考慮した checkintool コマンドを提供しています。

話を簡潔にするため、ここで取り上げるのは、これらの問題を避けたり起こらないようにした、ハッピーな手順です。

ワークフロー: git-tf の場合

ここでは、いくつか作業を終えて、 master に Git のコミットを2つ作成し、作業の成果を TFVC サーバで共有する準備ができているものとします。 Git リポジトリはこんな内容です。

$ git log --oneline --graph --decorate --all
* 4178a82 (HEAD, master) update code
* 9df2ae3 update readme
* d44b17a (tag: TFS_C35190, origin_tfs/tfs) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

4178a82 のコミットでスナップショットを取って、 TFVC サーバへプッシュしたいものとします。 大事なことから先にとりかかりましょう。最後にリポジトリへ接続した後に、チームの他のメンバーが何か作業をしていなかったか見てみます。

$ git tf fetch
Username: domain\user
Password:
Connecting to TFS...
Fetching $/myproject at latest changeset: 100%, done.
Downloaded changeset 35320 as commit 8ef06a8. Updated FETCH_HEAD.
$ git log --oneline --graph --decorate --all
* 8ef06a8 (tag: TFS_C35320, origin_tfs/tfs) just some text
| * 4178a82 (HEAD, master) update code
| * 9df2ae3 update readme
|/
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

やはり、他の誰かが作業をしているようです。歴史が分岐しています。 これは Git が得意とするところですが、進め方は2種類あります。

  1. Git ユーザなら、マージコミットを行うのが自然に感じられるでしょう( git pull が行うのがマージコミットなので)。 git-tf では単に git tf pull とすればマージコミットが行えます。 ですが、ここで注意が必要なのは、 TFVC はこれを自然とは考えないということです。マージコミットをプッシュしたら、歴史は Git 側と TFVC 側とで異なる見た目になりだし、ややこしいことになります。 一方、すべての変更をひとつのチェンジセットとして送信しようとしているのであれば、おそらくそれがもっとも簡単な選択肢です。

  2. リベースを行うと、歴史がリニアになります。つまり、 Git のコミットひとつひとつを TFVC のチェンジセットへ変換する選択肢がとれるということです。 これが、以降の選択肢をもっとも広く取れる方法なので、この方法をお勧めします。git-tf でも、git tf pull --rebase で簡単に行えるようになっています。

どの方法をとるかはあなた次第です。 この例では、リベースする方法をとったとします。

$ git rebase FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: update readme
Applying: update code
$ git log --oneline --graph --decorate --all
* 5a0e25e (HEAD, master) update code
* 6eb3eb5 update readme
* 8ef06a8 (tag: TFS_C35320, origin_tfs/tfs) just some text
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

さて、これで TFVC サーバへチェックインする準備ができました。 git-tf では、直近のチェンジセット以降のすべての変更を単一のチェンジセットにまとめる( --shallow 、こっちがデフォルト)か、Git のコミットそれぞれに対して新しくチェンジセットを作成する( --deep )かを選択できます。 この例では、単一のチェンジセットにまとめる方法をとったとします。

$ git tf checkin -m 'Updating readme and code'
Username: domain\user
Password:
Connecting to TFS...
Checking in to $/myproject: 100%, done.
Checked commit 5a0e25e in as changeset 35348
$ git log --oneline --graph --decorate --all
* 5a0e25e (HEAD, tag: TFS_C35348, origin_tfs/tfs, master) update code
* 6eb3eb5 update readme
* 8ef06a8 (tag: TFS_C35320) just some text
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

新しくタグ TFS_C35348 ができています。これは、 TFVC がコミット 5a0e25e とまったく同じスナップショットを格納していることを意味します。 ここが重要なのですが、Git の各コミットが、すべて TFVC 側と対応づいている必要はありません。例えば、コミット 6eb3eb5 は、 TFVC サーバには存在しません。

以上が主なワークフローです。 他にも、考慮すべき点として気をつけるべきものが2つほどあります。

  • ブランチはできません。 git-tf にできるのは、 TFVC のブランチから、 Git のリポジトリを作ることだけで、それも一度にひとつずつしか作れません。

  • 共同作業の際は、 TFVC と Git のいずれかだけを使用し、両方は使用しないでください。 ひとつの TFVC リポジトリから、 git-tf で複数のクローンを作成した場合、各コミットの SHA-1 ハッシュそれぞれ異なります。コミットが作成されます。これは終わることのない頭痛の種になります。

  • チームのワークフローに Git との協調作業が含まれており、定期的に TFVC との同期を取る場合、 TFVC へ接続する Git リポジトリはひとつだけにしてください。

ワークフロー: git-tfs の場合

git-tfs を使用した場合と同じシナリオを見ていきましょう。 Git リポジトリには、 master ブランチに対して行った新しいコミットが入っています。

PS> git log --oneline --graph --all --decorate
* c3bd3ae (HEAD, master) update code
* d85e5a2 update readme
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 (tfs/default) Hello
* b75da1a New project

さて、我々が作業している間に、他の誰かが作業をしていなかったか見てみましょう。

PS> git tfs fetch
C19 = aea74a0313de0a391940c999e51c5c15c381d91d
PS> git log --all --oneline --graph --decorate
* aea74a0 (tfs/default) update documentation
| * c3bd3ae (HEAD, master) update code
| * d85e5a2 update readme
|/
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

同僚が新しく TFVC のチェンジセットを追加しており、それが新しいコミット aea74a0 として表示されている他に、リモートブランチ tfs/default が移動されていることがわかりました。

git-tf と同様に、この分岐した歴史を処理する基本的な方法は2つあります。

  1. 歴史をリニアに保つためにリベースを行う。

  2. 実際に起こったことを残しておくためマージを行う。

この例では、Git の各コミットが TFVC のチェンジセットになる “ディープ” なチェックインを行おうとしているので、リベースをします。

PS> git rebase tfs/default
First, rewinding head to replay your work on top of it...
Applying: update readme
Applying: update code
PS> git log --all --oneline --graph --decorate
* 10a75ac (HEAD, master) update code
* 5cec4ab update readme
* aea74a0 (tfs/default) update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

これで、コードを TFVC サーバへチェックインして、 作業を完了する準備ができました。 ここでは rcheckin コマンドを使用し、HEAD から始めて最初の tfs リモートブランチが見つかるまでの Git の各コミットに対して、 TFVC のチェンジセットを作成します( checkin コマンドでは、 Git のコミットをスカッシュするのと同様に、チェンジセットをひとつだけ作成します)。

PS> git tfs rcheckin
Working with tfs remote: default
Fetching changes from TFS to minimize possibility of late conflict...
Starting checkin of 5cec4ab4 'update readme'
 add README.md
C20 = 71a5ddce274c19f8fdc322b4f165d93d89121017
Done with 5cec4ab4b213c354341f66c80cd650ab98dcf1ed, rebasing tail onto new TFS-commit...
Rebase done successfully.
Starting checkin of b1bf0f99 'update code'
 edit .git\tfs\default\workspace\ConsoleApplication1/ConsoleApplication1/Program.cs
C21 = ff04e7c35dfbe6a8f94e782bf5e0031cee8d103b
Done with b1bf0f9977b2d48bad611ed4a03d3738df05ea5d, rebasing tail onto new TFS-commit...
Rebase done successfully.
No more to rcheckin.
PS> git log --all --oneline --graph --decorate
* ff04e7c (HEAD, tfs/default, master) update code
* 71a5ddc update readme
* aea74a0 update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

TFVC サーバへのチェックインが成功した後に行われている処理に注目してください。 git-tfs は直前の処理結果に対して、残りの作業結果をリベースしています。 これは、コミットメッセージの末尾に git-tfs-id フィールドを追記しており、SHA-1 ハッシュが変化するためです。 これは仕様通りの動作であり、何も心配することはありません。ですが、そのような変更がなされていることは(特に、 Git のコミットを他の人と共有している場合は)認識しておいてください。

TFS には、ワークアイテム、レビュー依頼、ゲートチェックインなど、バージョン管理システムと統合されている機能が数多くあります。 これらの機能をコマンドラインツールだけで使うのは大変ですが、幸いなことに、 git-tfs ではグラフィカルなチェックインツールを簡単に起動できるようになっています。

PS> git tfs checkintool
PS> git tfs ct

だいたいこんな感じで表示されます。

git-tfs のチェックインツール
Figure 147. git-tfs のチェックインツール

TFS ユーザは見慣れていると思いますが、これは Visual Studio から表示されるものと同じダイアログです。

git-tfs では TFVC のブランチを Git のリポジトリから管理することもできます。 例として、ひとつ作成してみましょう。

PS> git tfs branch $/tfvc-test/featureBee
The name of the local branch will be : featureBee
C26 = 1d54865c397608c004a2cadce7296f5edc22a7e5
PS> git log --oneline --graph --decorate --all
* 1d54865 (tfs/featureBee) Creation branch $/myproject/featureBee
* ff04e7c (HEAD, tfs/default, master) update code
* 71a5ddc update readme
* aea74a0 update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

TFVC でブランチを作成するということは、そのブランチが存在する場所にチェンジセットを追加するということなので、結果としてそれは Git のコミットへ反映されます。 git-tfs はリモートブランチ tfs/featureBee作成は しましたが、 HEAD は相変わらず master を指していることに注意してください。 新しく作成したブランチ上で作業をしたい場合は、コミット 1d54865 から新しいコミットを作成することになりますが、この場合は恐らくそのコミットから新しくトピックブランチを作成することになるでしょう。

Git と TFS のまとめ

git-tf と git-tfs は、いずれも TFVC サーバに接続するための優れたツールです。 これらのツールにより、チーム全体を Git へ移行することなしに、ローカルで Git のパワーを享受でき、中央の TFVC サーバを定期的に巡回しなくて済み、開発者としての生活をより楽にすることができます。 Windows 上で作業をしているのなら(チームが TFS を使用しているなら多分そうだと思いますが)、機能がより網羅的な git-tfs を使用したいことと思います。また別のプラットフォーム上で作業をしているのなら、より機能の限られている git-tf を使用することになると思います。 この章で取り上げているほとんどのツールと同様、バージョン管理システムのうちひとつだけを正式なものとして、他は従属的な使い方にしておくべきです – Git か TFVC の両方ではなく、いずれか片方を共同作業の中心に置くべきです。