259 lines
13 KiB
Markdown
259 lines
13 KiB
Markdown
|
> 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [www.cnblogs.com](https://www.cnblogs.com/michael-xiang/p/13179837.html)
|
|||
|
|
|||
|
Git 基本命令 merge 和 rebase,你真的了解吗?
|
|||
|
|
|||
|
前言[#](#1952709178)
|
|||
|
------------------
|
|||
|
|
|||
|
Git 中的分支合并是一个常见的使用场景。
|
|||
|
|
|||
|
* 仓库的 bugfix 分支修复完 bug 之后,要回合到主干分支,这时候两个分支需要合并;
|
|||
|
* 远端仓库的分支 A 有其他小伙伴合入了代码,这时候,你需要和远端仓库的分支 A 进行合并;
|
|||
|
|
|||
|
以上只是列举了分支合并的一些常见场景,关于 `merge` 和 `rebase` 命令你足够了解吗?
|
|||
|
|
|||
|
HEAD 的理解[#](#1939069430)
|
|||
|
------------------------
|
|||
|
|
|||
|
在介绍本文的主要内容之前,我们先理解一下 `HEAD` 。
|
|||
|
|
|||
|
`HEAD` 指向**当前所在的分支**,类似一个活动的指针,表示一个「引用」。例如当前在 `develop` 分支,`HEAD` 内容就是 `ref: refs/heads/develop`。
|
|||
|
|
|||
|
`HEAD` 既可以指向「当前分支」的最新 `commit`,也可以指向历史中的某一次 `commit` (「分离头指针」的情况)。归根结底,`HEAD` 指向的就是某个提交点。
|
|||
|
|
|||
|
当我们做分支切换时,`HEAD` 会跟着切换到对应分支。
|
|||
|
|
|||
|
fast-forward 与 --no-ff 的区别[#](#98971869)
|
|||
|
----------------------------------------
|
|||
|
|
|||
|
假如有一个场景:有两个分支,master 分支和 feature 分支。现在,feautre 分支需要合并回 master 分支。
|
|||
|
|
|||
|
[![](assets/wOR7JK.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/wOR7JK.png)
|
|||
|
|
|||
|
`fast-forward` 合并方式是**条件允许**的情况,通过将 master 分支的 HEAD 位置移动到 feature 分支的最新提交点上,这样就实现了快速合并。这种情况,是不会新生成 commit 的。
|
|||
|
|
|||
|
[![](assets/QpdH5g.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/QpdH5g.png)
|
|||
|
|
|||
|
`--no-ff` 的方式进行合并,master 分支就会新生成一次提交记录。
|
|||
|
|
|||
|
[![](assets/BIqlQW.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/BIqlQW.png)
|
|||
|
|
|||
|
> 如果条件满足时,merge 默认采用的 `fast-forward` 方式进行合并,除非你显示的加上 `--no-ff` 选项;而条件不满足时,merge 也是无法使用 `fast-forward` 合并成功的!
|
|||
|
|
|||
|
merge 操作[#](#2315086939)
|
|||
|
------------------------
|
|||
|
|
|||
|
上面用图解的方式介绍了 `fast-forward` 和 `--no-ff` 的区别。下面,结合实际的代码仓进行合并操作,举几个栗子理解一下。
|
|||
|
|
|||
|
> `git merge` 操作是区分上下文的。**当前分支始终是目标分支**,其他一个或多个分支始终合并到当前分支。这个注意点记住了,方便记忆!所以,当需要将某个分支合并到目标分支时,需要先切到目标分支上。
|
|||
|
|
|||
|
### fast-forward 合并[#](#856326221)
|
|||
|
|
|||
|
刚刚一直在强调条件允许的时候,`fast-forward` 才能合并成功。条件指的是什么呢?
|
|||
|
|
|||
|
其实指的是源分支和目标分支之间没有分叉(单词 `diverge`),这种情况才可以进行快速合并。如果是下图中的场景,无法通过 HEAD 的快速移动实现分支的合并!
|
|||
|
|
|||
|
[![](assets/RQ39Rv.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/RQ39Rv.png)
|
|||
|
|
|||
|
下面进行一个不分叉的场景的示例:
|
|||
|
|
|||
|
[![](assets/A87zPe.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/A87zPe.png)
|
|||
|
|
|||
|
现在需要将 feature 分支合入到 master 分支,默认使用 `fast-forward` 方式:
|
|||
|
|
|||
|
```
|
|||
|
# 切到目标分支
|
|||
|
git checkout master
|
|||
|
git merge feature
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
命令行里显示了 `Fast-forward` 的提示:
|
|||
|
|
|||
|
[![](assets/J0Zku9.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/J0Zku9.png)
|
|||
|
|
|||
|
看一眼 master 分支合入的前后对比(注意 HEAD 的位置):
|
|||
|
|
|||
|
[![](assets/KmsbrV.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/KmsbrV.png)
|
|||
|
|
|||
|
[![](assets/M9PM7Q.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/M9PM7Q.png)
|
|||
|
|
|||
|
### no-ff 合并[#](#2340007676)
|
|||
|
|
|||
|
不分叉的场景是否可以强制采用 `--no-ff` 方式合并呢?可以!
|
|||
|
|
|||
|
```
|
|||
|
# master 回到合入前的状态
|
|||
|
git reset --hard d2fa1ae
|
|||
|
git merge feature --no-ff
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[![](assets/LTzh9p.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/LTzh9p.png)
|
|||
|
|
|||
|
这次命令行没有 `Fast-forward` 的提示了。
|
|||
|
|
|||
|
看一眼 master 分支图:
|
|||
|
|
|||
|
[![](assets/Sklh3v.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/Sklh3v.png)
|
|||
|
|
|||
|
这个图和上面讲解 `no-ff` 命令时的示意图一致,果然会有新 `commit` 生成。
|
|||
|
|
|||
|
### 分叉场景的合并[#](#3682856066)
|
|||
|
|
|||
|
[![](https://gitee.com/michael_xiang/images/raw/master/uPic/X69y67.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/X69y67.png)
|
|||
|
|
|||
|
上面的图展示了我们经常遇到的一个场景,特性分支创建之后,源分支也会有新的提交。这就是形成分叉了。
|
|||
|
|
|||
|
这时候如果我们进行合并呢?
|
|||
|
|
|||
|
```
|
|||
|
git merge feautre
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[![](assets/g9NjTO.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/g9NjTO.png)
|
|||
|
|
|||
|
可以看到,虽然默认会尝试 `fast-forward` 的方式进行合并,但是因为分叉了,所以此时会采用 `no-ff` 的方式进行合并!有新的 `commit` 生成了!
|
|||
|
|
|||
|
> fast-forward 方式对应的合并参数是 `--ff`
|
|||
|
|
|||
|
我们试试这个参数 `--ff-only`,顾名思义,就是强制只使用 `ff` 方式进行合并:
|
|||
|
|
|||
|
```
|
|||
|
# 回到合并前
|
|||
|
git reset --hard 3793081
|
|||
|
git merge feature --ff-only
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[![](assets/yIaYHq.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/yIaYHq.png)
|
|||
|
|
|||
|
经过测试,当分叉时,因为无法使用 `ff` 方式合并,即使你强制指定使用该方式合并也不行,会提示终止!
|
|||
|
|
|||
|
附上 Git 官方文档中的解释,方便理解:
|
|||
|
|
|||
|
```
|
|||
|
With --ff, when possible resolve the merge as a fast-forward (only update the branch pointer to match the merged branch; do not create a merge commit). When not possible (when the merged-in history is not a descendant of the current history), create a merge commit.
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
rebase 操作[#](#3837433697)
|
|||
|
-------------------------
|
|||
|
|
|||
|
`rebase` 命令是一个经常听到,但是大多数人掌握又不太好的一个命令。`rebase` 合并往往又被称为 「变基」,我称为 「基化」🤣。「基」的理解很重要,理解了它,其实 `rebase` 命令你就掌握了。
|
|||
|
|
|||
|
我的描述可能并不准确,只是为了能够帮助你理解。这里的「基」就是一个「基点」、「起点」的意思。「变基」就是改变当前分支的起点。**注意,是当前分支!** `rebase` 命令后面紧接着的就是「基分支」。
|
|||
|
|
|||
|
变基前:
|
|||
|
|
|||
|
[![](assets/RQ39Rv-167247781367949.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/RQ39Rv.png)
|
|||
|
|
|||
|
`git reabse master feature` 变基后:
|
|||
|
|
|||
|
[![](assets/HepjTM.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/HepjTM.png)
|
|||
|
|
|||
|
> git rebase 命令通常称为向前移植(`forward porting`)。
|
|||
|
|
|||
|
### 变基提交示例[#](#579017063)
|
|||
|
|
|||
|
我们接下来进行实际的测试,将代码库状态构造成分叉的状态,状态图如下:
|
|||
|
|
|||
|
[![](assets/o9hGJa.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/o9hGJa.png)
|
|||
|
|
|||
|
以 master 分支为基,对 feautre 分支进行变基:
|
|||
|
|
|||
|
```
|
|||
|
git checout feature
|
|||
|
git rebase master
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
以上两行命令,其实可以简写为:`git rebase master feature`
|
|||
|
|
|||
|
> 特性分支 feature 向前移植到了 master 分支。经常使用 git rebase 操作把本地开发分支移植到远端的 `origin/<branch>` 追踪分支上。也就是经常说的,「把你的补丁变基到 xxx 分支的头」
|
|||
|
|
|||
|
[![](assets/8jMmax.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/8jMmax.png)
|
|||
|
|
|||
|
可以发现,在 master 分支的最新节点(`576cb7b`)后面多了 2 个提交(生成了新的提交记录,仅仅提交信息保持一致),而这两个提交内容就是来自变基前 feature 分支,feature 分支的提交历史发生了改变。
|
|||
|
|
|||
|
观察上图还可以发现,变基后,改变的只是 feature 分支,基分支(master 分支)的 HEAD 指针依然在之前的 commit (`576cb7b`)处。这时候要将 feature 分支合入到 master 分支上,就满足 `fast-forward` 的条件了,`master` 分支执行快速合并,将 HEAD 指针指向刚刚最新合入的提交点:
|
|||
|
|
|||
|
```
|
|||
|
git checkout master
|
|||
|
git merge feature
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[![](assets/BUiz44.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/BUiz44.png)
|
|||
|
|
|||
|
看下图 master 分支图,观察 HEAD 指针的位置:
|
|||
|
[![](assets/dgYPmX.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/dgYPmX.png)
|
|||
|
|
|||
|
rebase 变基操作最适合的是本地分支和远端对应跟踪分支之间的合并。这样理解可能会更清晰一点。比如,远端仓库里有一个特性分支 feature,除了你开发之外,还有其他人往这个分支进行合入。当你每次准备提交到远端之前,其实可以尝试变基,这时候基分支就是远端的追踪分支。
|
|||
|
|
|||
|
下图是仓库的分支图:
|
|||
|
|
|||
|
[![](assets/PDdv1I.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/PDdv1I.png)
|
|||
|
|
|||
|
```
|
|||
|
git fetch
|
|||
|
git rebase origin/feature feature
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[![](assets/UBCOSo.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/UBCOSo.png)
|
|||
|
|
|||
|
观察上图,我们本地的提交以远端分支的最新提交为「基」,将差异提交重新进行了提交!远端分支的提交记录依然是一条直线~ 如果分叉的情况,不采用这种「变基操作」,而直接采用 `merge` 的方式合并,就会有如下这种分支提交图:
|
|||
|
|
|||
|
[![](assets/hkVfzT.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/hkVfzT.png)
|
|||
|
|
|||
|
因为分叉了,采用 `git pull` 时也没法 `fast-forward` 合并,只能采用 `no-ff` 方式合并,最后的提交历史就会像上图那样。会产生一个合并提交。同时,分支图也显得稍微杂乱了一点,因为 feature 分支不是一条直线了。但是,其实也有好处,可以实际的看出来合并的提交历史。该选择哪个,往往取决于团队的选择策略。
|
|||
|
|
|||
|
### rebase 总结[#](#2277554924)
|
|||
|
|
|||
|
`rebase` 命令其实关键在于理解「基」,`git rebase <基分支>`,就是将当前基分支与当前分支的差异提交获取到,然后在「基分支」最新提交点后面将差异提交逐个再次提交,最后将当前分支的 HEAD 指针指向最新的提交点。
|
|||
|
|
|||
|
「基分支」的 HEAD 位置是不变的。要想完成分支合并,完成变基之后,需要再进行分支间的合并等操作。
|
|||
|
|
|||
|
rebase 命令的用法也不止于此,计划后期会专门写一篇介绍她的文章。本文本来是计划介绍 merge 命令的,但是合并的方式中,其实也经常涉及变基操作之后的合并,因此,干脆就放一起比较好了,这样易于理解记忆。
|
|||
|
|
|||
|
补充[#](#849436669)
|
|||
|
-----------------
|
|||
|
|
|||
|
* `git merge --abort` 当合并的过程中,由于冲突难解决,你想放弃合并,回到未合并之前的状态;
|
|||
|
* `git log --graph --pretty=oneline --abbrev-commit` 可以在命令行方便地查看提交图
|
|||
|
|
|||
|
一言[#](#2974805274)
|
|||
|
------------------
|
|||
|
|
|||
|
在 Git 这个专辑里有一篇介绍 cherry-pick 的文章,有个小伙伴给了如下的留言,说明自己分享的内容获得了肯定,欣慰啊!
|
|||
|
|
|||
|
[![](https://gitee.com/michael_xiang/images/raw/master/uPic/vhpyi8.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/vhpyi8.png)
|
|||
|
|
|||
|
今天肝的这篇文章,介绍了 Git 中的 merge 和 rebase 的基本概念和用法,同时,又自己手动绘制了图!俗话说,一图胜千言,但写完才发现,是真的耗时啊…… 不过,总结绘图的过程,自己也加深了理解,有些概念也变得更加清晰了!希望,我的总结也能让其他人读懂~
|
|||
|
|
|||
|
之前我经常会开启文章的「赞赏」,但发现收效甚微,很少有小伙伴会打赏。后来我就每次发文就关闭了这个选项。本文应该是 6 月份的「月末总结」了,就开启一次「月末赞赏」吧!期待小伙伴的支持与鼓励!
|
|||
|
|
|||
|
参考[#](#2616863506)
|
|||
|
------------------
|
|||
|
|
|||
|
我将本文的参考文章也都注明了,他们也都很有阅读的价值。但由于微信外链的缘故,可以点击右下角的「阅读原文」浏览!
|
|||
|
|
|||
|
* [StackoverFlow-What is the difference between `git merge` and `git merge --no-ff`?](https://stackoverflow.com/questions/9069061/what-is-the-difference-between-git-merge-and-git-merge-no-ff)
|
|||
|
* [git-scm-git-merge](https://git-scm.com/docs/git-merge)
|
|||
|
* [分支的合并](https://backlog.com/git-tutorial/cn/stepup/stepup1_4.html)
|
|||
|
* [Gitlab-Fast-forward merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/fast_forward_merge.html)
|
|||
|
* [颜海镜 - 图解 4 种 git 合并分支方法](https://yanhaijing.com/git/2017/07/14/four-method-for-git-merge/)
|
|||
|
|
|||
|
> 生命不息,折腾不止!关注 「Coder 魔法院」,祝你 Niubilitiy !🐂🍺
|
|||
|
|
|||
|
[![](https://gitee.com/michael_xiang/images/raw/master/uPic/%E5%85%AC%E4%BC%97%E5%8F%B7-%E4%BA%8C%E7%BB%B4%E7%A0%81-%E6%88%AA%E5%9B%BE.png)](https://gitee.com/michael_xiang/images/raw/master/uPic/%E5%85%AC%E4%BC%97%E5%8F%B7-%E4%BA%8C%E7%BB%B4%E7%A0%81-%E6%88%AA%E5%9B%BE.png)
|