【Git教程】(六)分支合并 —— 合并过程,各类合并冲突及解决思绪 ~ ...

锦通  金牌会员 | 2024-10-9 16:59:23 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 832|帖子 832|积分 2496


使用 merge 命令来举行分支合并是 Git 中最紧张的操纵之一。虽然这一操纵的底层算法很复杂,但调用起来却很简单。我们可以通过指定分支名称来选择待合并修改的分支。然后, Git 会基于合并的内容来创建一次新的提交。
下面,我们来看下图中的这个例子:在一群开发者在一个名为 feature 分支上开发新功能的同时,另一位开发者则刚刚修复了 master 分支上的某个错误(提交E) 。 然后过了不多久,feature 部分的任务也完成了,并将交付使用。因此 master 分支的下一个版本中应该同时包罗被修复的部分和新的 feature 部分。这时候,我们要对这些分支使用 merge 命令,其结果会产生一次合并提交(即这里的提交 F), 该提交将会有两个父级提交 (D 和 E)。
  1. > #on the branch "master"
  2. > git merge feature
复制代码


1️⃣ 合并过程中发生的事

Git的设计目标之一就是为了能让开发者之间的分布式协作变得尽大概容易一些。因此从 很大程度上来说,merge 命令应能自动对分支举行合并,完全不需要用户交互。但这是怎么做到的呢?
例如在下图中,我们会看到某一个文件有两个不同版本,它们分别属于分支a 和分支b。 我们很容易就能看出这其不同之处位于哪几行。但究竟哪一个才是精确的呢?是 “Freitag” 照旧 “Montag”? 是 “Git” 照旧 “Fit”? 合并算法应该怎样作出决定呢?

问题关键就藏在其提交历史之中。这里的窍门就是要找到它们最后一个共同的祖辈提交。 换一种相对简单点的说法,就是要找到其提交路径上岔出分支的谁人点。只要我们将该源版本与眼前的这两个分支的版本比对一下,整个画面就会变得更为清晰。
如你所见,在图下这个例子中,分支 b 中的第一行 “Freitagabend” 被替换成了 “Montagabend” 。 而在分支a 中,第一行则没有被修改。这在举行分支合并时是一个强烈信号, 它告诉我们应该采用包罗 “Montagabend” 的版本。通过同样的方式,我们也可以安全地确认,对于最后一行我们应该采用包罗“Git”的版本,而不是“Fit”的版本,其最闭幕果如图所示。

固然从事实上来说,真要想找到它们共同的祖辈提交可不是一件容易的事。为解决这个问题,Git 实现了3种不同的合并算法。其在默认情况下采用的是递归算法。但除此之外,它还实现了经典的3路算法和所谓的 “octopus” 算法。其中, “octopus” 还可以同时处置惩罚多个分支。

2️⃣ 冲突

Git 非常适合于在几个开发者对同一软件做多处修改时,被用来合并他们对程序源代码中 所做的修改。这些操纵甚至常会涉及到那些受移动或重命名操纵影响的文件。而不幸的是,这些文件每每会引发一些无法用 Git 自动化解决的冲突。


  • 编辑冲突:通常发生在两个开发者对同一行代码做了不同修改的时候。在这种情况下, Git 每每无法自行确定两种修改中的哪一种才是精确的。
  • 内容冲突:通常发生在两个开发者对某份代码的几个部分做出各自修改的时候。例如 这种情况就容易导致这类冲突:当一个开发者在修改某一函数的时候,另一个开发者 也在同一时间修改了同一函数。

3️⃣ 编辑冲突

当Git 遇到了自身无法解决的冲突时,就会显示以下错误消息。
  1. > git merge one-branch
  2. Auto-merging foo.txt
  3. CONFLICT(content):Merge  conflict in foo.txt
  4. Automatic merge failed; fix conflicts and then commit the result.
复制代码
下面我们来看看具体发生了什么。

  • Git 无法创建提交。Git 通常会在合并后自动创建提交。而在发生冲突的情况下,我们就必须要先解决问题,然后再手动创建提交了。
  • .git/MERGE_HEAD 中将保存另一分支的提交散列值。
  • 工作区中的文件代表了合并结果。
  • 无冲突部分的修改合并将会被纪录在暂存区中,以便纳入下一次提交。
  • 将会有冲突标志被插入。
  • 冲突所在之处将不会被注册到下一次提交中。
