Git
章节 ▾ 第二版

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

世界并不完美。通常情况下,您无法立即将接触到的每个项目都切换到 Git。有时您会陷入使用其他 VCS 的项目中,并希望它是 Git。本章的第一部分将学习如何使用 Git 作为客户端,即使您正在处理的项目托管在不同的系统中。

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

Git 作为客户端

Git 为开发者提供了如此美妙的体验,以至于许多人想出了如何在工作站上使用它,即使他们的团队其他成员正在使用完全不同的 VCS。有很多这样的适配器,被称为“桥接器”。这里我们将介绍您在实际中可能遇到的桥接器。

Git 和 Subversion

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

Git 的一个很棒的功能是与 Subversion 的双向桥接器,名为 git svn。该工具允许您将 Git 用作 Subversion 服务器的有效客户端,因此您可以使用 Git 的所有本地功能,然后推送到 Subversion 服务器,就好像您在本地使用 Subversion 一样。这意味着您可以进行本地分支和合并,使用暂存区,使用变基和 cherry-picking 等等,而您的协作者则继续以他们黑暗而古老的方式工作。这是一种将 Git 偷偷带入公司环境并帮助您的同事提高效率的好方法,同时您也在游说改变基础设施以完全支持 Git。Subversion 桥接器是进入 DVCS 世界的入门药。

git svn

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

需要注意的是,当你使用git svn时,你是在与Subversion进行交互,而Subversion是一个与Git工作方式截然不同的系统。虽然你可以进行本地分支和合并,但通常最好通过变基你的工作来保持你的历史尽可能地线性,并避免同时与Git远程仓库进行交互。

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

设置

为了演示这个功能,你需要一个你拥有写入权限的典型SVN仓库。如果你想复制这些示例,你将不得不创建一个SVN测试仓库的可写副本。为了轻松地做到这一点,你可以使用一个名为svnsync的工具,它随Subversion一起提供。

要继续学习,你首先需要创建一个新的本地Subversion仓库

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

然后,允许所有用户更改revprops——最简单的方法是添加一个pre-revprop-change脚本,它始终退出0

$ 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

这相当于执行两个命令——git svn init后跟git svn fetch——在您提供的URL上。这可能需要一段时间。例如,如果测试项目只有大约75次提交,并且代码库并不大,但Git仍然必须逐个检出每个版本,并单独提交。对于一个拥有数百或数千次提交的项目,这可能需要几个小时甚至几天才能完成。

-T trunk -b branches -t tags部分告诉Git这个Subversion仓库遵循基本的branching和tagging约定。如果你对trunk、branches或tags的命名方式不同,你可以更改这些选项。由于这很常见,你可以用-s替换整个部分,这意味着标准布局,并且暗示所有这些选项。以下命令等效

$ git svn clone file:///tmp/test-svn -s

此时,你应该已经拥有一个有效的Git仓库,它已经导入了你的branches和tags

$ 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 tags作为远程refs管理的。让我们用Git plumbing命令show-ref仔细看看

$ git show-ref
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/heads/master
0fb585761df569eaecd8146c71e58d70147460a2 refs/remotes/origin/my-calc-branch
bfd2d79303166789fc73af4046651a4b35c12f0b refs/remotes/origin/tags/2.0.2
285c2b2e36e467dd4d91c8e3c0c0e1750b3fe8ca refs/remotes/origin/tags/release-2.0.1
cbda99cb45d9abcb9793db1d4f70ae562a969f1e refs/remotes/origin/tags/release-2.0.2
a9f074aa89e826d6f9d30808ce5ae3ffe711feda refs/remotes/origin/tags/release-2.0.2rc1
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/remotes/origin/trunk

Git在从Git服务器克隆时不会这样做;以下是一个带有tags的仓库在fresh clone后的样子

$ 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直接将tags获取到refs/tags,而不是将其视为远程branches。

提交回Subversion

现在你已经拥有了一个工作目录,你可以对项目进行一些工作,并将你的commits推送到上游,有效地使用Git作为SVN客户端。如果你编辑了其中一个文件并提交了它,那么你在本地Git中就有一个commit,而在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的方式——你可以在离线状态下进行多次commits,然后将它们一次性推送到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服务器代码之上所做的所有commits,为每个commits执行Subversion提交,然后重写你本地的Git commits,以包含一个唯一的标识符。这很重要,因为它意味着你所有commits的SHA-1校验和都会改变。部分原因是,与Subversion服务器同时使用基于Git的项目的远程版本不是一个好主意。如果你查看最后一次commit,你可以看到新添加的git-svn-id

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

    Adding git-svn instructions to the README

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

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

拉取新的更改

如果你与其他开发者合作,那么在某个时候,你们中的一人会推送,然后另一个人会尝试推送一个冲突的更改。这个更改将被拒绝,直到你合并他们的工作。在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要求你在推送之前合并你本地还没有的upstream工作,而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中,你永远不能确定commit之前和commit之后的状态是否完全相同。

即使你还没有准备好自己提交,你也应该运行这个命令从Subversion服务器拉取更改。你可以运行git svn fetch来获取新数据,但git svn rebase会执行fetch,然后更新你本地的commits。

