章节 ▾ 第二版

5.3 分布式 Git - 维护项目

维护项目

除了知道如何有效地为项目贡献之外,你很可能还需要知道如何维护一个项目。这包括接受并应用通过 format-patch 生成并已发送至你的补丁,或者集成你已添加到项目作为远程仓库的远程分支中的更改。无论你是维护一个规范的仓库,还是想通过验证或批准补丁来提供帮助,你都需要知道如何以一种对其他贡献者最清晰、对你自己长期可持续的方式来接受工作。

在主题分支上工作

当你想集成新工作时,通常最好在一个主题分支上进行尝试——即一个专门用于尝试这项新工作的临时分支。这样,你就可以轻松地单独修改补丁,如果它不起作用,就暂时搁置,直到有时间回来处理。如果你创建一个基于你打算尝试的工作主题的简单分支名称,例如 ruby_client 或其他描述性的名称,那么如果你暂时放弃它,稍后回来处理时,很容易记住它。Git 项目的维护者倾向于为这些分支添加命名空间——例如 sc/ruby_client,其中 sc 是贡献者姓名的缩写。正如你记得的,你可以基于你的 master 分支创建分支,如下所示:

$ git branch sc/ruby_client master

或者,如果你想立即切换到它,可以使用 checkout -b 选项:

$ git checkout -b sc/ruby_client master

现在,你就可以将收到的贡献工作添加到这个主题分支,并决定是否将其合并到你更长期的分支中。

应用电子邮件补丁

如果你通过电子邮件收到一个需要集成到你项目中的补丁,你需要在你的主题分支上应用该补丁进行评估。应用电子邮件补丁有两种方法:使用 git applygit 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 是一个“全部应用或全部中止”的模型,要么全部应用,要么都不应用;而 patch 可以部分应用补丁文件,使你的工作目录处于一种奇怪的状态。总的来说,git applypatch 更保守。它不会为你创建一个提交——运行之后,你必须手动暂存并提交引入的更改。

你也可以使用 git apply 来查看补丁是否能干净地应用,然后再尝试实际应用——你可以运行 git apply --check 加上补丁文件:

$ git apply --check 0001-see-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(该命令命名为 am,因为它用于“从邮箱应用一系列补丁”)。技术上讲,git am 被设计用来读取 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

这是你在上一节看到的 git format-patch 命令的输出开头;它也代表一个有效的 mbox 电子邮件格式。如果有人通过 git send-email 正确地向你发送了补丁,并且你将其下载为 mbox 格式,那么你就可以将 mbox 文件指向 git am,它将开始应用它看到的所有补丁。如果你使用的邮件客户端可以将多个电子邮件保存为 mbox 格式,你可以将整个补丁系列保存到一个文件中,然后使用 git am 一次应用一个。

但是,如果有人将通过 git format-patch 生成的补丁文件上传到票务系统或其他类似系统中,你可以将文件本地保存,然后将保存在磁盘上的文件传递给 git am 来应用它:

$ git am 0001-limit-log-function.patch
Applying: Add limit to log function

你可以看到它干净地应用了,并且自动为你创建了新的提交。作者信息是从邮件的 FromDate 标头中获取的,提交信息是从邮件的主题和正文(在补丁之前)中获取的。例如,如果这个补丁是从上面的 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-see-if-this-helps-the-gem.patch
Applying: See 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".

与有冲突的合并或 rebase 操作类似,此命令会在遇到问题的任何文件中放入冲突标记。你可以通过类似的方式解决此问题——编辑文件以解决冲突,暂存新文件,然后运行 git am --resolved 继续下一个补丁。

$ (fix the file)
$ git add ticgit.gemspec
$ git am --resolved
Applying: See if this helps the gem

如果你希望 Git 更智能地尝试解决冲突,你可以传递一个 -3 选项,它会使 Git 尝试进行三向合并。此选项默认不开启,因为它在你仓库中不存在补丁所说的基础提交时不起作用。如果你确实有这个提交——如果补丁是基于一个公开提交的——那么 -3 选项通常在应用冲突补丁时更明智。

$ git am -3 0001-see-if-this-helps-the-gem.patch
Applying: See 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 选项,补丁将被视为冲突。由于使用了 -3 选项,补丁干净地应用了。

如果你正在从 mbox 文件应用多个补丁,你也可以在交互模式下运行 am 命令,它会在找到每个补丁时停止,并询问你是否要应用它:

$ git am -3 -i mbox
Commit Body is:
--------------------------
See if this helps the gem
--------------------------
Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all

如果你保存了多个补丁,这会很有用,因为你可以先查看补丁,如果你不记得它是什么,或者如果你已经做过了,就不要应用补丁。

当你的主题的所有补丁都已应用并提交到你的分支中后,你可以选择是否以及如何将它们集成到更长期的分支中。

