1. 痛苦吗?代码历史中的迷失羔羊

我们先来看一个真实的代码提交历史图形化截图:

你在开发过程中使用Git Rebase还是Git Merge?git rebase和git merge的区别_git

不知道大家看到这张图以后有什么感受?是不是很无语呢?我是无语凝噎的感受。代码历史到了这个地步,基本上是废了的!!!

通过本文我们就来谈一下代码历史线的问题。这里涉及到一个代码历史的管理问题,如果你希望拥有比较清晰的代码历史,从而可以很快速地追溯你以前的代码记录的话,你一定要关注这个话题。

如果你作为一个程序员用过mercurial,你可能对这个rebase概念已经有所了解。目前程序员用的最多的source control工具是git。在git里面经常的一个争论是到底用rebase还是用merge?

说争论实际上是不太准确的。因为在实际工作中,这主要有两种情况,一是根本就不知道rebase,另外一种是根本就不知道怎么用rebase。

那我们就说一下rebase和merge的区别,在功能上rebase把你当前的修改工作提升到最前沿,
你自己独有的修改记录顺序不会改变,但是时间主要会基于当前目标分支的时间戳进行调整。

Merge不会改变时间戳。这样就会导致不同的人在合并自己修改代码记录的时候,会出现相互交错的情形。本文开篇是一张代码提交历史的截屏,里面的连线就像电路板一样复杂。

出现这样的情况,主要是因为程序员不做rebase直接merge造成的。

而类似这样电路板状的代码提交历史是没有意义的,因为太过复杂了。几乎没有可读性。如果你想去查看某些历史记录的时候,很容易被迅速淹没在这些信息海洋里面无法自拔,最后失去自我,迷失了。

2. Git Merge vs. Rebase


Git merge 和rebase的目的是一样的,它们都是将多个分支合并成一个。虽然他们最终的目标是一样的,但这两种方法实现的方式是不同的。那么我们应该用哪个呢?

这里我们有一个示例仓库,它有两个不同的分支:主分支和特性分支。我们想把它们融合在一起。让我们来看看如何使用这些方法来解决这个问题。

你在开发过程中使用Git Rebase还是Git Merge?git rebase和git merge的区别_历史记录_02

Merge

当你运行 git merge 时,你的 HEAD 分支会生成一个新的提交,并保留每个提交历史的祖先。

你在开发过程中使用Git Rebase还是Git Merge?git rebase和git merge的区别_历史记录_03


Fast forward merge是一种不创建提交的合并类型,会更新分支指针到上一次提交。

Rebase

Rebase是将一个分支的修改重写到另一个分支上,而不需要创建新的提交。

你在特性分支上的每一个提交,都会在主分支上创建一个新的提交。这看起来就像这些提交一直是写在主分支之上的一样。

你在开发过程中使用Git Rebase还是Git Merge?git rebase和git merge的区别_分支合并_04

Merge的优点和缺点

优点

  • 使用简单,易于理解。
  • 保持源分支的原始上下文。
  • 源分支上的提交与其他分支的提交是分开的。
  • 可以保留提交历史。

缺点


Rebase的优点和缺点

优点

  • 代码历史是简化的、线性的、可读的。
  • 与许多独立的特性分支的提交历史相比,操作单个提交历史更容易。
  • 干净、清晰的提交信息可以更好地跟踪一个bug或何时引入的某个功能。可以避免众多的单行提交污染历史。

缺点

  • 会更改历史提交时间,可能会丢失上下文。

比起Merge,你需要更加小心的使用Rebase。

应该用Merge还是Rebase?

当你的团队对于rebase不熟悉时,那么git merge就是你的正确选择。

  • Merge允许保存任何给定功能的提交历史,而不必担心覆盖提交和改变历史。
  • 它可以避免不必要的 git revert或reset。

另一方面,如果你更看重干净、线性的代码历史,那么git rebase是最合适的。这种方式可以避免不必要的提交,并保持更集中和线性的变化!

这里要注意的是,如果你不正确地重写了历史,可能会导致严重的问题,所以在使用Rebase时请确保知道你在做什么。

3. 一杯水与一桶水

先说一下一杯水和一桶水的关系,这个关系可以用来描述老师向学生传授知识的情形。主要意思是说老师要是给学生一杯水的话,这个老师必须要有一桶水才行。

其实对于我们软件行业来说,这个道理也是适用的。

