LV01-Git-04-Git本地仓库-06-分支的新建与合并

本文主要是Git中分支的新建与合并的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
点击查看本文参考资料
点击查看相关文件下载
--- ---

【说明】本节笔记的相关操作在Windows下进行,因为VS Code有个Git的插件,可以很直观的演示一些东西。由于Git安装后自带一个Git-Bash终端,所以就不用Win下的命令行啦,就用的这个终端,因为它里边的命令与Linux很类似,而windows中的命令行有些命令与linux并不相同,为了统一,还是用用Git自带的终端啦。另外VS Code是可以选择使用的终端的,我直接将VS Code使用的终端改成了git-bash,这样更方便一些。

一、准备工作

1. 工作场景

让我们来看一个简单的分支新建与分支合并的例子,实际工作中可能会用到类似的工作流。 我们将经历如下步骤:

(1)用C语言开发某个项目。

(2)为实现某个新的用户需求,创建一个分支。

(3)在这个分支上开展工作。

突然发现,系统整体功能出现了BUG,我们需要在新的分支上修复这个BUG:

(1)新建修复BUG分支。

(2)在BUG分支修复BUG。

正在此时,突然接到一个电话说有个很严重的问题需要紧急修补。 我们将按照如下方式来处理:

(1)切换到我们的线上分支(production branch)。

(2)为这个紧急任务新建一个分支,并在其中修复它。

(3)在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支。

(4)切换回我们最初工作的分支上,继续工作。

2. 文件准备

我们需要先准备一个仓库,里边包含一些提交记录,我们提交三次,完成C0、C1和C2这三个功能的开发。

点击查看 main.c 文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <unistd.h>

// C0功能实现
void func_C0(void)
{
printf("C0功能开始执行!\n");
printf("C0>>>>>>步骤1!\n");
printf("C0>>>>>>步骤2!\n");
printf("C0>>>>>>步骤3!\n");
printf("C0功能执行完毕!\n");
}

// C1功能实现
void func_C1(void)
{
printf("C1功能开始执行!\n");
printf("C1>>>>>>步骤1!\n");
printf("C1>>>>>>步骤2!\n");
printf("C1>>>>>>步骤3!\n");
printf("C1功能执行完毕!\n");
}

// C2功能实现
void func_C2(void)
{
printf("C2功能开始执行!\n");
printf("C2>>>>>>步骤1!\n");
printf("C2>>>>>>步骤2!\n");
printf("C2>>>>>>步骤3!\n");
printf("C2功能执行完毕!\n");
}

int main(int argc, const char *argv[])
{
/* code */
while (1)
{
func_C0(); // 实现C0功能
func_C1(); // 实现C1功能
func_C2(); // 实现C2功能
sleep(1);
}
return 0;
}

我们来看一下现在的提交记录:

image-20230623094425851

画一张简图就如下所示:

image-20230623094415782

二、BUG处理

1. 情况描述

按照前边说的,这个时候需要有一个BUG出现,我们按照《Pro Git Book》中的例子,将这个BUG编号定为 #53 ,现在我们就要创建一个分支,然后在此分支上进行#53 BUG的修复。

2. 新建并切换分支

前边我们知道通过以下命令可以新建分支,然后切换分支:

1
2
git branch iss53
git checkout iss53

可是我们其实有更简单的方式,可以直接创建并切换到新建的分支,那就是带-b参数的git checkout命令:

1
2
$ git checkout -b iss53
Switched to a new branch 'iss53'

它其实就等价于上边两条指令。现在我们的仓库情况如下图:

image-20230623094804875

3. 排查BUG并提交

我们经过排查,功能C1上有问题,我们先修复C1的功能缺陷:

image-20230623094816938

然后我们进行一次提交:

1
git commit -a -m  "feat:C3 修复C1出现的BUG[issue 53]"

当前仓库的情况如下:

image-20230623095643939

但是这个BUG似乎没有完全解决,我们还要继续再排查,按照剧情需要,此时需要有紧急事件出现。

三、紧急事件

剧情发展需要,果然出现了紧急事件。

1. 情况说明

