Git
章节 ▾ 第二版

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 <[email protected]>
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 <[email protected]>
AuthorDate: Sun Apr 6 10:17:23 2008 -0700
Commit:     Scott Chacon <[email protected]>
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 <[email protected]>
Date:   Fri Oct 24 09:53:59 2008 -0700

    See if this helps the gem

commit 7482e0d16d04bea79d0dba8988cc78df655f16a0
Author: Scott Chacon <[email protected]>
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,你确定只有在切出一个非常稳定的版本,并且所有新代码都集成到 develop 分支时才更新 master。你定期将这两个分支推送到公共仓库。每次你有一个新的主题分支需要合并进来(合并主题分支之前),你将它合并到 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 会偶尔被变基,而 seen 更频繁地被变基。

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

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

变基和 cherry-pick 工作流

其他维护者更喜欢将贡献者提交的工作变基到他们的 master 分支上,而不是合并它,以保持一个基本上线性的历史记录。当你有一个主题分支中的工作,并且你已经确定想要将其整合进来时,你移动到该分支,并运行变基命令,以在当前的 master(或 develop 等)分支之上重建更改。如果这很顺利,你就可以快速向前移动你的 master 分支,最终你会得到一个线性的项目历史记录。

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

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

如果你做了大量的合并和变基,或者你维护了一个长期存在的主题分支,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 <[email protected]>"
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 <[email protected]>
sub   2048g/45D02282 2009-02-09 [expires: 2010-02-09]

然后,你可以通过将其导出并通过 git hash-object 传递,将密钥直接导入 Git 数据库,这会将这些内容写入 Git 中的一个新的 blob,并向你返回 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

如果有人打开该压缩包,他们将在 project 目录下获得项目的最新快照。你也可以以几乎相同的方式创建 zip 存档,但通过将 --format=zip 选项传递给 git archive

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

现在你有了项目的发布版的一个不错的压缩包和一个 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