保姆级教程 | Merge Request 分支合并请求

10,082 阅读18分钟

保姆级教程 | Merge Request 分支合并请求

What is it ?

首先我想先来讲讲什么是分支合并请求Merge Request(也可叫Pull Request,下文中全用Merge Request或其缩写MR指代),以及它有什么作用(如果你对此概念有所了解,你完全可以跳过What is it)。

MR(或者PR)就是指将你开发的代码的内容以一种请求合并的方式来合并到它想去的分支上,这个请求的接收人(Reviewer)一般是项目、团队的负责人或者其他成员。

一般来讲,开发团队都对Code Review(代码复审/审查/检视)的重视程度比较高。因为Code Review的确实能够提升代码的质量以及减少BUG的产生率。

Merge RequestCode review中就是重要的一环。如果使用MR来发起合并请求,那么在代码审查时就完全可以以你本次请求的合并内容为单元进行代码审查,如果审查通过那么就成功合并。审查交由Reviewer进行,他可以是请求的接收人。如果团队多个成员坐在一起来看你的本次合并内容,那么自然Reviewer就是这些人了。一份代码经过多人的审查,代码问题发生率自然会降低,开发者在开发时也会保持良好的编码习惯,毕竟没人想被别人指点自己的代码。

不过有些团队可能并不重视Merge Request,最多也就是在dev分支(大家共用的开发分支)上检出一个新分支,然后在新分支上进行开发,然后commit -> push最后mergedev 分支上就完事了。

下面我们将以Merge Request为目标,从建立仓库开始讲述一个完整的git工作流以及其中的git操作。

How to do?

接下来我们从0开始,以Gitee(码云)代码托管和研发协作平台为例,来讲讲如何在正常的git工作流程中使用Merge request

1.创建一个远程仓库,默认创建master分支

image-20211104220309545

2. 创建本地仓库,并关联远程仓库

初始化本地仓库后,随便创建一个文件,然后提交到远程仓库的master分支。

git init
touch README.md
git add README.md
git commit -m "first commit"
git remote add origin https://gitee.com/yaodao666/git-merge-requet.git
git push -u origin master

git push -u的作用就是关联并推送内容到上远程仓库的分支。(后面还有别的关联远程分支的方法。)

3. 以master分支为起点创建一个dev分支

我们后面就将以dev分支作为开发分支(仓库成员共用的一个分支)。

image-20211104222705941

4. 仓库中再添加另一个成员

该成员将在后面作为Reviewer来处理自己的Merge Request,看我们的提交内容,从而达到代码审查的目的。

image-20211104223305466

image-20211104223417246

然后为该用户设置为代码审查人员:

image-20211105000210014

以上的操作都在Gitee仓库的管理设置选项中。

4. 本地切换到dev分支,并连接远程dev分支

此时本地是没有dev分支的。可以用

git branch -a

命令来查看所有分支。

image-20211104223744003

现在就用

git checkout -b dev

来创建一个dev的本地分支。如果此时执行该命令时没有加-b参数:

git checkout dev
error: pathspec 'dev' did not match any file(s) known to git

会报错如上。因为该命令是只是切换分支,而此时并没有dev分支,自然无法切换过来会报错。

image-20211104224132282

OK,现在我们创建了本地dev分支,现在我们让他关联上远程仓库的dev分支吧。(只有这样才能进行pullpush操作啊!)

git branch --set-upstream-to=origin/dev

这样子就好啦!

执行下面的命令

git pull
Already up to date.

出现上述信息,说明链接上远程的dev了。

其实还有一种链接远程仓库分支的方式,比如我们又在远程仓库上以dev分支为起点,创建了test分支,那么我们在本地创建test分支时,就可以执行:

git checkout -b test origin/test

创建本地test分支的同时又链接到远程的test分支上。

image-20211104230315250

在后文中5. 新建一个feature(特性)分支中还会有另一种方式来连接远程仓库分支。

5. 新建一个feature(特性)分支

在实际开发中,我们往往会新建一个特性分支,该分支专门为你服务,并且它专门用于处理某个bug,或者开发某个新的功能。即当有个新功能需要开发或者有bug以及优化重构部分代码时,我们就应该单独拿出一个新分支来专门处理这些事情。

与上面创建dev的方式相同,不过我们这次不先在远程仓库创建分支了,而是在本地直接创建分支后,再将该分支推送到远程

我们先切换到dev分支,正常工作中,你必须要pull一下,保证你之后写的代码将是建立在dev上最新的代码的基础上,以避免一些不必要的麻烦。

git checkout dev
git pull

