黑帽联盟

 找回密码
 会员注册
查看: 808|回复: 0
打印 上一主题 下一主题

[基础服务] Git之旅(11):合并分支详解

[复制链接]

852

主题

38

听众

3175

积分

白金VIP

Rank: 8Rank: 8

  • TA的每日心情
    开心
    2024-3-7 12:52
  • 签到天数: 1538 天

    [LV.Master]伴坛终老

    前文中已经介绍过什么是分支,我们知道,不同分支中的提交是互不影响的,在实际的工作中,不同的分支通常对应了不同的功能和场景,或者对应着不同的开发人员,又或者对应着不同的代码模块,总之,我们可以利用分支,将"提交"从某个逻辑角度隔离开,让它们之间互不影响,以便在不影响他人或者其他逻辑的情况下完成工作,不过,当某个分支中的工作完成后,我们通常需要将这条分支与其他分支进行合并,以便多个分支的代码能够汇聚到一起,从而获取到一份更加全面、更加完整的可用代码,所以,合并分支也是我们必须掌握的技能,这篇文章我们就来总结一下怎样合并分支。

    合并分支的操作其实不难,只是想要说明白,需要费一些口舌,因为在执行"合并"操作时,我们可以根据具体情况,选择不同的合并模式去合并,不同的合并模式对应了不同的命令参数,而且在合并的过程中,还可能遇到"冲突",听到这里是不是觉得有些麻烦,不用害怕,其实理解了原理以后是非常简单的,为了说明白原理,我们先来看一些示意图,理解了示意图以后,再去结合实际命令,搞定合并分支的操作简直不要太简单,也请阅读这篇文章的朋友一口气将此文读完,阅读中如果遇到问题,先记下来,等读完了这篇文章,再去回顾这些问题。

    为了更好的理解,我们从头开始聊,先聊聊合并分支之前会发生的事情,示意图如下
    1.png
    此示意图并没有涉及到任何合并操作,而是描述了合并分支之前,两个分支的创建过程。
    上图中的第1步表示已经存在的一条分支,这条分支的名字是base。
    第2步表示基于base分支,创建了new分支,此时,base分支的指针和new分支的指针都指向了最新的提交。
    第3步表示我们在new分支中创建了新的提交。
    第4步表示base分支也产生了新的提交,new分支也产生了新的提交,两个分支的指针分别指向了自己分支的最新提交,换句话说就是,从分叉点开始以后,两个分支各自产生了属于自己的提交。
    有了前文作为基础,看明白上图肯定不在话下,我们接着聊。

    上图中有两个分支,base分支和new分支,如果,我们想要将两个分支合并,该怎么做呢?没错,我们只需要将一条分支合并到另外一条分支上就行了(听上去似乎是句废话),但是,仔细想想,此处有一个小问题,我是把new分支合并到base分支上呢?还是将base分支合并到new分支上呢?有的朋友可能会问,有什么区别吗?听上去似乎是没有任何区别的呀。其实,还是有一些区别的,这里牵扯到一个合并方向的问题,让我们带着这个疑问,看看如下示意图:
    2.png
    上图左侧的第4步我们已经解释过,第4步表示两个分支合并之前的状态,上图右侧有上下两个图示,分别表示两种合并方向,图示一表示将new分支合并到base分支上以后的状态,图示二表示将base分支合并到new分支上以后的状态。现在,我们分别解释一下上图中的两个图示。
    我们先聊聊图示一(对比着第4步去看图示一更容易理解,第4步表示合并前,图示一表示合并后),图示一表示将new分支合并到base分支上,合并操作完成后(后文会细说具体操作,此处不用纠结),会产生一个新的提交(蓝色提交),这个新提交就是合并后的提交,它包含了两个分支中的最新代码,并且将它们合并到了一起,这个提交就是我们想要的合并后的状态,base分支的指针会指向这个新的蓝色提交,而new分支的指针则没有移动位置,仍然指向了new分支的最新提交(绿色提交)。为什么base分支的指针会指向最新的蓝色提交,而new分支的指针却保持原位呢?我们可以这样理解,在合并之前,base分支和new分支都有属于自己独有的提交(最新的黄色提交只属于base分支,绿色提交只属于new分支),如果我们是把new分支合并到base分支上,就表示要把只属于new分支上的变更合并到base分支上,对于base分支来说,会有新的变更进入(原来只属于new分支的变更对于base分支来说就是新变更),新变更进入后,base分支的内容会产生变化,所以,base分支需要一个新的提交(蓝色提交)来对应变化后的状态,于是,base分支的指针会指向最新产生的合并提交(蓝色提交),而对于new分支来说,并没有任何内容发生变动,所以new分支的指针仍然保持原位。
    理解了图示一,再去理解图示二就非常简单了,道理其实是一样的,我们仍然对比着第4步去看图示二,图示二表示将base分支合并到new分支上,合并后会产生一个新的合并提交(蓝色提交),这个蓝色提交对应了合并后的状态,这个新的蓝色提交属于new分支,而不属于base分支,因为我们是把base分支合并到new分支上,这表示只属于base分支的变更会加入到new分支中,对于new分支来说,内容会发生变化,new分支需要一个新的提交来对应变化后的状态,而这个新的提交正是合并后产生的蓝色提交,于是,new分支将指针指向了蓝色提交,base分支的指针仍然保持原位。

    你肯定已经总结出了规律,规律就是,在上述情况下,合并后的新提交属于合并到的目标分支。

    除了上述情况,还有一种更加简单的合并场景,为了方便描述,还是先看示意图
    3.png
    上例示意图中的前3步与之前描述的场景是一样的。
    前3步表示:有一条base分支,基于base分支创建了new分支,在new分支上创建了新提交。
    只是,上图中的第4步与之前描述的场景略微有些不同。
    第4步表示:基于base分支创建new分支以后,只有new分支中产生了两个新提交,而base分支中还没有产生任何新提交。
    如果在这种情况下,我想要将new分支合并到base分支,那么合并后会是什么样子呢?如果我们仍然按照之前介绍的思路去思考,合并后应该如下图中第5步所示:
    4.png
    上图中第4步代表合并前的样子,第5步代表合并后的样子,因为new分支中有两个专属于new分支的提交,所以当我们把new分支合并到base分支时,base分支应该使用一个新提交来保存新进入的变更,所谓的新提交就是上图中的蓝色提交,上述示意图是完全没有任何问题的,只不过,在上图中第4步的情况下,我们还可以选择另外一种更加快捷的方式完成合并,这种快捷的合并方式被称之为"Fast-forward"(可译为"快进"或者"快速重定向"),我们一起来看看,使用"Fast-forward"的方式处理上述情况,会合并成什么样子,示意图如下:
    5.png
    上图中第4步代表合并前的样子,第5步代表使用"Fast-forward"的方式合并后的样子,你肯定已经看明白了,由于基于base分支创建new分支以后,base分支中并没有产生任何新的提交,如果此时想要将new分支合并到base分支,只需要将base分支的指针指向到new分支的最新提交,即可让base分支包含new分支中的所有新变更。
    这样说可能不容易理解,我们换个方式再解释一遍,new基于base创建,new新产生的所有变更都包含在上图中的绿色提交中,将new合并到base,就表示将new中的变更(所有绿色提交中包含的变更)也加入到base中,让绿色提交属于base分支最快的方法就是直接将base分支的指针直接指向最新的绿色提交。
    上述直接移动指针进行合并的方式就是就是所谓的"Fast-forward","Fast-forward"的合并方式不会在base分支中产生任何合并提交(即不会产生示意图中的蓝色提交),而是利用了指针的移动,快速的实现了将new分支合并到base分支中的目的。

    需要注意的是,"Fast-forward"的合并方式必须在满足条件的情况下才能使用,那么,什么情况下才能使用"Fast-forward"的合并方式呢?示意图如下:
    6.png
    上图表示了在合并分支之前的两种状态,如果合并分支之前,base分支和new分支都有了属于自己分支的独有提交,就不能使用fast-forward的方式进行合并,如上图状态二所示,分叉点以后各分支都产生了属于自己的提交,这种情况下,就不能使用fast-forward,只能使用创建合并提交的方式进行合并。
    当合并分支前的状态如状态一所示时,才能使用fast-forward的方式将new分支合并到base分支中,当然,在状态一的情况下,我们也可以选择使用创建合并提交的方式进行合并,后文会进行详细的演示,此处先行略过。
    有的朋友可能会问,为什么上图中状态二的情况下就不能使用fast-forward模式呢?
    其实仔细想想,就能想明白了,合并分支的最终目的是将两条分支中的内容完整的合并在一起,如果在状态二的情况下使用fast-forward模式直接移动分支指针,能够保证合并后的状态包含两个分支的所有内容吗?你可以在脑海中模拟一遍,如果移动base指针到绿色提交,就会丢失最新的黄色提交,如果移动new指针到最新的黄色提交,就会丢失绿色提交,这就是为什么在状态二的情况下不能使用fast-forward的原因,因为在这种情况下,fast-forward无法满足合并的最基本需求。

    好了,概念说的差不多了,该动动手了。

    为了能够更加方便的进行演示,我们来创建一个测试仓库,在测试仓库的master分支中创建一些基础的可以用于测试的提交,操作如下
    $ git init test_repo
    Initialized empty Git repository in D:/workspace/git/test_repo/.git/

    $ cd test_repo/

    $ echo "test1" > m1

    $ echo "test11" > m11

    $ git add -A

    $ git commit -m "Initializes files of module 1"
    [master (root-commit) 0da419c] Initializes files of module 1
    2 files changed, 2 insertions(+)
    create mode 100644 m1
    create mode 100644 m11

    $ echo "test2" > m2

    $ echo "test22" > m22

    $ git add -A

    $ git commit -m "Initializes files of module 2"
    [master 5b8c4c8] Initializes files of module 2
    2 files changed, 2 insertions(+)
    create mode 100644 m2
    create mode 100644 m22
    如上述操作所示,我创建了一个测试仓库,创建了一些测试文件,用这些文件创建了两个用于基础测试的提交。此处假设,测试仓库中的这些测试文件就是我的程序代码,假设我的程序由两个模块组成,模块一和模块二,m1文件和m11文件属于模块一,m2文件和m22文件属于模块二,我会为模块一和模块二分别创建两个分支,以便针对两个模块的修改互不影响,当我需要一份完整的代码时,会将模块一和模块二对应的分支合并到master分支中,以便从master分支获取到相对完整的代码,现在,我们需要分别为两个模块创建分支,b1分支和b2分支,操作如下:
    $ git status
    On branch master
    nothing to commit, working tree clean

    $ git branch b1

    $ git branch b2
    如上述操作所示,我基于master分支,创建了b1分支和b2分支,使用"gitk --all"命令,查看图形化界面,如下:
    7.png
    目前来说,这三条分支时完全相同的。
    现在,我切换到b1分支,修改一些文件,模拟针对模块一代码的修改工作,并且在b1分支上创建提交,操作如下:
    $ git checkout b1
    Switched to branch 'b1'

    $ cat m1
    test1

    $ echo "test m1" >> m1

    $ cat m1
    test1
    test m1

    $ git add m1

    $ git commit -m "modify m1"
    [b1 be27bc9] modify m1
    1 file changed, 1 insertion(+)
    同样,切换到b2分支,进行一些修改,模拟针对模块二的修改。操作如下
    $ git checkout b2

    $ cat m2
    test2

    $ cat m22
    test22

    $ echo "test m2" >> m2

    $ echo "test m22" >> m22

    $ cat m2
    test2
    test m2

    $ cat m22
    test22
    test m22

    $ git add -A

    $ git commit -m "modify module2"
    [b2 a73e5ca] modify module2
    2 files changed, 2 insertions(+)
    完成上述操作后,再次使用"gitk --all"命令,查看图形化界面,如下:
    8.png
    如上图所示,b1分支和b2分支分别产生了属于自己的独有提交,也就是说,通过上述操作,这两个提交中分别存放了两个模块的最新代码,master分支中不包含这两个模块中任何一个模块的最新代码,如果我想要将两个模块的最新代码汇聚到master分支中,只需要将b1分支和b2分支合并到master分支中即可,那么具体该怎么操作呢?
    如果你想要的将A分支合并到B分支,就需要先检出到B分支,然后再执行合并命令将A分支合并进来,也就是说,需要先检出到目标分支,再执行合并命令。
    先以合并b1分支为例,看看怎样将b1分支合并到master分支,具体操作如下:
    #如果我们想要将某个分支的代码合并到master分支,需要先切换到master分支
    $ git checkout master
    Switched to branch 'master'

    #查看一下m1文件的内容,并不是模块一最新的文件内容,m1的最新版本目前只存在于在b1分支中
    $ cat m1
    test1

    #使用如下命令即可将b1分支合并到当前分支(当前分支是master分支),git merge命令就是用于合并分支的命令,此命令会将指定的分支合并到当前分支。
    $ git merge b1
    Updating 5b8c4c8..be27bc9
    Fast-forward
    m1 | 1 +
    1 file changed, 1 insertion(+)
    从上述命令的返回信息可以看出,当我们把b1分支合并到master分支时,git默认使用了"Fast-forward"模式,这是因为git发现,b1分支是基于master分支创建的,并且master分支并没有产生属于自己的独有的提交,所以,当我们需要把b1分支合并到master分支时,只需要将master的指针指向b1分支的最新提交即可,执行上述命令后,使用"gitk --all"查看图形化界面,如下:
    9.png
    正如我们所想,master分支的指针指向了b1分支的最新提交,也就是说,此时b1分支已经合并到了master分支中。
    再次查看master分支中的m1文件内容,发现m1的内容已经变成了最新的版本
    $ cat m1
    test1
    test m1
    如上述操作所示,我们把b1分支合并到了master分支中,master分支中已经包含了模块一的最新版本的代码,但是目前,master分支中还不包含模块二的最新代码,查看master分支中模块二的文件,内容仍然是最初的,如下:
    $ cat m2
    test2

    $ cat m22
    test22
    我们可以使用同样的方法即可将b2分支合并到master分支中。既然是想将b2分支合并到master分支中,就需要先检出到master分支,但是由于我们当前就处于master分支,所以就不用执行checkout命令了,直接执行merge命令即可,不过,在执行merge命令之前,请先思考一个问题,在当前状态下,如果将b2分支合并到master分支,还能使用"Fast-forward"模式吗?
    答案是:不能。
    为什么不能呢?为了更加容易理解,我们可以对比着图形化界面中的状态和之前的示意图中的状态去理解,如下:
    10.png
    看到此处,聪明如你肯定已经完全明白了,master分支就相当于图示中的base分支,b2分支就相当于图示中的new分支,所以,在这种状态下,如果我们想要将b2分支合并到master分支中,则不能使用"Fast-forward"的模式进行合并,只能使用创建新提交的方法进行合并,换句话说就是,当我们将b2分支合并到master分支以后,会产生一个新的合并提交,注意,我们必须为这个新提交填写注释信息,否则将无法完成合并操作。
    好了,理解了原理以后,我们开始动手合并吧。
    执行如下命令将b2分支合并到当前分支(master分支):
    $ git merge b29
    注意,执行上述命令后,git会自动调用vim编辑器,并且自动生成如下图中的注释信息,下图中的注释信息都是为了"新提交"而准备的,刚才说过,我们必须为新提交填写注释信息,而下图中的注释信息则是git自动默认生成的,"Merge branch b2"表示这个新提交就是为了合并b2分支而产生的提交,你也可以根据自己的需要,修改如下注释信息,但是此处为了方便,直接使用默认的注释信息。
    11.png
    在vim编辑器的命令模式下输入wq,保存退出,即可使用默认的注释信息,退出vim后,看到如下返回信息
    $ git merge b2
    Merge made by the 'recursive' strategy.
    m2  | 1 +
    m22 | 1 +
    2 files changed, 2 insertions(+)
    此时,再次查看图形化界面,如下:
    12.png
    从上图可以看出,当我们将b2分支合并到master分支以后,产生了一个新的提交,这个提交属于master分支,这个提交中包含了来自b2分支中的变更,这个提交的注释信息是"Merge branch b2",正是刚才git默认生成的注释信息。

    其实,我们每次执行git merge命令时,git都会先去尝试能不能使用"Fast-forward"的模式进行合并,如果能,就默认使用"Fast-forward"的模式进行合并,如果不能,就创建一个合并提交进行合并。

    好了,说了这么多,其实都是非常简单的理论和命令,多看两遍,多做两边,就能很快的理解了。

    现在,我们着重的看一下merge命令以及常用的一些参数。
    git merge A
    上述命令表示将A分支合并到当前分支。
    git merge --no-ff A
    上述命令表示将A分支合并到当前分支,但是明确指定不使用"Fast-forward"的模式进行合并, "--no-ff"中的"ff"表示 "Fast-forward",即使合并条件满足"Fast-forward"模式,也不使用快进的方式进行合并,而是使用创建合并提交的方式进行合并。
    git merge --ff-only A
    上述命令表示将A分支合并到当前分支,但是只有在符合"Fast-forward"模式的前提下才能合并成功,在不符合"Fast-forward"模式的前提下,合并操作会自动终止,换句话说就是,当能使用"Fast-forward"模式合并时,合并正常执行,当不能使用"Fast-forward"模式合并时,则不进行合并。
    git merge --no-edit A
    上述命令表示将A分支合并到当前分支,但是没有编辑默认注释的机会,也就是说,在创建合并提交之前不会调用编辑器(上文的示例中会默认调用vim编辑器,以便使用者能够有机会编辑默认的注释信息),换句话说就是,让合并提交直接使用默认生成的注释,默认注释为" Merge branch 'BranchName' "
    git merge A --no-ff -m "merge A into master,test merge message"
    上述命令表示将A分支合并到当前分支,并且使用-m参数指定合并提交对应的注释信息。
    注意,为了保险起见,需要同时使用"--no-ff"参数,否则在满足"Fast-forward"模式的情况下,会直接使用"Fast-forward"模式进行合并,从而忽略了-m选项对应的注释信息(因为使用"Fast-forward"模式合并后不会产生新提交,所以为提交准备的注释信息会被忽略)

    说了这么多,合并分支的常见基本操作应该已经说明白了,不过我们还有很多问题没有聊,比如,在合并时遇到"冲突"该怎么办呢?这些话题就留到之后的文章中再行讨论吧,希望这篇文章能够对你有所帮助,加油~

    帖子永久地址: 

    黑帽联盟 - 论坛版权1、本主题所有言论和图片纯属会员个人意见,与本论坛立场无关
    2、本站所有主题由该帖子作者发表,该帖子作者与黑帽联盟享有帖子相关版权
    3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和黑帽联盟的同意
    4、帖子作者须承担一切因本文发表而直接或间接导致的民事或刑事法律责任
    5、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责
    6、如本帖侵犯到任何版权问题,请立即告知本站,本站将及时予与删除并致以最深的歉意
    7、黑帽联盟管理员和版主有权不事先通知发贴者而删除本文

    勿忘初心,方得始终!
    您需要登录后才可以回帖 登录 | 会员注册

    发布主题 !fastreply! 收藏帖子 返回列表 搜索
    回顶部