章节 ▾ 第二版

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 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 是一种“全部应用或全部中止”的模型,要么全部应用,要么全部不应用,而 patch 可以部分应用补丁文件,使你的工作目录处于奇怪的状态。git apply 总体上比 patch 更保守。它不会为你创建提交——运行它之后,你必须手动暂存并提交所引入的更改。

你还可以使用 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 格式,那么你可以将 git am 指向该 mbox 文件,它将开始应用它看到的所有补丁。如果你运行的邮件客户端可以将多封邮件保存为 mbox 格式,你可以将整个补丁系列保存到一个文件中,然后使用 git am 一次应用一个。

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

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

你可以看到它已干净地应用并自动为你创建了新的提交。作者信息取自电子邮件的 FromDate 标头,提交的消息取自电子邮件的 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-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,它将把每个提交引入的差异附加到输出中。

要查看如果你将此主题分支与另一个分支合并将发生什么的完整差异,你可能需要使用一个“奇怪的技巧”才能获得正确的结果。你可能会想到运行:

$ git diff master

此命令会给你一个差异,但它可能具有误导性。如果你的 master 分支在你从中创建主题分支后已向前推进,那么你将得到看似奇怪的结果。之所以会发生这种情况,是因为 Git 直接比较你所在主题分支的最后一个提交的快照和 master 分支的最后一个提交的快照。例如,如果你在 master 分支上的文件中添加了一行,则快照的直接比较会看起来像主题分支将删除该行。

如果 master 是你主题分支的直接祖先,这不是问题;但如果这两个历史已经分歧,差异将看起来像你正在添加主题分支中的所有新内容并删除 master 分支特有的所有内容。

你真正想看到的是添加到主题分支的更改——如果你将此分支与 master 合并,你将引入的工作。你通过让 Git 将你的主题分支上的最后一个提交与它与 master 分支的第一个共同祖先进行比较来做到这一点。

从技术上讲,你可以通过明确找出共同祖先,然后在其上运行你的差异比较来做到这一点:

$ 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 分支;当 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 工作流

其他维护者倾向于在其 master 分支之上 rebase 或 cherry-pick 贡献的工作,而不是合并它,以保持大部分线性的历史记录。当你的主题分支中有工作并确定要集成它时,你切换到该分支并运行 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 代表“重用已记录的解决方案”(“reuse recorded resolution”)——这是一种简化手动解决冲突的方法。当 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 归档,但要向 git archive 传递 --format=zip 选项:

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

现在你有了你的项目发布的漂亮 tarball 和 zip 归档,你可以将它们上传到你的网站或通过电子邮件发送给人们。

短日志

是时候给那些想了解你的项目进展的人发送邮件列表了。快速获取自上次发布或邮件以来添加到项目中的更改日志的一种好方法是使用 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 以来所有提交的清晰摘要,按作者分组,你可以将其通过电子邮件发送给你的列表。

scroll-to-top