“合并前文件还在的,合并后就不见了”、“我遇到 Git 合并的 bug 了” 是两句经常听到的话,但真的是 Git 的 bug 么?或许只是你的预期不对。本文通过讲解三向合并和 Git 的合并策略,step by step 介绍 Git 是怎么做一个合并的,让大家对 Git 的合并结果有一个准确的预期,并且避免发生合并事故。

故事时间

在开始正文之前,先来听一下这个故事。

如下图,小明从节点 A 拉了一条 dev 分支出来,在节点 B 中新增了一个文件 http.js,并且合并到 master 分支,合并节点为 E。这个时候发现会引起线上 bug,赶紧撤回这个合并,新增一个 revert 节点 E'。过了几天小明继续在 dev 分支上面开发新增了一个文件 main.js,并在这个文件中 import 了 http.js 里面的逻辑,在 dev 分支上面一切运行正常。可当他将此时的 dev 分支合并到 master 时候却发现,http.js 文件不见了,导致 main.js 里面的逻辑运行报错了。但这次合并并没有任何冲突。他又得重新做了一下 revert,并且迷茫的怀疑是 Git 的 bug。

两句经常听到的话:

—— ”合并前文件还在的,合并后就不见了“

—— ”我遇到 Git 的 bug 了“

相信很多同学或多或少在不熟悉 Git 合并策略的时候都会发生过类似上面的事情,明明在合并前文件还在的,为什么合并后文件就不在了么?一度还怀疑是 Git 的 bug。这篇文章的目的就是想跟大家讲清楚 Git 是怎么去合并分支的,以及一些底层的基础概念,从而避免发生如故事中的问题,并对 Git 的合并结果有一个准确的预期。

如何合并两个文件

在看怎么合并两个分支之前,我们先来看一下怎么合并两个文件,因为两个文件的合并是两个分支合并的基础。

大家应该都听说过“三向合并”这个词,不知道大家有没有思考过为什么两个文件的合并需要三向合并,只有二向是否可以自动完成合并。如下图

很明显答案是不能,如上图的例子,Git 没法确定这一行代码是我修改的,还是对方修改的,或者之前就没有这行代码,是我们俩同时新增的。此时 Git 没办法帮我们做自动合并。

所以我们需要三向合并,所谓三向合并,就是找到两个文件的一个合并 base,如下图,这样子 Git 就可以很清楚的知道说,对方修改了这一行代码,而我们没有修改,自动帮我们合并这两个文件为 Print("hello")。

接下来我们了解一下什么是冲突?冲突简单的来说就是三向合并中的三方都互不相同,即参考合并 base,我们的分支和别人的分支都对同个地方做了修改。

Git 的合并策略

了解完怎么合并两个文件之后,我们来看一个使用 git merge 来做分支合并。如上图,将 master 分支合并到 feature 分支上,会新增一个 commit 节点来记录这次合并。

Git 会有很多合并策略,其中常见的是 Fast-forward、Recursive 、Ours、Theirs、Octopus。下面分别介绍不同合并策略的原理以及应用场景。默认 Git 会帮你自动挑选合适的合并策略,如果你需要强制指定,使用git merge -s <策略名字>

了解 Git 合并策略的原理可以让你对 Git 的合并结果有一个准确的预期。

Fast-forward

Fast-forward 是最简单的一种合并策略,如上图中将 some feature 分支合并进 master 分支,Git 只需要将 master 分支的指向移动到最后一个 commit 节点上。

Fast-forward 是 Git 在合并两个没有分叉的分支时的默认行为,如果不想要这种表现,想明确记录下每次的合并,可以使用git merge --no-ff。

Recursive

Recursive 是 Git 分支合并策略中最重要也是最常用的策略,是 Git 在合并两个有分叉的分支时的默认行为。其算法可以简单描述为:递归寻找路径最短的唯一共同祖先节点,然后以其为 base 节点进行递归三向合并。说起来有点绕,下面通过例子来解释。

