チャプター ▾ 第2版

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

この世界は完璧ではありません。通常、接触するすべてのプロジェクトをすぐにGitに切り替えることはできません。時には、別のVCSを使用しているプロジェクトに固執し、それがGitだったらいいのにと思うことがあります。この章の最初の部分では、作業しているプロジェクトが異なるシステムでホストされている場合に、Gitをクライアントとして使用する方法について学びます。

ある時点で、既存のプロジェクトをGitに変換したくなるかもしれません。この章の後半では、いくつかの特定のシステムからGitにプロジェクトを移行する方法と、事前に構築されたインポートツールが存在しない場合にも機能する方法について説明します。

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

Gitは開発者にとって非常に素晴らしい体験を提供するため、チームの他のメンバーがまったく異なるVCSを使用している場合でも、多くの人が自分のワークステーションでGitを使用する方法を見つけ出しました。これらの「ブリッジ」と呼ばれるアダプターが多数利用可能です。ここでは、遭遇する可能性が最も高いものを紹介します。

Git と Subversion

オープンソース開発プロジェクトの大部分と、かなりの数の企業プロジェクトがSubversionをソースコードの管理に使用しています。これは10年以上前から存在し、そのほとんどの期間、オープンソースプロジェクトのデファクトVCSでした。また、それ以前のソース管理の世界で一大勢力だったCVSと多くの点で非常に似ています。

Git の優れた機能の 1 つに、git svn と呼ばれる Subversion への双方向ブリッジがあります。このツールを使用すると、Git を Subversion サーバーの有効なクライアントとして使用できるため、Git のすべてのローカル機能を使用し、Subversion をローカルで使用しているかのように Subversion サーバーにプッシュできます。これにより、ローカルでのブランチ作成とマージ、ステージングエリアの使用、リベースとチェリーピックなどの操作が可能になりますが、共同作業者は依然として昔ながらの方法で作業を続けることができます。これは、企業環境に Git をこっそり導入し、インフラを完全に Git をサポートするように変更するよう働きかけながら、同僚の開発者の効率向上を支援する良い方法です。Subversion ブリッジは、DVCS の世界への入門薬です。

git svn

GitのすべてのSubversionブリッジングコマンドのベースコマンドはgit svnです。かなりの数のコマンドがあるため、いくつかの簡単なワークフローを通じて最も一般的なものを紹介します。

git svnを使用している場合、Gitとは大きく異なるシステムであるSubversionとやり取りしていることに注意することが重要です。ローカルでのブランチ作成やマージは**可能**ですが、リベースによって履歴をできるだけ線形に保ち、Gitリモートリポジトリとの同時対話のようなことを避けるのが一般的に最善です。

履歴を書き換えて再度プッシュしたり、並行するGitリポジトリにプッシュして同時にGit開発者と共同作業したりしないでください。Subversionは単一の線形履歴しか持てず、混乱させるのは非常に簡単です。チームで作業していて、一部がSVNを使用し、他がGitを使用している場合は、全員がSVNサーバーを使用して共同作業するようにしてください。そうすることで、作業が楽になります。

セットアップ

この機能をデモンストレーションするには、書き込みアクセス権のある典型的なSVNリポジトリが必要です。これらの例をコピーしたい場合は、SVNテストリポジトリの書き込み可能なコピーを作成する必要があります。それを簡単に行うには、Subversionに付属しているsvnsyncというツールを使用できます。

続けるには、まず新しいローカルのSubversionリポジトリを作成する必要があります

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

次に、すべてのユーザーがrevpropsを変更できるようにします。簡単な方法は、常に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をtoおよびfromリポジトリとともに呼び出すことで、このプロジェクトをローカルマシンに同期できます。

$ svnsync init file:///tmp/test-svn \
  http://your-svn-server.example.org/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未満であっても、プロセスに1時間近くかかります。Subversionは一度に1つのリビジョンをクローンし、それを別のリポジトリにプッシュし直す必要があります。これは途方もなく非効率ですが、これを行う唯一の簡単な方法です。

はじめに

