章节 ▾ 第二版

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 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 格式,那么你可以将 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 分支的第一个共同祖先进行比较来做到这一点。

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

$ 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 分支。当你在一个主题分支中完成了你认为已完成的工作,或者其他人贡献了你已验证的工作时,你将其合并到你的主分支中,删除刚刚合并的主题分支,然后重复此过程。

例如,如果我们有一个存储库,其中有两个名为 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 中。然后从 master 重建 nextseen 分支。这意味着 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 等) 分支之上重建更改。如果一切顺利,您可以 fast-forward 您的 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 存档,但通过将 --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 以来所有提交的简洁摘要,按作者分组,您可以将其通过电子邮件发送到您的列表。

scroll-to-top