如下图这种简单的情况,圆圈里面的英文字母为当前 commit 的文件内容,当我们要合并中间两个节点的时候,找到他们的共同祖先节点(左边第一个),接着进行三向合并得到结果为 B。(因为合并的 base 是“A”,下图靠下的分支没有修改内容仍为“A”,下图靠上的分支修改成了“B”,所以合并结果为“B”)。

但现实情况总是复杂得多,会出现历史记录链互相交叉等情况,如下图:

当 Git 在寻找路径最短的共同祖先节点的时候,可以找到两个节点的,如果 Git 选用下图这一个节点,那么 Git 将无法自动的合并。因为根据三向合并,这里是是有冲突的,需要手动解决。(base 为“A“,合并的两个分支内容为”C“和”B“)

而如果 Git 选用的是下图这个节点作为合并的 base 时,根据三向合并,Git 就可以直接自动合并得出结果“C”。(base 为“B“,合并的两个分支内容为”C“和”B“)

作为人类,在这个例子里面我们很自然的就可以看出来合并的结果应该是“C”(如下图,节点 4、5 都已经是“B”了,节点 6 修改成“C”,所以合并的预期为“C”)

那怎么保证 Git 能够找到正确的合并 base 节点,尽可能的减少冲突呢?答案就是,Git 在寻找路径最短的共同祖先节点时,如果满足条件的祖先节点不唯一,那么 Git 会继续递归往下寻找直至唯一。还是以刚刚这个例子图解。

如下图所示,我们想要合并节点 5 和节点 6,Git 找到路径最短的祖先节点 2 和 3。

因为共同祖先节点不唯一,所以 Git 递归以节点 2 和节点 3 为我们要合并的节点,寻找他们的路径最短的共同祖先,找到唯一的节点 1。

接着 Git 以节点 1 为 base,对节点 2 和节点 3 做三向合并,得到一个临时节点,根据三向合并的结果,这个节点的内容为“B”。

再以这个临时节点为 base,对节点 5 和节点 6 做三向合并,得到合并节点 7,根据三向合并的结果,节点 7 的内容为“C”

至此 Git 完成递归合并,自动合并节点 5 和节点 6,结果为“C”,没有冲突。

Recursive 策略已经被大量的场景证明它是一个尽量减少冲突的合并策略,我们可以看到有趣的一点是,对于两个合并分支的中间节点(如上图节点 4,5),只参与了 base 的计算,而最终真正被三向合并拿来做合并的节点,只包括末端以及 base 节点。

需要注意 Git 只是使用这些策略尽量的去帮你减少冲突,如果冲突不可避免,那 Git 就会提示冲突,需要手工解决。(也就是真正意义上的冲突)。

Ours & Theirs

Ours 和 Theirs 这两种合并策略也是比较简单的,简单来说就是保留双方的历史记录,但完全忽略掉这一方的文件变更。如下图在 master 分支里面执行git merge -s ours dev,会产生蓝色的这一个合并节点,其内容跟其上一个节点(master 分支方向上的)完全一样,即 master 分支合并前后项目文件没有任何变动。

而如果使用 theirs 则完全相反,完全抛弃掉当前分支的文件内容,直接采用对方分支的文件内容。

这两种策略的一个使用场景是比如现在要实现同一功能,你同时尝试了两个方案,分别在分支是 dev1 和 dev2 上,最后经过测试你选用了 dev2 这个方案。但你不想丢弃 dev1 的这样一个尝试,希望把它合入主干方便后期查看,这个时候你就可以在 dev2 分支中执行git merge -s ours dev1。

Octopus

这种合并策略比较神奇,一般来说我们的合并节点都只有两个 parent(即合并两条分支),而这种合并策略可以做两个以上分支的合并,这也是 git merge 两个以上分支时的默认行为。比如在 dev1 分支上执行git merge dev2 dev3。

他的一个使用场景是在测试环境或预发布环境,你需要将多个开发分支修改的内容合并在一起,如果不用这个策略,你每次只能合并一个分支,这样就会导致大量的合并节点产生。而使用 Octopus 这种合并策略就可以用一个合并节点将他们全部合并进来。

Git rebase

git rebase 也是一种经常被用来做合并的方法,其与 git merge 的最大区别是,他会更改变更历史对应的 commit 节点。