现在我们接到那个电话,有个紧急问题等待我们来解决,我们需要在C2功能的基础上再开发一个C4功能。 有了 Git 的帮助,我们不必把这个紧急问题和 iss53 的修改混在一起, 也不需要花大力气来还原关于 #53 问题的修改,然后再添加关于这个紧急问题的修改,最后将这个修改提交到线上分支。 我们所要做的仅仅是切换回 master 分支,然后新建分支,处理紧急事件,BUG分支就可以先扔一边。

2. 切换回master分支

在这么做之前,要留意工作目录和暂存区里那些还没有被提交的修改, 它可能会和我们即将检出的分支产生冲突从而阻止 Git 切换到该分支。 最好的方法是,在我们切换分支之前,保持好一个干净的状态。 有一些方法可以绕过这个问题(即,暂存(stashing) 和 修补提交(commit amending)), 会在 贮藏与清理 中看到关于这两个命令的介绍。 现在,我们假设已经把你的修改全部提交了,这时可以切换回 master 分支了:

1
2
$ git checkout master
Switched to branch 'master'

这个时候,我们的工作目录和我们在开始 #53 问题之前一模一样,现在可以专心修复紧急问题了。 当我们切换分支的时候,Git 会重置我们的工作目录,使其看起来像回到了我们在那个分支上最后一次提交的样子。 Git 会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样。

image-20230623095708428

3. 新建并切换分支

接下来,要修复这个紧急问题。 我们来建立一个 hotfix 分支,在该分支上工作直到问题解决:

1
2
$ git checkout -b hotfix
Switched to a new branch 'hotfix'

现在仓库的情况如下:

image-20230623100350794

4. 修复紧急问题并提交

我们上一步已经切换到hotfix分支,排查后发现这个问题出现在C2功能上:

image-20230623100607272

我们处理完紧急问题后,进行一次提交:

1
git commit -a -m "feat:C4 修复紧急问题"
image-20230623100836073

当前分支的提交记录如下:

image-20230623100916660

四、分支合并

1. 情况说明

我们上边已经修复了紧急问题,那么现在需要将修复紧急问题后的代码合并到master分支。

2. git merge

我们可以通过下边的命令来完成分支的合并:

1
git merge branch_name

branch_name为需要合并到当前分支上的分支名。需要注意的是,我们要合并分支,需要先切换到目标分支去,例如这里,我们想要把hotfix分支的内容合并到mater分支,我们就需要先切换到mater分支。

3. 切换到master分支

我们将hotfix分支的内容都提交后,就可以切换到master分支了:

1
git checkout master

我们查看一下提交记录们就会发现master分支根本没有变化,还是最开始的三次提交。

image-20230623101753441

当前仓库的情况如下:

image-20230623101848979

4. 合并hotfix分支

我们执行以下命令:

1
2
3
4
5
$ git merge hotfix
Updating 08ec435..f5a3adf
Fast-forward
main.c | 5 +++++
1 file changed, 5 insertions(+)

在合并的时候,我们应该注意到了“快进(fast-forward)”这个词。 由于我们想要合并的分支 hotfix 所指向的提交 C4 是我们所在的提交 C2直接后继, 因此 Git 会直接将指针向前移动。换句话说,当我们试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。当前仓库的情况如下图:

image-20230623102418086

然后我们来看一下提交记录:

image-20230623102025200

发现C4的修复已经合并到master这里来了,我们看一下main.c的func_C2函数:

image-20230623102127913

会发现,对应的紧急问题已经被修复了。

五、继续BUG处理

上边的紧急情况处理完了,但是我们的BUG还没有完全解决掉,只是修复了一小部分,我们还需要继续排查。

1. 切换到iss53分支

我们切换到iss53分支继续进行BUG的修复:

1
2
$ git checkout iss53
Switched to branch 'iss53'

此时的仓库情况如下:

image-20230623102951487

还可以看一下当前的提交记录:

image-20230623103021721

可以看到此分支还停留在修复部分#53 BUG的时候,并没有受到任何影响。我们在 hotfix 分支上所做的工作并没有包含到 iss53 分支中。 如果我们需要拉取 hotfix 所做的修改,可以使用 git merge master 命令将 master 分支合并入 iss53 分支,或者也可以等到 iss53 分支完成其使命,再将其合并回 master 分支。

