在分布式版本控制系统中,合并代码比在Subversion中更好的主要原因是基于Subversion早期的分支和合并工作方式。在1.5.0之前的Subversion没有存储关于分支合并时间的任何信息,因此当您想要合并时,必须指定要合并的修订版本范围。
考虑以下示例:
1 2 4 6 8
trunk o-->o-->o---->o---->o
\
\ 3 5 7
b1 +->o---->o---->o
当我们想要将b1的更改合并到主干时,我们需要在检出主干的文件夹中执行以下命令:
请注意保留HTML标签。svn merge -r 2:7 {link to branch b1}
... 将尝试将 b1
中的更改合并到您的本地工作目录中。然后在解决任何冲突并测试结果后提交更改。当您提交修订时,修订树将如下所示:
1 2 4 6 8 9
trunk o-->o-->o---->o---->o-->o "the merge commit is at r9"
\
\ 3 5 7
b1 +->o---->o---->o
12 14
trunk …-->o-------->o
"Okay, so when did we merge last time?"
13 15
b1 …----->o-------->o
当使用分布式版本控制系统时,第一件要做的事情就是克隆存储库(git的clone
,hg的clone
和bzr的branch
)。克隆在概念上与在版本控制中创建分支相同。有些人称此为forking或branching(尽管后者常用于指代共同位于一个位置的分支),但实际上它们都是同一件事情。每个用户都运行自己的存储库,这意味着您有一个per-user branching正在进行。
origin
的中央仓库和一个用户Alice将仓库克隆到她的机器上。 a… b… c…
origin o<---o<---o
^master
|
| clone
v
a… b… c…
alice o<---o<---o
^master
^origin/master
a… b… c…
origin o<---o<---o
^ master
"what'll happen after a push?"
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
origin
存储库执行的操作是接收所有新的修订版本并将其分支移动到最新的修订版本(git 称之为“快进”)。 a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
我刚才举的例子根本不需要合并任何东西。因此,问题并不在于合并算法,因为所有版本控制系统之间的三向合并算法基本相同。问题更多地与结构有关。
诚然,上面的例子非常简单,所以我们来做一个更加复杂但更为常见的例子。还记得“origin”最初有三个修订版本吗?好的,那个做这些修订的人,我们叫他“Bob”,一直在自己的仓库里工作,并进行了提交:
a… b… c… f…
bob o<---o<---o<---o
^ master
^ origin/master
"can Bob push his changes?"
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
pull
,或者hg的pull
和merge
,或者bzr的merge
来合并更改。这是一个两步骤的过程。首先,Bob必须获取新的修订版本,这将从origin
仓库按原样复制它们。现在我们可以看到图表分叉了: v master
a… b… c… f…
bob o<---o<---o<---o
^
| d… e…
+----o<---o
^ origin/master
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
^ origin/master
fetch
和merge
手动执行两个步骤)。接下来需要做的是再次将这些更改推送到origin
,这将导致快进式合并,因为合并提交是最新的origin
存储库中直接后代。 v origin/master
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
v master
a… b… c… f… 1…
origin o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
此外,还存在将补丁发送给每个开发人员的问题,在Subversion中这是一个巨大的问题,但在git、hg和bzr中通过唯一可识别的修订版本得到缓解。一旦有人合并了他的更改(即进行了合并提交)并将其发送给团队中的其他人来使用,可以通过推送到中央存储库或发送补丁来消耗它们,那么他们就不必担心合并,因为合并已经完成。Martin Fowler将这种工作方式称为混合集成。
由于结构与Subversion不同,而是采用DAG,因此使得分支和合并不仅对系统而言更容易,对用户也更容易。
从历史上看,Subversion只能执行直接的双向合并,因为它没有存储任何合并信息。这涉及将一组更改应用于树。即使有合并信息,这仍然是最常用的合并策略。
Git默认使用3路合并算法,它涉及查找正在合并的头的公共祖先,并利用存在于合并的两侧的知识。这使得Git在避免冲突方面更加智能。
Git还具有一些复杂的重命名查找代码,这也有所帮助。它没有存储更改集或任何跟踪信息--它只存储每个提交时文件的状态,并根据需要使用启发式方法来定位重命名和移动的代码(磁盘上的存储比此更复杂,但它呈现给逻辑层的接口不会暴露任何跟踪)。
此外,SVN 声称跟踪重命名以帮助移动文件的合并。但实际上,它仍将其存储为复制和单独的删除操作,并且合并算法仍会在修改/重命名情况下遇到问题,即在一个分支上修改文件,在另一个分支上重命名,在这些分支被合并时。这种情况仍会产生虚假的合并冲突,并且在目录重命名的情况下,甚至会导致修改的静默丢失。(然后 SVN 的人们倾向于指出修改仍然在历史记录中,但当它们不在应该出现的合并结果中时,这并没有太大帮助。)
Git则完全不跟踪重命名,但会在合并时(merge)神奇地发现它们。SVN合并表示法也存在问题;在1.5/1.6版本中,你可以随意从主干自动合并到分支,但反向合并需要手动指定(--reintegrate选项),而且会导致分支处于无法使用的状态。最终他们发现这实际上并非如此,并且a)--reintegrate选项可以自动设置,b)在两个方向上进行多次合并是可能的。但在经历了所有这一切后(这表明他们缺乏对自己正在做什么的理解),我将非常谨慎地在任何复杂的分支场景中使用SVN,并希望能够查看Git对合并结果的看法。在答案中提到的其他观点,例如SVN中分支的强制全局可见性,并不涉及合并能力(但对于可用性很重要)。此外,“Git存储更改而SVN存储(不同的东西)”大多数情况下都不是关键点。Git在概念上将每个提交存储为单独的树(类似于tar文件),然后使用相当多的启发式方法来有效地存储它们。计算两个提交之间的更改与存储实现是分开的。真正的是,Git以比SVN更直接的方式存储历史DAG,而SVN则存储其合并信息。任何试图理解后者的人都会知道我的意思。
简而言之:Git使用比SVN更简单的数据模型来存储修订版本,因此它可以将大量精力放在实际合并算法上,而不是试图处理表示=>实际上更好的合并。
编辑:这主要针对问题的此部分:
这是因为两个系统本质上的差异,还是像Git/Mercurial这样的特定DVCS实现只是拥有更聪明的合并算法?
TL;DR - 这些特定工具有更好的算法。分布式具有一些工作流优势,但与合并优势无关。
结束编辑
我看了被接受的答案。它就是错的。
SVN合并可能很麻烦,也可能很笨重。但是,请暂时忽略它的实际工作方式。没有信息是Git可以保持或推导出来而SVN不行的。更重要的是,保持版本控制系统的分离(有时是部分的)不会为您提供更多实际信息。这两种结构完全等效。
假设你想做Git“更擅长”的“一些聪明事情”。而且你的内容已经被检入到SVN中。
将您的SVN转换为等效的Git形式,在Git中执行它,然后进行检查,可能使用多个提交、一些额外的分支。如果您可以想象一种自动化方式将SVN问题转化为Git问题,那么Git就没有根本性的优势。
最终,任何版本控制系统都将让我
1. Generate a set of objects at a given branch/revision.
2. Provide the difference between a parent child branch/revisions.
此外,对于合并来说,知道以下内容也很有用(或至关重要)
3. The set of changes have been merged into a given branch/revision.
Mercurial、Git和Subversion(现在原生支持,之前使用svnmerge.py)都可以提供这三个信息。为了展示DVC的根本优势,请指出一些第四个信息,它在Git / Mercurial / DVC中可用,而在SVN / 集中式VC中不可用。
这并不是说它们不是更好的工具!
git merge-base
。使用 Git,您可以说“分支 a 和 b 在修订版 x 处分开”。但 SVN 存储“从 foo 复制文件到 bar”,因此需要使用启发式方法来确定复制到 bar 是创建新分支还是在项目内复制文件。诀窍在于 SVN 中的修订版由修订号和基本路径定义。尽管大多数情况下可能会假设“主干”,但如果实际上有分支,则会出现问题。 - Douglas在其他答案中没有提到的一件事,而DVCS真正的优势之一是,在推送更改之前,您可以本地提交。在SVN中,当我有一些更改要检入时,并且与此同时有人已将同一分支提交,则意味着我必须在提交之前执行svn update
。这意味着我的更改和其他人的更改现在混合在一起,没有办法中止合并(例如使用git reset
或hg update -C
),因为没有提交可以回退。如果合并不太简单,这意味着在清理合并结果之前,您无法继续在您的功能上工作。
但是,也许这只是那些太傻以致于不会使用独立分支的人的优势(如果我记得正确,我们在使用SVN的公司中只有一个用于开发的分支)。