如下图,当在 feature 分支中执行 rebase master 时,Git 会以 master 分支对应的 commit 节点为起点,新增两个全新的 commit 代替 feature 分支中的 commit 节点。其原因是新的 commit 指向的 parent 变了,所以对应的 SHA1 值也会改变,所以没办法复用原 feature 分支中的 commit。(这句话的理解需要这篇文章的基础知识)

对于合并时候要使用 git merge 还是 git rebase 的争论,我个人的看法是没有银弹,根据团队和项目习惯选择就可以。git rebase 可以给我们带来清晰的历史记录,git merge 可以保留真实的提交时间等信息,并且不容易出问题,处理冲突也比较方便。唯一有一点需要注意的是,不要对已经处于远端的多人共用分支做 rebase 操作。

我个人的一个习惯是:对于本地的分支或者确定只有一个人使用的远端分支用 rebase,其余情况用 merge。

rebase 还有一个非常好用的东西叫 interactive 模式,使用方法是git rebase -i。可以实现压缩几个 commit,修改 commit 信息,抛弃某个 commit 等功能。比如说我要压缩下图 260a12a5、956e1d18,将他们与 9dae0027 合并为一个 commit,我只需将 260a12a5、956e1d18 前面的 pick 改成“s”,然后保存就可以了。

限于篇幅,git rebase -i 还有很多实用的功能暂不展开,感兴趣的同学可以自己研究一下。

总结

现在我们再来看一下文章开头的例子,我们就可以理解为什么最后一次 merge 会导致 http.js 文件不见了。根据 Git 的合并策略,在合并两个有分叉的分支(上图中的 D、E‘)时,Git 默认会选择 Recursive 策略。找到 D 和 E’的最短路径共同祖先节点 B,以 B 为 base,对 D,E‘做三向合并。B 中有 http.js,D 中有 http.js 和 main.js,E’中什么都没有。根据三向合并,B、D 中都有 http.js 且没有变更,E‘删除了 http.js,所以合并结果就是没有 http.js,没有冲突,所以 http.js 文件不见了。

这个例子理解原理之后解决方法有很多,这里简单带过两个方法:1. revert 节点 E'之后,此时的 dev 分支要抛弃删除掉,重新从 E'节点拉出分支继续工作,而不是在原 dev 分支上继续开发节点 D;2. 在节点 D 合并回 E’节点时,先 revert 一下 E‘节点生成 E’‘(即 revert 的 revert),再将节点 D 合并进来。

Git 有很多种分支合并策略,本文介绍了 Fast-forward、Recursive、Ours/Theirs、Octopus 合并策略以及三向合并。掌握这些合并策略以及他们的使用场景可以让你避免发生一些合并问题,并对合并结果有一个准确的预期。

参考

  • 三向合并 http://blog.plasticscm.com/2016/02/three-way-merging-look-under-hood.html
  • Recursive 合并【视频】https://www.youtube.com/watch?v=Lg1igaCtAck
  • 书籍 Scott Chacon, Ben Straub - Pro Git-Apress (2014)
  • 书籍 Jon Loeliger, Matthew McCullough - Version Control with Git, 2nd Edition - O’Reilly Media (2012)