書き込みアクセス権のある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 initgit svn fetchという2つのコマンドと同等の処理を実行します。これには時間がかかる場合があります。たとえば、テストプロジェクトのコミットが約75件しかなく、コードベースがそれほど大きくない場合でも、Gitは各バージョンを一度に1つずつチェックアウトし、個別にコミットする必要があります。数百、数千のコミットがあるプロジェクトでは、完了までに文字通り数時間、場合によっては数日かかることもあります。

-T trunk -b branches -t tagsの部分は、このSubversionリポジトリが基本的なブランチングとタグ付けの慣例に従っていることをGitに伝えます。もしあなたのトランク、ブランチ、タグの命名規則が異なる場合は、これらのオプションを変更できます。これは非常に一般的なため、この部分全体を-sに置き換えることができます。これは標準レイアウトを意味し、これらのオプションすべてを含みます。次のコマンドは同等です。

$ 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はタグをリモートブランチとして扱うのではなく、直接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チェックサムが変更されることを意味するため重要です。この理由もあって、Subversionサーバーと並行してプロジェクトのGitベースのリモートバージョンを操作することは良い考えではありません。最後のコミットを見ると、追加された新しい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

コミット時に4af61fdで始まっていたSHA-1チェックサムが、今では95e0222で始まっていることに注目してください。GitサーバーとSubversionサーバーの両方にプッシュしたい場合は、最初にSubversionサーバーにプッシュ(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の動作と非常によく似ています)。他の誰かが1つのファイルに変更をプッシュし、その後あなたが別のファイルに変更をプッシュした場合、あなたの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を実行すると、コードが常に最新の状態に保たれます。ただし、これを行う際には作業ディレクトリがクリーンであることを確認する必要があります。ローカルに変更がある場合は、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バージョンに表示されます。

他の誰かがその作業をクローンすると、彼らが見るのは、すべての作業が1つにまとめられたマージコミットだけで、まるでgit merge --squashを実行したかのようです。コミットの発生元やコミット日時に関するデータは見えません。

Subversion のブランチ機能

SubversionでのブランチはGitでのブランチと同じではありません。あまり使用しない方がおそらく最善です。ただし、git svnを使用してSubversionでブランチを作成し、コミットすることができます。

新しい SVN ブランチの作成