关于本文的聚焦点代码历史,我们想给外部呈现的是清晰干净的代码历史记录。要到的这个目标,我们需要做很多的工作,这项工作相较于干净的代码历史就是一桶水与一杯水的关系。

这也是我们经常说的,对自己狠一点,让别人舒服一点。对自己狠一点不是一句空话,实际上是让自己绞尽脑汁的去想,如何把这个事情做好?把自己放在对方的角度上去看我们的输出结果。我们问自己,我作为这些代码的维护者和接收者,是不是可以接受这样混乱的代码历史记录,答案当然是否定的。

明白了这一点,在我们做代码历史管理的时候,再苦再累也是值得的。

一些有多年工作经验的程序员,他们可能用过很多代码管理的工具,这些代码管理工具在使用的时候经常用的一个操作就是merge。进入到git时代以后,这些程序员也保留了这样的习惯,直接就merge。

我跟一些程序员聊过,他们甚至都没有意识到有rebase这个操作。有的则觉得rebase太麻烦了,用了几次就放弃了。

这绝对是人之常情。

当我们习惯了用一种方式做事的时候,我们做得越久感觉越安全,因为它是行之有效的,能够解决问题的。当有另外一种更好的方式,但是却与以前的方式有很大差异的时候,我们就会有本能的排斥心理。这是源于我们对未知领域的一种恐惧感。

离开舒适区以后,我们大多数人都会有一种不适应,有的甚至会产生很强烈的失落感。

但是当我们回归本心的时候,我们会发现我们所付出的一切,经受的痛苦都是值得的。因为我们的初心就是让用户满意。代码历史的用户就是我们这些程序员。

4. 如何正确做rebase?

要点备注:

Rebase相关的要点有这几个:

  1. 在自己的分支上做Rebase;
  2. 要Rebase正确的目标分支,注意这个目标分支不是你的远程分支,这个地方经常有人犯错;
  3. Rebase要常做,以避免出现冲突,或者冲突的难度增加;
  4. 在合入代码之前,一定要最后做一次Rebase再Merge;
  5. 最好用Squash Merge, 添加正确的提交消息;
  6. 不要在主分支上做Rebase;
  7. 尽量不要force push;

在主分支上应该rebase吗? 怎么去做rebase?

首先,我要强调一下,在主分支上不要做rebase。这是因为如果你在主分支上做了rebase,如果你是第1次push可能没有问题,但是如果别的人也做了一个rebase,这个时候就导致你的主分支上有两个不同的head,这个时候如果想再push的话就存在问题了。

如果一切可控的话,你可以非常粗暴的使用强制push。但是如果团队成员比较多,会导致这样的操作会冲掉其他已经进来的提交。

除非万不得已,我们坚决不能在主分支上去做rebase。

接下来说一个正确使用rebase的场景。在接到一个任务以后,我们会在主分支上最新的节点处提交上创建一个新的分支。在新的分支上,我们会不断的进行新的提交,直到完成这个分支任务。

到这个时候,我们想创建一个merge request或者pull request,在此之前,我们首先要做的就是rebase,rebase选的目标分支是我们的主分支。

完成这次rebase操作以后,我们就可以创建mr或者pr了。在审查过程中可能有很多修改意见,修改完成以后需要先
rebase再push。如此反复几次,mr或者pr审批通过。此时要做一次squash merge把数次提交打包合成一个到主分支上。

以上是我对在正常工作流程中如何使用rebase进行了一个现实案例的描述讲解,希望大家去体验一下,告诉我体会,请留言说说你的想法。


PS:
其他知友的答案都说到冲突的问题:

1. 用merge确实只需要解决一遍冲突,比较简单粗暴
2. 用rebase有时候会需要多次fix冲突(原因在于本地分支已经提交了非常多的commit,而且很久都没有和上游合并过)

还有一点说明的是,在项目中经常使用git pull来拉取代码,git pull相当于是git fetch + git merge,如果此时运行git pull -r,也就是git pull --rebase,相当于git fetch + git rebase

最后推荐一些​​git可视化工具​​,我用的是gitkraken,这些工具功能基本一样,看个人喜欢好使用


延伸:

git rebase和git merge的区别

1、前言