用下面的命令创建一个新的特性分支,我们就命名为feature-beer吧。

git checkout -b feature-beer

6. 在feature-beer分支上开发,并推送到远程。

随便在readme.md文件上改点东西。然后执行:

git add -A
git commit -m "这是我第一次在feature-beer分支上提交。"

最后将内容推送到远程的仓库上,

git push -u origin feature-beer

这时候再去远程仓库上看,就会有feature-beer分支了,并且提交内容也有了。所以git push -u的作用不仅仅是关联并推送内容到上远程仓库的分支,当没有远程分支时还会创建该分支!

image-20211104231520643

7. 直接合并到dev上。

很多时候,有些开发团队根本就在乎使用Merge Request来在合并时进行Code review,那么他们就会直接合并代码到dev分支。

切换到dev分支执行merge命令。

git checkout dev
git pull				# 在合并前同样先pull一下dev
git merge feature-beer    # 这里的合并是本地合并,将feature-beer中的内容合并到dev中
git push				# 将本地内容推送到远程仓库

image-20211104233806097

此时再去远程dev分支上看一下:

image-20211104233830098

我们在feature-beer上的修改内容已经放在dev上了。

8. Merge Request

不过这种简单粗暴的方式往往会带来很多问题,比如没有人去注意你往dev上合并了什么内容,这种没人关注自己写的代码情形往往就会导致开发人员在开发时不注意代码规范,甚至会提交上很明显的bug,也懒得去测试,更有甚者则会上传使得项目启动失败的代码。

这时候如果能有一个人来帮你再把把关,看看你写的代码咋样,则会促使自己写代码时更加注意代码规范和代码健壮性,毕竟谁也不想被别人批评,要是因为代码写的好被表扬就更好了。而Merge Request就可以达到这种效果。

现在,我们就模拟启动一个Merge Request的过程。

重新切换到feature-beer分支上写点内容,并分两次提交,并push到远程分支上。

git checkout feature-beer
# 改点内容:这是我第二次在feature-beer上进行开发工作,现在是晚上十一点五十一分了。
git add -A
git commit -m "这是第二次在feature-beer上的开发的第一次提交。"
# 改点内容:这是我第二次在feature-beer上进行开发工作,现在是晚上十一点五十二分了。
git add -A
git commit -m "这是第二次在feature-beer上的开发的第二次提交。"
git push

这样远程上的分支就能看到这次开发的两次提交啦。

image-20211104235530606

Gitee上提供了Pull Request操作来实现Merge Request。

image-20211105000041960

在创建的Pull Request中,你必须选中源分支、目标分支。还能看到提交记录和文件改动信息。

image-20211105000659267

点击创建后,Beer Bear成员就会收到这个请求。

image-20211105001102665

他可以在提交和文件中看到提交的内容。审查通过后,就可以合并了。在前面创建时我们勾选了删除提交分支(不过截图中未勾选,实际操作上是勾选了的),合并后就没有这个分支了

image-20211105001450519

这时候再看分支和分支内容,发现feature-beer没了,dev中有了合并过来的内容。

9. 删除本地分支和远程分支

这个时候我们就应该删除这个本地分支了。

git checkout dev    		# 先切换到dev分支
git pull
git branch -d feature-beer

如果这个命令报错,往往是

  • 很有可能是你正处于该分支上。
  • 该分支包含了还未合并的工作,可以先合并或者使用 -D参数强制删除。

如果你的远程分支还没有删除(在本文中,远程分支在Merge Request通过时就一起删除了),可以使用:

git push origin --delete feature-beer

当然你也可以登录到Gitee的网页上删除远程分支。

为啥要删除呢?继续用不行么?

其实可以继续用,但是不推荐,因为我们创建这个分支的目的就是为了开发一个新模块或者修复一个BUG,当开发工作完成后删除该分支,处理别的事情时再新建一个就好了。

Some Questions

1. 如果本地dev有修改内容,是否可以把这些修改内容带到新分支上

比如有一天,你困意十足,打开编译器直接开始干活了,干了半天才发现这是dev分支,这时候已经有很多代码的改动了,咋整?

现在我们在dev分支上新增一行,不提交。

image-20211108213642784

然后切换到新分支:feature-new-beer.

git checkout -b feature-new-beer

再打开文件会发现会有这行信息的。所以这个未提交的信息是可以带过来的

那我要是不想带过来怎么办?我们先删除这个分支换一个分支看一下如何做到不带过来。

git checkout dev
git branch -d feature-new-beer

我们使用

git stash

将dev分支上的内存暂存起来,这时dev上就看不到这一行了。

然后我们再次切换到新分支:feature-new-beer.

