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 のクライアントです。 インストール済みでないなら、 https://mercurial-scm.org/ から入手してインストールしてください。

これで準備が整いました。 必要なのはプッシュが可能な 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 仮想マシンのブート画面
図 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 のリビジョングラフ
図 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 ほどの柔軟性や完全性はありません。ですが、やりたいであろうことの大半を、サーバ環境に対して侵襲的になることなく実施できます。

注記

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 サーバ用の第一級のバージョン管理クライアントにしてくれます。

scroll-to-top