如果你作为一名开发人员,许多时候必须在 Merge和 Rebase 之间进行选择。 我们从互联网上获得的所有参考文档文章,几乎都告诉我们“不要使用 Rebase,它可能会导致严重的问题。”在这里,我将解释什么是 merge 和 rebase,为什么你应该(或者不应该)使用它们,以及如何做。Git Merge 和 Git Rebase 其实是都是为了完成同样的目的。 它们旨在将来自多个分支的更改集成到一个分支中。 虽然最终目标是相同的,但这两种方法以不同的方式实现。这个问题在 Git 社区中引起了分歧。有些人认为应该总是使用 rebase ,而另一些人则认为应该总是使用 merge 。双方都有一些令人信服的好处。

2、git merge

  1. merge 是一种常见的做法,不管创建分支是为了测试、修复 bug 还是其他原因,将提交更改合并到另一个位置。 更具体地说,merge 获取源分支的内容并将它们与目标分支集成。 在此过程中,仅更改目标分支。 源分支历史保持不变。
  2. 优点:
  • 简单而又熟悉
  • 保留完整的历史和时间顺序
  • 维持分支的上下文
  1. 缺点:
  • 提交历史记录可能会被许多合并提交污染
  • 使用 git bisect 进行调试会变得更难
  1. 如何操作(假设A、B两个分支,需要将A分支合并到B分支上):
git checkout B    切换到B分支上
git merge A 将A分支合并到B分支上

或是通过:

git merge A B      将A分支合并到B分支上

3、git rebase

  1. Rebase 是将更改从一个分支集成到另一个分支的另一种方法。 Rebase 将所有更改压缩为单个“补丁”。然后它将补丁集成到目标分支上。与 merge 不同,重定位使历史变得扁平,因为它将完成的工作从一个分支转移到另一个分支。在这个过程中,不需要的历史记录被消除。Rebases 是更改应从层次结构顶部向下传递的方式,并且 Merge 是它们向上流回的方式
  2. 优点:
  • 简化可能复杂的历史记录
  • 操作单个提交很容易(例如还原它们)
  • 避免分支频繁合并提交,频繁回购。
  • 通过将它们作为单个提交来清理中间提交,这对DevOps团队很有帮助
  1. 缺点:
  • 将该功能分解为少量提交可以隐藏上下文
  • 在团队合作时,重新定位公共存储库可能会很危险
  • 可能带来更多的工作:使用rebase 来保持您的 feature 分支始终更新
  • 使用远程分支 Rebase 需要强制推送。人们面临的最大问题是他们强制推送,但还没有设置 git push 默认值。这会导致对本地和远程上具有相同名称的所有分支进行更新,这是非常可怕的,并且处理起来非常糟糕。
  1. 如何操作(将 feature 分支 Rebase 到主分支上):
git checkout feature
git rebase master
  • 这会将整个 feature 分支移动到主分支的顶部。它通过为 原始(feature)分支中的每个提交创建全新的提交来重写项目历史。

4、交互式的 rebase

这允许在将提交移动到新分支时更改提交。 这比自动 rebase 更强大,因为它提供了对分支的提交历史的完全控制。 通常,这用于在将 feature 分支合并到主分支之前,清理杂乱的历史记录。
bash 代码:

$ git checkout feature
$ git rebase -i master

这将通过打开编辑器,列出即将移动的所有提交。
bash 代码:

pick 22d6d7c Commit message#1
pick 44e8a9b Commit message#2
pick 79f1d2h Commit message#3

这精确定义了 rebase 执行后分支的确切内容。通过重新排序实体,可以使历史记录看起来像您想要的任何内容。例如,可以使用 fixup ,squash ,edit 等命令代替 pick。

你在开发过程中使用Git Rebase还是Git Merge?git rebase和git merge的区别_历史记录_05

5、我们应选择哪种方式

团队在设置 Git rebase 与 merge 策略时,需要考虑几个问题:

  • 考虑跨组织的重基和Git能力级别。确定相对于可追溯性和合并历史,您更重视重基的简单性的程度。
  • 考虑整个组织的 rebase 和 Git能力水平。 相对于 merge 的可追溯性和历史记录,确定您重视 rebase 的简单程度。
  • 最后,应该在清晰的分支策略的上下文中考虑关于 merge 和 rebase 的决策,成功的分支策略是围绕团队组织设计的。

6、个人意见

随着团队的增长,使用始终 merge 策略管理或跟踪开发更改将变得非常困难。要有一个清晰易懂的提交历史记录,使用 Rebase 是合理和有效的。