$ 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服务器,你可能希望每次将你的工作重新设置到一个单独的分支上,而不是将多个分支合并在一起。首选rebase的原因是Subversion具有线性的历史记录,并且不像Git那样处理合并,因此git svn在将快照转换为Subversion commits时只遵循第一个父节点。

假设你的历史记录如下:你创建了一个experiment分支,进行了两次commits,然后将它们合并回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分支上进行的任何commits——相反,所有这些更改都出现在SVN版本的单个合并commit中。

当其他人克隆这个工作时,他们只看到带有所有工作的合并commit,就好像你运行了git merge --squash一样;他们看不到关于它来自哪里或何时提交的commit数据。

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服务器上操作。需要注意的是,它不会将你检出到该分支;如果你在此时提交,该commit将提交到服务器上的trunk,而不是opera

切换活动分支

Git通过查看你的历史记录中任何Subversion分支的顶端来确定你的dcommits将提交到哪个分支——你应该只有一个,并且它应该是你当前分支历史记录中最后一个带有git-svn-id的分支。

如果你想同时处理多个分支,你可以设置本地分支,以便通过从该分支的导入的Subversion commit处开始,将dcommits提交到特定的Subversion分支。如果你想要一个opera分支,可以单独对其进行操作,你可以运行

$ git branch opera remotes/origin/opera

现在,如果你想将你的opera分支合并到trunk(你的master分支),你可以用正常的git merge来做到这一点。但你需要提供一个描述性的commit信息(通过-m),否则合并会显示“Merge branch opera”,而不是有用的信息。

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

Subversion命令

git svn工具集提供了一些命令来帮助简化向Git的过渡,这些命令提供了一些类似于你在Subversion中所拥有的功能。以下是一些命令,可以让你获得Subversion以前的功能。

SVN风格的历史记录

如果你习惯了Subversion,并且想以SVN输出风格查看你的历史记录,你可以运行git svn log以SVN格式查看你的commit历史记录

$ 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命令不同,真正的svn log命令会向Subversion服务器请求数据。其次,它只显示你已经提交到Subversion服务器的commits。你还没有dcommit的本地Git commits不会显示出来;同样,人们在同时提交到Subversion服务器的commits也不会显示出来。它更像是Subversion服务器上已知commits的最后状态。

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 文件中的行打印到标准输出,这样你就可以将输出重定向到你的项目排除文件。

$ 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,并拒绝包含没有 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://www.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 创建的,而编号为 34 的变更集是由 git-remote-hg 创建的,通过推送使用 Git 创建的提交来创建。

分支和书签

Git 只有一种分支:一个在进行提交时移动的引用。在 Mercurial 中,这种引用被称为“书签”,它的行为与 Git 分支非常相似。

Mercurial 的“分支”概念更重量级。变更集所在的 分支会与变更集一起记录,这意味着它将始终存在于仓库历史记录中。以下是一个在 develop 分支上创建的提交的示例

$ hg log -l 1
changeset:   6:8f65e5e02793
branch:      develop
tag:         tip
user:        Ben Straub <[email protected]>
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 <[email protected]>
|  date:        Thu Aug 14 20:21:09 2014 -0700
|  summary:     A permanent change
|
| @  changeset:   6:8f65e5e02793
|/   branch:      develop
|    user:        Ben Straub <[email protected]>
|    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 <[email protected]>
| |  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

变更集 8910 已经创建,并且属于 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 用户。选择底部的“登录”选项并按回车键(或 SSH 到机器),以 root 身份登录。然后使用以下命令创建用户

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

第一个命令将打开一个 VI 编辑器来自定义用户,但您可以通过输入 :wq 并按回车键来接受默认值。第二个命令将提示您两次输入密码。这就是我们需要使用 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://[email protected]':
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 中的作者/提交者字段。反过来转换时,默认行为是使用 Git 提交的作者字段中存储的电子邮件地址查找 Perforce 用户,并以该用户身份提交变更集(并应用权限)。在大多数情况下,此行为就可以了,但请考虑以下映射文件

john [email protected] "John Doe"
john [email protected] "John Doe"
bob [email protected] "Anon X. Mouse"
joe [email protected] "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://[email protected]':
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 认为它成功了。让我们从 Perforce 的角度看一下 README 文件的历史记录,使用 p4v 的修订图功能

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 那样灵活或完整,但它确实允许您在不侵入服务器环境的情况下完成大多数您想做的事情。

注意

您需要在您的 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

请注意,存在一个名为“p4”的 Perforce 服务器远程,但其他所有内容看起来都像标准克隆。实际上,这有点误导;那里实际上没有远程。

$ 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 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 [email protected] 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 <[email protected]>
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 的简写,它试图报告如果实际运行 submit 命令会发生什么。在这种情况下,看起来我们将创建三个 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

我们的历史变得线性,就像我们在提交之前重新整理一样(实际上正是发生了这种情况)。这意味着您可以随意在 Git 一侧创建、处理、丢弃和合并分支,而无需担心您的历史记录将以某种方式与 Perforce 不兼容。如果您能够重新整理它,您就可以将它贡献到 Perforce 服务器。

分支

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

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

假设您有一个 dev 分支,它的视图规范如下所示。

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

Git-p4 可以自动检测这种情况并执行正确操作。

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

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

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

--detect-branches 标志告诉 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 服务器的一流版本控制客户端。

scroll-to-top