章节 ▾ 第二版

9.1 Git 和其他系统 - Git 作为客户端

世界并不完美。通常,您无法立即将遇到的每个项目都切换到 Git。有时您会陷入一个使用其他版本控制系统的项目,并希望它是 Git。我们将花本章的第一部分来学习当您正在处理的项目托管在不同的系统上时,如何将 Git 作为客户端使用。

在某个时候,您可能希望将现有项目转换为 Git。本章的第二部分将介绍如何从几个特定的系统迁移项目到 Git,以及一种适用于没有预构建导入工具的情况的方法。

Git 作为客户端

Git 为开发者提供了如此好的体验,以至于许多人已经找到了在工作站上使用它的方法,即使他们的团队其他成员正在使用完全不同的版本控制系统。有许多这样的适配器,称为“桥接器”,可用。在这里,我们将介绍您在实践中最可能遇到的。

Git 和 Subversion

很大一部分开源开发项目和许多企业项目使用 Subversion 来管理它们的源代码。它已经存在十多年了,并且在大部分时间里是开源项目的事实上的版本控制系统选择。它在许多方面与 CVS 非常相似,而 CVS 在此之前是源代码控制领域的巨头。

Git 的一个伟大特性是与 Subversion 的双向桥接,称为 `git svn`。这个工具允许您将 Git 作为 Subversion 服务器的有效客户端,因此您可以使用 Git 的所有本地功能,然后将更改推送到 Subversion 服务器,就像您在本地使用 Subversion 一样。这意味着您可以进行本地分支和合并,使用暂存区,使用 rebase 和 cherry-pick 等等,而您的协作者可以继续以他们古老的方式工作。这是将 Git 悄悄引入企业环境并帮助您的同事提高效率的好方法,同时您也可以游说更改基础设施以完全支持 Git。Subversion 桥是通往分布式版本控制系统世界的一条捷径。

git svn

Git 中所有 Subversion 桥接命令的基础命令是 `git svn`。它接受许多命令,因此我们将通过几个简单的流程演示最常用的命令。

需要注意的是,当您使用 `git svn` 时,您正在与 Subversion 交互,这是一个与 Git 工作方式非常不同的系统。尽管您可以进行本地分支和合并,但通常最好通过 rebase 您的工作来保持历史记录的尽可能线性,并避免同时与 Git 远程仓库进行交互。

不要重写您的历史记录并再次尝试推送,也不要同时推送到并行的 Git 仓库以与 Git 开发者协作。Subversion 只能有一个单一的线性历史记录,并且很容易弄混它。如果您与团队一起工作,并且有些人使用 SVN 而其他人使用 Git,请确保每个人都使用 SVN 服务器进行协作——这样做会使您的生活更轻松。

设置

为了演示此功能,您需要一个您拥有写访问权限的典型 SVN 仓库。如果您想复制这些示例,您必须制作一个可写的 SVN 测试仓库副本。为了轻松做到这一点,您可以使用一个名为 `svnsync` 的工具,它随 Subversion 一起提供。

要继续,您首先需要创建一个新的本地 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`,指定目标和源仓库,来将此项目同步到您的本地计算机。

$ 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 次提交。Subversion 必须一次克隆一个版本,然后将其推送到另一个仓库——这效率低下得离谱,但它是最容易做到这一点的方法。

入门

现在您有了一个拥有写访问权限的 Subversion 仓库,您可以进行典型的工作流程。您将从 `git svn clone` 命令开始,该命令将整个 Subversion 仓库导入到本地 Git 仓库。请记住,如果您从实际托管的 Subversion 仓库导入,您应该将 `file:///tmp/test-svn` 替换为您的 Subversion 仓库的 URL。

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

这会针对您提供的 URL 执行 `git svn init` 和 `git svn fetch` 这两个命令。这可能需要一些时间。例如,即使测试项目只有大约 75 次提交,代码库也不是很大,Git 仍然必须一次签出每个版本并单独提交。对于有数百甚至数千次提交的项目,这可能需要数小时甚至数天才能完成。