git checkout -b feature-new-beer

这个时候该分支上也不会有这一行信息了。但如果此时执行:

git stash pop

会发现那一行又出现了,并且再切换到dev时,dev上也又出现这一行了。

为啥会出现上面的现象呢,其实是因为git中存在工作区和暂存区,这两个区都是被所有本地分支共享的。

当有内容修改时,修改信息就会放在工作区中,此时如果直接检出一个新的分支,就会把工作区的内容都带过去。

而如果把修改信息暂存(stash)到暂存区时,都暂存起来了自然就不会带过去,但是由于该区也是共享的,当pop出暂存内容时,所有分支又同时恢复了这些修改内容。

如果我在dev上进行stash后,检出新分支,又加了一行,再pop会怎样呢?

image-20211108215701354

git stash pop

image-20211108215906879

会提示报错:你的本地更改(新分支上的更改)会被覆盖。

这个时候如果:

git stash
git stash pop

那么本地更改将覆盖掉dev的修改,即dev上也是下面的信息了。

image-20211108215701354

就像该问题开头说的那样,如果你在dev上写了不少东西了,那么就先pull一下最新的代码,然后检出到新分支上继续开发就可以了。

你可以在任意时间回到dev上直接进行discard changed(回滚到最初的未修改的版本)操作。

如果实在不放心就等新分支代码都写完了提交了你再到dev上删除。

2. 如果我在新分支上有很多次提交,我是否可以合并这些提交到一个提交上再提交

这是可以的,并且很多时候我们推荐这么做,比如一个模块需要三天去完成,这三天你可能提交了六七次,而实际上你只是完成了一个新模块的开发而已。

如果在你的几次提交中,有人往dev推送了代码,那么当你最后向dev发起合并请求并且成功通过后,commit的记录会是怎样呢?下面一起来试一下。

我们先把Question1中的在dev分支上的修改discard changes一下(回到未修改的状态,即上一个版本),然后删除刚刚那个分支并创建一个新分支feature-many-commits

# use "git checkout -- <file>..." to discard changes in working directory 这是官方提示
git checkout -- README.md		# 该文件回到未修改的状态
git branch -d feature-new-beer
git checkout -b feature-many-commits

现在我们在新分支上随便写点内容并提交。

# 随便加一行:我在 feature-many-commits 分支上写东西。现在是晚上22:25,写完这行立马提交。
git add -A
git commit -m"feature-many-commits,晚上22:25"

现在我们再到dev分支上写点东西并提交和push(模拟这是另一个人提交上来的代码),不过为了避免冲突,我们就不在README文件中写了,新建一个txt文件。

image-20211108223041824

现在我们再回到新分支上随便写点内容并提交,然后push到远程,并去Gitee上进行MergeRequest

# 随便加一行:我在 feature-many-commits 分支上写东西。现在是晚上22:34,写完这行立马提交。
git add -A
git commit -m"feature-many-commits,晚上22:34"
git push -u origin feature-many-commits

image-20211108223653127

image-20211108223830938

image-20211108223855435

这里提供两个选项,一个是合并分支一个是扁平化分支。看看解释应该能猜出啥意思,第一个的意思是指直接合并,后者则是先把feature-many-commits上的提交合并成一个新的commit——这就是我们想要的目标,然后再合并到dev上。

不过我们先来看一下直接合并

image-20211108224118720

现在我们根据该图显示的提交记录就可以回答开头的那个问题了:“如果在你的几次提交中,有人往dev推送了代码,那么当你最后向dev合并并成功后,commit的记录会是怎样呢?”。

正如图中展示的那样,dev的提交记录夹在了新分支上的两次的提交记录中间,这样确实凌乱、不美观。

所以我们在很多时候应该采用扁平化分支的方式来合并。

写点内容提交,再用扁平化的方式试一次:

# 随便加一行:我在 feature-many-commits 分支上写东西。现在是晚上22:43,写完这行立马提交,希望这个提交最后会与接下来的commit被合并到一个commit中。
git add -A
git commit -m"feature-many-commits,晚上22:43"

# 随便加一行:我在 feature-many-commits 分支上写东西。现在是晚上22:44,写完这行立马提交,希望这个提交最后会与之前的commit被合并到一个commit中。
git add -A
git commit -m"feature-many-commits,晚上22:44"
git push

image-20211108224725647

image-20211108224804382

image-20211108224906307

image-20211108224918291

发现确实成功将那两次commit合并成一个了!并且提交的时间就是通过Merge Request(Pull Request)的时间。

