5.3 维护项目
维护项目
除了如何有效地参与一个项目的贡献之外,你可能也需要了解如何维护项目。这包含接受并应用别人使用 format-patch
生成并通过电子邮件发送过来的补丁,或对项目添加的远程版本库分支中的更改进行整合。但无论是管理版本库,还是帮忙验证、审核收到的补丁,都需要同其他贡献者约定某种长期可持续的工作方式。
在特性分支中工作
如果你想向项目中整合一些新东西,最好将这些尝试局限在特性分支——一种通常用来尝试新东西的临时分支中。这样便于单独调整补丁,如果遇到无法正常工作的情况,可以先不用管,等到有时间的时候再来处理。如果你基于你所尝试进行工作的特性为分支创建一个简单的名字,比如 ruby_client
或者具有类似描述性的其他名字,这样即使你必须暂时抛弃它,以后回来时也不会忘记。项目的维护者一般还会为这些分支附带命名空间,比如 sc/ruby_client
(其中 sc
是贡献该项工作的人名称的简写)。你应该记得,可以使用如下方式基于 master 分支建立特性分支:
$ git branch sc/ruby_client master
或者如果你同时想立刻切换到新分支上的话,可以使用 checkout -b
选项:
$ git checkout -b sc/ruby_client master
现在你已经准备好将别人贡献的工作加入到这个特性分支,并考虑是否将其合并到长期分支中去了。
应用来自邮件的补丁
如果你通过电子邮件收到了一个需要整合进入项目的补丁,你需要将其应用到特性分支中进行评估。有两种应用该种补丁的方法:使用 git apply
,或者使用 git am
。
使用 apply
命令应用补丁
如果你收到了一个使用 git diff
或 Unix diff
命令(不推荐使用这种方式,具体见下一节)创建的补丁,可以使用 git apply
命令来应用。假设你将补丁保存在了 /tmp/patch-ruby-client.patch
中,可以这样应用补丁:
$ git apply /tmp/patch-ruby-client.patch
这会修改工作目录中的文件。它与运行 patch -p1
命令来应用补丁几乎是等效的,但是这种方式更加严格,相对于 patch 来说,它能够接受的模糊匹配更少。它也能够处理 git diff
格式文件所描述的文件添加、删除和重命名操作,而 patch
则不会。最后,git apply
命令采用了一种“全部应用,否则就全部撤销(apply all or abort all)”的模型,即补丁只有全部内容都被应用和完全不被应用两个状态,而 patch
可能会导致补丁文件被部分应用,最后使你的工作目录保持在一个比较奇怪的状态。总体来看,git apply
命令要比 patch
谨慎得多。并且,它不会为你创建提交——在运行之后,你需要手动暂存并提交补丁所引入的更改。
在实际应用补丁前,你还可以使用 git apply 来检查补丁是否可以顺利应用——即对补丁运行 git apply --check
命令:
$ git apply --check 0001-seeing-if-this-helps-the-gem.patch
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
如果没有产生输出,则该补丁可以顺利应用。如果检查失败了,该命令还会以一个非零的状态退出,所以需要时你也可以在脚本中使用它。
使用 am
命令应用补丁
如果补丁的贡献者也是一个 Git 用户,并且其能熟练使用 format-patch
命令来生成补丁,这样的话你的工作会变得更加轻松,因为这种补丁中包含了作者信息和提交信息供你参考。如果可能的话,请鼓励贡献者使用 format-patch
而不是 diff
来为你生成补丁。而只有对老式的补丁,你才必须使用 git apply
命令。
要应用一个由 format-patch
命令生成的补丁,你应该使用 git am
命令。从技术的角度看,git am
是为了读取 mbox 文件而构建的,mbox 是一种用来在单个文本文件中存储一个或多个电子邮件消息的简单纯文本格式。其大致格式如下所示:
From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith <jessica@example.com>
Date: Sun, 6 Apr 2008 10:17:23 -0700
Subject: [PATCH 1/2] add limit to log function
Limit log functionality to the first 20
这其实就是你前面看到的 format-patch 命令输出的开始几行。而同时它也是有效的 mbox 电子邮件格式。如果有人使用 git send-email 命令将补丁以电子邮件的形式发送给你,你便可以将它下载为 mbox 格式的文件,之后将 git am 命令指向该文件,它会应用其中包含的所有补丁。如果你所使用的邮件客户端能够同时将多封邮件保存为 mbox 格式的文件,你甚至能够将一系列补丁打包为单个 mbox 文件,并利用 git am
命令将它们一次性全部应用。
然而,如果贡献者将 format-patch
生成的补丁文件上传到类似 Request Ticket 的任务处理系统,你可以先将其保存到本地,之后通过 git am
来应用补丁:
$ git am 0001-limit-log-function.patch
Applying: add limit to log function
你会看到补丁被顺利地应用,并且为你自动创建了一个新的提交。其中的作者信息来自于电子邮件头部的 From
和 Date
字段,提交消息则取自 Subject
和邮件正文中补丁之前的内容。比如,应用上面那个 mbox 示例后生成的提交是这样的:
$ git log --pretty=fuller -1
commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Author: Jessica Smith <jessica@example.com>
AuthorDate: Sun Apr 6 10:17:23 2008 -0700
Commit: Scott Chacon <schacon@gmail.com>
CommitDate: Thu Apr 9 09:19:06 2009 -0700
add limit to log function
Limit log functionality to the first 20
其中 Commit
信息表示的是应用补丁的人和应用补丁的时间。Author
信息则表示补丁的原作者和原本的创建时间。
但是,有时候无法顺利地应用补丁。这也许是因为你的主分支和创建补丁的分支相差较多,也有可能是因为这个补丁依赖于其他你尚未应用的补丁。这种情况下,git am
进程将会报错并且询问你要做什么:
$ git am 0001-seeing-if-this-helps-the-gem.patch
Applying: seeing if this helps the gem
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
Patch failed at 0001.
When you have resolved this problem run "git am --resolved".
If you would prefer to skip this patch, instead run "git am --skip".
To restore the original branch and stop patching run "git am --abort".
该命令将会在所有出现问题的文件内加入冲突标记,就和发生冲突的合并或变基操作一样。而你解决问题的手段很大程度上也是一样的——即手动编辑那些文件来解决冲突,暂存新的文件,之后运行 git am --resolved
继续应用下一个补丁:
$ (fix the file)
$ git add ticgit.gemspec
$ git am --resolved
Applying: seeing if this helps the gem
如果你希望 Git 能够尝试以更加智能的方式解决冲突,你可以对其传递 -3
选项来使 Git 尝试进行三方合并。该选项默认并没有打开,因为如果用于创建补丁的提交并不在你的版本库内的话,这样做是没有用处的。而如果你确实有那个提交的话——比如补丁是基于某个公共提交的——那么通常 -3
选项对于应用有冲突的补丁是更加明智的选择。
$ git am -3 0001-seeing-if-this-helps-the-gem.patch
Applying: seeing if this helps the gem
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
No changes -- Patch already applied.
比如上面这种情况,我在之前已经应用过同样的补丁。如果没有 -3
选项的话,这看起来就像是存在一个冲突。
如果你正在利用一个 mbox 文件应用多个补丁,也可以在交互模式下运行 am
命令,这样在每个补丁之前,它会停住询问你是否要应用该补丁:
$ git am -3 -i mbox
Commit Body is:
--------------------------
seeing if this helps the gem
--------------------------
Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all
这在你保存的补丁较多时很好用,因为你可以在应用之前查看忘掉内容的补丁,并且跳过已经应用过的补丁。
当与你的特性相关的所有补丁都被应用并提交到分支中之后,你就可以选择是否以及如何将其整合到更长期的分支中去了。
检出远程分支
如果你的贡献者建立了自己的版本库,并且向其中推送了若干修改,之后将版本库的 URL 和包含更改的远程分支发送给你,那么你可以将其添加为一个远程分支,并且在本地进行合并。
比如 Jessica 向你发送了一封电子邮件,内容是在她的版本库中的 ruby-client
分支中有一个很不错的新功能,为了测试该功能,你可以将其添加为一个远程分支,并在本地检出:
$ git remote add jessica git://github.com/jessica/myproject.git
$ git fetch jessica
$ git checkout -b rubyclient jessica/ruby-client
如果她再次发邮件说另一个分支中包含另一个优秀功能,因为之前已经设置好远程分支了,你就可以直接进行抓取及检出操作。
这对于与他人长期合作工作来说很有用。而对于提交补丁频率较小的贡献者,相对于每个人维护自己的服务器,不断增删远程分支的做法,使用电子邮件来接收可能会比较省时。况且你也不会想要加入数百个只提供一两个补丁的远程分支。然而,脚本和托管服务在一定程度上可以简化这些工作——这很大程度上依赖于你和你的贡献者开发的方式。
这种方式的另一种优点是你可以同时得到提交历史。虽然代码合并中可能会出现问题,但是你能获知他人的工作是基于你的历史中的具体哪一个位置;所以Git 会默认进行三方合并,不需要提供 -3
选项,你也不需要担心补丁是基于某个你无法访问的提交生成的。
对于非持续性的合作,如果你依然想要以这种方式拉取数据的话,你可以对远程版本库的 URL 调用 git pull
命令。这会执行一个一次性的抓取,而不会将该 URL 存为远程引用:
$ git pull https://github.com/onetimeguy/project
From https://github.com/onetimeguy/project
* branch HEAD -> FETCH_HEAD
Merge made by recursive.
确定引入了哪些东西
你已经有了一个包含其他人贡献的特性分支。现在你可以决定如何处理它们了。本节回顾了若干命令,以便于你检查若将其合并入主分支所引入的更改。
一般来说,你应该对该分支中所有 master 分支尚未包含的提交进行检查。通过在分支名称前加入 --not
选项,你可以排除 master 分支中的提交。这和我们之前使用的 master..contrib
格式是一样的。假设贡献者向你发送了两个补丁,为此你创建了一个名叫 contrib
的分支并在其上应用补丁,你可以运行:
$ git log contrib --not master
commit 5b6235bd297351589efc4d73316f0a68d484f118
Author: Scott Chacon <schacon@gmail.com>
Date: Fri Oct 24 09:53:59 2008 -0700
seeing if this helps the gem
commit 7482e0d16d04bea79d0dba8988cc78df655f16a0
Author: Scott Chacon <schacon@gmail.com>
Date: Mon Oct 22 19:38:36 2008 -0700
updated the gemspec to hopefully work better
如果要查看每次提交所引入的具体修改,你应该记得可以给 git log
命令传递 -p
选项,这样它会在每次提交后面附加对应的差异(diff)。
而要查看将该特性分支与另一个分支合并的完整 diff,你可能需要使用一个有些奇怪的技巧来得到正确的结果。你可能会想到这种方式:
$ git diff master
这个命令会输出一个 diff,但它可能并不是我们想要的。如果在你创建特性分支之后,master
分支向前移动了,你获得的结果就会显得有些不对。这是因为 Git 会直接将该特性分支与 master
分支的最新提交快照进行比较。比如说你在 master
分支中向某个文件添加了一行内容,那么直接比对最新快照的结果看上去就像是你在特性分支中将这一行删除了。
如果 master
分支是你的特性分支的直接祖先,其实是没有任何问题的;但是一旦两个分支的历史产生了分叉,上述比对产生的 diff 看上去就像是将特性分支中所有的新东西加入,并且将 master
分支所独有的东西删除。
而你真正想要检查的东西,实际上仅仅是特性分支所添加的更改——也就是该分支与 master 分支合并所要引入的工作。要达到此目的,你需要让 Git 对特性分支上最新的提交与该分支与 master 分支的首个公共祖先进行比较。
从技术的角度讲,你可以以手工的方式找出公共祖先,并对其显式运行 diff 命令:
$ git merge-base contrib master
36c7dba2c95e6bbb78dfa822519ecfec6e1ca649
$ git diff 36c7db
然而,这种做法比较麻烦,所以 Git 提供了一种比较便捷的方式:三点语法。对于 diff
命令来说,你可以通过把 ...
置于另一个分支名后来对该分支的最新提交与两个分支的共同祖先进行比较:
$ git diff master...contrib
该命令仅会显示自当前特性分支与 master 分支的共同祖先起,该分支中的工作。这个语法很有用,应该牢记。
将贡献的工作整合进来
当特性分支中所有的工作都已经准备好整合进入更靠近主线的分支时,接下来的问题就是如何进行整合了。此外,还有一个问题是,你想使用怎样的总体工作流来维护你的项目?你的选择有很多,我们会介绍其中的一部分。
合并工作流
一种非常简单的工作流会直接将工作合并进入 master
分支。在这种情况下,master
分支包含的代码是基本稳定的。当你完成某个特性分支的工作,或审核通过了其他人所贡献的工作时,你会将其合并进入 master 分支,之后将特性分支删除,如此反复。如果我们的版本库包含类似 Figure 5-20 的两个名称分别为 ruby_client
和 php_client
的分支,并且我们先合并 ruby_client
分支,之后合并 php_client
分支,那么提交历史最后会变成 Figure 5-21 的样子。
Figure 5-21. 合并特性分支之后。
这也许是最简单的工作流了,但是当项目更大,或更稳定,你对自己所引入的工作更加在意时,它可能会带来问题。
如果你的项目非常重要,你可能会使用两阶段合并循环。在这种情况下,你会维护两个长期分支,分别是 master
和 develop
,master
分支只会在一个非常稳定的版本发布时才会更新,而所有的新代码会首先整合进入 develop
分支。你定期将这两个分支推送到公共版本库中。每次需要合并新的特性分支时(Figure 5-22),你都应该合并进入 develop
分支(Figure 5-23);当打标签发布的时候,你会将 master
分支快进到已经稳定的 develop
分支(Figure 5-24)。
Figure 5-23. 合并特性分支后。
这样会拉取和 e43a6
相同的更改,但是因为应用的日期不同,你会得到一个新的提交 SHA-1 值。现在你的历史会变成这样:
更多建议: