在开发过程中使用 git rebase 还是 git merge,优缺点分别是什么?
在开发过程中使用 git rebase 还是 git merge,优缺点分别是什么? - 知乎 (zhihu.com)
作者:时光和月云
链接:https://www.zhihu.com/question/36509119/answer/2398542519
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
先说结论:
1,尽量用rebase而不是merge
2,尽量在本地分支合并提交再push
3,psuh到远程分支后再合并提交和rebase的话,都需要强制push
日常工作中merge使用是比较简便的,那为什么还需要使用rebase 呢?
首先rebase & merge都可以合并分支,但是merge会多出一条合并的提交记录和分支合并线,如果有很多分支合并的话,会显得杂不是很清晰。rebase 是重置基线的意思,代码提交记录清晰,特别是大项目团队开发时,使用merge有时会将别人提交的代码合并后和自己的代码一起提交,甚至在一个提交线上夹杂着多个用户的提交,不利于代码审查,使用rebase 则可以解决这个问题,让你的代码提交集中于一次,利于代码审查,提交记录清晰明了,下面实际操作:
首先在服务器新建一个git仓库取名“demo”,如果不想搭建亦可使用现成的如“GitHub”或者“码云”都可。
创建远程仓库
创建远程仓库
然后我们把它克隆下来
克隆到本地
创建3个文件用于演示提交以及push到远程仓库
本地目录
初次提交
仅仅是提交到了本地
推送到了远程仓库
合并提交
为何需要合并提交,有时候因为同一个机能可能是很小的改动,就提交了很多次,产生了很多提交记录,而这些改动很有可能是反复的,改过去又改回来,有时可能就是为了测试一个参数,这样多次提交显得冗余又无意义,使之合并显得提交很简洁。
接下来新创建一个dev分支,演示合并提交
合并提交需要注意的是最好是在本地进行,也就是说在本地分支多次本地提交而不push,最后合并为一个提交后再push,如果多次提交已经push到了远程仓库中,那么此时本地还是可以合并,但是push时会报错。有的git软件可能会有强制推送来解决这个问题,如果没有的话做法是删除远程仓库中的该分支,然后再将合并后的本地分支push到远程仓库即可,下面演示:
创建dev分支
然后修改dev.txt文件,添加一些内容,这里分三次添加了1 2 3并提交三次
现在想把dev 1和dev 2 以及dev 3合并成一个提交
点击未提交的修改就可以看到文件
然后输入提交消息后提交
提交完后并推送dev到远程仓库中,完成后如下图所示,现在dev的三次commit已经合并成了一次commit
这里补充几条常用命令:
1.先切换到别的分支: git checkout dev
2.删除本地分支: git branch -d dev
3.如果删除不了可以强制删除:git branch -D dev
4.有必要的情况下,删除远程分支(慎用):git push origin --delete dev
软件删除远程分支:
多次提交已经push的情况下合并:
一样的pull到最新,在需要合并的前一次提交处选择“重置当前分支到此提交”,不用管那个拉取,拉取会报错。
点击未提交的更改查看文件,点击提交
提交后一定要强制推送
合并成一个提交了!
git rebase演示
上图我们可以看出,在init的接点上,创建了dev分支然后进行开发,此时master也进行了一次提交,此时如果需要将dev分支的内容merge到master的话,那么红色的分支线会连接到最上方接点处。
此处我们使用rebase再看看,变基顾名思义,就是改变基线,也就是说dev分支的变更内容是基于最新的master的。流程如下
1,切换到master分支并pull到最新
2,切换回dev分支
3,选择master分支,点击“将当前变更变基到master“。这里仅仅是变的本地dev分支,使它的内容变更基于最新的master
(这样dev和master是在一条线上,没有红色的合并线。dev的节点也变到了master的上面来了)
4,切换到master,把最新的本地dev合并到本地master后push到远程
最后结果如下:
本地master,远程master以及本地dev都是一致的。现在即使删除本地dev分支也可以。
上面我们本地dev分支并没有push到远程dev,当我们本地创建了分之后,为了避免电脑故障,还是有必要push到远程分支的,接下来我们创建dev2来看看:
现在dev2进行了两次提交并push到远程了,master也进行了新的一次提交模拟其他用户的更新,并保持本地和远程一致,现在rebase一下。
rebase成功,dev2的基线变成了最新的master
合并
push
psuh成功
可以看到现在远程master和本地master以及本地dev2是一致的,但是rebase后的dev2并没有push到远程dev2,直接psuh会报错:
这里需要强制推送,使用本地dev分支覆盖远程dev分支
然后在rebase后,将dev合并到master,push的时候一并将这两个分支强制push
再总结一下步骤:
1,从master新建dev分支开发(如果有必要,建议可以本地多次提交合并后再psuh)
2,pull master到最新
3,切换回dev,选择变基到master(相当于将master的最新代码合并到dev,现在dev包含了master最新代码)
4,将rebase后的dev分支合并到master(现在master代码包含你的dev分支代码了)
5,强制推送master和dev到远程
结果4个分支应该保持一致才对。
git merger
会产生一条merge的提交,有合并线
将master合并到dev并push后,4个分支保持一致了
赞同 15添加评论
分享
收藏喜欢收起
更多回答
知乎用户
638 人赞同了该回答
搞清楚这个问题首先要搞清楚merge和rebase背后的含义。
先看merge,官方文档给的说明是:
git-merge - Join two or more development histories together
顾名思义,当你想要两个分支交汇的时候应该使用merge。
根据官方文档给的例子,是master merge topic,如图:
A---B---C topic
/ \
D---E---F---G---H master
然而在实践中,在H这个commit上的merge经常会出现merge conflict。为了避免解决冲突的时候引入一些不必要的问题,工程中一般都会规定no conflict merge。比如你在github上发pull request,如果有conflict就会禁止merge。
所以才会有题主问的问题:在当前的topic分支,想要引入master分支的F、G commit上的内容以避免merge conflict,方便最终合并到master。
这种情况下用merge当然是一个选项。用merge代表了topic分支与master分支交汇,并解决了所有合并冲突。然而merge的缺点是引入了一次不必要的history join。如图:
A--B--C-X topic
/ / \
D---E---F---G---H master
其实仔细想一下就会发现,在引入master分支的F、G commit这个问题上,我们并没有要求两个分支必须进行交汇(join),我们只是想避免最终的merge conflict而已。
rebase是另一个选项。rebase的含义是改变当前分支branch out的位置。这个时候进行rebase其实意味着,将topic分支branch out的位置从E改为G,如图:
A---B---C topic
/
D---E---F---G master
在这个过程中会解决引入F、G导致的冲突,同时没有多余的history join。但是rebase的缺点是,改变了当前分支branch out的节点。如果这个信息对你很重要的话,那么rebase应该不是你想要的。rebase过程中也会有多次解决同一个地方的冲突的问题,不过可以用squash之类的选项解决。个人并不认为这个是rebase的主要问题。
综上,其实选用merge还是rebase取决于你到底是以什么意图来避免merge conflict。实践上个人还是偏爱rebase。一个是因为branch out节点不能改变的情况实在太少。另外就是频繁从master merge导致的冗余的history join会提高所有人的认知成本。
赞同 63831 条评论
分享
收藏喜欢收起
调参码农
110 人赞同了该回答
编辑说明:这个答案目前还有人点赞,所以想要完善下,之前的答案有些问题。添加上图片,让说明更加直观。截图来自于网站Learn Git Branching。因为Git是分布式的并且本地仓库和远程仓库可以看作一个整体,所以将本地分支和服务器上的分支显示在同一张图上做说明。本地分支是master,服务器分支是server_ma。
不管用什么风格的操作流程来整合你的本地分支,最终,服务器上的目标分支是要和本地分支做一次类似于fast-forward merge的合并的。
通常情况下有这么两种情形:
1. 本地分支是C0-C1-C2-C3,服务器上是C0-C1。这时候直接push,服务器上也会变成C0-C1-C2-C3,这种是快进式合并。结束后,历史树是一条线。
push前如下图
push后如下图
2.本地分支C0-C1-C2-C3,服务器上是C0-C1-C4。这时候直接push会提示non-fast-forward而失败,需要先pull一下再push。git pull会先fetch然后执行git merge。优点:记录下合并动作;缺点:很多时候这种合并动作是垃圾信息,记录意义不大,反而把历史树搞得复杂不直观,会对实际工作带来负面作用。
push前如下图
pull后如下图
push后如下图
负面作用实例:某工在push完提交以后,想用当前分支最新的代码编译一个版本,于是就在C3上打了个tag,因为他错误地认为C3是最新的一笔提交,然后让编译任务根据这个tag去抓代码编译了,编完后同事跑来跟他反馈说,这版本不对呀,我改的C4好像没起效果嘛。我比你先push的,你说你取了最新的代码,怎么会没有我的改动呢?经过排查后才发现tag应该打在C5上。
现实中某工其实不大可能会打错tag,因为他自己先pull了,会看到C5在本地生成。但假如推送代码时不是直接推送的,而是类似于Github的pull request或者Gerrit的review那样先推送到审核分支再由审核人员将其合并到目标分支,那即使不是fast-forward形式,也是可以直接推送到pull request或者review分支的。这样C5会在服务器端合并操作的时候形成,而不是在本地生成以后再推送上去。这种情况下,某工很可能会错把C3当作最新的提交,因为他对C4的存在感不强烈,而且对C5毫无感知。
某工应该把tag打在C5上,才能取到当时最新的版本。他在取版本时不用tag而是用branch,那是可以避免这种情况的,因为branch一般指向的commit就是最新的。但也存在风险:编译任务去取分支最新代码时,分支上又被其他人上了新的提交,这个新提交很可能并不是他想要的,还可能带来不好的副作用。
这种有分叉的历史树还是有一个优点的,C3和C4分别代表了某工和同事各自的版本,C3上不包含C4的改动,C4上不包含C3和C2的改动。如果C1代码没问题,但C5却引入了bug,那可以分别验证C3和C4来初步定位是某工还是同事引入的。如果是某工引入的,那同事就不用耗费在这个问题上了。如果提交历史是一条线的话,那排查起来要把两个人都困在里面。
为了要让提交历史变成一条线的话,那某工在提交之前,如果先做一次git pull --rebase,本地分支就会被更新成C0-C1-C4-C2'-C3',把C2-C3的基从C1上变到C4上。这时再去push,那服务器上也变成C0-C1-C4-C2'-C3'. 那tag打在C3'上就行了。
pull rebase之前如下图
pull rebase之后如下图
push之后如下图
rebase和merge的使用得看具体的情形,并不存在谁比谁好。rebase最大的一个禁忌场景是:如果用了rebase以后,已经push并且合并到中央仓库分支的commit会被重写的话,不到万不得已的情况绝对不要用rebase!这也适用于git commit --amend命令、git reset --hard && git cherry-pick命令、git filter-branch等会重新生成commit的操作。重写已经发布的commit会需要用强制推送更新远程仓库的分支,这会需要所有已经下载了该分支的地方强制更新本地的分支,否则新旧commit历史会像两个分支一样糅合在一起,让分支历史变得很杂乱。可以想象,你强制push以后,其他对pull和rebase不熟悉的同学百分之一百会用pull(默认是merge操作)以后push的方式来处理直接push失败的场景,这样会把他本地的旧分支和你强制推送的新分支合并到一起。这种现象有被称为merging hell的。虽然最终代码可能是对的,但是看log的时候,那些只有sha1不一样,但commit log都一样的一对一对的commit会让你怀疑人生。
另外,按照我的感受和理解,如果是在同一个逻辑分支之间的合并,譬如本地master和服务器的master分支、还有我和你在同一条pull request分支或者feature分支上开发时,尽量用rebase的方式使其历史是一条线。而如果是逻辑上不同分支之间的合并,用non-fast-foward-merge保留下合并记录比较好,用git merge --no-ff。也有团队会用squash merge,这种merge方式的主分支和其他分支的形态相比是真地像一棵树。看起来只有分叉,没有合并,但实际上是合并了代码修改的,只是没有合并历史。此外,有些场景下,还会只用git cherry-pick来同步不同分支之间的修改。rebase和merge比起来,相对难以理解和使用一些。在学习rebase的过程中,可以把它分解成cherry-pick来做。
更新,2020-4-3
今天突然发现,我原来的回答陷入了一个小坑:某个目标既可以用rebase又可以用merge实现,哪个做法更好?其实还有些场景下是不能用merge解决的,有些用rebase是解决不了的。
譬如,将某两个commit调换一下先后顺序,或者将某个不是head的commit从历史中去掉。这两个需求用rebase来做是非常方便的,但用merge是没法实现的。
再譬如,把一个分支的历史合并到另一个分支上,但是不合并修改。这个用merge可以实现,用rebase是做不到的。
所以,具体用哪个git命令需要根据实际情况来分析,目标是解决问题并且最大程度地满足需求。