`-T trunk -b branches -t tags` 部分告诉 Git 该 Subversion 仓库遵循基本的分支和标签约定。如果您命名您的 trunk、branches 或 tags 不同,您可以更改这些选项。由于这是非常常见的,您可以将整个部分替换为 `-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 校验和都会发生变化。部分原因在于,同时与项目的 Git 远程版本以及 Subversion 服务器一起工作不是一个好主意。如果您查看最后一次提交,您可以看到添加了新的 `git-svn-id`。

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

    Adding git-svn instructions to the README

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

请注意,最初提交时以 `4af61fd` 开头的 SHA-1 校验和现在以 `95e0222` 开头。如果您想同时推送到 Git 服务器和 Subversion 服务器,您必须先推送到(`dcommit`)Subversion 服务器,因为该操作会更改您的提交数据。

拉取新更改

如果您与其他开发者一起工作,那么总有一天,您中的一个人会推送,然后另一个人会尝试推送一个冲突的更改。该更改将被拒绝,直到您合并他们的工作。在 `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`,它会拉取服务器上您尚未拥有的任何更改,并将您上面的所有工作 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 要求您在推送之前合并您尚未本地拥有的上游工作,而 `git svn` 只在更改冲突时才要求您这样做(很像 Subversion 的工作方式)。如果其他人推送了对一个文件的更改,然后您推送了对另一个文件的更改,那么您的 `dcommit` 将正常工作。

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

这一点很重要,因为结果是您在推送时,您的计算机上都不存在一个项目状态。如果更改不兼容但没有冲突,您可能会遇到难以诊断的问题。这与使用 Git 服务器不同——在 Git 中,您可以在发布之前在本地系统上完全测试状态,而在 SVN 中,您永远无法确定提交前和提交后的状态是相同的。

即使您不准备自己提交,也应该运行此命令来拉取 Subversion 服务器上的更改。您可以运行 `git svn fetch` 来获取新数据,但 `git svn rebase` 会执行 fetch 并然后更新您的本地提交。

$ 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` 之前将您的工作 stash 或暂时提交——否则,如果命令发现 rebase 会导致合并冲突,它将停止。

Git 分支问题

当您熟悉了 Git 工作流程后,您可能会创建主题分支,在上面进行工作,然后将它们合并。如果您通过 `git svn` 推送到 Subversion 服务器,您可能希望每次都将您的工作 rebase 到一个单独的分支上,而不是合并分支。偏好 rebase 的原因是 Subversion 具有线性历史记录,并且不像 Git 那样处理合并,因此 `git svn` 在将快照转换为 Subversion 提交时只跟随第一个父级。

假设您的历史记录如下:您创建了一个 `experiment` 分支,进行了两次提交,然后将它们合并回 `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 服务器上运行。需要注意的是,它不会将您签出到该分支;如果您此时提交,该提交将发送到服务器上的 `trunk`,而不是 `opera`。

切换活动分支

Git 通过查找您在历史记录中任何 Subversion 分支的尖端来确定您的 dcommits 将发送到哪个分支——您应该只有一个,并且它应该是当前分支历史记录中带有 `git-svn-id` 的最后一个。

如果您想同时处理多个分支,您可以设置本地分支以 `dcommit` 到特定的 Subversion 分支,方法是从该分支的导入的 Subversion 提交开始。如果您想要一个可以单独处理的 `opera` 分支,您可以运行:

$ git branch opera remotes/origin/opera

现在,如果您想将您的 `opera` 分支合并到 `trunk`(您的 `master` 分支),您可以使用普通的 `git merge` 来完成。但是您需要提供一个描述性的提交消息(通过 `-m`),否则合并将显示“Merge branch opera”而不是有用的信息。

请记住,尽管您正在使用 `git merge` 来执行此操作,并且合并很可能比在 Subversion 中更容易(因为 Git 会自动为您检测适当的合并基准),但这并不是一个普通的 Git 合并提交。您需要将这些数据推送到一个无法处理跟踪多个父项的提交的 Subversion 服务器;因此,在将其推上去之后,它将看起来像一个合并了另一个分支所有工作的单个提交。在一个分支合并到另一个分支之后,您就无法像在 Git 中那样轻松地返回并继续处理该分支。您运行的 `dcommit` 命令会删除任何表明哪个分支被合并的信息,因此后续的合并基准计算将是错误的——`dcommit` 会使您的 `git merge` 结果看起来像是您运行了 `git merge --squash`。不幸的是,没有好的方法可以避免这种情况——Subversion 无法存储此信息,因此当您将其用作服务器时,您将永远受其限制的限制。为避免问题,您应该在将分支(在本例中为 `opera`)合并到 trunk 后将其删除。

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`,您应该知道两件重要的事情。第一,它离线工作,不像真正的 `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)

这与 `blame` 和 `log` 类似,它离线运行,并且仅在您上次与 Subversion 服务器通信时才更新。

忽略 Subversion 忽略的内容

如果您克隆了一个设置了 `svn:ignore` 属性的 Subversion 仓库,您很可能希望设置相应的 `.gitignore` 文件,以免意外提交不应提交的文件。`git svn` 有两个命令可以帮助解决此问题。第一个是 `git svn create-ignore`,它会自动为您创建相应的 `.gitignore` 文件,以便您的下一次提交可以包含它们。

第二个命令是 `git svn show-ignore`,它将需要放入 `.gitignore` 文件中的行打印到标准输出,这样您就可以将输出重定向到您的项目排除文件。

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

这样,您就不会在项目中散布 `.gitignore` 文件。如果您是 Subversion 团队中唯一的 Git 用户,而您的队友不希望项目中有 `.gitignore` 文件,这是一个不错的选择。

Git-Svn 总结

如果您被困于 Subversion 服务器,或者处于需要运行 Subversion 服务器的开发环境中,`git svn` 工具集就很有用。但是,您应该将其视为被削弱的 Git,否则您会遇到翻译上的问题,这些问题可能会让您和您的协作者感到困惑。为了避免麻烦,请尝试遵循以下准则:

  • 保持线性的 Git 历史记录,不包含由 `git merge` 创建的合并提交。将您在主线分支之外所做的任何工作 rebase 回主线分支;不要将其合并进来。

  • 不要设置并协作一个独立的 Git 服务器。可能有一个来加快新开发者的克隆速度,但不要推任何没有 `git-svn-id` 条目的内容。您甚至可能想添加一个 `pre-receive` 钩子,该钩子检查每个提交消息中是否有 `git-svn-id` 并拒绝包含该消息的推送。

如果您遵循这些准则,与 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`。这基本上需要将它的文件放到您的 PATH 中的某个地方,如下所示:

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

