-
A1. 附录 A: Git 在其他环境
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小结
-
A2. 附录 B: 在应用程序中嵌入 Git
-
A3. 附录 C: 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 交互,这是一个与 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 仓库遵循基本的拉分支和标签约定。如果你将主干、分支或标签命名不同,可以更改这些选项。由于这非常常见,你可以用 -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 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 分支的尖端来确定你的 dcommit 将去哪个分支——你应该只有一个,并且它应该是你当前分支历史记录中带有 git-svn-id
的最后一个。
如果你想同时在多个分支上工作,你可以通过在导入的 Subversion 提交上启动它们来设置本地分支以 dcommit
到特定的 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
)。
Subversion 命令
git svn
工具集提供了许多命令来帮助简化向 Git 的过渡,提供了与 Subversion 中类似的一些功能。以下是一些为你提供 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 总结
如果你的开发环境必须使用 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 与服务器仓库的通信是通过远程进行的,因此这种桥接以远程 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”(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 端删除书签(这是远程 helper 的限制)。
你也可以在一个“重量级”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 用户的密码(root
、perforce
和 git
),并提供一个实例名称,该名称可用于区分此安装与同一网络上的其他安装。完成所有这些操作后,你将看到以下内容

你应该记下这里显示的 IP 地址,我们稍后会用到它。接下来,我们将创建一个 Perforce 用户。选择底部的“Login”选项并按回车(或 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://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 中作者/提交者字段。当进行反向转换时,默认是查找 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 提交。这发生在服务器本地,所以相对较快,但如果你有大量历史记录,仍然可能需要一些时间。随后的 fetches 会进行增量转换,所以感觉会更像 Git 的原生速度。
如你所见,我们的仓库看起来与你可能使用的任何其他 Git 仓库完全相同。有三个分支,Git 贴心地创建了一个本地 master
分支,它跟踪 origin/master
。让我们做一些工作,并创建几个新的提交
# ...
$ git log --oneline --decorate --graph --all
* cfd46ab (HEAD, master) Add documentation for new feature
* a730d77 Whitespace
* d254865 (origin/master, origin/HEAD) Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]
我们有两个新提交。现在我们看看是否有人在工作
$ git fetch
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://10.0.1.254/Jam
d254865..6afeb15 master -> origin/master
$ git log --oneline --decorate --graph --all
* 6afeb15 (origin/master, origin/HEAD) Update copyright
| * cfd46ab (HEAD, master) Add documentation for new feature
| * a730d77 Whitespace
|/
* d254865 Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]
看起来有人在!从这个视图你可能看不出来,但 6afeb15
提交实际上是使用 Perforce 客户端创建的。从 Git 的角度来看,它看起来就像另一个提交,这正是其要点所在。让我们看看 Perforce 服务器如何处理合并提交
$ git merge origin/master
Auto-merging README
Merge made by the 'recursive' strategy.
README | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Perforce: 100% (3/3) Loading commit tree into memory...
remote: Perforce: 100% (5/5) Finding child commits...
remote: Perforce: Running git fast-export...
remote: Perforce: 100% (3/3) Checking commits...
remote: Processing will continue even if connection is closed.
remote: Perforce: 100% (3/3) Copying changelists...
remote: Perforce: Submitting new Git commit objects to Perforce: 4
To https://10.0.1.254/Jam
6afeb15..89cba2b master -> master
Git 认为它工作了。让我们使用 p4v
的修订图功能,从 Perforce 的角度看看 README
文件的历史记录

如果你以前从未见过这种视图,它可能看起来令人困惑,但它显示的概念与 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 那样灵活或完整,但它允许你在不侵入服务器环境的情况下完成大部分你想做的事情。
注意
|
要使用 git-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
在 775a46f
之后,Git 和 Perforce 的历史记录出现分歧。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
我们的历史记录变得线性,就像我们提交前进行了变基一样(这实际上正是发生的情况)。这意味着你可以自由地在 Git 端创建、工作、抛弃和合并分支,而不必担心你的历史记录会以某种方式与 Perforce 不兼容。如果你可以变基它,你就可以将其贡献给 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 引用。如果 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 服务器的一等版本控制客户端。