如今,根据 status 命令返回的信息,我们可以看到 “Changes to be committed” 这部分显示的是自动合并的文件。而 “Unmerged paths” 这部分就是用户必须举行手动编辑的文件。
  1. > git status
  2. #On branch master
  3. #  (fix conflicts and run "git commit")
  4. #
  5. #Changes to be committed:
  6. #
  7. #modified:  blah.txt
  8. #
  9. #Unmerged  paths:
  10. #    (use( "git add <file>..."to mark resolution)
  11. #
  12. #both  modified:  foo.txt
  13. #
复制代码

4️⃣ 冲突标志

冲突标志通常会形貌两组修改。起首是这些被修改的行在当前分支(HEAD) 中的内容。
接下来又列出了他们在另外一个分支(即MERGE_HEAD, 在这里是 one-branch) 的内容:
  1. In the early morning dew
  2. <<<<<<< HEAD
  3. to the valley
  4. =======
  5. for swimming
  6. >>>>>>> one-branch
  7. We're going.Fallera!;
复制代码
出于各种历史缘故原由,这些分支提交的共同祖辈在默认情况下是不显示的,但我们可以将 其配置成3路显示格式。
  1. > git config merge.conflictstyle diff3
复制代码
如许一来,编辑冲突就会如下所示。
  1. In the early morning dew
  2. <<<<<<< HEAD
  3. to the valley
  4. ||||||| merged common ancestors
  5. to mountains
  6. =======
  7. for swimming
  8. >>>>>>> one-branch
  9. We're going Fallera!;
复制代码

5️⃣ 解决编辑冲突

解决编辑冲突最好的办法是使用像 kdiff3 如许的合并工具。在这里,我们可以从mergetool 命令启动合并工具。
  1. > git mergetool
复制代码
在这个工具中,我们可以解决冲突、保存修改以及停止这个应用程序。然后,合并之后 的修改将会出如今暂存区中,它们可以被确认为一次提交。
固然对于二进制文件来说,上面这种文本化的冲突标志是不存在的。在这种情况下,我 们就必须要去查看其原始版本。该文件的3个版本在冲突中扮演了各自的角色:即当前分(我们的)的版本、其他分支(他们的)的版本、以及这两个分支最后的共同祖先(祖辈版本)。
我们可以用 show 命令检出这些版本。
  1. > git show :1:picture.png >ancestor.png
  2. > git show :2:picture.png >ours.png
  3. > git show :3:picture.png >theirs.txt
复制代码
  1a. 编辑受影响的文件
对于每一个冲突所在之处,我们都思量自己想要采用的选项,然后在文本编辑器中删除冲突标志所在的剩余部分即可,但这种方法对二进制文件是不实用的,因此我们就需要用到步骤1b 了。
  
1b. 采用 --ours 或--theirs选项
或者,我们也可以用checkout 命令来完全选择只采用自己的(或者是别人的)谁人版本的文件。
  1. git checkout --theirs tests/
复制代码

2. 注册修改
git add

3. 提交
git commit


  另外,合并和比力工具每每也会将一些空白符方面的修改显示出来。例如,假如某个开 发者将制表符替换成了空格符,其涉及到的全部行都会被标志,尽管他没有在内容上着任何修改。这些工具通常会有干系的选项可以忽略掉空白符的修改,我们发起你使用这个选项。
固然,更好的选择是全部开发者都能用相同的工具来举行源代码的自动格式化,那我们
就即是解决了格式冲突的一个根源。
然而事情总有不测!假如我们在合并时犯了一个错误或者在解决冲突时出了错的话,就不应该再继承做下去了,相反,这时候我们应该果断地取消合并,如许我们就不会在工作区中留下合并操纵的踪迹,而且 Git 中也不会在下轮提交中出现合并提交,合并操纵可以通过reset 命令来取消。
  1. > git reset --merge
复制代码

6️⃣ 内容冲突

