分支

1. 分支指针

理解分支「指针」是理解分支的基础。对于初学者而言,对于分支的难懂、难用的原因在于容易混淆分支指针和分支的概念。

分支指针的核心关键在于:它是一个指针,这个指针总是指向你的提交记录中的某个节点的。

Git Repository 的默认的初始情况有一个名为 master 的指针。

为什么一个 Git Repository 初始情况下必须要有一个提交?

因为 master 指针天生存在,而指针又必然指向某个提交节点,那么一个 Git Repository 任意时刻至少必须要有一个提交节点,否则 master 指针就是无处安放啊?!

下图中有 5 个指针,分别指向 5 个不同的节点( 提交记录 )

git-branch-01

更复杂的一点的情况是:两个( 甚至更多 )的指针指向同一个节点。例如,下图的 hello 和 bye 指针就指向了 D 节点 。

git-branch-02

在任意时刻,你( 程序员 )一定是在使用某个指针的。在 GitKraken 的图形化界面上,这个指针的前面就有个 ✓ 符号。上两张图中分别就是 master 和 hello 前面带 ✓ 。

「某个指针指向某个节点」这个事情是会变的!例如,通过「提交」和「硬撤」你会发现指针可以指向不同的节点。

当然,变动的总是那个带 ✓ 的指针,你想变动『别的指针』,你就要「想办法」让『别的指针』带这个 ✓ 。

2. 分支

以 xxx 指针所指向的提交( 节点 )作为「末端」,回溯( 树藤摸瓜往回摸 )到起点,这一条路就被称为 xxx 分支。

根据以上的定义,我们知道:

  • xxx 指针是定义 xxx 分支的依据和根本;
  • xxx 指针一定是 xxx 分支的「末端」;
  • xxx 指针「变」了位置,那么就意味着 xxx 分支有了变化。

我们未来会经常看到如下的各种情况:

  1. 有 2 个分支有部分节点是重合的:

    git-branch-03

    上图中,以 hello 指针所指向的节点作为末端的分支( 即,hello 分支 )和以 world 指针所指向的节点作为末端的分支( 即,world 分支 )有 3 个节点( 提交记录 )是重合的。

  2. 有 2 个分支的所有节点是全部重合的:

    git-branch-04

    上图中,以 hello 指针所指向的节点作为末端的分支( 即,hello 分支 )和以 master 指针所指向的节点作为末端的分支( 即,master 分支 )的全部节点都是重合的。

    在 GitKraken 的图形化显示上,两个指针是「叠」在一起的,显示在前面的是你( 程序员 )正在使用的当前指针,也就是带 ✓ 的那个指针。当你把鼠标移上去的时候,这个「叠」的效果会展开,露出指向这个节点的所有的指针。

  3. 有一个分支是另一个分支的一部分:

    git-branch-05

    master 分支所记录的提交记录是 good 分支所记录的提交记录一半。或者说,good 分支所记录的提交记录「完全涵盖」了 master 分支所记录的提交历史。

有人将分支比喻成泳道,但我个人不太喜欢这种比喻。因为有些场景用泳道比喻不出来,所以这里就不展开说泳道这个说法了。

3. 分支基本操作

#本质
查看分支git branch

本质上,查看分支就是在查看所有的分支指针。
创建分支git branch <新分支指针> [某提交记录]

本质上,创建分支是新增一个新的分支指针,指向当前( 或指定的节点 )。以这个指针所指向的这个节点作为末端,自然也就意味着新增了一个分支。
切换分支git checkout <分支指针>

本质上,切换分支是切换你( 程序员 )当前所使用的分支指针,简单来说,就是然另一个分支指针前面带 ✓ 。
删除分支git branch -d <分支指针>

本质上,删除分支实际上是要删除一个分支指针。这个分支指针消失了,你自然就无法使用这个分支指针指向某个节点,再溯源了,自然也就没有这个分支了。

4. 为什么需要分支

版本的提交不可能『依次进行,以便形成一条直线型的提交历史记录』,原因有二:

需要并行式开发

有两个以上的开发者在对同一个项目进行并行式开发。张三在 B 版本的基础上开发了 xxx 功能,形成了 C 版本;李四在 B 版本的基础上开发了 yyy 功能,形成了 D 版本。

  • 个人视角:
张三视角如下:        李四视角如下:

A <--- B <--- C       A <--- B <--- D
  • 上帝视角:
         C
        /
A <--- B
        \
         D
如何「造」出这种情况?
  1. 基于 B 节点创建一个新分支并使用;
  2. 做些许变动,提交;
  3. 基于 B 节点再创建一个新分支并使用;
  4. 做些许变动,提交。

修复旧版本中的 bug

一方面要修复旧版本中的 bug ,而与此同时又要创建和发布新的版本。

  • 前期
