在开发项目中,Git是一个天赐良机。然而,如果舞池里有很多舞者,总有一两个会互相踩到对方的脚趾。对于您的项目来说,这意味着两个开发者可能会在同一套代码上工作,并且都有可能提交。在这种情况下,您需要采取一些 Git 合并策略来解决冲突。
虽然 Git 合并可以很简单,但很多时候您还需要更高级的方法。比如递归合并、三方合并等等。您甚至可能需要在某些时候撤销 Git 合并。
本教程将讨论一些复杂的 Git 合并技术。事实上,我们可以直接进入正题!
Git 合并策略介绍
合并的核心概念很简单:把两个分支合并起来,把多个提交变成一个提交。然而,为了确保提交和合并的代码是正确的,您可以使用一些技巧。
下面我们将介绍一些您需要了解的重要策略。它们并不按顺序排列,在你的开发生涯中,你可能会用到它们。此外,您还需要对指针、分支和提交等Git基本概念有扎实的理解。
双向合并和三向合并的区别
了解双向合并和三向合并的区别很有帮助。我们接下来介绍的大多数合并策略都是针对三向合并的。事实上,三向合并更简单明了。请看下面这个例子:
- 您有一个有若干提交的主分支和一个也有提交的特性分支。
- 然而,如果主分支现在进一步提交,两个分支就会出现分歧。
- 通俗地说,主分支和特性分支都有提交,而另一个没有。如果用双向方法合并,就会丢失一个提交(很可能是主分支上的。)
- 相反,Git 会从当前的主分支和特性分支中创建一个新的合并提交。
一言以蔽之,Git 会查看三个不同的快照来合并变更:主分支的头,特性分支的头,以及共同祖先。这将是主分支和特性分支共同的最终提交。
在实践中,你不需要担心某个合并策略是双向的还是三向的。很多时候,你必须使用某种策略。无论如何,了解 Git 在合并分支和版本库时是如何 “思考” 的,是很有帮助的。
快速合并
第一个策略可能不需要执行任何操作。快进合并会将指针移到主分支的最新提交上,而不会产生额外的提交(这可能会造成混乱)。这是一种简洁的方法,许多开发者会将其作为标准。
该技术从一个可能有提交也可能没有提交的主分支开始。在这种情况下,您打开一个新的分支,编写代码并提交。此时,您还需要将这些变更合并回主分支。快进合并有一个要求:
- 您需要确保在新分支上工作时,主分支上没有其他提交。
这并不总是可能的,尤其是当你在一个大团队中工作时。即便如此,如果您选择将您的提交与当前的、没有自己提交的主分支合并,这将执行一次快进合并。有几种不同的方法:
git merge <branch> git merge --ff-only
很多时候,你不需要指定要运行快进合并。这种类型的合并发生在单人项目或小团队项目中。在快节奏的环境中,这种合并很少见。因此,其他类型的合并更为常见。
递归合并
递归合并通常是默认的,因为它比其他类型的合并更常见。递归合并是指在一个分支上进行提交,但在主分支上也进行提交。
当需要合并时,Git 会对分支进行递归,以完成最终提交。这意味着,一旦完成合并,就会有两个父提交。
和快进合并一样,通常不需要指定递归合并。不过,你可以用下面的命令和标志确保 Git 不会选择类似快进合并的方式:
git merge --no-ff git merge -s recursive <branch1> <branch2>
第二行使用 -s
策略选项和显式命名来执行合并。与快进合并不同,递归合并会创建一个专门的合并提交。对于双向合并,递归策略是可靠的,并且运行良好。
我们的和他们的
开发过程中常见的一种情况是,你在项目中创建了一个新特性,但最终没有获得绿灯。在很多情况下,你会有很多代码需要合并,而这些代码又是相互依赖的。我们的合并是解决这些冲突的最佳方式。
这种类型的合并可以根据需要处理多个分支,并忽略其他分支上的所有变更。如果你想清除旧特性或不需要的开发,它是个不错的选择。下面是你需要的命令:
git merge -s ours <branch1> <branch2>
我们的合并本质上意味着当前分支包含了法律上的代码。这与 “他们的” 合并有关,后者将其他分支视为正确的。不过,您需要在这里传递另一个策略选项:
git merge -X theirs <branch2>
使用 “我们的” 和 “他们的” 合并可能会引起混淆,但一般来说,坚持典型的使用情况(即保留当前分支中的所有内容并丢弃其余部分)是安全的。
Octopus
处理多头合并–即把多个分支合并到另一个分支–对于 git 合并来说是个棘手的问题。可以说您需要两只以上的手来解决冲突。这对于octopus合并来说再合适不过了。
octopus合并与我们和他们的合并截然相反。典型的用例是你想把多个类似功能的提交合并成一个。下面是如何传递的:
git merge -s octopus <branch1> <branch2>
但如果需要手动解决,Git会拒绝octopus合并。对于自动决议,如果需要将多个分支合并为一个,则默认使用octopus合并。
解决
决议合并是最安全的合并提交方式之一,如果遇到交叉合并的情况,决议合并是个不错的选择。这也是一种快速的解决方法。您可能还想用它来处理更复杂的合并历史–但仅限于双头合并。
git merge -s resolve <branch1> <branch2>
由于解析合并使用一种三向算法,同时处理您当前的分支和您从其中提取的分支,因此它可能不如其他合并方法灵活。不过,就您需要它完成的工作而言,解析合并近乎完美。
子树
递归合并的同伴可能会让你感到困惑。我们试着用一个清晰的例子来解释:
- 首先,考虑两个不同的树–X和Y。
- 你想把这两个树合并成一个。
- 如果Y树与X中的一个子树相对应,那么Y树就会被修改以匹配X的结构。
这意味着,如果您想将多个版本合并为一篇明确的文章,子树合并就非常有用。它还会对两个分支的共同 “ancestor” 树进行必要的修改。
git merge -s subtree <branch1> <branch2>
简而言之,如果您需要合并两个版本库,子树合并就是您想要的。事实上,你可能很难理解哪种合并策略适合你。稍后,我们将讨论一些可以提供帮助的工具。
在此之前,你必须知道如何解决一些高级的合并冲突。
如何处理更复杂的 Git 合并冲突
在 Git 中合并分支更像是管理和解决冲突。团队和项目规模越大,发生冲突的几率就越大。有些冲突可能很复杂,很难解决。
考虑到冲突会侵蚀时间、金钱和资源,您需要想办法把它们快速消灭在萌芽状态。在大多数情况下,两个开发人员会在同一套代码上工作,并且两人都会决定提交。
这可能意味着你可能会因为待处理的变更而根本无法开始合并,或者在合并过程中出现故障,需要人工干预。一旦工作目录 “干净” 了,就可以开始了。很多时候,Git 会在你开始合并后通知你有冲突:
终端窗口显示Git中的合并冲突。
不过,您可以运行 git status
查看详细信息:
显示 git status 命令结果的终端窗口。
从这里,您可以开始处理导致冲突的各种文件。我们接下来讨论的一些工具和技术会有所帮助。
中止和重置合并
有时,您需要完全停止合并,从一片净土开始。事实上,我们提到的两个命令都适用于你还不知道如何处理冲突的情况。
你可以用下面的命令中止或重置正在进行的合并:
git merge --abort git reset
这两个命令类似,但在不同的情况下使用。例如,中止合并会简单地将分支恢复到合并前的状态。在某些情况下,这并不起作用。例如,如果你的工作目录中包含了未提交和未合并的修改,你就无法中止合并。
然而,重置合并意味着将文件恢复到已知的 “良好” 状态。如果 Git 启动合并失败,可以考虑后者。需要注意的是,这条命令会删除任何你没有提交的改动,这意味着这是一个需要小心谨慎的行为。
检查冲突
大多数合并冲突都可以直接确定和解决。然而,在某些情况下,你可能需要深入挖掘,以找出冲突发生的原因,以及如何开始修复它。
您可以在 git merge
后使用 checkout 获取更多信息:
git checkout --conflict=diff3 <filename>
这将使用检出所提供的典型导航,并创建两个文件之间的比较,以显示合并冲突:
检查特定项目文件中的冲突。
从技术意义上讲,这将再次检查文件并替换冲突标记。在整个解决过程中,您可能会多次这样做。在这里,如果您通过 diff3
参数,它将给出基本版本和 “我们的” 和 “他们的” 版本的替代版本。
注意默认的参数选项是 merge
,除非你改变了合并冲突的样式,否则不必指定。
忽略Negative Space
负空格(Negative space)及其用法是一个常见的讨论点。一些编程语言会使用不同类型的间距,甚至个别开发人员也会使用不同的格式。
空格与制表符是我们不会加入的战场。不过,如果您遇到格式根据文件和编码实践而变化的情况,您可能会遇到这个 Git 合并问题。
您会发现合并失败的原因,因为当您查看冲突时,会发现有一些行被移除或添加:
在编辑器中显示冲突差异的文件。
这是因为 Git 会查看这些行,并将负空格视为修改。
不过,您可以在 git merge
命令中添加特定参数,这样就可以忽略相关文件中的负空格:
git merge -Xignore-all-space git merge -Xignore-space-change
虽然这两个参数看似相似,但它们有一个独特的区别。如果您选择忽略所有负空格,Git 会这样做。这是一种一刀切的做法,但相比之下, -Xignore-space-change
只将一个或多个负空格字符的序列视为等价字符。因此,它会忽略行尾的单空格。
为了更安全起见,你也可以使用 --no-commit
命令查看合并结果,以检查你是否以正确的方式忽略和计算了负空格。
合并日志
日志对于几乎所有传递数据的软件都至关重要。对于 Git 来说,您可以使用日志来确定合并冲突的更多细节。您可以使用 git log
访问这些信息:
在终端运行并查看 Git 日志。
它本质上是一个文本文件转储站,记录 repo 中的每个操作。不过,您可以添加更多参数来细化视图,只查看您希望看到的提交:
git log --oneline --left-right <branch1>...<branch2>
它使用 “Triple Dot” 来提供合并过程中两个分支所涉及的提交列表。它将过滤两个分支共享的所有提交,留下一部分提交供进一步调查。
您也可以使用 git log --oneline --left-right --merge
来只显示合并时两边 “触及 “冲突文件的提交。 -p
选项将显示特定 “diff” 的确切改动,但注意这只针对非合并提交。有一个解决方法,我们接下来介绍。
使用组合差分格式调查 Git 合并冲突
您可以利用 git log
的视图进一步调查合并冲突。在通常情况下,Git 会合并代码,并将合并成功的代码分期。这将只留下有冲突的行,您可以使用 git diff
命令查看它们:
在终端运行 git diff 命令。
这种 “合并差异” 格式增加了两列额外的信息。第一列告诉您某行在您的分支(”我们的”)和工作拷贝之间是否不同;第二列给您 “他们的” 分支的相同信息。
对于符号,加号表示该行是否是工作副本中的新增行,但不在合并的那一侧;减号表示该行是否被移除。
在 Git 日志中也可以看到这种组合的差异格式:
git show git log --cc -p
第一个命令用于查看合并提交的历史。第二个命令使用 -p
的功能来显示对非合并提交的修改,同时显示合并的 diff 格式。
如何撤销 Git 合并
错误可能会发生,你可能会进行一些需要收回的合并。在某些情况下,您可以使用 git commit --amend
简单地修改最近的提交。这会打开编辑器,让您修改最近的提交信息。
虽然您可以逆转更复杂的合并冲突和由此产生的改动,但这可能很困难,因为提交通常是永久性的。
因此,你需要遵循很多步骤:
- 首先,您需要查看提交,找到您需要的合并的引用。
- 其次,检查分支,查看提交历史。
- 一旦了解了所需的分支和提交,就可以根据需要执行特定的Git命令了。
让我们详细了解一下这些命令,从审查流程开始。接下来,我们将向您展示如何快速撤销 Git 合并,然后针对更高级的用例介绍具体的命令。
审核提交
如果想查看当前分支的修订ID和提交信息, git log --oneline
命令是个不错的选择:
在终端运行一行 git diff 命令。
git log --branches=*
命令会显示同样的信息,但针对所有分支。无论如何,您可以在使用 git checkout
的同时使用引用 ID 来创建一个 “分离的 HEAD
“状态。这意味着从技术角度看,您不会在任何分支上工作,而一旦您切换回已建立的分支,您就成了修改的 “孤儿”。
因此,您几乎可以把签出当作一个无风险的沙箱来使用。不过,如果您想保留修改,可以签出该分支,然后用 git checkout -b <branch-name>
给它起个新名字。这是撤销 Git 合并的可靠方法,但对于高级用例,还有更细致的方法。
使用 git 重置
许多合并冲突可能发生在本地仓库。在这种情况下,您需要使用 git reset
命令。不过,这个命令有更多的参数。下面是该命令的实际使用方法:
git reset --hard <reference>
第一部分 – git reset --hard
– 经过三个步骤:
- 将参考分支移动到合并提交前的位置。
- 硬重置使 “索引”(即下一个提交快照)看起来像参考分支。
- 使工作目录看起来像索引。
一旦你调用了这个命令,提交历史就会删除后来的提交,并重置历史到引用的ID。这是撤销 Git 合并的简洁方法,但并非适用于所有情况。
例如,如果你试图将本地重置的提交推送到包含该提交的远程仓库,就会导致错误。在这种情况下,您可以使用另一种命令。
使用 git revert
虽然 git reset
和 git revert
看起来很相似,但还是有一些重要的区别。在目前的例子中,撤销过程涉及移动引用指针和 HEAD 到特定的提交。这类似于洗牌来创建一个新的顺序。
相比之下, git revert
会根据回溯的改动创建一个新的提交,然后更新参考指针,使该分支成为新的 “顶端”。这也是为什么您应该用这条命令来处理远程仓库合并冲突的原因。
您可以用 git revert <reference>
来撤销 Git 合并。注意,您需要指定一个提交引用,否则命令无法运行。你也可以给命令传递 HEAD
来恢复到最新的提交。
不过,你可以让 Git 更清楚地知道你想做什么:
git revert -m 1 <reference>
当您调用合并时,新提交将有两个 “父分支”。一个是你指定的引用,另一个是你要合并的分支的顶端。在这种情况下, -m 1
会告诉 Git 保留第一个父分支,即指定的引用,作为 “主线”。
git revert
的默认选项是 -e
或 --edit
。这会打开编辑器,以便在还原之前修改提交信息。不过,您也可以通过 --no-edit
,它不会打开编辑器。
也可以通过 -n
或 --no-commit
。这会告诉 git revert 不创建新的提交,而是 “反向” 修改并将其添加到暂存索引和工作目录中。
Git 合并与重定向的区别
除了使用 git merge
命令,您还可以使用 git rebase
。这也是一种将改动整合到一个目录中的方法,但有所不同:
- 当您使用
git merge
时,默认情况下是三路合并。它结合了两个当前分支的快照,然后与两个分支的共同祖先合并,创建一个新的提交。 - 而重置则是将不同分支中的补丁应用到另一个分支中,而不需要祖先分支。这意味着不会有新的提交。要使用这个命令,先签出到你想重定向的分支。
在那里,你可以使用下面的命令:
git rebase -i <reference>
在很多情况下,您的参考分支就是您的主分支。 -i
选项启动 “交互式重定向”。这使您有机会在提交移动时修改它们。您可以用它来清理提交历史,这也是使用 git rebase
的一大好处。
运行该命令将在编辑器中显示潜在的提交列表。这样您就可以完全改变提交历史的外观。如果将 pick
命令改为 fixup
,您还可以合并提交。一旦您保存了修改,Git 就会执行重置(rebase)。
总的来说,Git 合并可以解决很多冲突。不过,重置也有很多好处。例如,合并简单易用,可以保留合并历史的上下文,而重置则可以简化提交历史。
尽管如此,在重定向时你必须更加小心,因为出错的可能性很大。此外,你不应该在公共分支上使用这种技术,因为rebasing只会影响你的repo。要修复由此产生的问题,您需要进行更多合并,并会看到多次提交。
帮助您更好地管理 Git 合并的工具
鉴于 Git 合并冲突的复杂性,你可能需要帮手。有很多工具可以帮助您成功合并,如果您使用的是Intellij IDEA,您可以使用 “Branches” 菜单中的内置方法:
在Intellij IDEA中查看分支。
VSCode在其用户界面(UI)中也包含了类似的功能。Atom的老用户会发现微软在这里继承了其出色的Git集成,无需其他扩展或插件即可连接GitHub。
您还可以通过命令面板(Command Palette)获得更多选项。这甚至适用于基于VSCode开源框架的编辑器,如Onivim2:
在Onivim2的命令面板中使用 “合并分支 “命令。
和本列表中的所有工具一样,这个命令的好处是你不需要命令行来执行合并。您通常需要从下拉菜单中选择源分支和目标分支,然后让编辑器执行合并。即便如此,您也不必放手不管。你可以在合并后查看修改,然后进行你需要的提交。
Sublime Text是一款为Git工作提供独立图形用户界面(GUI)的编辑器。如果你使用的是Sublime Text,那么Sublime Merge将是你工作流程的理想补充:
Sublime Merge应用程序。
无论你选择哪种代码编辑器,通常都能在不使用命令行的情况下使用Git。Vim和Neovim甚至可以使用Tim Pope的Git Fugitive插件。
不过,也有一些专门的第三方合并工具专注于这项任务。
专用的Git合并
例如,Mergify是一款企业级的代码合并工具,可以集成到持续集成/持续交付(CI/CD)管道和工作流中:
Mergify 网站。
这里的一些功能可以帮助您在合并前自动更新您的拉取请求,根据优先级重新排序,以及批处理它们。对于开源解决方案,Meld可能是有价值的:
Meld应用程序界面。
其稳定版本支持Windows和Linux,并在GPL许可下运行。它为您提供了比较分支、编辑合并等基本功能。您甚至可以进行双向或三向比较,并支持Subversion等其他版本控制系统。
小结
Git是高效协作和管理代码变更的重要工具。然而,如果多个开发者在同一代码上工作,可能会产生冲突。Git 合并策略可以帮助您解决这些冲突,而且有很多方法。对于更复杂的 Git 合并策略,您需要使用高级策略。
这甚至可以简单到忽略负空格或搜索日志。不过,您也不必总是使用命令行。有很多应用程序可以帮助你,你的代码编辑器通常也会使用内置界面。
评论留言