真正的贫苦是内容冲突,由于Git 无法识别这类冲突,自动化解决固然是肯定不消想了。 其真正的伤害来自于当内容冲突存在时,merge 命令照旧会天生有用的合并提交。
请注意! 这也就是说,纵然全部的合并版本都是精确的,且 Git 也没有陈诉任何编辑冲
突,该合并提交也大概是坏的!
假如我们想制止内容冲突扰乱软件版本,就得要做更多事。


  • 借由自动化测试构建保护机制:假如这些测试能够定期举行,而且有一个很好的覆盖 面的话,各种内容冲突就能很快被发现。
  • 使用断言、以及前置与后置条件:基本上,我们执行越多明白的断言查抄,就越能更 早地发现问题。
  • 定义清晰的接口,使实在现松耦合:以目前所讨论的点来说,显然体系结构设计得越 干净利落,其代码因不同地方被混入修改而引发不测副作用的大概性就越小。
  • 静态类型查抄:只要我们的编程语言支持这一特性,那么任何署名变革所引发的问题 都将会在编译时被检测到。
趁便说一句, merge 命令在这里对于多分支的合并也是有用的,这就是 octopus 合并。

7️⃣ 快进合并

我们经常会遇到如许的情况:即多少个分支中中每每只有一个分支仍在持续工作。例如 在下图的这个项目中。开发者们不绝都在 a-branch 分支下开发,而 b-branch 分支上则什么 事也没有发生。当 b-branch 与a-branch 这两个分支要举行合并时,Git 要做的工作就非常简单了:只要前移一下指针即可,不再需要产生合并提交了,我们称这种情况为快进合并。
  1. > git checkout b-branch
  2. > git merge a-branch
  3. Updating 9d4caed..9332b08
  4. Fast-forward
  5. foo.txt | 2 +-
  6. 1 files changed,1 insertions(+),1 deletions(-)
复制代码
快进合并的优点是它能简化版本库的历史纪录并使其保持线性发展。而缺点则是我们不能根据已经合并过的历史纪录来看版本库的这一发展。正是由于它存在如许的缺点, 我们才需要在本书的一些工作流中使用 --no-ff 选项,以强制其产生一次新的提交。
  1. > git merge --no-ff a-branch
复制代码


8️⃣ 第一父级提交历史

合并提交通常都会有两个父级提交,甚至 octopus 合并中还会有两个以上的父级提交存
在。例如在下面的例子中,我们会看到两个父级提交 ed1c70e 和 fld55be。
  1. > git log --merges
  2. commit 7f3eae07c42df05f894fdd4754e38ab9e66a5051
  3. Merge: ed1c70e f1d55be
  4. Author: ...
复制代码
这个例子中的第一次提交 (ed1c70e) 叫做第一父级提交,它是合并执行完后HEAD 所在的谁人提交。代表的是该合并所发生的地方。假如全部的开发者都在同一分支上工作,那么它无论何时何地执行合并都不会影响结果。
在这种情况下,我们去穷究哪一个是第一父级提交就显得毫偶然义了。
另一方面,当我们需要将自己在某些特性分支上所开发的一个个特性集成到特定的特性 分支上时,这个集成后的结果分支 (即本例中的 master 分支) 就是一个合并提交的序列(见下图)。它的第一父级提交通常就是其上一级特性的合并提交。

假如我们沿着第一父级提交链一路追踪到根提交上,就会得到一份特性集成的概览。我 们将其称之为第一父级提交历史。你可以通过带 --first-parent 选项的log 命令来显示这份特性集成概览:
  1. > git log --first-parent --oneline R1.0..master
  2. 7f3eae0 Merge branch 'Feature-C' Finished(M4)
  3. ed1c70e Merge branch 'Feature-A' Finished(M3)
  4. eeb6ec2 Merge branch 'Feature-B' Finished(M2)
  5. 8ce3213 Merge branch 'Feature-A' Partial delivery(M1)