检出远程分支

如果你的贡献来自一个 Git 用户,他设置了自己的仓库,推送了多项更改到其中,然后将仓库的 URL 和更改所在的远程分支名称发给了你,你可以将其添加为远程仓库并在本地进行合并。

例如,如果 Jessica 发邮件告诉你,她的仓库中的 ruby-client 分支有一个很棒的新功能,你可以通过添加远程仓库并本地检出该分支来测试它:

$ git remote add jessica https://github.com/jessica/myproject.git
$ git fetch jessica
$ git checkout -b rubyclient jessica/ruby-client

如果她稍后再次发送邮件给你另一个包含另一个很棒功能的 الآخر分支,你可以直接 fetchcheckout,因为你已经设置好了远程仓库。

如果你一直与某人合作,这种方式最有用。如果某人偶尔只贡献一个补丁,那么通过电子邮件接受它可能比要求每个人运行自己的服务器并需要不断添加和删除远程仓库来获取少量补丁更耗时。你也不太可能想要数百个远程仓库,每个仓库只对应贡献一两个补丁的人。然而,脚本和托管服务可能会使这更容易——这很大程度上取决于你的开发方式和你贡献者如何开发。

这种方法的另一个优点是你还可以获得提交的历史记录。虽然你可能会遇到合法的合并问题,但你知道他们的工作是基于你历史记录中的哪个位置;一个正确的三向合并是默认的,而不是需要提供 -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 the 'recursive' strategy.

确定引入的内容

现在你有一个包含贡献工作的主题分支。此时,你可以确定你想要对它做什么。本节将回顾几个命令,以便你可以了解如何使用它们来准确地审查如果你将此分支合并到你的主分支中会引入什么内容。

通常,获取对该分支中存在但不在你的 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

    See if this helps the gem

commit 7482e0d16d04bea79d0dba8988cc78df655f16a0
Author: Scott Chacon <schacon@gmail.com>
Date:   Mon Oct 22 19:38:36 2008 -0700

    Update gemspec to hopefully work better

要查看每个提交引入了什么更改,请记住你可以将 -p 选项传递给 git log,它会在每个提交后面附加引入的 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 merge-base contrib master)

但是,这两种方法都不太方便,所以 Git 提供了另一种实现相同功能的快捷方式:三点语法。在 git diff 命令的上下文中,你可以在另一个分支后加上三个句点,以对你所在分支的最后一个提交与其与另一个分支的共同祖先进行 diff

$ git diff master...contrib

此命令仅显示自与 master 的共同祖先以来,你当前主题分支引入的工作。这是一个非常有用的语法,需要记住。

集成贡献工作

当你的主题分支中的所有工作都准备好集成到更主线的​​分支时,问题是如何做到这一点。此外,你希望使用什么样的总体工作流来维护你的项目?你有多种选择,我们将介绍其中一些。

合并工作流

一个基本的工作流是直接将所有工作合并到你的 master 分支。在这种情况下,你有一个包含基本稳定代码的 master 分支。当你有一个主题分支中的工作,你认为已经完成,或者其他人贡献了你已经验证过的工作,你就将其合并到你的 master 分支,删除刚刚合并的主题分支,然后重复。

例如,如果我们有一个仓库,其中有两个名为 ruby_clientphp_client 的分支,其历史记录如下图所示(带有多个主题分支的历史记录),并且我们先合并 ruby_client,然后是 php_client,那么你的历史记录最终会看起来像这样(合并主题分支后的历史记录)。

History with several topic branches
图 72. 带有多个主题分支的历史记录
After a topic branch merge
图 73. 合并主题分支后的历史记录

这可能是最简单的工作流,但如果处理大型或更稳定的项目,并且你希望非常谨慎地引入内容,它可能会有问题。

如果你有一个更重要的项目,你可能希望使用一个两阶段的合并周期。在这种情况下,你有两个长期运行的分支,masterdevelop,其中你确定 master 只在非常稳定的版本发布时更新,并且所有新代码都集成到 develop 分支中。你会定期将这两个分支推送到公共仓库。每次有新的主题分支需要合并时(合并主题分支前),你将其合并到 develop合并主题分支后);然后,当你标记一个版本时,你将 master 快进到现在已稳定下来的 develop 分支所在的位置(项目发布后)。

Before a topic branch merge
图 74. 合并主题分支前
After a topic branch merge
图 75. 合并主题分支后
After a project release
图 76. 项目发布后

这样,当人们克隆你的项目仓库时,他们可以选择检出 master 来构建最新的稳定版本并轻松保持更新,或者他们可以检出 develop,它包含更前沿的内容。你也可以通过有一个 integrate 分支来扩展这个概念,所有工作都在其中合并。然后,当该分支的代码库稳定并且通过测试时,你将其合并到 develop 分支;当它稳定一段时间后,你将 master 分支快进。