Subversionで新しいブランチを作成するには、git svn branch [new-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 は、現在のブランチ履歴にある Subversion ブランチの先端 (git-svn-id を持つ最後のブランチ) を探して、dcommit がどのブランチに送られるかを判断します。通常は 1 つだけ存在します。

複数のブランチで同時に作業したい場合、そのブランチのインポートされたSubversionコミットから開始することで、特定のSubversionブランチにdcommitするためのローカルブランチを設定できます。個別に作業できるoperaブランチが必要な場合は、以下を実行できます。

$ git branch opera remotes/origin/opera

これで、operaブランチをtrunk(あなたのmasterブランチ)にマージしたい場合は、通常のgit mergeでマージできます。ただし、説明的なコミットメッセージ(-m経由)を提供する必要があります。そうしないと、マージは有用な情報ではなく「Merge branch opera」と表示されます。

この操作にgit mergeを使用しているにもかかわらず、マージはSubversionで行うよりもはるかに簡単になる可能性が高い(Gitが自動的に適切なマージベースを検出してくれるため)ことを覚えておいてください。しかし、これは通常のGitマージコミットではありません。このデータを、複数の親を追跡するコミットを扱えないSubversionサーバーにプッシュする必要があるため、プッシュ後には、別のブランチのすべての作業が1つのコミットに圧縮された単一のコミットのように見えます。あるブランチを別のブランチにマージした後、通常Gitで行うように、簡単に元に戻ってそのブランチでの作業を続けることはできません。実行するdcommitコマンドは、どのブランチがマージされたかを示す情報をすべて消去するため、その後のマージベースの計算は間違ったものになります。dcommitは、git mergeの結果がgit merge --squashを実行したかのように見せます。残念ながら、この状況を回避する良い方法はありません。Subversionはこの情報を保存できないため、サーバーとして使用している間は常にその制限に縛られます。問題を避けるためには、ブランチをトランクにマージした後、ローカルブランチ(この場合はopera)を削除する必要があります。

Subversion コマンド

git svnツールセットは、Subversionにあった機能の一部を提供することで、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について知っておくべき重要な点が2つあります。まず、実際のsvn logコマンドとは異なり、Subversionサーバーにデータを問い合わせるのではなく、オフラインで動作します。次に、Subversionサーバーにコミットされたコミットのみが表示されます。まだdcommitしていないローカルのGitコミットは表示されません。また、その間にSubversionサーバーに他の人が行ったコミットも表示されません。これは、Subversionサーバー上のコミットの最後の既知の状態に近いものです。

SVN アノテーション

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

$ 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には、この問題を解決するのに役立つ2つのコマンドがあります。1つ目はgit svn create-ignoreで、これは対応する.gitignoreファイルを自動的に作成し、次のコミットに含めることができます。

2つ目のコマンドはgit svn show-ignoreで、.gitignoreファイルに記述する必要がある行を標準出力に表示するため、その出力をプロジェクトの除外ファイルにリダイレクトできます。

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

そうすれば、プロジェクトを.gitignoreファイルで汚すことはありません。これは、Subversionチームであなたが唯一のGitユーザーであり、チームメイトがプロジェクトに.gitignoreファイルを望まない場合に良い選択肢です。

Git-Svn の概要

git svnツールは、Subversionサーバーに縛られている場合や、Subversionサーバーの実行を必要とする開発環境にいる場合に役立ちます。しかし、これを不完全なGitと見なさないと、変換の問題に遭遇し、あなたや共同作業者を混乱させる可能性があります。問題を避けるために、次のガイドラインに従うようにしてください。

  • git mergeによって作成されたマージコミットを含まない線形なGit履歴を維持してください。メインラインブランチ外で行った作業はすべて、そこにリベースし、マージしないでください。

  • 独立したGitサーバーを設定して共同作業しないでください。新しい開発者のクローンを高速化するために1つ持つことは可能かもしれませんが、git-svn-idエントリのないものは何もプッシュしないでください。各コミットメッセージにgit-svn-idが含まれているかを確認し、含まれていないコミットのプッシュを拒否するpre-receiveフックを追加することもできます。

これらのガイドラインに従えば、Subversionサーバーとの作業はより耐えやすくなります。しかし、実際のGitサーバーに移行できるのであれば、そうすることでチームはより多くの恩恵を得ることができます。

Git と Mercurial

DVCSの世界はGitだけではありません。実際、この分野には他にも多くのシステムがあり、それぞれが分散バージョン管理を正しく行う方法について独自の視点を持っています。Git以外で最も人気があるのはMercurialで、この2つは多くの点で非常に似ています。

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にはもう1つの依存関係があります:Python用のmercurialライブラリです。Pythonがインストールされていれば、これは次のように簡単です。

$ pip install mercurial

Pythonがインストールされていない場合は、まずhttps://www.python.org/にアクセスして入手してください。

最後に必要なのはMercurialクライアントです。まだインストールしていない場合は、https://www.mercurial-scm.org/にアクセスしてインストールしてください。

さあ、準備は万端です。あとはプッシュできるMercurialリポジトリが必要です。幸いなことに、すべてのMercurialリポジトリはこのように動作するため、Mercurialを学ぶために誰もが使用する「ハローワールド」リポジトリを使用します。

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

はじめに

適切な「サーバーサイド」リポジトリができたので、典型的なワークフローを見ていきましょう。見ての通り、これら2つのシステムは十分似ており、あまり摩擦はありません。

いつものように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

Mercurialリポジトリを操作する場合でも、標準のgit cloneコマンドを使用することに気づくでしょう。これは、git-remote-hgがGitのHTTP/Sプロトコルが実装されているのと同様のメカニズム(リモートヘルパー)を使用して、かなり低いレベルで動作しているためです。GitとMercurialはどちらも、すべてのクライアントがリポジトリ履歴の完全なコピーを持つように設計されているため、このコマンドはプロジェクトの履歴全体を含む完全なクローンをかなり迅速に作成します。

logコマンドは2つのコミットを表示し、最新のコミットは多数のrefsによって指し示されています。これらのうち一部は実際には存在しないことが判明しました。.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を含むGit参照ファイルであり、これはmasterが指すコミットです。したがって、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は、ツリー内のアイテムのモード、タイプ、オブジェクトハッシュ、およびファイル名を出力します。ツリーアイテムの1つを深く掘り下げると、その中に「ac9117f」(masterが指すコミットのSHA-1ハッシュ)という名前のBLOBがあり、その内容は「0a04b98」(defaultブランチの先端にあるMercurialチェンジセットのID)であることがわかります。

良いニュースは、これらすべてについてほとんど心配する必要がないことです。典型的なワークフローは、Gitリモートを扱うのとあまり変わりません。

続ける前に、もう一つ対処すべきことがあります。それは「無視」です。MercurialとGitは非常に似たメカニズムを使用していますが、Mercurialリポジトリに実際に.gitignoreファイルをコミットしたくない場合があります。幸いなことに、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フラグを使用したため、git-remote-hgが内部的に使用する「notes」参照が表示されますが、これらは無視できます。残りは予想通りです。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の「ブランチ」の概念はより重いものです。チェンジセットが作成されたブランチは、**チェンジセットとともに**記録されるため、常にリポジトリ履歴に残ります。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ブランチとまったく同じように機能しますが、1つ例外があります。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

チェンジセット8、9、10は作成され、permanentブランチに属していますが、古いチェンジセットはまだ存在しています。これはMercurialを使用しているチームメイトにとって**非常に**混乱を招く可能性があるため、避けるようにしてください。

Mercurial の概要

GitとMercurialは十分に似ているため、境界を越えて作業してもほとんど苦痛はありません。マシンから離れた履歴の変更を避ければ(一般的に推奨されているように)、相手がMercurialであることにさえ気づかないかもしれません。

Git と Perforce

Perforceは、企業環境で非常に人気のあるバージョン管理システムです。1995年から存在しており、この章で扱うシステムの中で最も古いものです。そのため、当時の制約を考慮して設計されており、常に単一の中央サーバーに接続されており、ローカルディスクには1つのバージョンのみが保持されると仮定しています。確かに、その機能と制約はいくつかの特定の問題によく適合していますが、Gitの方が実際にうまく機能するようなPerforceを使用しているプロジェクトもたくさんあります。

PerforceとGitを組み合わせて使用したい場合、2つの選択肢があります。最初に説明するのは、Perforceのメーカーが提供する「Git Fusion」ブリッジです。これにより、Perforceデポのサブツリーを読み書き可能なGitリポジトリとして公開できます。2番目はgit-p4です。これはクライアントサイドのブリッジで、Perforceサーバーの再設定を必要とせずに、GitをPerforceクライアントとして使用できます。

Git Fusion

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

セットアップ

例では、PerforceデーモンとGit Fusionが動作する仮想マシンをダウンロードするという、Git Fusionの最も簡単なインストール方法を使用します。仮想マシンイメージはhttps://www.perforce.com/downloadsから入手でき、ダウンロードが完了したら、お好みの仮想化ソフトウェア(VirtualBoxを使用します)にインポートします。

マシンを初めて起動すると、3人のLinuxユーザー(rootperforcegit)のパスワードをカスタマイズし、同じネットワーク上の他のインストールと区別するために使用できるインスタンス名を提供するよう求められます。すべてが完了すると、次の画面が表示されます。

The Git Fusion virtual machine boot screen
図 171. Git Fusion 仮想マシンの起動画面

ここに表示されているIPアドレスに注意してください。後で使用します。次に、Perforceユーザーを作成します。下部の「Login」オプションを選択してエンターキーを押す(またはマシンにSSHで接続する)か、rootとしてログインします。次に、これらのコマンドを使用してユーザーを作成します。

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

最初のコマンドはVIエディターを開いてユーザーをカスタマイズしますが、:wqと入力してエンターキーを押すことでデフォルトを受け入れることができます。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はこの接続の認証情報を要求しますが、認証情報キャッシュを使用すると、その後の要求でこの手順をスキップできます。

フュージョン設定

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

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

498 directories, 287 files

objectsディレクトリはGit FusionがPerforceオブジェクトをGitに、またはその逆に対応付けるために内部的に使用されます。そこに何かをいじる必要はありません。このディレクトリにはグローバルなp4gf_configファイルがあり、各リポジトリにも1つずつあります。これらが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"

各行は<user> <email> "<full name>"の形式で、単一のユーザーマッピングを作成します。最初の2行は、2つの異なるメールアドレスを同じPerforceユーザーアカウントにマップします。これは、複数の異なるメールアドレス(または変更されたメールアドレス)でGitコミットを作成したが、それらを同じPerforceユーザーにマップしたい場合に役立ちます。PerforceチェンジセットからGitコミットを作成する場合、Perforceユーザーに一致する最初の行がGitの著者情報に使用されます。

最後の2行は、作成されるGitコミットからBobとJoeの実際の名前とメールアドレスを隠します。これは、社内プロジェクトをオープンソース化したいが、従業員ディレクトリを全世界に公開したくない場合に便利です。すべてのGitコミットを単一の架空の作者に帰属させたい場合を除き、メールアドレスとフルネームは一意であるべきであることに注意してください。

ワークフロー

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://john@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コミットに変換しているためです。これはサーバー側でローカルに行われるため比較的速いですが、履歴が多い場合は still some time. かかることがあります。その後のフェッチは増分変換を行うため、Gitのネイティブな速度に近い感覚になります。

ご覧のとおり、私たちのリポジトリは、あなたが扱う可能性のある他のGitリポジトリとまったく同じように見えます。3つのブランチがあり、Gitは親切にもorigin/masterを追跡するローカルのmasterブランチを作成しました。少し作業をして、新しいコミットをいくつか作成してみましょう。

# ...
$ 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のリビジョングラフ機能を使って、Perforceから見たREADMEファイルの履歴を見てみましょう。

Perforce revision graph resulting from Git push
図 172. Git プッシュの結果としての Perforce リビジョングラフ

このビューを初めて見た方には混乱するかもしれませんが、Gitの履歴をグラフィカルに表示するのと同様の概念を示しています。READMEファイルの履歴を見ています。左上のディレクトリツリーには、そのファイルがさまざまなブランチにどのように現れるかだけが表示されています。右上に、ファイルの異なるリビジョンがどのように関連しているかを視覚的に示したグラフがあり、このグラフの全体像が右下に表示されています。残りのビューは、選択されたリビジョン(この場合は2)の詳細ビューに割かれています。

注目すべきは、グラフがGitの履歴とまったく同じように見えることです。Perforceは12のコミットを保存する名前付きブランチを持っていなかったため、それらを保持するために.git-fusionディレクトリに「匿名」ブランチを作成しました。これは、名前付きPerforceブランチに対応しない名前付きGitブランチでも発生します(そして、後で構成ファイルを使用してPerforceブランチにマッピングできます)。

このほとんどは舞台裏で行われますが、最終的な結果として、チームの一人がGitを使用し、別の人がPerforceを使用しても、互いの選択について知ることはありません。

Git-Fusion の概要

Perforceサーバーへのアクセスがある(またはアクセスできる)場合、Git FusionはGitとPerforceを相互に通信させる優れた方法です。いくつかの設定が必要ですが、学習曲線はそれほど急ではありません。Gitの全機能を使用する際の注意点が現れない、この章の数少ないセクションの1つです。これは、Perforceがあなたが行うすべてに満足するわけではないという意味ではありません。すでにプッシュされた履歴を書き換えようとすると、Git Fusionはそれを拒否します。しかし、Git Fusionはネイティブな感覚を提供するために非常に努力しています。Gitのサブモジュールを使用することもできますし(Perforceユーザーには奇妙に見えるかもしれませんが)、ブランチをマージすることもできます(これはPerforce側で統合として記録されます)。

サーバー管理者にGit Fusionのセットアップを納得させられない場合でも、これらのツールを一緒に使う方法はあります。

Git-p4

Git-p4はGitとPerforceの間の双方向ブリッジです。これはGitリポジトリ内で完全に動作するため、Perforceサーバーへのアクセスは(もちろんユーザー認証情報以外に)必要ありません。Git-p4はGit Fusionほど柔軟性や完全性のあるソリューションではありませんが、サーバー環境に干渉することなく、必要なことのほとんどを行うことができます。

git-p4を操作するには、p4ツールがPATHのどこかにある必要があります。本執筆時点では、https://www.perforce.com/downloads/helix-command-line-client-p4から無料で入手できます。

セットアップ

例示目的で、上記で示したGit Fusion OVAからPerforceサーバーを実行しますが、Git FusionサーバーをバイパスしてPerforceバージョン管理に直接アクセスします。

git-p4が依存するp4コマンドラインクライアントを使用するには、いくつかの環境変数を設定する必要があります。

$ 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

Perforceサーバーにコミットする準備ができた新しいコミットが2つあります。今日、他の誰かが作業していたか確認しましょう。

$ 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 syncgit rebase p4/masterのショートカットです。特に複数のブランチで作業している場合はそれよりも少し賢いですが、これは良い近似です。

これで履歴は再び線形になり、変更をPerforceに貢献する準備ができました。git p4 submitコマンドは、p4/mastermaster間のすべてのGitコミットに対して新しい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>

これはほとんどp4 submitを実行したときに表示される内容と同じですが、git-p4が親切にも含めた最後の部分は異なります。Git-p4は、コミットまたはチェンジセットに名前を提供する必要がある場合、GitとPerforceの設定を個別に尊重しようとしますが、場合によってはそれを上書きしたいことがあります。たとえば、インポートしているGitコミットがPerforceユーザーアカウントを持たない貢献者によって書かれた場合でも、結果のチェンジセットが彼らが書いたように見せたい場合があります(あなたが書いたようにではなく)。

Git-p4は、GitコミットからのメッセージをこのPerforceチェンジセットの内容として親切にインポートしてくれました。したがって、私たちは保存して終了するだけで、2回(各コミットごとに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チェンジセットに変換されることに注意してください。それらを単一のチェンジセットにまとめる場合は、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ヘッドとのマージコミットがあり、さらに別のコミットがあります。これらの変更を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の短縮形で、submitコマンドが実際に実行された場合に何が起こるかを報告しようとします。この場合、Perforceサーバーにまだ存在しない3つの非マージコミットに対応する3つのPerforceチェンジセットを作成することになるようです。それはまさに私たちが望むように聞こえますが、どうなるか見てみましょう。

$ 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フラグは、Perforceのブランチ仕様を使用してブランチをGitの参照にマッピングするようにgit-p4に指示します。もしこれらのマッピングが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.branchList設定変数をmain:devに設定すると、git-p4に「main」と「dev」が両方ともブランチであり、2番目のブランチが最初のブランチの子であると伝えます。

もし今、git checkout -b dev p4/project/devを実行してコミットを行った場合、git p4 submitを実行する際に、git-p4は適切なブランチをターゲットにするのに十分賢いです。残念ながら、git-p4はシャロークローンと複数のブランチを混在させることはできません。もし巨大なプロジェクトで複数のブランチで作業したい場合、提出したい各ブランチごとにgit p4 cloneを一度ずつ実行する必要があります。

ブランチの作成や統合には、Perforceクライアントを使用する必要があります。Git-p4は既存のブランチへの同期と提出しかできず、一度に1つの線形チェンジセットしか処理できません。Gitで2つのブランチをマージして新しいチェンジセットを提出しようとすると、記録されるのは単なるファイルの変更の束だけであり、統合に関与するブランチに関するメタデータは失われます。

Git と Perforce の概要

Git-p4は、PerforceサーバーでGitワークフローを使用することを可能にし、それなりに優れています。ただし、Perforceがソースを管理しており、あなたはローカルで作業するためにGitを使用しているだけであることを覚えておくことが重要です。Gitコミットの共有には本当に注意してください。他の人が使用するリモートがある場合は、Perforceサーバーにまだ提出されていないコミットをプッシュしないでください。

PerforceとGitをソース管理のクライアントとして自由に併用したい場合で、サーバー管理者を説得してインストールさせることができれば、Git FusionはGitをPerforceサーバーの第一級バージョン管理クライアントにします。

scroll-to-top