章节 ▾ 第二版

9.1 Git 与其他系统 - 将 Git 作为客户端使用

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

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

将 Git 作为客户端使用

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

Git 与 Subversion

很大一部分开源开发项目和相当数量的企业项目使用 Subversion 来管理其源代码。 它已经存在了十多年,并且在那段时间的大部分时间里,它是开源项目的 * 事实 * VCS 选择。 它在许多方面也与 CVS 非常相似,后者是之前源代码控制领域的老大哥。

Git 的一项强大功能是与 Subversion 的双向桥接,称为 git svn。 此工具允许你使用 Git 作为 Subversion 服务器的有效客户端,因此你可以使用 Git 的所有本地功能,然后像在本地使用 Subversion 一样推送到 Subversion 服务器。 这意味着你可以进行本地分支和合并,使用暂存区,使用变基和 Cherry-Pick 等等,而你的协作者则继续以黑暗和古老的方式工作。 这是将 Git 偷偷带入企业环境并帮助你的同伴开发人员提高效率的好方法,同时你也在游说更改基础架构以完全支持 Git。 Subversion 桥是通往 DVCS 世界的入门药物。

git svn

Git 中所有 Subversion 桥接命令的基本命令是 git svn。 它需要相当多的命令,因此在完成一些简单的工作流程时,我们将展示最常见的命令。

重要的是要注意,当你使用 git svn 时,你正在与 Subversion 进行交互,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

现在,你可以通过使用 to 和 from 存储库调用 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,它会拉取您尚未拥有的服务器上的任何更改,并将您所做的任何工作变基到服务器上的内容之上

$ git svn rebase
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: eaa029d99f87c5c822c5c29039d19111ff32ef46 and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 65536c6e30d263495c17d781962cfff12422693a b34372b25ccf4945fe5658fa381b075045e7702a M	README.txt
First, rewinding head to replay your work on top of it...
Applying: update foo
Using index info to reconstruct a base tree...
M	README.txt
Falling back to patching base and 3-way merge...
Auto-merging README.txt
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

现在,您的所有工作都在 Subversion 服务器上的内容之上,因此您可以成功地 dcommit

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r85
    M	README.txt
r85 = 9c29704cc0bbbed7bd58160cfb66cb9191835cd8 (refs/remotes/origin/trunk)
No changes between 5762f56732a958d6cfda681b661d2a239cc53ef5 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

请注意,与 Git 要求您在推送之前合并您尚未在本地拥有的上游工作不同,git svn 仅在更改冲突时才要求您这样做(很像 Subversion 的工作方式)。如果其他人将更改推送到一个文件,然后您将更改推送到另一个文件,您的 dcommit 将可以正常工作

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

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

您还应该运行此命令以从 Subversion 服务器拉取更改,即使您尚未准备好提交自己。您可以运行 git svn fetch 来获取新数据,但 git svn rebase 会执行获取操作,然后更新您的本地提交。

$ git svn rebase
    M	autogen.sh
r88 = c9c5f83c64bd755368784b444bc7a0216cc1e17b (refs/remotes/origin/trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/origin/trunk.

不时运行 git svn rebase 可确保您的代码始终是最新的。但是,您需要确保在运行此命令时您的工作目录是干净的。如果您有本地更改,您必须先隐藏您的工作或暂时提交它,然后再运行 git svn 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 分支的尖端来确定您的 dcommit 转到哪个分支——您应该只有一个,并且它应该是当前分支历史记录中最后一个带有 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)

这就像 blamelog 一样,它可以离线运行,并且只更新到你上次与 Subversion 服务器通信时为止。

忽略 Subversion 忽略的内容

如果你克隆了一个在任何地方设置了 svn:ignore 属性的 Subversion 仓库,你可能会想设置相应的 .gitignore 文件,这样你就不会意外地提交不应该提交的文件。git svn 有两个命令可以帮助解决这个问题。第一个是 git svn create-ignore,它会自动为你创建相应的 .gitignore 文件,以便你的下一次提交可以包含它们。

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

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

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

Git-Svn 总结

如果你的处境是被困在使用 Subversion 服务器,或者处于需要运行 Subversion 服务器的开发环境中,git svn 工具非常有用。但是,你应该把它看作是残缺的 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 通过远程库与服务器仓库进行通信,因此这个桥梁被实现为一个远程助手也就不足为奇了。该项目的名称是 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”(由 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

编号为 *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 方面来看,使用这两种分支样式的工作方式是相同的:只需像往常一样检出、提交、获取、合并、拉取和推送即可。你应该知道的一件事是 Mercurial 不支持重写历史记录,只支持添加到历史记录。这是在交互式变基和强制推送之后,我们的 Mercurial 仓库的样子

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

变更集 *8*、*9* 和 *10* 已创建并属于 permanent 分支,但旧的变更集仍然存在。这对于使用 Mercurial 的队友来说可能 非常 令人困惑,因此请尽量避免这种情况。

Mercurial 总结

Git 和 Mercurial 非常相似,以至于跨边界工作相当轻松。如果你避免更改已离开你的机器的历史记录(通常建议这样做),你甚至可能没有意识到另一端是 Mercurial。

Git 和 Perforce

Perforce 是企业环境中非常流行的版本控制系统。它自 1995 年就已存在,这使其成为本章中涵盖的最古老的系统。因此,它是根据当时的限制设计的;它假设你始终连接到单个中央服务器,并且本地磁盘上仅保留一个版本。可以肯定的是,它的特性和约束非常适合于几个特定的问题,但是有很多项目在使用 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 用户(rootperforcegit)的密码,并提供一个实例名称,该名称可用于将此安装与其他同一网络上的安装区分开来。 完成所有这些操作后,您将看到以下内容:

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 允许您将 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 提交作者字段中的电子邮件地址查找 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 提交。 这发生在服务器本地,因此速度相对较快,但如果您有很多历史记录,仍然可能需要一些时间。 后续的提取执行增量转换,因此它会感觉更像 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 没有命名分支来存储 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 那样灵活或完整,但它确实允许您执行您想要执行的大部分操作,而不会侵入服务器环境。

注意

您需要在 PATH 中的某个位置找到 p4 工具才能使用 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 建议您对提交进行变基,甚至还提供了一个快捷方式来执行此操作:

$ 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/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 head 的合并提交,然后是另一个提交。我们将尝试在 Perforce 端的一个变更集之上提交这些提交。让我们看看现在尝试提交会发生什么。

$ git p4 submit -n
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would synchronize p4 checkout in /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would apply
  b4959b6 Trademark
  cbacd0a Table borders: yes please
  3be6fd8 Correct email address

-n 标志是 --dry-run 的缩写,它会尝试报告如果实际运行提交命令会发生什么。在这种情况下,看起来我们将创建三个 Perforce 变更集,它们对应于 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 一样(实际上就是发生了 rebase)。这意味着你可以自由地在 Git 端创建、处理、丢弃和合并分支,而不用担心你的历史会以某种方式与 Perforce 不兼容。如果你可以 rebase 它,你就可以将其贡献给 Perforce 服务器。

分支

如果你的 Perforce 项目有多个分支,你也不会倒霉;git-p4 可以用感觉像 Git 的方式处理它。假设你的 Perforce depot 像这样布局:

//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

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

--detect-branches 标志告诉 git-p4 使用 Perforce 的分支规范将分支映射到 Git refs。如果这些映射不存在于 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 提交;如果你有一个其他人使用的 remote,请不要推送任何尚未提交到 Perforce 服务器的提交。

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

scroll-to-top