PEP 374 – 为 Python 项目选择分布式 VCS
- 作者:
- Brett Cannon <brett at python.org>, Stephen J. Turnbull <stephen at xemacs.org>, Alexandre Vassalotti <alexandre at peadrop.com>, Barry Warsaw <barry at python.org>, Dirkjan Ochtman <dirkjan at ochtman.nl>
- 状态:
- 最终版
- 类型:
- 流程
- 创建日期:
- 2008年11月7日
- 发布历史:
- 2008年11月7日,2009年1月22日
基本原理
Python 多年来一直使用集中式版本控制系统(VCS;最初是 CVS,现在是 Subversion),效果显著。拥有 Python 官方版本的主副本,为人们提供了始终获取官方 Python 源代码的单一位置。它还允许存储语言的历史,主要用于开发,但也用于保存。当然,VCS 中的 V 在开发时也非常有用。
但集中式版本控制系统有其缺点。首先,为了无缝地从 Python 的版本控制中获益,必须是“核心开发者”(即,对 Python 主副本拥有提交权限的人)。非核心开发者但希望使用 Python 修订树的人,例如任何为 Python 编写补丁或创建自定义版本的人,都没有直接的修订工具支持。这可能是一个很大的限制,因为这些非核心开发者无法轻松执行基本任务,例如将更改恢复到以前保存的状态、创建分支、发布具有完整修订历史的更改等。对于非核心开发者来说,上次安全树状态是 Python 开发者设置的,这阻碍了安全开发。这种二等公民身份阻碍了希望通过任何复杂性的补丁为 Python 做出贡献,并希望有一种方法可以增量保存其进度以简化其开发生活的人。
还有一个问题是,要提交工作必须在线。由于集中式 VCS 维护一个存储所有修订的中央副本,因此必须具有互联网访问权限才能存储其修订;没有网络,就无法提交。如果您碰巧在旅行且没有互联网,这可能会很烦人。还有一种情况是,有人希望为 Python 做出贡献,但互联网连接不佳,提交既耗时又昂贵,而一步完成可能会更好。
集中式 VCS 的另一个缺点是,一个常见的用例是开发者根据审查意见修改补丁。这在集中式模型中更困难,因为没有地方可以存放中间工作。要么全部检入,要么全部不检入。在集中式 VCS 中,在处理功能或错误修复分支时,也很难跟踪主干中的更改。这增加了此类分支变得过时、陈旧或将其合并到主干中会产生太多冲突而难以解决的风险。
最后,是 Python 的维护问题。在任何时候,至少有一个主要版本的 Python 正在开发中(在撰写本文时有两个)。对于每个正在开发的主要版本的 Python,至少有上一个次要版本的维护版本和正在开发的次要版本(例如,2.6 刚刚发布,这意味着 2.6 和 2.7 都在开发中)。发布完成后,将在代码库之间创建分支,其中一个版本中的更改不(但可以)属于另一个版本。目前,集中式 VCS 中没有对时间分支的自然支持;您必须使用模拟分支的工具。跟踪合并对于开发者来说同样痛苦,因为修订通常需要在四个活动分支之间合并(例如 2.6 维护、3.0 维护、2.7 开发、3.1 开发)。在这种情况下,Subversion 等 VCS 只能通过神秘的第三方工具来处理。
分布式 VCS(DVCS)解决了所有这些问题。虽然可以保留修订树的主副本,但任何人都可以自由地复制该树供自己使用。这使每个人都可以提交对其副本的更改,无论是在线还是离线。它还更自然地与修订树历史中的分支概念结合起来,用于维护和开发 Python 的新功能。DVCS 还提供了许多集中式 VCS 不提供或无法提供的额外功能。
本 PEP 探讨了将 Python 的 Subversion 使用更改为任何当前流行的 DVCS 的可能性,以获得上述好处。本 PEP 不保证在本 PEP 结束时会切换到 DVCS。很有可能找不到明确的赢家,并且 svn 将继续使用。如果发生这种情况,本 PEP 将在未来随着 DVCS 状态的演变而重新审视和修订。
术语
就共同术语达成一致出人意料地困难,主要是因为每个 VCS 在描述细微不同的任务、对象和概念时都使用这些术语。在可能的情况下,我们尝试提供概念的通用定义,但您应查阅各个系统的词汇表以获取详细信息。以下是一些术语的基本参考,来自每个 VCS 的一些标准基于网络的参考。您还可以参考每个 DVCS 的词汇表
- Subversion : https://svnbook.subversion.org.cn/en/1.5/svn.basic.html
- Bazaar : http://bazaar-vcs.org/BzrGlossary
- Mercurial : http://www.selenic.com/mercurial/wiki/index.cgi/UnderstandingMercurial
- git : http://book.git-scm.com/1_the_git_object_model.html
- 分支 (branch)
- 一条开发线;修订的集合,按时间排序。
- 检出/工作副本/工作树 (checkout/working copy/working tree)
- 开发者可以编辑的代码树,链接到分支。
- 索引 (index)
- 一个“暂存区”,用于构建修订(git 独有)。
- 仓库 (repository)
- 修订的集合,组织成多个分支。
- 克隆 (clone)
- 分支或仓库的完整副本。
- 提交 (commit)
- 在仓库中记录修订。
- 合并 (merge)
- 将一个分支/仓库的所有更改和历史应用到另一个分支/仓库。
- 拉取 (pull)
- 从原始分支/仓库(可以是远程或本地)更新检出/克隆。
- 推送/发布 (push/publish)
- 将一个修订及其所有依赖修订从一个仓库复制到另一个仓库。
- 精选 (cherry-pick)
- 将一个或多个特定修订从一个分支合并到另一个分支,可能在不同的仓库中,可能不带其依赖修订。
- 变基 (rebase)
- “分离”一个分支,并将其移动到新的分支点;将提交移动到分支的开头,而不是它们在时间上发生的位置。
典型工作流程
目前,Python 核心开发者的典型工作流程是
- 在一个检出中编辑代码,直到它足够稳定可以提交/推送。
- 提交到主仓库。
这是一个相当简单的工作流程,但它有缺点。首先,因为任何涉及仓库的工作都需要时间,这归因于网络,所以提交/推送不一定能尽可能地原子化。还有一个缺点是,除了递归复制检出目录之外,没有必要廉价地创建新检出。
DVCS 会导致更像这样的工作流程
- 从主仓库的本地克隆中创建分支。
- 编辑代码,以原子化方式提交。
- 将分支合并到主线,然后
- 将所有提交推送到主仓库。
虽然可能有更多的步骤,但工作流程比目前可能的情况更独立于主仓库。通过能够以磁盘速度本地提交,核心开发者能够更频繁地进行原子提交,最大限度地减少一次提交中包含多项代码更改的情况。此外,通过使用分支,更改(如果需要)与其他开发者进行的更改隔离。由于分支是廉价的,因此很容易创建和维护许多针对特定问题(例如一个错误或一个新功能)的小型分支。DVCS 更复杂的功能允许开发者更轻松地跟踪长期运行的开发分支,因为官方主线在不断进展。
竞争者
| 名称 | 简称 | 版本 | 2.x 主干镜像 | 3.x 主干镜像 |
|---|---|---|---|---|
| Bazaar | bzr | 1.12 | http://code.python.org/python/trunk | http://code.python.org/python/3.0 |
| Mercurial | hg | 1.2.0 | http://code.python.org/hg/trunk/ | http://code.python.org/hg/branches/py3k/ |
| git | 不适用 | 1.6.1 | git://code.python.org/python/trunk | git://code.python.org/python/branches/py3k |
本 PEP 不考虑 darcs、arch 或 monotone。这些 DVCS 的主要问题是它们不够流行,当它们不提供其他 DVCS 所提供的一些非常引人注目的功能时,就不值得支持。Arch 和 darcs 也存在严重的性能问题,这些问题在不久的将来似乎不太可能得到解决。
互操作性
对于那些已经决定要使用哪个 DVCS 并愿意自己维护本地镜像的人来说,所有三种 DVCS 都支持通过 git “fast-import” 变更集格式进行交换。当然,git 本身就支持,Bazaar 的原生支持正在积极开发中,截至 2009 年 2 月中旬,早期评价良好。Mercurial 通过其 *hg convert* 命令进行导入的支持比较特殊,并且有 第三方 fast-import 支持 用于导出。此外,Tailor 工具支持根据任何候选格式的官方仓库自动维护任何格式的本地镜像。
使用场景
可能最好的方法来决定是否/哪个 DVCS 应该取代 Subversion 是查看它需要什么来执行开发者(核心和非核心)必须处理的一些实际使用场景。每个使用场景都概述了它是什么,基本步骤的要点列表(每个 VCS 可能略有不同),以及如何在各种 VCS(包括 Subversion)中执行使用场景。
每个 VCS 都有一位作者负责编写每个场景的实现(除非另有说明)。
| 名称 | VCS |
|---|---|
| 布雷特 (Brett) | svn |
| 巴里 (Barry) | bzr |
| 亚历山大 (Alexandre) | hg |
| 史蒂芬 (Stephen) | git |
初始设置
一些 DVCS 如果您提前进行一些初始设置,会提供一些额外好处。本节介绍在运行任何使用场景之前可以做什么,以便更好地利用这些工具。
所有 DVCS 都支持配置您的项目识别。与集中式系统不同,它们使用您的电子邮件地址来识别您的提交。(访问控制通常由 DVCS 外部的机制完成,例如 ssh 或控制台登录)。此身份可以与全名关联。
所有 DVCS 都会查询系统以获取此信息的近似值,但这可能不是您想要的。它们还支持按用户和按项目设置此信息。设置这些属性的便捷命令各不相同,但都允许直接编辑配置文件。
某些 VCS 支持在检出/检入时进行行尾 (EOL) 转换。
svn
无需设置,但建议您遵循开发常见问题解答中的指南。
bzr
无需设置,但为了更快速和节省空间的本地分支,您应该创建一个共享仓库来保存所有 Python 分支。共享仓库实际上只是一个包含 .bzr 目录的父目录。当 bzr 提交修订时,它会从本地目录向上搜索文件系统中的 .bzr 目录以保存修订。通过在多个分支之间共享修订,您可以减少磁盘空间的使用量。这样做
cd ~/projects
bzr init-repo python
cd python
现在,您的所有 Python 分支都应该在 ~/projects/python 中创建。
您还可以在 ~/.bzr/bazaar.conf 和 ~/.bzr/locations.conf 文件中放置一些设置,以设置与 Python 代码交互的默认值。它们都不是必需的,尽管有些是推荐的。例如,我建议对所有提交进行 gpg 签名,但这对于开发者来说可能门槛太高。此外,您可以根据您希望默认推送分支的位置设置默认推送位置。如果您对主分支具有写入权限,则推送位置可以是 code.python.org。否则,它可能是一个免费的 Bazaar 代码托管服务,例如 Launchpad。如果选择 Bazaar,我们应该决定策略和建议是什么。
至少,我会设置你的电子邮件地址
bzr whoami "Firstname Lastname <email.address@example.com>"
与下面的 hg 和 git 一样,有多种方法可以按仓库设置您的电子邮件地址(或者实际上,几乎任何参数)。您可以通过在 $HOME/.bazaar/locations.conf 文件中进行设置来实现,该文件具有与其他 DVCS 相同的 ini 风格格式。有关详细信息,请参阅 Bazaar 文档,其中大部分与本次讨论无关。
hg
至少,您应该设置您的用户名。为此,在您的主目录中创建文件 .hgrc 并添加以下内容
[ui]
username = Firstname Lastname <email.address@example.com>
如果您使用 Windows 并且您的工具不支持 Unix 风格的换行符,您可以通过在配置中添加以下内容来启用自动换行转换
[extensions]
win32text =
这些选项也可以通过自定义 <repo>/.hg/hgrc 而不是 ~/.hgrc 在给定仓库中进行本地设置。
git
不需要。然而,git 支持一些功能,只需稍作准备,即可使您的工作更加顺畅。git 支持在工作区、用户和系统级别设置默认值。系统级别超出了本 PEP 的范围。在类 Unix 系统上,用户配置文件是 $HOME/.gitconfig,工作区配置文件是 $REPOSITORY/.git/config。
您可以使用 git-config 工具来全局(针对您的系统登录帐户)或本地(针对给定的 git 工作副本)设置 user.name 和 user.email 首选项,或者您可以编辑配置文件(它们与上面 Mercurial 部分中显示的格式相同)。
# my full name doesn't change
# note "--global" flag means per user
# (system-wide configuration is set with "--system")
git config --global user.name 'Firstname Lastname'
# but use my Pythonic email address
cd /path/to/python/repository
git config user.email email.address@python.example.com
如果您使用的是 Windows,您可能希望使用 git-config 将 core.autocrlf 和 core.safecrlf 首选项设置为 true。
# check out files with CRLF line endings rather than Unix-style LF only
git config --global core.autocrlf true
# scream if a transformation would be ambiguous
# (eg, a working file contains both naked LF and CRLF)
# and check them back in with the reverse transformation
git config --global core.safecrlf true
尽管仓库通常会包含一个 .gitignore 文件,指定很少或从不应该在 VCS 中注册的文件名,但您可能有一些个人约定(例如,始终将日志消息编辑到名为 “.msg” 的临时文件中),您可能希望指定这些约定。
# tell git where my personal ignores are
git config --global core.excludesfile ~/.gitignore
# I use .msg for my long commit logs, and Emacs makes backups in
# files ending with ~
# these are globs, not regular expressions
echo '*~' >> ~/.gitignore
echo '.msg' >> ~/.gitignore
如果像其他 VCS 一样使用多个分支,可以通过将所有对象放入公共对象存储中来节省大量空间。这也可以节省下载时间,如果分支的来源在不同的仓库中,因为即使上游仓库中没有这些对象,您的仓库中的对象也会在分支之间共享。git 非常节省空间和时间,并自动应用多项优化,因此此配置是可选的。(示例已省略。)
一次性检出
作为非核心开发者,我希望创建一个一次性补丁来修复错误,以便核心开发者可以审查它并将其纳入主线。
- 检出/分支/克隆主干。
- 编辑一些代码。
- 生成补丁(基于 VCS 最好支持的,例如分支历史)。
- 接收审阅者评论并解决问题。
- 生成第二个补丁供核心开发者提交。
svn
svn checkout http://svn.python.org/projects/python/trunk
cd trunk
# Edit some code.
echo "The cake is a lie!" > README
# Since svn lacks support for local commits, we fake it with patches.
svn diff >> commit-1.diff
svn diff >> patch-1.diff
# Upload the patch-1 to bugs.python.org.
# Receive reviewer comments.
# Edit some code.
echo "The cake is real!" > README
# Since svn lacks support for local commits, we fake it with patches.
svn diff >> commit-2.diff
svn diff >> patch-2.diff
# Upload patch-2 to bugs.python.org
bzr
bzr branch http://code.python.org/python/trunk
cd trunk
# Edit some code.
bzr commit -m 'Stuff I did'
bzr send -o bundle
# Upload bundle to bugs.python.org
# Receive reviewer comments
# Edit some code
bzr commit -m 'Respond to reviewer comments'
bzr send -o bundle
# Upload updated bundle to bugs.python.org
bundle 文件就像一个超级补丁。它可以被 patch(1) 读取,但它包含额外的元数据,因此可以将其馈送到 bzr merge 以生成一个完全可用的分支,并带有历史记录。请参阅下面的补丁审查部分。
hg
hg clone http://code.python.org/hg/trunk
cd trunk
# Edit some code.
hg commit -m "Stuff I did"
hg outgoing -p > fixes.patch
# Upload patch to bugs.python.org
# Receive reviewer comments
# Edit some code
hg commit -m "Address reviewer comments."
hg outgoing -p > additional-fixes.patch
# Upload patch to bugs.python.org
虽然 hg outgoing 没有这个标志,但大多数 Mercurial 命令通过 --git 命令支持 git 的扩展补丁格式。这可以在 .hgrc 文件中设置,以便所有生成补丁的命令都使用扩展格式。
git
补丁也可以通过 git diff master > stuff-i-did.patch 创建,但 git format-patch | git am 知道一些普通补丁无法处理的技巧(空文件、重命名等)。git 从提交消息中获取“Stuff I did”来创建文件名 0001-Stuff-I-did.patch。有关 git-format-patch 格式的描述,请参见下面的补丁审查。
# Get the mainline code.
git clone git://code.python.org/python/trunk
cd trunk
# Edit some code.
git commit -a -m 'Stuff I did.'
# Create patch for my changes (i.e, relative to master).
git format-patch master
git tag stuff-v1
# Upload 0001-Stuff-I-did.patch to bugs.python.org.
# Time passes ... receive reviewer comments.
# Edit more code.
git commit -a -m 'Address reviewer comments.'
# Make an add-on patch to apply on top of the original.
git format-patch stuff-v1
# Upload 0001-Address-reviewer-comments.patch to bugs.python.org.
回退更改
作为一名核心开发者,我想撤销一个尚未准备好纳入主线的更改。
- 撤销不需要的更改。
- 将补丁推送到服务器。
svn
# Assume the change to revert is in revision 40
svn merge -c -40 .
# Resolve conflicts, if any.
svn commit -m "Reverted revision 40"
bzr
# Assume the change to revert is in revision 40
bzr merge -r 40..39
# Resolve conflicts, if any.
bzr commit -m "Reverted revision 40"
请注意,如果您要回滚的更改是最后一次更改,您可以直接使用 bzr uncommit。
hg
# Assume the change to revert is in revision 9150dd9c6d30
hg backout --merge -r 9150dd9c6d30
# Resolve conflicts, if any.
hg commit -m "Reverted changeset 9150dd9c6d30"
hg push
请注意,您可以使用“hg rollback”和“hg strip”来撤销您在本地仓库中提交但尚未推送到其他仓库的更改。
git
# Assume the change to revert is the grandfather of a revision tagged "newhotness".
git revert newhotness~2
# Resolve conflicts if any. If there are no conflicts, the commit
# will be done automatically by "git revert", which prompts for a log.
git commit -m "Reverted changeset 9150dd9c6d30."
git push
补丁审查
作为一名核心开发者,我希望审查其他人提交的补丁,以确保只有经批准的更改才会被添加到 Python 中。
核心开发者必须审查其他人提交的补丁。这需要应用补丁,测试它,然后丢弃更改。可以假设核心开发者已经有一个主干的检出/分支/克隆。
- 从主干分支。
- 应用补丁,不带补丁提交者生成的任何注释。
- 将补丁推送到服务器。
- 删除现在无用的分支。
svn
Subversion 并不完全适合这种开发风格,因为它没有本 PEP 中定义的那种“分支”。相反,开发者需要创建另一个检出用于测试补丁,或者在服务器上创建分支。到目前为止,核心开发者尚未采用“在服务器上分支”的方法来处理单个补丁。对于此场景,假设开发者创建了主干的本地检出以进行工作。
cp -r trunk issue0000
cd issue0000
patch -p0 < __patch__
# Review patch.
svn commit -m "Some patch."
cd ..
rm -r issue0000
另一种选择是只运行一个检出,并使用 svn diff 和 svn revert -R 来存储您可能已进行的独立更改。
bzr
bzr branch trunk issueNNNN
# Download `patch` bundle from Roundup
bzr merge patch
# Review patch
bzr commit -m'Patch NNN by So N. So' --fixes python:NNNN
bzr push bzr+ssh://me@code.python.org/trunk
rm -rf ../issueNNNN
或者,既然您很可能会将这些更改提交到主干,那么您可以直接进行检出。这将为您提供一个本地工作树,而分支(即所有修订)将继续存在于服务器上。这类似于 svn 模型,可能允许您更快地审查补丁。在这种情况下,不需要推送。
bzr checkout trunk issueNNNN
# Download `patch` bundle from Roundup
bzr merge patch
# Review patch
bzr commit -m'Patch NNNN by So N. So' --fixes python:NNNN
rm -rf ../issueNNNN
hg
hg clone trunk issue0000
cd issue0000
# If the patch was generated using hg export, the user name of the
# submitter is automatically recorded. Otherwise,
# use hg import --no-commit submitted.diff and commit with
# hg commit -u "Firstname Lastname <email.address@example.com>"
hg import submitted.diff
# Review patch.
hg push ssh://alexandre@code.python.org/hg/trunk/
git
我们假设一个由 git-format-patch 创建的补丁。这是一个 Unix mbox 文件,包含一个或多个补丁,每个补丁都格式化为 RFC 2822 消息。git-am 如下解释每条消息为一个提交。补丁的作者取自 From: 头部,日期取自 Date 头部。提交日志是通过连接主题行的内容、一个空行以及消息正文直到补丁开头来创建的。
cd trunk
# Create a branch in case we don't like the patch.
# This checkout takes zero time, since the workspace is left in
# the same state as the master branch.
git checkout -b patch-review
# Download patch from bugs.python.org to submitted.patch.
git am < submitted.patch
# Review and approve patch.
# Merge into master and push.
git checkout master
git merge patch-review
git push
反向移植
作为一名核心开发者,我希望将一个补丁应用到 2.6、2.7、3.0 和 3.1 版本,以便解决所有三个版本中的问题。
由于始终有最前沿和最新发布版本正在开发中,Python 目前同时有四个分支在工作。这使得更改易于在各个分支中传播变得非常重要。
svn
由于 Python 使用 svnmerge,更改从主干(2.7)开始,然后合并到 2.6 的发布版本。要将更改引入 3.x 系列,更改合并到 3.1,修复,然后合并到 3.0(2.7 -> 2.6;2.7 -> 3.1 -> 3.0)。
这与端口转发策略形成对比,在该策略中,补丁将被添加到 2.6,然后向前拉取到较新的版本(2.6 -> 2.7 -> 3.0 -> 3.1)。
# Assume patch applied to 2.7 in revision 0000.
cd release26-maint
svnmerge merge -r 0000
# Resolve merge conflicts and make sure patch works.
svn commit -F svnmerge-commit-message.txt # revision 0001.
cd ../py3k
svnmerge merge -r 0000
# Same as for 2.6, except Misc/NEWS changes are reverted.
svn revert Misc/NEWS
svn commit -F svnmerge-commit-message.txt # revision 0002.
cd ../release30-maint
svnmerge merge -r 0002
svn commit -F svnmerge-commit-message.txt # revision 0003.
bzr
Bazaar 在这里非常直接,因为它支持手动精选修订。在下面的示例中,我们可以提供修订 ID 而不是修订号,但这通常不是必需的。Martin Pool 建议“我们通常建议首先在最古老的受支持分支中进行修复,然后将其向前合并到后续版本。”
# Assume patch applied to 2.7 in revision 0000
cd release26-maint
bzr merge ../trunk -c 0000
# Resolve conflicts and make sure patch works
bzr commit -m 'Back port patch NNNN'
bzr push bzr+ssh://me@code.python.org/trunk
cd ../py3k
bzr merge ../trunk -r 0000
# Same as for 2.6 except Misc/NEWS changes are reverted
bzr revert Misc/NEWS
bzr commit -m 'Forward port patch NNNN'
bzr push bzr+ssh://me@code.python.org/py3k
hg
Mercurial,像其他 DVCS 一样,不太支持 Python 核心开发者当前用于反向移植补丁的工作流程。目前,错误修复首先应用于开发主线(即主干),然后反向移植到维护分支,并在必要时向前移植到 py3k 分支。此工作流程需要能够精选单个更改。Mercurial 的 transplant 扩展提供了此功能。以下是使用此工作流程的场景示例
cd release26-maint
# Assume patch applied to 2.7 in revision 0000
hg transplant -s ../trunk 0000
# Resolve conflicts, if any.
cd ../py3k
hg pull ../trunk
hg merge
hg revert Misc/NEWS
hg commit -m "Merged trunk"
hg push
在上面的示例中,transplant 的作用很像当前的 svnmerge 命令。当调用 transplant 时不带修订号,该命令会启动一个交互式循环,用于移植多个更改。另一个有用的功能是 –filter 选项,可用于以编程方式修改变更集(例如,可用于自动删除对 Misc/NEWS 的更改)。
或者,对于传统的工作流程,我们可以通过将错误修复提交到最旧的受支持版本,然后将这些修复向上合并到更新的分支来避免移植变更集。
cd release25-maint
hg import fix_some_bug.diff
# Review patch and run test suite. Revert if failure.
hg push
cd ../release26-maint
hg pull ../release25-maint
hg merge
# Resolve conflicts, if any. Then, review patch and run test suite.
hg commit -m "Merged patches from release25-maint."
hg push
cd ../trunk
hg pull ../release26-maint
hg merge
# Resolve conflicts, if any, then review.
hg commit -m "Merged patches from release26-maint."
hg push
尽管这种方法会使历史非线性且稍微难以理解,但它鼓励在所有受支持的版本中修复错误。此外,当有许多更改需要回溯时,它的扩展性更好,因为我们不需要寻找特定的修订 ID 进行合并。
git
在 git 中,我将有一个包含所有相关主仓库分支的工作区。git cherry-pick 不能跨仓库工作;您需要将分支放在同一个仓库中。
# Assume patch applied to 2.7 in revision release27~3 (4th patch back from tip).
cd integration
git checkout release26
git cherry-pick release27~3
# If there are conflicts, resolve them, and commit those changes.
# git commit -a -m "Resolve conflicts."
# Run test suite. If fixes are necessary, record as a separate commit.
# git commit -a -m "Fix code causing test failures."
git checkout master
git cherry-pick release27~3
# Do any conflict resolution and test failure fixups.
# Revert Misc/NEWS changes.
git checkout HEAD^ -- Misc/NEWS
git commit -m 'Revert cherry-picked Misc/NEWS changes.' Misc/NEWS
# Push both ports.
git push release26 master
如果您经常从给定分支合并(而不是精选),那么您可以通过合并然后恢复来阻止给定提交在将来被意外合并。这并不能阻止精选拉入不需要的补丁,并且此技术要求阻止所有您不想合并的东西。我不确定这与 svn 在这一点上是否不同。
cd trunk
# Merge in the alpha tested code.
git merge experimental-branch
# We don't want the 3rd-to-last commit from the experimental-branch,
# and we don't want it to ever be merged.
# The notation "^N" means Nth parent of the current commit. Thus HEAD^2^1^1
# means the first parent of the first parent of the second parent of HEAD.
git revert HEAD^2^1^1
# Propagate the merge and the prohibition to the public repository.
git push
新功能的协作开发
有时核心开发者会与多位开发者一起开发一个主要功能。作为一名核心开发者,我希望能够将功能分支发布到一个公共位置,以便我可以与其他开发者协作。
这需要在一个服务器上创建一个分支,其他开发者可以访问。所有 DVCS 都支持在开发者已经能够提交的主机上创建新的仓库,并对仓库主机进行适当的配置。这在概念上类似于 svn 中现有的沙盒,尽管仓库初始化的细节可能有所不同。
对于非核心开发者,有各种或多或少面向公众的仓库托管服务。Bazaar 有 Launchpad,Mercurial 有 bitbucket.org,而 git 有 GitHub。所有这些都为维护自己服务器的开发者提供了易于使用的 CGI 接口。
- 分支主干。
- 从服务器上的分支拉取。
- 从主干拉取。
- 将合并推送到主干。
svn
# Create branch.
svn copy svn+ssh://pythondev@svn.python.org/python/trunk svn+ssh://pythondev@svn.python.org/python/branches/NewHotness
svn checkout svn+ssh://pythondev@svn.python.org/python/branches/NewHotness
cd NewHotness
svnmerge init
svn commit -m "Initialize svnmerge."
# Pull in changes from other developers.
svn update
# Pull in trunk and merge to the branch.
svnmerge merge
svn commit -F svnmerge-commit-message.txt
此场景不完整,因为在工作完成之前就决定了使用哪个 DVCS。
分离问题依赖关系
有时,在处理一个问题时,会发现正在处理的问题实际上是各种较小问题的复合问题。能够将当前工作分离出来,然后开始处理一个单独的问题,这对于将问题分解为独立的任务单元,而不是将其复合为一个单一的大型任务单元非常有帮助。
- 创建分支 A(例如 urllib 有一个 bug)。
- 编辑一些代码。
- 创建分支 A 依赖的新分支 B(例如 urllib bug 暴露了一个 socket bug)。
- 在分支 B 中编辑一些代码。
- 提交分支 B。
- 在分支 A 中编辑一些代码。
- 提交分支 A。
- 清理。
svn
为了弥补 svn 缺乏廉价分支的不足,它有一个变更列表选项,可以将文件与单个变更列表关联。这不如能够与提交级别关联强大。也没有办法表达变更列表之间的依赖关系。
cp -r trunk issue0000
cd issue0000
# Edit some code.
echo "The cake is a lie!" > README
svn changelist A README
# Edit some other code.
echo "I own Python!" > LICENSE
svn changelist B LICENSE
svn ci -m "Tell it how it is." --changelist B
# Edit changelist A some more.
svn ci -m "Speak the truth." --changelist A
cd ..
rm -rf issue0000
bzr
这是一种使用 bzr shelf(现在是 bzr 的标准部分)暂时搁置一些更改的方法,同时您绕道修复 socket 错误。
bzr branch trunk bug-0000
cd bug-0000
# Edit some code. Dang, we need to fix the socket module.
bzr shelve --all
# Edit some code.
bzr commit -m "Socket module fixes"
# Detour over, now resume fixing urllib
bzr unshelve
# Edit some code
另一种方法是使用 loom 插件。Loom 可以极大地简化处理依赖分支的工作,因为它们会自动为您处理堆叠依赖。想象一下 loom 是一堆依赖分支(在 loom 术语中称为“线程”),可以轻松地在线程堆栈中上下移动,将更改向上合并到后代线程,在线程之间创建差异等。有时,您可能需要或希望将您的 loom 线程导出到单独的分支中,以便进行审查或提交。更高的线程会自动包含较低线程中的所有更改。
bzr branch trunk bug-0000
cd bug-0000
bzr loomify --base trunk
bzr create-thread fix-urllib
# Edit some code. Dang, we need to fix the socket module first.
bzr commit -m "Checkpointing my work so far"
bzr down-thread
bzr create-thread fix-socket
# Edit some code
bzr commit -m "Socket module fixes"
bzr up-thread
# Manually resolve conflicts if necessary
bzr commit -m 'Merge in socket fixes'
# Edit me some more code
bzr commit -m "Now that socket is fixed, complete the urllib fixes"
bzr record done
额外加分点:假设另一个人以您刚刚完成的方式修复了套接字模块。也许这个人甚至获取了您的 fix-socket 线程并将其仅应用到主干。您希望能够将他们的更改合并到您的 loom 中并删除您现在多余的 fix-socket 线程。
bzr down-thread trunk
# Get all new revisions to the trunk. If you've done things
# correctly, this will succeed without conflict.
bzr pull
bzr up-thread
# See? The fix-socket thread is now identical to the trunk
bzr commit -m 'Merge in trunk changes'
bzr diff -r thread: | wc -l # returns 0
bzr combine-thread
bzr up-thread
# Resolve any conflicts
bzr commit -m 'Merge trunk'
# Now our top-thread has an up-to-date trunk and just the urllib fix.
hg
一种方法是使用 shelve 扩展;这个扩展不包含在 Mercurial 中,但它很容易安装。使用 shelve,您可以选择暂时搁置的更改。
hg clone trunk issue0000
cd issue0000
# Edit some code (e.g. urllib).
hg shelve
# Select changes to put aside
# Edit some other code (e.g. socket).
hg commit
hg unshelve
# Complete initial fix.
hg commit
cd ../trunk
hg pull ../issue0000
hg merge
hg commit
rm -rf ../issue0000
有几种其他方法可以使用 Mercurial 来处理这种情况。Alexander Solovyov 在 Mercurial 邮件列表中提出了几种替代方法。
git
cd trunk
# Edit some code in urllib.
# Discover a bug in socket, want to fix that first.
# So save away our current work.
git stash
# Edit some code, commit some changes.
git commit -a -m "Completed fix of socket."
# Restore the in-progress work on urllib.
git stash apply
# Edit me some more code, commit some more fixes.
git commit -a -m "Complete urllib fixes."
# And push both patches to the public repository.
git push
加分项:假设您花了一些时间,而另一个人以您刚刚完成的相同方式修复了 socket,并将其提交到主干。在这种情况下,您的推送将失败,因为您的分支不是最新的。如果修复只是一行代码,那么很有可能它 *完全* 相同,字符对字符。git 会注意到这一点,您就完成了;git 会默默地合并它们。
假设我们没那么幸运
# Update your branch.
git pull git://code.python.org/public/trunk master
# git has fetched all the necessary data, but reports that the
# merge failed. We discover the nearly-duplicated patch.
# Neither our version of the master branch nor the workspace has
# been touched. Revert our socket patch and pull again:
git revert HEAD^
git pull git://code.python.org/public/trunk master
与 Bazaar 和 Mercurial 类似,git 也有管理补丁堆栈的扩展。您可以使用 Andrew Morton 的原始 Quilt,或者有 StGit(“stacked git”),它以类似于 Mercurial Queues 或 Bazaar looms 的方式将大量补丁的补丁跟踪集成到 VCS 中。
发布 Python
使用 DVCS 时,PEP 101 将如何改变?
bzr
它会改变,但不会实质性改变。在进行维护分支时,我们只会推送到新位置,而不是进行 svn cp。标签完全不同,因为在 svn 中它们是目录副本,但在 bzr(我猜 hg 也一样)中,它们只是特定分支上修订的符号名称。release.py 脚本必须更改为使用 bzr 命令。DVCS(特别是 bzr)的精选和合并功能足够好,我们有可能更早地创建维护分支。尝试从 bzr/hg 镜像发布将是一个有益的练习。
hg
显然,PEP 101 和发布脚本中特定于 Subversion 的细节需要更新。特别是,发布标记和维护分支创建过程必须修改以使用 Mercurial 的功能;这将简化和精简发布过程的某些方面。例如,标记和重新标记发布将成为一项微不足道的操作,因为在 Mercurial 中,标记只是给定修订的符号名称。
git
它会改变,但不会实质性改变。在进行维护分支时,我们只会 git push 到新位置,而不是进行 svn cp。标签完全不同,因为在 svn 中它们是目录副本,但在 git 中它们只是修订的符号名称,分支也是如此。(标签和分支的区别在于,标签引用特定的提交,除非您使用 git tag -f 强制它们移动,否则永远不会改变。另一方面,检出的分支会自动由 git commit 更新。)release.py 脚本必须更改为使用 git 命令。使用 git,我会在发布工程师选定后立即创建一个(本地)维护分支。然后我会“git pull”,直到我不喜欢某个补丁,这时我会“git pull; git revert ugly-patch”,直到它开始看起来明智的做法是分叉出去,并开始对好的补丁进行“git cherry-pick”。
平台/工具支持
操作系统
| DVCS | Windows | OS X | UNIX |
|---|---|---|---|
| bzr | 是 (安装程序) 带 tortoise | 是 (安装程序, fink 或 MacPorts) | 是 (各种软件包格式) |
| hg | 是 (第三方安装程序) 带 tortoise | 是 (第三方安装程序, fink 或 MacPorts) | 是 (各种软件包格式) |
| git | 是 (第三方安装程序) | 是 (第三方安装程序, fink 或 MacPorts) | 是 (.deb 或 .rpm) |
如上表所示,所有三个 DVCS 都可在所有三个主要的操作系统平台上使用。但它还显示,Bazaar 是唯一直接通过二进制安装程序支持 Windows 的 DVCS,而 Mercurial 和 git 需要您依赖第三方提供二进制文件。bzr 和 hg 都有一个 tortoise 版本,而 git 没有。
Bazaar 和 Mercurial 还有优势在于它们以纯 Python 形式提供,并可选择扩展以提高性能。
CRLF -> LF 支持
- bzr
- 据我了解,在我打字时,对它的支持正在进行中,很快就会发布一个版本。我将尝试挖掘细节。
- hg
- 通过 win32text 扩展支持。
- git
- 我无法从个人经验判断,但看起来通过 core.autocrlf 和 core.safecrlf 配置属性支持相当好。
不区分大小写的文件系统支持
- bzr
- 应该没问题。我经常在 Linux 和 OS X 之间共享分支。我做过大小写更改(例如
bzr mv Mailman mailman),只要我在 Linux 上完成(显然),当我在 OS X 上拉取更改时,一切都很好。 - hg
- Mercurial 使用区分大小写的仓库机制,并检测大小写折叠冲突。
- git
- 由于 OS X 保留大小写,您也可以在那里进行大小写更改。git 在任何方向的重命名都没有问题。然而,不区分大小写的文件系统支持通常被理解为抱怨区分大小写文件系统上的冲突。git 不会这样做。
工具
在代码审查工具方面,例如 Review Board 和 Rietveld,前者支持所有三个,而后者支持 hg 和 git 但不支持 bzr。Bazaar 尚未有在线审查板,但它有几种管理基于电子邮件的审查和主干合并的方式。有 Bundle Buggy、Patch Queue Manager (PQM) 和 Launchpad 的代码审查。
这三者都有一些在线网站,为希望在线放置仓库的人提供基本的托管支持。Bazaar 有 Launchpad,Mercurial 有 bitbucket.org,git 有 GitHub。Google Code 还提供了关于如何将 git 与该服务一起使用的说明,包括如何托管仓库和如何充当只读镜像。
在 Subversion 之上使用
| DVCS | svn 支持 |
|---|---|
| bzr | bzr-svn (第三方) |
| hg | 多个第三方 |
| git | git-svn |
所有三种 DVCS 都支持 svn,尽管 git 是唯一开箱即用支持 svn 的。
服务器支持
| DVCS | 网页界面 |
|---|---|
| bzr | loggerhead |
| hg | hgweb |
| git | gitweb |
所有三种 DVCS 都支持客户端和服务器端的各种钩子,例如提交前/提交后验证。
开发
所有三个项目都在积极开发中。Git 似乎是每月发布一次。Bazaar 是按时间每月发布一次。Mercurial 是按 4 个月定时发布一次。
特殊功能
bzr
Martin Pool 补充道:“bzr 拥有稳定的 Python 脚本接口,区分公共接口和私有接口,并为正在更改的 API 设有弃用窗口。一些插件列在 https://edge.launchpad.net/bazaar 和 http://bazaar-vcs.org/Documentation。”
hg
亚历山大·索洛夫 (Alexander Solovyov) 评论道
Mercurial 拥有易于使用的丰富 API,提供主要事件的钩子和扩展命令的能力。此外,还有与 Mercurial 一起分发的 mq(Mercurial 队列)扩展,它简化了补丁处理。
git
git 有一个 cvsserver 模式,也就是说,您可以使用 CVS 从 git 检出树。您甚至可以提交到树中,但是缺少合并等功能,并且分支被作为 CVS 模块处理,这可能会让资深 CVS 用户感到震惊。
测试/印象
由于我(Brett Cannon)最终负责做出选择哪个/任何 DVCS 的最终决定,而不是我的合著者,我觉得有必要记录下我进行的测试和我在评估各种工具时的印象,以便尽可能透明。
入门门槛
检出 Python 仓库所需的时间和精力至关重要。如果难度或时间过长,那么希望为 Python 做出贡献的人很可能会放弃。这种情况绝不能发生。
我测量了作为非核心开发者检出 2.x 主干的时间。计时使用 zsh 中的 time 命令完成,空间使用 du -c -h 计算。
| DVCS | 旧金山 | 温哥华 | 空间 |
|---|---|---|---|
| svn | 1:04 | 2:59 | 139 M |
| bzr | 10:45 | 16:04 | 276 M |
| hg | 2:30 | 5:24 | 171 M |
| git | 2:54 | 5:28 | 134 M |
将这些数字与 svn 进行比较时,重要的是要认识到这不是一对一的比较。Svn 不像所有 DVCS 那样拉取整个修订历史。这意味着 svn 可以比 DVCS 更快地执行初始检出,这纯粹基于它需要下载的网络信息更少的事实。
基本信息功能的性能
为了查看这些工具在执行需要查询历史记录的命令时的表现,我们对 README 文件的日志进行了计时。
| DVCS | 时间 |
|---|---|
| bzr | 4.5 秒 |
| hg | 1.1 秒 |
| git | 1.5 秒 |
在此测试中值得注意的是,git 花费的时间比其他三个工具更长,才能找出如何在不使用分页器的情况下获取日志。虽然分页器使用总体上是一个不错的功能,但没有它自动开启却花费了一些时间(结果是主 git 命令有一个 --no-pager 标志来禁用分页器的使用)。
从内置帮助中找出要使用的命令
我最终试图找出查看仓库克隆自哪个 URL 的命令。为此,我只使用了工具本身或其手册页提供的帮助。
Bzr 最简单:bzr info。运行 bzr help 没有显示我想要的,但提到了 bzr help commands。该列表有我想要的命令,并且描述合理。
Git 是第二简单的。命令 git help 没有显示太多内容,也没有列出所有命令的方法。这时我查看了手册页。阅读各种命令后,我发现了 git remote。该命令本身只输出了 origin。尝试 git remote origin 显示错误并打印出命令用法。这时我注意到了 git remote show。运行 git remote show origin 给了我想要的信息。
对于 hg,我从未自己找到我想要的信息。事实证明我想要 hg paths,但这从 hg help 打印的“显示符号路径名的定义”的描述中并不明显(值得注意的是,在 PEP 中报告此事确实促使 Mercurial 开发者澄清措辞,使 hg paths 命令的使用更清晰)。
更新检出
为了查看更新过时仓库所需的时间,我计时了更新一个落后 700 次提交和 50 次提交的仓库(分别过时三周和一周)。
| DVCS | 700 次提交 | 50 次提交 |
|---|---|---|
| bzr | 39 秒 | 7 秒 |
| hg | 17 秒 | 3 秒 |
| git | 不适用 | 4 秒 |
注意
Git 缺少 *700 次提交* 场景的值,因为它似乎不允许在特定修订处检出仓库。
Git 因其 git pull 命令的输出而值得特别提及。它不仅列出了每个文件的增量更改信息,还对信息进行了颜色编码。
决定
在 PyCon 2009 上,决定选择 Mercurial。
为什么选择 Mercurial 而不是 Subversion
虽然 svn 很好地服务了开发团队,但必须承认 svn 并不能像 DVCS 那样很好地满足非提交者的需求。由于 svn 仅向对仓库具有提交权限的人提供版本控制、分支等功能,因此它可能会阻碍那些缺乏提交权限的人。但 DVCS 没有这种限制,因为任何人都可以创建 Python 的本地分支并执行自己的本地提交,而无需承担克隆整个 svn 仓库的负担。允许任何人拥有与核心开发者相同的工作流程是选择从 svn 切换到 hg 的关键原因。
与允许任何人轻松地本地提交到自己的分支的好处正交的是离线、快速操作。由于 hg 在本地存储所有数据,因此无需向远程服务器发送请求,而是从本地磁盘工作。这极大地提高了响应时间。它还允许在没有互联网连接时离线使用。但这种好处是次要的,被认为只是一个副作用,而不是从 Subversion 切换的驱动因素。
为什么选择 Mercurial 而不是其他 DVCS
Git 未被选中主要有三个原因(参见 PyCon 2009 闪电演讲,Brett Cannon 在其中列出了这些确切的原因;演讲开始于 3:45)。首先,git 的 Windows 支持是正在考虑的三种 DVCS 中最弱的,这是不可接受的,因为 Python 需要支持在其运行的任何平台上进行开发。由于 Python 在 Windows 上运行,并且有些人确实在该平台上进行开发,因此它需要可靠的支持。虽然 git 的支持正在改进,但截至目前,它的差距足够大,足以被认为是一个问题。
第二,与第一个问题同样重要的是,Python 核心开发者对三种 DVCS 选项中 git 的喜欢程度最低,差距很大。如果您查看下表,您将看到对核心开发者的调查结果,以及 git 作为最不受欢迎的版本控制系统的巨大差距。
| DVCS | ++ | 相等 | -- | 不了解 |
|---|---|---|---|---|
| git | 5 | 1 | 8 | 13 |
| bzr | 10 | 3 | 2 | 12 |
| hg | 15 | 1 | 1 | 10 |
最后,在所有条件相同的情况下(如前两个问题所示,情况并非如此),最好使用和支持用 Python 编写的工具,而不是用 C 和 shell 编写的工具。我们足够务实,不会仅仅因为它用 Python 编写就选择一个工具,但我们确实看到了在合理的情况下推广使用 Python 的工具的用处,就像在这种情况下一样。
至于为什么选择 Mercurial 而不是 Bazaar,归结为受欢迎程度。正如核心开发者调查所示,hg 比 bzr 更受欢迎。但在 PyCon 上宣布 git 被排除在考虑之外后,社区似乎也更喜欢 hg。许多人来到 Brett 面前,以各种方式表示他们希望选择 hg。虽然没有人说他们不希望选择 bzr,但也没有人说他们希望选择 bzr。
基于所有这些信息,Guido 和 Brett 决定 Mercurial 将成为 Python 的下一个版本控制系统。
过渡计划
PEP 385 概述了从 svn 到 hg 的过渡。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0374.rst