复制代码
第一父级提交历史的奇妙之处在于,它为我们提供了一份历史的总结陈诉。你可以从中
清楚地看到哪些已被集成的特性,无需再去侦查那些特性分支上的每一次提交。
请注意! 这只实用那些执行了快进合并的集成分支。否则,独立的特性分支提交只能被
直接放置到 master 分支的第一父级历史中。
另有一件事也需要注意! 我们不应该对集成分支(即这里的 master) 执行内部合并。相 反,我们要确保这些特性都是依次一连地被集成进来的,如许我们才能得到一份线性的特性合并历史。

9️⃣ 棘手的合并冲突

在 Git 中,大多数合并操纵都可以在没有或者只有少量人工辅助的情况下自动完成。但
假如两个分支各自演变的轨迹非常的不同,也有大概会带来一些棘手的冲突。
固然,在这一节我们只讨论两个分支之间的合并。假如你是在 octopus 合并上遇到了如许的问题,发起你应该取消这次合并,试着采用逐个解决的思绪来解决这个问题。
起首我们需要将重点放在信息收集上,以便了解目标分支上目前所发生的事。在这里, 在 log 命令中使用 .. 这符号大概会很有帮助。例如,a…b 可用来表示来自于分支 b, 但不属于 分支 a 的提交。它可以显示出“我们” (在当前分支上)做了哪些事,而这些事应该不会被提交到其他分支中。
  1. > git log MERGE_HEAD..HEAD
复制代码
反之亦然,我们也可以用该符号来显示“别人”所做的事情。
  1. > git log HEAD..MERGE_HEAD
复制代码
另外,分支的图形化表示也会很有用。
  1. > git log --graph --oneline --decorate HEAD MERGE_HEAD
复制代码
我们也可以在 log 命令中使用 --merge 选项,限定其只输出合并提交。
  1. > git log --merge
复制代码
除此之外,我们也可以使用一些比对原版本时会用到一些实用的分支技巧。但这需要以
合并操纵为基础,即该版本必须在合并操纵中是这些分支共同的祖辈提交。
  1. > git merge-base HEAD MERGE_HEAD
  2. ed3b1832c48b359111d00bddb071c42ba6f38324
  3. > git diff --stat ed3b18  HEAD %Our changes
  4. > git diff --stat ed3b18  MERGE_HEAD %Changes by others
复制代码
假如想用图形化工具代替这种文本输出的话,你也可以使用 difftool 命令。
如许一来,我们就可以看到涉及冲突的是那几个开发者。这时候,我们最好能与他们每
个人都谈一谈,使得每个人都能自行确保他或她被纳入合并的修改是精确的。
假如其他人对此无能为力,那事情就更加难办了,由于我们通常对别人分支上的事情并 不精通。从技术上来说,合并本来应该是一个对称的操纵。但我们在意识中每每对此会有一 个不对称的视角。即我们一般会问自己问题:“我应该怎样将别人的代码纳入到自己的代码呢?” 实在,偶然将问题反过来看会更有帮助,即我们不妨以别人的版本条理为出发点,去找出可以将自己的修改整合进去的方法。如许的视角转换偶然候确实会很有帮助。
大概是由于时间紧迫,我们经常倾向于直接用合并工具选取这份或那份代码变更了事。 这种贪图方便的行为是应该被抵抗的。假如颠末diff和 log工具共同“其他”版本的分析之后, 你依然无法确定解决冲突的方式,那么你就应该取消合并,然后再思量一下以下几种大概的计谋。


  • 分支重构:最干净利落的解决方案大概就是以重构的方式其中一个分支举行清算,并 执行交互式变基。但这是一个很大的工作量。
  • 分小步合并:假如两个分支中的一个分支存在细粒度的提交,我们可以采用一次一提 交的方式来处置惩罚。这种方法的上风在于,究竟粒度越小的提交所带来的冲突每每越容易解决。但假如这种提交的数量很大,它也大概会非常耗时,无论是哪一种情况,为此创建一个当地分支都是值得推荐的做法。
  • 扬弃与捡取:在某些情况下,拒绝某个劣质分支上的某些修改是一个不错的做法,我 们可以通过 cherry-pick 命令来对其采取些改进步伐。
  • 评级和测试:假如受合并影响的功能可以通过测试,那么我们自然在解决冲突时候据 此来推演,并将其结果改善到能通过全部测试为止。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

锦通

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表