章节 ▾ 第二版

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

世界并非完美。通常,你不能立即将所有接触到的项目都切换到 Git。有时你会被困在一个使用其他 VCS 的项目中,并希望它是 Git。本章的第一部分将学习在项目托管于不同系统时,如何使用 Git 作为客户端。

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

Git 作为客户端

Git 为开发者提供了如此愉快的体验,以至于许多人已经想出了如何在自己的工作站上使用它,即使他们的团队其他人都在使用完全不同的 VCS。有许多这样的适配器,被称为“桥梁”。在这里,我们将介绍你在实际中最可能遇到的那些。

Git 和 Subversion

很大一部分开源开发项目和相当多的企业项目都使用 Subversion 来管理其源代码。它已经存在十多年了,而且在这大部分时间里,它是开源项目_事实上的_VCS 选择。它在许多方面也与 CVS 非常相似,后者在 Subversion 之前是源代码控制领域的大佬。

Git 的一个强大功能是与 Subversion 的双向桥梁,称为 `git svn`。这个工具允许你将 Git 作为 Subversion 服务器的有效客户端使用,这样你就可以使用 Git 的所有本地功能,然后像在本地使用 Subversion 一样推送到 Subversion 服务器。这意味着你可以进行本地分支和合并、使用暂存区、使用变基和樱桃拣选等,而你的协作者则继续以他们古老而黑暗的方式工作。这是将 Git 偷偷引入企业环境并帮助你的开发同事提高效率的好方法,同时你也可以游说改变基础设施以完全支持 Git。Subversion 桥梁是通往 DVCS 世界的入门级毒品。

git svn

Git 中所有 Subversion 桥接命令的基础命令是 `git svn`。它接受相当多的命令,所以我们将通过几个简单的工作流程展示最常见的命令。

重要的是要注意,当你使用 `git svn` 时,你正在与 Subversion 交互,这是一个与 Git 工作方式非常不同的系统。虽然你**可以**进行本地分支和合并,但通常最好通过变基你的工作来保持你的历史记录尽可能线性,并避免同时与 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` 并指定源和目标仓库来将此项目同步到你的本地机器。

$ 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 仓库遵循基本的 branching 和 tagging 约定。如果你的主干、分支或标签命名不同,你可以更改这些选项。因为这很常见,所以你可以用 `-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`,它会拉取服务器上你尚未拥有的任何更改,并将你拥有的任何工作变基到服务器上的内容之上

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

Git 分支问题

当你习惯了 Git 工作流后,你可能会创建主题分支,在上面进行工作,然后将它们合并进来。如果你通过 `git svn` 推送到 Subversion 服务器,你可能希望每次都将你的工作变基到单个分支上,而不是将分支合并在一起。偏爱变基的原因是 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 服务器信息

你还可以通过运行 `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` 有两个命令可以帮助解决这个问题。第一个是 `git svn create-ignore`,它会自动为你创建相应的 `.gitignore` 文件,以便你的下一次提交可以包含它们。

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

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

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

Git-Svn 总结

`git svn` 工具非常有用,如果你被困在 Subversion 服务器上,或者身处需要运行 Subversion 服务器的开发环境中。但是,你应该将其视为被阉割的 Git,否则你会在转换过程中遇到问题,这可能会让你和你的协作者感到困惑。为了避免麻烦,请尝试遵循以下指导方针

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

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

如果你遵循这些指导方针,使用 Subversion 服务器会更轻松。但是,如果可能迁移到真正的 Git 服务器,这样做可以为你的团队带来更多好处。

Git 和 Mercurial

DVCS 的世界比 Git 还要大。事实上,这个领域还有许多其他系统,每个系统都有自己正确进行分布式版本控制的角度。除了 Git,最受欢迎的是 Mercurial,两者在许多方面都非常相似。

好消息是,如果你更喜欢 Git 的客户端行为,但正在处理一个源代码由 Mercurial 控制的项目,那么有一种方法可以使用 Git 作为 Mercurial 托管仓库的客户端。由于 Git 与服务器仓库的通信是通过远程进行的,因此这个桥接器以远程助手(remote helper)的形式实现也就不足为奇了。该项目的名称是 git-remote-hg,可以在 https://github.com/felipec/git-remote-hg 找到。

git-remote-hg

首先,你需要安装 git-remote-hg。这基本上只需要将其文件放到你的路径中的某个位置,如下所示

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

... 假设 `~/bin` 在你的 `$PATH` 中。Git-remote-hg 还有一个依赖项:Python 的 `mercurial` 库。如果你安装了 Python,这就像这样简单

$ pip install mercurial

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

你还需要 Mercurial 客户端。如果没有安装,请前往 https://www.mercurial-scm.org/ 并安装它。

现在你准备好了。你只需要一个可以推送的 Mercurial 仓库。幸运的是,每个 Mercurial 仓库都可以这样操作,所以我们只使用每个人学习 Mercurial 时都会使用的“hello world”仓库

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

入门

现在我们有了一个合适的“服务器端”仓库,我们可以进行一个典型的工作流程。你会看到,这两个系统足够相似,以至于摩擦很小。

与 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` 命令显示了两个提交,其中最新的提交被一堆引用指向。事实证明,其中一些实际上并不存在。让我们看看 `.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”的 blob(`master` 指向的提交的 SHA-1 哈希),内容是“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 内部使用的“注释”引用,但我们可以忽略它们。其余的是我们预期的;`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

编号为 _2_ 的变更集是由 Mercurial 制作的,而编号为 _3_ 和 _4_ 的变更集是由 git-remote-hg 制作的,通过推送用 Git 制作的提交。

分支和书签

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 会关心。

创建 Mercurial 书签就像创建 Git 分支一样容易。在 Git 端

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

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

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

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

你也可以在一个“重量级”的 Mercurial 分支上工作:只需在 `branches` 命名空间中放置一个分支

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

在 Mercurial 端,它看起来像这样

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

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

从 Git 方面来看,使用这两种分支风格的工作方式是相同的:只需像往常一样 checkout、commit、fetch、merge 和 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 仓库的子树公开为读写 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”选项并按回车键(或 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 会要求提供此连接的凭据,但凭据缓存将允许我们跳过后续请求的此步骤。

融合配置

安装 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` 允许你将一个在 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>"`,并创建一个单一的用户映射。前两行将两个不同的电子邮件地址映射到同一个 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 贴心地创建了一个跟踪 `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.
[...]

我们有两次新的提交。现在我们看看是否还有其他人在工作

$ 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 没有命名分支来存储 `1` 和 `2` 提交,因此它在 `.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

看起来他们是,而且 `master` 和 `p4/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 rebase` 是 `git p4 sync` 后跟 `git rebase p4/master` 的快捷方式。它比这更智能一些,尤其是在处理多个分支时,但这只是一个很好的近似。

现在我们的历史记录再次变为线性,我们准备将更改贡献回 Perforce。`git p4 submit` 命令将尝试为 `p4/master` 和 `master` 之间每个 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` 之前通过交互式变基来完成。另请注意,所有作为变更集提交的提交的 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 使得将 Git 工作流程与 Perforce 服务器一起使用成为可能,并且它做得相当好。但是,重要的是要记住 Perforce 负责源代码,而你只是使用 Git 在本地工作。请务必谨慎分享 Git 提交;如果你有一个其他人使用的远程仓库,不要推送任何尚未提交到 Perforce 服务器的提交。

如果你想自由地混合使用 Perforce 和 Git 作为源代码控制客户端,并且你可以说服服务器管理员安装它,那么 Git Fusion 会使 Git 成为 Perforce 服务器的一等版本控制客户端。