A <--- B <--- C <--- D
  • 修复 bug 之后
        C <--- D
       /
A <--- B  
       \
        E
如何「造」出这种情况?
  1. 当前在 D 节点;
  2. 基于 B 节点创建分支,并使用;
  3. 做出变动,提交。

5. 分支合并 项目经理操作

在大多数情况下,项目的分支都会被合并到主( master )分支。合并项目分支需要使用 git merge 命令:

git merge <另一个分支名>

该命令会把『另一个分支』合并到当前分支,合并后的 Commit 属于当前分支。

你站在哪个分支上?当前分支是谁?

考虑这个问题的关键点在于:合并分支是合并「进来」。体会下,什么叫合「进」来。

模拟 git merge 的操作思路/流程:

  1. 查看「那个分支」和「我」是从哪个节点开始「分叉」的。最极端的情况,可能除了第一个节点之外,它和我就已经不一样了。

  2. 统计一下,自分叉开始,那个分支做了哪些变动。新增了啥?删除了啥?改动了啥?

  3. 把「它」的那些变动,在「我」身上做一遍。我的提交记录会向前「走」一步,多一个节点出来。

例如:

git-branch-05

合并前:

A <--- B <--- F <---- I    world
        \
         G <--------- H  good
  1. 因为是站在 world 角度,要把 good 分支的提交记录「合进来」,所以是要站在 world 分支上( ✓ 在 world 处 ),用鼠标去「点」 good 分支,去操作 good 分支。

  2. world 分支和 good 分支是从 B 节点开始分叉的,所以 Git 会去统计 B 节点之后 good 分支一共发生了哪些变动,也就是 G 和 H 提交记录中的那些变动。

  3. Git 会将这些变动在 world 分支的末端( 也就是 world 指针所指向的那个节点,即 I 节点 )的基础上同样执行一些变动,从而形成一个新的节点。

合并后:

A <--- B <--- F <---- I <---- 合并点   world
        \                    /
         G <--------------- H  good

提示 2 点

  • 合并后 world 指针向前迈了一步,而 good 指针原地没动。

  • 想要撤销合并,可以让 world 指针「硬撤」一步,即,在 world 指针当前节点的前一个身上执行 git reset --hard 操作。

git-merge-6

6. 冲突

什么是冲突

前面提到过

合并时的逻辑是 git 去统计「那个」分支自你俩「分叉」之后发生了哪些变动,然后在「我」身上执行一次这些变动。

合并 git merge 过程是自动的,但又不是完全自动。即,有些情况下,能自动执行;有些情况下,还是需要你( 程序员 )人工干预。

因为「我」分支和「那个」分支在同一个文件的同一个位置做出了不同的提交。对于合并后的成果,对于这个文件的这个位置,Git 不知道是像「我」分支这样,还是像「那个」分支那样。

例如:

  • 在 B 节点时,文档的内容是 hello 和 world 。

  • hello 分支基于 B 节点,在文档第 3 行添加了 xxx ,形成了 C 节点。

  • world 分支基于 B 节点,在文档第 3 行添加了 yyy ,形成了 D 节点。

现在「站在」hello 分支的角度上,要把 world 分支合进来( 纳入 world 分支的所有变动 ),Git 就会遇到一个无法自决的难题,需要你( 程序员 )的人为干预:

hello 分支和 world 分支在 B 节点分叉,即,在 B 节点之后,hello 分支和 world 分支对 Git Repository 的变动就不一样了; world 分支在文档的第 3 行加了 yyy ,那么,现在 hello 分支需要照着这个操作也来一遍。

但是,现在 hello 分支当前的情况是文档的第 3 行已经有了一行内容 xxx ,怎么办?Git 不知道,或者说,它不敢动了。

这就是冲突。

git-merge-7

上图有 4 处与分支有关:

  • ① 和 ② 是提示信息,GitKraken 在告诉你,合并过程中发生了冲突。即,有些文档的变动,它做不了主,需要你( 程序员 )来裁决。
  • ③ 处列出的是在这次合并过程中,有哪个、哪些文件有冲突。这些有冲突的文件此时处于一种特殊的状态:Conflicted( 有冲突 )状态。
  • ④ 处是对冲突的后续处理。因为此时此刻你正在「合并过程中」,所以接下啦要么你取消合并,回到合并前,当作啥事都没发生;要么就人工裁决这些有冲突的文件该如何如何,然后继续合并。

警告

再次提醒,发生冲突时你的「合并动作」失败暂停了,即,你此时此刻仍然是处于「合并中」,后续,你要么取消合并,要么必须裁决文件的冲突,然后继续合并。不能吊在半空,占不着村,后不着店!

冲突文件的内容

你可以在磁盘上( 即,工作区 )去查看冲突文件的内容。你会发现,因为需要你的人工裁决,Git 为了便于你的裁决,它将「我分支」和「那个分支」对于这个文件的同一个位置的两种不同的改动「并排」地放在了哪个地方。