三向合并和 Git 的合并策略,手把手教你Git 合并相关推荐

  1. 手把手教你git提交到码云(完整版)

    第一次提交: 1.新建码云仓库 新建仓库所选,根据自己需求 最后点击创建即可 2.在项目中右键 点击 git base 如下图: 会出现一个如下弹窗 3.输入 git init 会在当前目录中新建.g ...

  2. 如何合并PDF文件?两个方法教你轻松合并

    怎么把PDF文件合并到一起呢?经常使用PDF文件的小伙伴肯定有合并PDF文件的需求,因为很多时候我们的PDF文件不是一个两个,而是有很多个内容简短的单个文件,这样阅读起来就会很麻烦,需要挨个打开不说, ...

  3. vscode中如何拉取git代码_工具 | 手把手教你在VSCode中使用Git

    在一个目录下clone项目: git clone XXXXXX.git 使用VScode 打开项目 右击通过Code打开. 使用vscode提交代码 1.打开下面视图,添加一行文字: ## 测试提交 ...

  4. 手把手教你把 Git 子模块更新到主项目

    本文以 skywalking-rocketbot-ui子模块合并到 skywalking 为例,手把手教你如何把 Git 子模块更新到主项目中去. 首先,把fork的skywalking项目克隆到本地 ...

  5. 理解git结构与简单操作(四)合并分支的方法与策略

    接上节,此时的dev分支与master分支的进度就不一样了,所以需要将dev分支与master分支同步.这里需要的就是合并分支的操作,大家应该都知道用git merge或者git rebase. gi ...

  6. Git实战(四)| Git分支管理实操,解决在线合并和本地合并

    900×400 65.6 KB 类似于SVN这种集中式版本管理,三年前刚来上海工作时候,在华为驻场上班,华为用的就是SVN,印象最深的就是那个小乌龟的图标:后来到外面工作,渐渐发现用Git的非常多,慢 ...

  7. git 主干修改合并到分支_这才是真正的 Git——分支合并

    本文作者:lzaneli,腾讯 TEG 前端开发工程师"合并前文件还在的,合并后就不见了"."我遇到 Git 合并的 bug 了" 是两句经常听到的话,但真的是 ...

  8. git(gitlab)子模块功能在团队协同开发和生产环境中的应用(git,gitlab,TortoiseGit,CI/CD,Jenkins,docker-compose,分支合并,开发环境,测试环境)

    git(gitlab)子模块功能在团队协同开发和生产环境中的应用 目录 git(gitlab)子模块功能在团队协同开发和生产环境中的应用 一.前言 二.先决条件 三.创建gitlab项目(项目管理员操 ...

  9. 合并代码还在用git merge吗?我们都用git rebase!

    欢迎关注方志朋的博客,回复"666"获面试宝典 git merge 和 git rebase的区别 目的都是将一个分支的 commit 合并到到另外一个分支中去. git merg ...

  10. git 回滚 add 操作_炫技!git 优雅回滚一次错误的合并操作!

    点击蓝色小字关注! 关注一下你就不会有bug!合并操作 相对于常规的 commit,当使用 git merge 合并两个分支的时候,你会得到一个新的 merge commit. 当我们 git sho ...

最新文章

  1. linux直接控制IO叫,Linux IO控制命令生成
  2. CentOS7,使用tar命令解压缩文件
  3. C# ASP.Net 设置外网访问
  4. 从拼多多分享文案中,我们学会了什么?
  5. HTML 5 视频,音频
  6. 《系统集成项目管理工程师》必背100个知识点-09初步可行性研究可能出现的结果...
  7. 【数据资源】遥感资源大放送(下)| 11 个经典遥感数据集
  8. 一招搞定高等数学! | 今日最佳
  9. Java虚拟机--------JVM常见参数
  10. RabbitMQ小结
  11. vue入门:(class与style绑定)
  12. 农行研发中心软开2020秋招编程题全记录
  13. 斐讯路由器K2弹广告-刷机过程
  14. unittest框架中使用parameterized模块参数化
  15. Service与Android系统设计(5)-- libbinder
  16. 百度小程序怎么添加到主屏幕将百度小程序放到手机桌面?
  17. CentOS7.9安装Nextcloud+ocDownloader+aria2使用Nextcloud网盘做离线下载服务器
  18. csapp-buflab
  19. 原生JS 实现网易云音乐
  20. Scrapy第三(②)篇:创建scrapy项目

热门文章

  1. 逃逸分析、栈上分配、标量替换、同步消除、锁消除
  2. Vector原理与基本使用
  3. Java String详解、String原理、StringBuilder和StringBuffer的区别
  4. 如果Service有多个实现类,Spring怎么知道该注入哪个实现类
  5. Mysql查询字段去除指定列重复数据
  6. Spring源码系列 — BeanDefinition
  7. PanguVR获得400万种子轮融资,提供高效的VR家装解决方案
  8. conduit 安装试用
  9. WebApp的自动测试工具: Jenkins
  10. 开始学习yii2第一天