假设 `~/bin` 在您的 `$PATH` 中。`git-remote-hg` 还有另一个依赖项:Python 的 `mercurial` 库。如果您安装了 Python,这很简单:

$ pip install mercurial

如果您没有安装 Python,请访问 https://pythonlang.cn/ 并先获取它。

您需要的最后一件事是 Mercurial 客户端。前往 https://www.mercurial-scm.org/ 并安装它(如果您还没有安装)。

现在您可以开始使用了。您只需要一个您可以推送的 Mercurial 仓库。幸运的是,每个 Mercurial 仓库都可以充当此角色,因此我们将使用每个人都用来学习 Mercurial 的“hello world”仓库。

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

入门

现在我们有了一个合适的“服务器端”仓库,我们可以进行典型的工作流程。您会发现,这两个系统足够相似,因此摩擦很少。

一如既往,我们首先克隆:

$ 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` 命令显示两次提交,其中最新的由一堆引用指向。事实证明其中一些实际上并不存在。让我们看看 `.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 的习惯,但底层它管理着两个略有不同的系统之间的概念映射。`refs/hg` 目录是实际远程引用的存储位置。例如,`refs/hg/origin/branches/default` 是一个 Git 引用文件,其中包含以“ac7955c”开头的 SHA-1,这是 `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` 输出树中项目的模式、类型、对象哈希和文件名。一旦我们深入到其中一个树项,我们会发现里面有一个名为“ac9117f”(`master` 指向的提交的 SHA-1 哈希)的 blob,内容为“0a04b98”(这是 `default` 分支尖端的 Mercurial 变更集 ID)。

好消息是,我们大部分时间都不需要担心所有这些。典型的工作流程与使用 Git 远程仓库的工作流程差别不大。

还有一件事我们应该处理:忽略。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` 超前两次提交,但这两次提交仅存在于我们的本地机器上。让我们看看是否有人同时在进行重要的工作:

$ 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` 前进了一个提交,我们的历史记录现在已经分叉。与其他在本章中处理的系统不同,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

由 Mercurial 制作的变更集编号为 *2*,由 `git-remote-hg` 通过推送 Git 提交创建的变更集编号为 *3* 和 *4*。

分支和书签

Git 只有一种分支:当进行提交时会移动的引用。在 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 引用),但 `git-remote-hg` 需要理解区别,因为 Mercurial 会在意。

在 Git 端创建 Mercurial 书签与创建 Git 分支一样简单:

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

就这样。在 Mercurial 端,它看起来像这样:

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

请注意修订版 5 上的新 `[featureA]` 标签。这些在 Git 端的作用与 Git 分支完全相同,有一个例外:您无法从 Git 端删除书签(这是远程助手的限制)。

您也可以处理一个“重量级”Mercurial 分支:只需在 `branches` 命名空间中放置一个分支。

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

在 Mercurial 端,它看起来像这样:

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

名称为“permanent”的分支与标记为 *7* 的变更集一起记录。

从 Git 端来看,处理这两种分支风格是相同的:只需像往常一样 checkout、commit、fetch、merge、pull 和 push。您应该知道的一件事是,Mercurial 不支持重写历史,只支持添加。这是我们 Mercurial 仓库在交互式 rebase 和 force-push 后的样子:

$ 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 年以来一直存在,这使其成为本章中介绍的最古老的系统。因此,它按照其时代的限制进行设计;它假定您始终连接到单个中央服务器,并且本地磁盘上只保留一个版本。当然,它的功能和限制非常适合几种特定问题,但有许多使用 Perforce 的项目实际上使用 Git 会更好。

如果您想混合使用 Perforce 和 Git,有两种选择。我们首先介绍的是 Perforce 制造商提供的“Git Fusion”桥接器,它允许您将 Perforce depot 的子树暴露为读写 Git 仓库。第二个是 `git-p4`,一个客户端桥接器,它允许您将 Git 作为 Perforce 客户端使用,而无需重新配置 Perforce 服务器。

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)。

第一次启动机器时,它会要求您自定义三个 Linux 用户(`root`、`perforce` 和 `git`)的密码,并提供一个实例名称,该名称可用于区分同一网络上的其他安装。完成所有操作后,您将看到:

The Git Fusion virtual machine boot screen
图 171. Git Fusion 虚拟机启动屏幕

您应该注意这里显示的 IP 地址,我们稍后会使用它。接下来,我们将创建一个 Perforce 用户。选择底部的“Login”选项并按 Enter(或 SSH 到机器),然后以 `root` 身份登录。然后使用以下命令创建用户:

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

第一个命令将打开 VI 编辑器来定制用户,但您可以通过输入 `:wq` 并按 Enter 来接受默认值。第二个命令会提示您输入两次密码。这就是我们需要从 shell 提示符执行的所有操作,因此退出会话。

接下来,您需要告诉 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.

虚拟机镜像配备了一个样本项目供您克隆。这里我们通过 HTTPS 克隆,使用上面创建的 `john` 用户;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` 目录由 Git Fusion 内部用于映射 Perforce 对象到 Git 反之亦然,您无需处理其中的任何内容。该目录中有一个全局 `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

我们在此处不详细解释这些标志的含义,但请注意这是一个 INI 格式的文本文件,就像 Git 用于配置一样。此文件指定了全局选项,这些选项可以被仓库特定的配置文件(如 `repos/Talkhouse/p4gf_config`)覆盖。如果您打开此文件,您会看到一个 `[@repo]` 部分,其中包含一些与全局默认值不同的设置。您还会看到类似以下内容的节:

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

这是 Perforce 分支和 Git 分支之间的映射。该节的名称可以随意命名,只要名称是唯一的即可。`git-branch-name` 允许您将 Perforce 仓库路径转换为更友好的名称,这种路径在 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"

每行格式为 ` ""`,并创建一个用户映射。前两行将两个不同的电子邮件地址映射到同一个 Perforce 用户帐户。如果您在多个不同的电子邮件地址下创建了 Git 提交(或更改了电子邮件地址),但希望将它们映射到同一个 Perforce 用户,这会很有用。在从 Perforce 变更集创建 Git 提交时,第一条匹配 Perforce 用户的行将用于 Git 作者信息。

最后两行隐藏了 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 提交。这在服务器本地进行,所以速度相对较快,但如果您的历史记录很多,仍然可能需要一些时间。后续的 fetch 会进行增量转换,所以感觉上会更像 Git 的原生速度。