文件的内容的格式类似如下:

<<<<<<<< HEAD
这种、这种、这种改法
========
那种、那种、那种改法
>>>>>>>> xxx

====== 作为分隔符,<<<< HEAD==== 之间的内容是「我分支」在文档的这个地方的写法;而 ====>>>> xxx 之间的内容是「那个分支」( 这里的「那个分支」是 xxx 分支 ) 在文档的这个地方的写法。

解决冲突

所谓冲突就是对于同一个地方现在有两种不同写法,其解决方案无非就是 4 种:

  • 采用「我」分支的写法,舍弃「那个」分支的写法。
  • 采用「那个」分支的写法,舍弃「我」分支的写法。
  • 都保留
  • 都不要( 这种情况下,这个文件的内容就是两个分支为分叉时的那个样子 )

注意,根据具体情况的不同,对于当前的冲突无论最终你采用的是上述那种解决方案,最终你的冲突文件文件中肯定是没有 <<<< HEAD====>>>> xxx 这三个内容的。否则就是逻辑上就是有问题的,因为这些内容是 Git 加入到文档中的,最终冲突解决后应该被删除的。

冲突和解决冲突示意图:

git-merge-4

7. git mergetool

如果配置了 git mergetool 那么,在 Git 告知你合并冲突后,通过 git mergetool 命令启动第三方合并工具,来进行图形化界面的操作。例如,Beyond Compare 或者是 VS Code 。

如果使用 VS Code 作为第三方合并工具,那么需要在 .gitconfig 中追加如下配置:

[merge]
  tool = vscode
[mergetool "vscode"]
  keepbackup = false
  cmd = code --wait $MERGED
  trustexitcode = true

个人建议

目前我个人比较倾向于使用 VS Code 作为辅助的合并工具。因为,一方面它能够让你直面合并文件的本质,另一方面,它提供了必要的快捷操作按钮。基本上同时兼具了「本质」和「快捷」两方面。

vs code 作为 mergetool 的效果图:

git-mergetool-01.png

另一种产生冲突的原因

上面是以「两个分支对同一个文件的同一处地方做了不同的内容变动」为例来描述冲突的诞生,除了这种情况之外,还有一种情况能产生冲突:「两个分支对于同一个文件,一个是保留并改动,另一个是删除」,这种情况合并分支时,也会产生冲突。

提示

当然,你如果硬扣细节、讲本质,这两种造成冲突的情况本质都是一样的。

例如:

  • 在 B 节点时,文档的内容是 hello 和 world 。

  • hello 分支基于 B 节点,在文档第 3 行添加了 xxx ,形成了 C 节点。

  • world 分支基于 B 节点,把文档整个都删除了,形成了 D 节点。

最终合并时,会出现冲突提示,暂停合并过程,等待人工干预。如下图。

git-merge-8

对于这种情况,解决方案就三种( 比之前的那种情况少一种 )

  • 保留文件,采用「改」版。
  • 删除文件,采用「删」版。
  • 都不采用( 这种情况下,这个文件的内容就是两个分支为分叉时的那个样子 )

和前一种冲突的图形化形式相比,这种情况下,你点击查看具体冲突文件时,GitKraken 是在最上方提示如何处理。

8. 快速合并:git merge --ff 项目经理操作

之前的合并操作都会产生一个合并节点( 在 GitKraken 种是一个小的、实心的点 )

有一种情况的分支的合并可以不用生成合并节点:「一个分支的提交记录整个都是另一个分支的提交记录的一部分」这种情况下的分支合并可以不用产生合并节点。例如:

git-merge-9

如上图,world 分支上的所有提交记录在 hello 分支上都有。这种情况下,我们「站在」world 分支上想把 hello 分支的内容「合进来」。

我们模拟一下 Git 在合并时的逻辑:

  1. 先查找 hello 分支和 world 分支未分叉时的节点。这里是 B 节点。注意,此时此刻,world 分支指针正指向 B 节点。

  2. Git 会统计 hello 节点自分叉节点开始,一共做了哪些改动。

  3. 在 world 分支上将 hello 分支做过的变动也照样做一遍。

此时,你会发现,world 分支如果把这些变动照做一遍,那么它就会变得和 hello 分支一模一样!这种情况下,只需要让 world 指针指向 hello 指针所指向的那个节点就行了!

git-merge-10

这种方式的合并就叫「快速合并」。和普通合并相比,快速合并不会生成合并节点,因为没有必要。

快速合并使用的命令时带 -ff 参数的 git merge 。从命名上看,这种合并也叫快进( Fast Forward )式合并。

当然,快速合并有个局限性:它只能在我们所说的这种情况下才能使用,而不像普通合并那么有普适性。

Last Updated:
Contributors: hemiao