通过考虑以下情况和指南,您可以充分利用 Rebase :

  1. 你在本地开发:如果您还没有与其他人合作。此时,你应该更喜欢 rebase 而不是 merge 以保持历史的整洁。如果您拥有存储库的个人分支并且未与其他开发人员共享,那么即使您已经推送到分支之后,也可以安全地进行 rebase 。
  2. 您的代码已经准备好接受评审:您创建了一个 pull 请求。其他人正在评审您的工作,并可能将其提取到他们的分支中进行本地评审。此时,您不应该 rebase 你的工作。您应该创建 rework 提交并更新您的 feature 分支。这有助于拉取请求中的可追溯性,并防止意外的历史记录破坏。
  3. 评审已经完成,准备集成到目标分支中:恭喜你!您将要删除您的 feature 分支。考虑到其他开发人员从现在起不会在这些更改中进行获取合并,这是您清理历史记录的机会。此时,您可以重写历史记录并折叠原始提交,并将那些讨厌的’pr rework’和’merge’提交到一小组重点提交中。为这些提交创建显式合并是可选的,但有价值。它记录了该功能何时升级为 master 。

1、什么是 rebase?

  1. git rebase 你其实可以把它理解成是“重新设置基线”,将你的当前分支重新设置开始点。这个时候才能知道你当前分支于你需要比较的分支之间的差异。
    原理很简单:rebase需要基于一个分支来设置你当前的分支的基线,这基线就是当前分支的开始时间轴向后移动到最新的跟踪分支的最后面,这样你的当前分支就是最新的跟踪分支。这里的操作是基于文件事务处理的,所以你不用怕中间失败会影响文件的一致性。在中间的过程中你可以随时取消rebase 事务。
    官方解释: https://git-scm.com/book/zh/v2/Git-分支-变基

2、git rebase 和 git merge 有啥区别?

  1. rebase会把你当前分支的 commit 放到公共分支的最后面,所以叫变基。就好像你从公共分支又重新拉出来这个分支一样。
    举例:如果你从 master 拉了个feature分支出来,然后你提交了几个 commit,这个时候刚好有人把他开发的东西合并到 master 了,这个时候 master 就比你拉分支的时候多了几个 commit,如果这个时候你 rebase master 的话,就会把你当前的几个 commit,放到那个人 commit 的后面。
  2. merge 会把公共分支和你当前的commit 合并在一起,形成一个新的 commit 提交。

3、注意:

  • 不要在公共分支使用rebase
  • 本地和远端对应同一条分支,优先使用rebase,而不是merge

4、抛出问题:

  1. 为什么不要再公共分支使用rebase?
    因为往后放的这些 commit 都是新的,这样其他从这个公共分支拉出去的人,都需要再 rebase,相当于你 rebase 东西进来,就都是新的 commit 了
    1-2-3 是现在的分支状态
    这个时候从原来的master ,checkout出来一个prod分支
    然后master提交了4.5,prod提交了6.7
    这个时候master分支状态就是1-2-3-4-5,prod状态变成1-2-3-6-7
    如果在prod上用rebase master ,prod分支状态就成了1-2-3-4-5-6-7
    如果是merge
    1-2-3-6-7-8
    … |4-5|
    会出来一个8,这个8的提交就是把4-5合进来的提交
  2. merge和rebase实际上只是用的场景不一样
    更通俗的解释一波,比如rebase,你自己开发分支一直在做,然后某一天,你想把主线的修改合到你的分支上,做一次集成,这种情况就用rebase比较好.把你的提交都放在主线修改的头上如果用merge,脑袋上顶着一笔merge的8,你如果想回退你分支上的某个提交就很麻烦,还有一个重要的问题,rebase的话,本来我的分支是从3拉出来的,rebase完了之后,就不知道我当时是从哪儿拉出来的我的开发分支同样的,如果你在主分支上用rebase, rebase其他分支的修改,是不是要是别人想看主分支上有什么历史,他看到的就不是完整的历史课,这个历史已经被你篡改了

5、常用指令

  • git rebase -I dev 可以将dev分支合并到当前分支
    这里的”-i“是指交互模式。就是说你可以干预rebase这个事务的过程,包括设置commit message,暂停commit等等。
  • git rebase –abort 放弃一次合并
  • 合并多次commit操作:
    1 git rebase -i dev
    2 修改最后几次commit记录中的pick 为squash
    3 保存退出,弹出修改文件,修改commit记录再次保存退出(删除多余的change-id 只保留一个)
    4 git add .
    5 git rebase --continue