正如您所见,我们的仓库看起来就像您可能使用的任何其他 Git 仓库一样。有三个分支,Git 已经很方便地创建了一个本地 `master` 分支来跟踪 `origin/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.
[...]

我们有两个新提交。现在让我们检查一下是否有人在工作:

$ 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 认为它已成功完成。让我们从 Perforce 的角度,使用 p4v 的修订图功能,来查看 README 文件的历史记录。

Perforce revision graph resulting from Git push
图 172. Git push 产生的 Perforce 修订图

如果您以前从未见过此视图,它可能会让您感到困惑,但它显示的概念与 Git 历史记录的图形查看器相同。我们正在查看 README 文件的历史记录,因此左上角的目录树仅显示该文件在各种分支中的出现。在右上角,我们有一个可视化的图,显示了文件不同修订版本之间的关系,而此图的全局视图则在右下角。视图的其余部分用于显示选定修订版本(本例中为 2)的详细信息。

值得注意的是,该图与 Git 历史记录中的图看起来完全相同。Perforce 没有一个命名的分支来存储 12 提交,因此它在 .git-fusion 目录中创建了一个“匿名”分支来保存它。对于未对应于 Perforce 命名分支的 Git 命名分支,也会发生这种情况(您以后可以使用配置文件将它们映射到 Perforce 分支)。

其中大部分发生在后台,但最终结果是,团队中的一个人可以使用 Git,另一个人可以使用 Perforce,而他们都不会知道对方的选择。

Git-Fusion 总结

如果您可以(或能够)访问您的 Perforce 服务器,Git Fusion 是让 Git 和 Perforce 进行通信的好方法。虽然需要一些配置,但学习曲线并不陡峭。这是本章中为数不多的没有关于使用 Git 全部强大功能的警告的部分之一。这并不是说 Perforce 会对您扔给它的所有东西都满意——如果您尝试重写已经推送的历史记录,Git Fusion 将会拒绝它——但 Git Fusion 非常努力地让您感觉它像原生工具一样。您甚至可以使用 Git 子模块(尽管 Perforce 用户看起来会很奇怪),以及合并分支(这将在 Perforce 端记录为集成)。

如果您无法说服您的服务器管理员设置 Git Fusion,仍然有一种方法可以一起使用这些工具。

Git-p4

Git-p4 是 Git 和 Perforce 之间的双向桥梁。它完全在您的 Git 存储库内部运行,因此您不需要任何 Perforce 服务器的访问权限(除了用户凭据,当然)。Git-p4 不如 Git Fusion 灵活或完整,但它允许您完成大部分您想要做的事情,而不会侵入服务器环境。

注意

您需要将 p4 工具放在 PATH 中才能使用 git-p4。截至目前,它可在 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 服务器。让我们检查一下今天是否还有其他人正在工作。

$ 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 建议您重新调整提交(rebase)您的提交,甚至提供了一个快捷方式来实现这一点。

$ 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 变更集的内容导入,因此我们只需保存并退出两次(每次提交一次)。生成的 shell 输出看起来大致如下。

$ 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 之前使用交互式 rebase 来实现。另请注意,所有被提交为变更集的提交的 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 端有两个提交,然后是一个带有 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 的缩写,它会尝试报告如果提交命令实际运行时会发生什么。在这种情况下,看起来我们将会创建三个 Perforce 变更集,它们对应于尚未在 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

我们的历史记录已变为线性,就像我们在提交之前进行了 rebase 一样(事实上,这正是发生的情况)。这意味着您可以自由地在 Git 端创建、处理、丢弃和合并分支,而无需担心您的历史记录会以某种方式与 Perforce 不兼容。如果您能 rebase 它,您就可以将其贡献到 Perforce 服务器。

分支

如果您的 Perforce 项目有多个分支,您不必绝望;git-p4 可以以一种让它感觉像 Git 的方式来处理这种情况。假设您的 Perforce 仓库布局如下。

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

假设您有一个 dev 分支,其视图规范如下。

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

Git-p4 可以自动检测这种情况并做正确的事情。

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

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

请注意仓库路径中的“@all”说明符;这会告诉 git-p4 不仅克隆该子树的最新变更集,还克隆所有曾经触及过这些路径的变更集。这更接近 Git 的克隆概念,但如果您正在处理一个历史悠久的项目,这可能需要一段时间。

--detect-branches 标志告诉 git-p4 使用 Perforce 的分支规范将分支映射到 Git 引用。如果 Perforce 服务器上不存在这些映射(这是使用 Perforce 的完全有效的方式),您可以告诉 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”都是分支,而第二个是第一个的子分支。

如果我们现在 git checkout -b dev p4/project/dev 并进行一些提交,git-p4 会足够智能地在执行 git p4 submit 时定位正确的分支。不幸的是,git-p4 无法混合使用浅层克隆和多个分支;如果您有一个大型项目并想处理多个分支,您将不得不为要提交的每个分支执行一次 git p4 clone

要创建或集成分支,您必须使用 Perforce 客户端。Git-p4 只能同步和提交到现有分支,而且它一次只能执行一个线性变更集。如果您在 Git 中合并两个分支并尝试提交新的变更集,所有记录的将是大量的文件更改;有关哪些分支参与集成的元数据将丢失。

Git 和 Perforce 总结

Git-p4 使在 Perforce 服务器上使用 Git 工作流程成为可能,并且它在这方面做得相当好。但是,重要的是要记住 Perforce 是源代码的管理者,而您仅使用 Git 进行本地工作。请务必小心共享 Git 提交;如果您有一个其他人使用的远程仓库,请不要推送任何尚未提交到 Perforce 服务器的提交。

如果您想自由地将 Perforce 和 Git 作为源代码控制的客户端混合使用,并且能够说服服务器管理员安装它,Git Fusion 可以让 Git 成为 Perforce 服务器的顶级版本控制客户端。