2. 修复BUG并提交

这一次我们将BUG完全修复,本次修复我们更改了以下内容:

image-20230623103542761

然后我们进行一次提交:

1
git commit -a -m "feat:C5 结束BUG的修复[issue 53]"

此时,iss53分支提交记录如下:

image-20230623103725644

整个仓库的情况如下:

image-20230623103911519

3. 合并到master分支

我们已经修正了 #53 问题,并且打算将我们的工作合并入 master 分支。 为此,我们需要合并 iss53 分支到 master 分支,这和之前合并 hotfix 分支所做的工作差不多。 我们只需要切换到想合并入的分支,然后运行 git merge 命令:

1
2
3
4
5
6
7
$ git checkout master
Switched to branch 'master'

$ git merge iss53
Auto-merging main.c
CONFLICT (content): Merge conflict in main.c
Automatic merge failed; fix conflicts and then commit the result.

发生了什么?我们看到了一个报错:Automatic merge failed,我们先来看一下master的提交记录:

image-20230623104336434

我们发现,master分支并没有产生新的提交记录,我们再看一下文件内容:

image-20230623104449722

我们发现这里出现了这么一段内容,这就叫做冲突

4. 冲突的产生?

有时候合并操作不会很顺利。 如果在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们。 如果我们对 #53 问题的修改和有关 hotfix 分支的修改都涉及到同一个文件的同一处,在合并它们的时候就会产生合并冲突:

1
2
3
4
$  git merge iss53
Auto-merging main.c
CONFLICT (content): Merge conflict in main.c
Automatic merge failed; fix conflicts and then commit the result.

此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待我们去解决合并产生的冲突。 我们可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件:

image-20230623104901587

任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。 Git 会在有冲突的文件中加入标准的冲突解决标记,这样我们可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:

image-20230623104955077

这表示 HEAD 所指示的版本(也就是我们的 master 分支所在的位置,因为你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。 为了解决冲突,我们必须选择使用由 ======= 分割的两部分中的一个,或者我们也可以自行合并这些内容。 我们先对比一下合并了hotfix分支的master分支、hotfix分支和iss53分支的main.c这几行:

image-20230623110250694

发现同一个文件同一行,两个分支中的内容是不一样的,这样合并的时候,Git无法自动判定要使用哪一个分支的内容,于是就产生了冲突,需要我们自己手动解决。

5. 解决冲突

我们既要修复紧急问题,也要完成BUG修复,所以两者都要,我们可以这样处理冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// C2功能实现
void func_C2(void)
{
printf("C2功能开始执行!\n");
printf("#53 继续BUG修复添加内容1!\n");
printf("#53 继续BUG修复添加内容2!\n");
printf("C2>>>>>>步骤1!\n");
printf("C4>>>>>紧急问题处理1!\n");
printf("C4>>>>>紧急问题处理2!\n");
printf("C4>>>>>紧急问题处理3!\n");
printf("C4>>>>>紧急问题处理4!\n");
printf("C4>>>>>紧急问题处理5!\n");
printf("#53 继续BUG修复添加内容3!\n");
printf("C2>>>>>>步骤2!\n");
printf("C2>>>>>>步骤3!\n");
printf("C2功能执行完毕!\n");
}

直接将多余的删掉即可。

6. 提交更新

在我们解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。 一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决,即便我们没有处理冲突。

1
2
git add .
git status
image-20230623110814490

然后我们提交此次合并:

1
git commit -m "feat:C6 合并iss53(冲突已解决)"
image-20230623111340721

可以看到master分支多出了两次提交,这两次就是iss53中的处理BUG的修改,最新的一次是我们在mater进行的提交,是进行的冲突解决。

7. 合并分析

这和我们在《四、分支合并》这一节笔记中合并 hotfix 分支的时候看起来有一点不一样。 在这种情况下,我们的开发历史从一个更早的地方开始分叉开来(diverged)。 因为,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4C5)以及这两个分支的公共祖先(C2),做一个简单的三方合并。

image-20230623112122799

和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。

image-20230623112318972