章节 ▾ 第二版

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 文件而构建的,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".

此命令在所有存在问题的文件中放置冲突标记,非常类似于冲突的合并或变基操作。你解决此问题的方式大致相同——编辑文件以解决冲突,暂存新文件,然后运行 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 分支的第一个共同祖先进行比较来实现。

技术上,你可以通过明确地找出共同祖先然后对其运行 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 会偶尔变基,而 seen 会更频繁地变基

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

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

变基和挑选工作流

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

将引入的工作从一个分支移动到另一个分支的另一种方法是挑选它。Git 中的挑选就像单个提交的变基。它获取提交中引入的补丁,并尝试在您当前所在的分支上重新应用它。如果您在主题分支上有多个提交,并且只想集成其中一个,或者如果您在主题分支上只有一个提交,并且您更喜欢挑选它而不是运行变基,这很有用。例如,假设您有一个项目,看起来像这样

Example history before a cherry-pick
图 79. 挑选前的示例历史记录

如果你想将提交 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. 主题分支上挑选提交后的历史记录

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

Rerere

如果你正在进行大量合并和变基,或者你正在维护一个长期存在的主题分支,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 目录下获得你项目的最新快照。你也可以通过向 git archive 传递 --format=zip 选项,以类似的方式创建 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 以来所有提交的清晰摘要,按作者分组,你可以将其通过电子邮件发送给你的列表。