总结一下这个问题:直接合并分支会让两个分支的每一次提交都按照commit的时间进行排序,这样看起来会比较凌乱。我们可以通过gitee上扁平化分支的方式通过MR

3. 除了Merge Request通过后合并时选择扁平化分支(将多次提交合并成一个commit)还有啥方法也能实现这个效果么

确实,这个将多次提交合并为一个提交的操作是在gitee上进行的,那要是我不去gitee上进行Pull Request,直接在本地进行merge,不就达不到这种效果了么?

需要说明的一点是,实际上像GiteeGitlab这种代码托管平台是都具备这种功能的,所以如果在它们的平台上通过Merge Request(或Pull Request)去做的话,一定能实现这种效果。

但如果我不去代码托管平台发起合并请求呢?我就想本地开发完了,然后合并到dev上(再由本地dev分支push到远程dev分支),那该怎么做呢?这时候一个git rebase命令就呼之欲出了。

这个命令的作用就是将某个分支的多次commit合并成一个commit,然后在merge到另一个分支上的时候就会只有一个提交啦。

这个过程是这样的(示例是将feature-many-commits分支内容合并到dev上):

# 假设现在开发完毕 并且已经在feature-many-commits上提交了多次
git checkout dev
git pull			# dev保持最新的代码
git checkout feature-many-commits
git rebase dev		# 将feature-many-commits上所有的commit,重新在新的dev的HEAD上commit一遍
git checkout dev	# 再次切换到dev上
git merge feature-many-commits # 将feature-many-commits上的内容合并到dev上
git push	        # 推送即可

这个过程还是比较简单的,很多人也在这样去做,在此就不演示了。

Summary

现在让我们再来梳理一下整个MR的关键流程。

假设dev分支就是大家共用的分支,我们要在此基础上检视出新分支进行开发工作,最后通过Merge Request的方法合并到远程分支。:

  1. 切换到dev分支

    git checkout dev
    
  2. 更新dev分支代码

    git pull
    
  3. 在dev分支的基础上,检视出一个新分支feature-beerbear

    git checkout -b feature-beerbear
    
  4. 进行开发并提交你的代码

    git add -A
    git commit -m"xxxxxx"
    
  5. 将本地feature-beerbear分支推送到远程

    git push -u origin feature-beerbear  # 会创建一个远程feature-beerbear分支
    

    如果不是第一次推送,可以直接使用

    git push
    

    4、5步骤将在实际的开发工作中重复执行。

  6. 开发工作完成后,到代码托管平台上进行Merge Request(或Pull Request)操作。

  7. 合并请求通过后。就可以删除本地分支。

    git branch -d feature-beerbear
    
  8. 新的任务来了,从1再来。

在第六步时,可能会发生以下情况:

① 如果不通过,那么继续从4开始执行,不过不再需要新提出一个新的MR(Merge Request)了。

这时候可能会疑问,为啥不需要重新提一个了呢?

因为这个合并是指分支之间的合并,当你的远程feature-beerbear分支发生变化时,MR会感知到做出相应变化的。

实际上,如果你要新建的MR的源分支和目标分支和之前的MR中的相同,这样的话是创建不了的。

你必须关闭掉之前的那个,当然完全没这个必要。

② 发生冲突

虽然你在创建新分支时pull了一下dev分支,然后在此基础上创建新分支了,但毕竟你的MR可能是好几天后发出的,dev分支肯定不是原来的状态了。所以就会导致在创建MR时提醒冲突。那当提示发生冲突的时候,怎么办呢?只需以下几个命令即可:

git checkout dev
git pull
git checkout feature-many-commits
git merge dev						# 如果在这一步发生冲突,那么手动解决冲突即可
git push

实际上就是将dev代码更新,然后将dev上的代码合并到feature-many-commits上来。

其实我们完全可以在提交MR之前就将上面的几行命令走一遍,这样就不会再提MR的时候报出冲突了。

总不至于你刚执行完上面的命令,在你准备提出MR时,就又有人往dev上提交了代码,又恰好导致冲突了吧——如果有,可以买张刮刮乐试试运气了。

Thanks

这篇关于Merge Request的文章到此就算结束了!首先,谢谢你看完这篇文章!

其次,我十分希望这篇文章能对你有所帮助,或许帮助不是很大。如果你喜欢这篇文章或者真的有一些收获,请也点赞或者收藏,最好是在评论区留言告诉我!

虽然付出了挺多精力来写这篇文章,但毕竟咱水平一般、能力有限,所以肯定会有一些语句不畅、表意不明甚至错误的地方。

如果你能指出问题、不吝赐教,我将十分感谢,你的建议和指教也将敦促我一直完善此文!欢迎友好交流!