Git
章 ▾ 第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

すべてのSubversionブリッジコマンドのGitの基本コマンドは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リポジトリができたので、典型的なワークフローを実行できます。まず、Subversionリポジトリ全体をローカルGitリポジトリにインポートするgit svn cloneコマンドから始めます。実際のホストされている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という2つのコマンドを実行するのと同等です。これには時間がかかる場合があります。たとえば、テストプロジェクトに約75のコミットしかなく、コードベースがそれほど大きくない場合でも、Gitは各バージョンを一度に1つずつチェックアウトし、個別にコミットする必要があります。数百または数千のコミットがあるプロジェクトの場合、これには文字通り数時間、または数日かかる場合さえあります。

-T trunk -b branches -t tagsの部分は、GitにこのSubversionリポジトリが基本的なブランチングとタグ付けの規則に従っていることを伝えます。トランク、ブランチ、またはタグに異なる名前を付ける場合は、これらのオプションを変更できます。これは非常に一般的なため、この部分全体を-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 plumbingコマンド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クライアントとして効果的に使用して、コミットをアップストリームにプッシュできます。ファイルの1つを編集してコミットすると、Subversionサーバーには存在しない、ローカルGitに存在するコミットが作成されます。

$ 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バージョンに表示されます。

他の誰かがその作業をクローンすると、すべてがそこに圧縮されたマージコミットのみが表示され、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サーバーで操作します。そのブランチにチェックアウトされないことに注意することが重要です。この時点でコミットした場合、そのコミットはoperaではなくサーバー上のtrunkに送信されます。

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

Gitは、履歴内のSubversionブランチの先端を探すことによって、dcommitがどのブランチに送信されるかを判断します。1つだけである必要があり、現在のブランチ履歴にgit-svn-idがある最後のブランチである必要があります。

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

$ git branch opera remotes/origin/opera

次に、operaブランチをtrunkmasterブランチ)にマージする場合は、通常の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 つの重要なことを知っておく必要があります。1 つ目は、実際の svn log コマンドとは異なり、Subversion サーバーにデータを要求することなくオフラインで動作することです。2 つ目は、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 サーバー情報

git svn info を実行して、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)

これは blame および log と同様に、オフラインで実行され、Subversion サーバーと最後に通信した時点まで最新の状態になります。

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

どこかに svn:ignore プロパティが設定されている Subversion リポジトリをクローンすると、誤ってコミットすべきでないファイルをコミットしないように、対応する .gitignore ファイルを設定する必要があるでしょう。git svn には、この問題を解決するための 2 つのコマンドがあります。1 つ目は git svn create-ignore で、対応する .gitignore ファイルを自動的に作成するため、次のコミットに含めることができます。

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

$ 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 で、両者は多くの点で非常によく似ています。

Git のクライアント側の動作を好むが、ソースコードが Mercurial で制御されているプロジェクトで作業している場合、Git を Mercurial ホストのリポジトリのクライアントとして使用する方法があるという良いニュースがあります。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 を学習するために誰もが使用する「hello world」リポジトリを使用します。

$ 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 つのコミットを表示し、その最新のコミットは大量の参照によって示されています。これらのいくつかは実際には存在しないことがわかります。実際に .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 リモートを使用する場合とそれほど違いはありません。

続行する前にもう 1 つ注意しておく必要があることがあります。それは無視です。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 フラグを使用しているため、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-remote-hg によって、Git で作成されたコミットをプッシュすることによって作成されました。

ブランチとブックマーク

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 側から見ると、これらのブランチスタイルのどちらを使用しても作業は同じです。通常どおりに、チェックアウト、コミット、フェッチ、マージ、プル、およびプッシュを行うだけです。知っておくべきことの 1 つは、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

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

Mercurial の概要

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

GitとPerforce

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

PerforceとGitの使用を混在させたい場合は、2つのオプションがあります。最初に説明するのは、Perforceのメーカーが提供する「Git Fusion」ブリッジで、Perforceデポのサブツリーを読み書き可能なGitリポジトリとして公開できます。2つ目は、Perforceサーバーの再構成を必要とせずに、GitをPerforceクライアントとして使用できるクライアントサイドブリッジであるgit-p4です。

Git Fusion

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

セットアップ

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

マシンを最初に起動すると、3つのLinuxユーザー(rootperforce、およびgit)のパスワードをカスタマイズし、このインストールを同じネットワーク上の他のインストールと区別するために使用できるインスタンス名を指定するように求められます。すべてが完了すると、これが表示されます。

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

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

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

最初のコマンドは、ユーザーをカスタマイズするための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のauthor/committerフィールドに使用します。逆方向に変換する場合、デフォルトでは、Gitコミットのauthorフィールドに保存されているメールアドレスで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行は、BobとJoeの実際の名前とメールアドレスを、作成されたGitコミットからマスクします。これは、内部プロジェクトをオープンソースにしたいが、従業員ディレクトリを全世界に公開したくない場合に便利です。メールアドレスとフルネームは、すべての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コミットに変換しています。これはサーバー上でローカルに行われるため、比較的高速ですが、履歴が多い場合は、それでも時間がかかる可能性があります。後続のフェッチは増分変換を行うため、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)の詳細ビューに表示されます。

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

このほとんどはバックグラウンドで実行されますが、最終的な結果は、チームの1人が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 バージョン管理に直接接続します。

p4 コマンドラインクライアント(git-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 sync に続いて git 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 チェンジセットの内容としてインポートしているため、保存して終了するだけで(コミットごとに 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 が変換する各コミットの最後に 1 行を追加するためです。

$ 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

775a46f 以降、Git と Perforce の履歴が分岐しています。Git 側には 2 つのコミットがあり、次に Perforce のヘッドとのマージコミットがあり、その後にもう 1 つコミットがあります。これらを 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

履歴は、送信前にリベースしたかのように(実際にはまさにそうであった)、線形になりました。これは、Perforce と互換性がなくなることを恐れることなく、Git 側でブランチを作成、作業、破棄、マージできることを意味します。リベースできるなら、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 は浅いクローンと複数のブランチを混在させることはできません。大規模なプロジェクトがあり、複数のブランチで作業したい場合は、送信するブランチごとに 1 回 git p4 clone を実行する必要があります。

ブランチを作成または統合するには、Perforce クライアントを使用する必要があります。Git-p4 は、既存のブランチへの同期と送信のみが可能であり、一度に 1 つの線形チェンジセットしか実行できません。Git で 2 つのブランチをマージして新しいチェンジセットを送信しようとすると、記録されるのは多数のファイル変更のみです。統合に関与するブランチに関するメタデータは失われます。

Git と Perforce の概要

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

Perforce と Git の使用をソース管理のクライアントとして自由に混在させたい場合は、サーバー管理者にインストールを説得できるなら、Git Fusion を使用すると、Perforce サーバーのファーストクラスのバージョン管理クライアントとして Git を使用できます。

scroll-to-top