大型合并工作流

Git 项目有四个长期运行的分支:masternextseen(以前称为 'pu' - 提议更新)用于新工作,以及 maint 用于维护回溯。当贡献者引入新工作时,它们会以类似我们描述的方式收集到维护者仓库的主题分支中(参见 管理复杂的并行贡献主题分支系列)。此时,主题会被评估,以确定它们是否安全且准备好被使用,或者是否需要更多工作。如果它们是安全的,它们会被合并到 next,然后该分支会被推送,以便每个人都可以尝试集成的所有主题。

Managing a complex series of parallel contributed topic branches
图 77. 管理复杂的并行贡献主题分支系列

如果主题仍需工作,它们将被合并到 seen。当确定它们完全稳定时,主题会被重新合并到 master。然后,nextseen 分支会从 master 重建。这意味着 master 几乎总是向前推进,next 会偶尔 rebase,而 seen 会更频繁地 rebase。

Merging contributed topic branches into long-term integration branches
图 78. 将贡献的主题分支合并到长期集成分支

当一个主题分支最终被合并到 master 后,它就会从仓库中删除。Git 项目还有一个 maint 分支,它从最后一个版本分叉出来,以在需要维护版本时提供回溯补丁。因此,当你克隆 Git 仓库时,你有四个分支可以检出,以评估项目在不同开发阶段的情况,具体取决于你想要多前沿或者你想要如何贡献;维护者有一个结构化的工作流来帮助他们审查新贡献。Git 项目的工作流是专门化的。要清楚地理解这一点,你可以查看 Git 维护者指南

Rebase 和 Cherry-pick 工作流

其他维护者更喜欢将贡献的工作 rebase 或 cherry-pick 到他们的 master 分支之上,而不是合并进去,以保持一个大致线性的历史。当你有一个主题分支中的工作并已决定将其集成,你切换到该分支并运行 rebase 命令,以在你的当前 master(或 develop 等)分支之上重建更改。如果工作进展顺利,你可以快进你的 master 分支,你最终会得到一个线性的项目历史。

将引入的工作从一个分支移动到另一个分支的另一种方法是 cherry-pick。Git 中的 cherry-pick 就像单个提交的 rebase。它会获取在提交中引入的补丁,并尝试将其重新应用到你当前所在的分支上。如果你在一个主题分支上有多个提交,而你只想集成其中一个,或者你只有一个提交在主题分支上,并且你宁愿 cherry-pick 它而不是运行 rebase,那么这很有用。例如,假设你有一个项目,看起来像这样:

Example history before a cherry-pick
图 79. Cherry-pick 前的示例历史

如果你想将提交 e43a6 拉到你的 master 分支,你可以运行:

$ git cherry-pick e43a6
Finished one cherry-pick.
[master]: created a0a41a9: "More friendly message when locking the index fails."
 3 files changed, 17 insertions(+), 3 deletions(-)

这会引入与 e43a6 中相同的更改,但你会得到一个新的提交 SHA-1 值,因为应用日期不同。现在你的历史看起来像这样:

History after cherry-picking a commit on a topic branch
图 80. Cherry-pick 主题分支上的提交后的历史

现在你可以删除你的主题分支,并丢弃你不想拉取的提交。

Rerere

如果你进行了大量的合并和 rebase,或者你正在维护一个长期的主题分支,Git 有一个名为“rerere”的功能可以提供帮助。

Rerere 的意思是“重用已记录的解决”——这是一种简化手动冲突解决的方法。当 rerere 启用时,Git 会保存成功合并的前后图像集,如果它注意到一个冲突与你已经修复过的冲突完全相同,它就会直接使用上次的修复,而不会打扰你。

此功能包含两个部分:一个配置设置和一个命令。配置设置是 rerere.enabled,将其添加到全局配置中非常方便:

$ git config --global rerere.enabled true

现在,无论何时你进行一个解决了冲突的合并,解决方法都会被记录在缓存中,以备将来需要。

如果需要,你可以使用 git rerere 命令与 rerere 缓存进行交互。当单独调用它时,Git 会检查其分辨率数据库,并尝试匹配任何当前合并冲突并解决它们(尽管如果 rerere.enabled 设置为 true,这会自动完成)。还有子命令可以查看将要记录的内容,从缓存中删除特定分辨率,以及清除整个缓存。我们将在 Rerere 中更详细地介绍 rerere。

标记你的发布

当你决定发布一个版本时,你可能希望分配一个标签,以便将来可以重新创建该版本。你可以在 Git 基础 中讨论的那样创建一个新标签。如果你决定作为维护者签署标签,标记过程可能看起来像这样:

$ git tag -s v1.5 -m 'my signed 1.5 tag'
You need a passphrase to unlock the secret key for
user: "Scott Chacon <schacon@gmail.com>"
1024-bit DSA key, ID F721C45A, created 2009-02-09

如果你确实签署了你的标签,你可能会遇到分发用于签名标签的公共 PGP 密钥的问题。Git 项目的维护者通过将他们的公钥作为 blob 包含在仓库中,然后添加一个直接指向该内容的标签来解决了这个问题。要做到这一点,你可以通过运行 gpg --list-keys 来确定你想要的密钥:

$ gpg --list-keys
/Users/schacon/.gnupg/pubring.gpg
---------------------------------
pub   1024D/F721C45A 2009-02-09 [expires: 2010-02-09]
uid                  Scott Chacon <schacon@gmail.com>
sub   2048g/45D02282 2009-02-09 [expires: 2010-02-09]

然后,你可以通过导出密钥并将其通过 git hash-object 管道将其直接导入 Git 数据库,后者会将新的 blob 写入 Git 并返回 blob 的 SHA-1 值:

$ gpg -a --export F721C45A | git hash-object -w --stdin
659ef797d181633c87ec71ac3f9ba29fe5775b92

现在你已经将你的密钥内容保存在 Git 中了,你可以通过指定 hash-object 命令给你的新 SHA-1 值来创建一个直接指向它的标签:

$ git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92

如果你运行 git push --tagsmaintainer-pgp-pub 标签将与所有人共享。如果有人想验证一个标签,他们可以直接通过从数据库中直接拉取 blob 并将其导入 GPG 来导入你的 PGP 密钥:

$ git show maintainer-pgp-pub | gpg --import

他们可以使用该密钥来验证你所有的签名标签。另外,如果你在标签消息中包含说明,运行 git show <tag> 将允许你向最终用户提供关于标签验证的更具体说明。

生成构建号

由于 Git 没有像 'v123' 这样的单调递增的数字或等价物来与每个提交关联,如果你想为提交提供一个人类可读的名称,你可以对该提交运行 git describe。作为响应,Git 会生成一个字符串,包含该提交之前的最近标签的名称,然后是自该标签以来的提交数量,最后是正在描述的提交的部分 SHA-1 值(前缀为字母“g”,表示 Git):

$ git describe master
v1.6.2-rc1-20-g8c5b85c

这样,你就可以导出一个快照或构建,并给它一个人们可以理解的名称。事实上,如果你从 Git 仓库克隆的源代码构建 Git,git --version 会给出类似这样的结果。如果你正在描述一个你直接标记过的提交,它只会给出标签名称。

默认情况下,git describe 命令需要带注释的标签(使用 -a-s 标志创建的标签);如果你想利用轻量级(非带注释)标签,请向命令添加 --tags 选项。你也可以将此字符串用作 git checkoutgit show 命令的目标,尽管它依赖于末尾的缩写 SHA-1 值,所以它可能不会永远有效。例如,Linux 内核最近从 8 个字符跳到了 10 个字符以确保 SHA-1 对象唯一性,因此旧的 git describe 输出名称失效了。

准备发布

现在你想发布一个构建。你想做的一件事是为你的代码的最新快照创建一个存档,以便那些不幸不使用 Git 的人可以使用。执行此操作的命令是 git archive

$ git archive master --prefix='project/' | gzip > `git describe master`.tar.gz
$ ls *.tar.gz
v1.6.2-rc1-20-g8c5b85c.tar.gz

如果有人打开那个 tarball,他们会得到一个名为 project 的目录下的你的项目的最新快照。你也可以以类似的方式创建一个 zip 存档,但通过将 --format=zip 选项传递给 git archive

$ git archive master --prefix='project/' --format=zip > `git describe master`.zip

现在你有一个漂亮的 tarball 和一个 zip 存档,可以作为你的项目发布,你可以将其上传到你的网站或发送给人们。

Shortlog

是时候将你的项目动态发送给你想了解项目进展的人员邮件列表了。一种快速获取自上次发布或邮件以来添加到你项目中的内容的变更日志的方式是使用 git shortlog 命令。它会汇总你给出的范围内的所有提交;例如,以下命令会提供自上次发布(假设上次发布名为 v1.0.1)以来的所有提交的摘要:

$ git shortlog --no-merges master --not v1.0.1
Chris Wanstrath (6):
      Add support for annotated tags to Grit::Tag
      Add packed-refs annotated tag support.
      Add Grit::Commit#to_patch
      Update version and History.txt
      Remove stray `puts`
      Make ls_tree ignore nils

Tom Preston-Werner (4):
      fix dates in history
      dynamic version method
      Version bump to 1.0.2
      Regenerated gemspec for version 1.0.2

你将获得自 v1.0.1 以来所有提交的干净摘要,按作者分组,你可以将其发送到你的列表。