# 分支

# 1. 关于分支

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

  • 并行式开发:有两个以上的开发者在对同一个项目进行并行式开发。

    版本库 1            版本库2
    A <--- B <--- C     A <--- B <--- D
    
             C
            /
    A <--- B  
            \
             D
    
  • 修复旧版本中的 bug:一方面要修复旧版本中的 bug,而与此同时又要创建和发布新的版本。

    前期
    A <--- B <--- C <--- D
    
    修复 bug 之后
             C <--- D
            /
    A <--- B  
            \
             E
    

有人将分支比喻成泳道

分支可以看作是开发过程当中的并行线,我们可以把提交图想象成游泳池中的泳道:

         E              release
        /
A <--- B <--- C <--- D  master
               \
                F       test

在一个 Git 版本库中,总是唯一存在着一个『活动分支(也叫『当前分支』)。我们可以用 branch 命令(不带选项)来列出所有的分支。Git 会用星号(*)凸显出当前活动分支。

在 GitKraken 中当前分支名字前面有一个 符号。

# 2. 分支基本操作

  • 查看分支

    通过 git branch 可以看到 Git 仓库的所有分支。

    当执行 git init 指令的时候,Git 就会自动产生一个名为 master 的分支:主分支。主分支是默认的、初始的。

  • 创建分支

    创建新的分支,也是使用 git branch 命令。

    git branch 新分支名 [从当前分支的 Commit]
    

    如果省略 Commit,那么就是从当前分支的 HEAD 分化出新分支。

  • 切换分支

    建立分支后,可以通过 git checkout 命令来切换当前分支:

    git checkout <分支名>
    

    有个简单的办法可以创建新分支并切换:

    git checkout -b <新分支名> [Commit节点]
    

    分支指针主要用于指向活动分支(即它总是在当前分支上的),每次提交时,它会移动到最新提交上。

  • 删除分支

    删除分支使用:

    git branch -d <被删除分支名>
    

    如果在删除一个分支时,自己还未转移到其他分支上,git 会拒绝删除操作。如果坚持要删除的话,则使用 -D 选项替换 -d 选项。

    一般情况下,分支应该合并到另一个分支。如果要删除还未合并的分支,Git 会显示错误信息,并拒绝删除。当然你也通过 -D 选项来强制删除。

# 3. 分支合并

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

git merge <另一个分支名>

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

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

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

假设是 A 和 B 两个分支合并,这里就有 2 种情况:

  • 站在 A 分支上(你的当前分支是 A ),把 B 分支合并 “进来” 。
  • 站在 B 分支上(你的当前分支是 B ),把 A 分支合并 “进来”。

这 2 种情况都合情合理合法,关键是你要考虑清楚你需要到的是哪一种情况。

前期:
A <--- B <--- E    master
        \
         C <--- D  feature

后期:
A <--- B <--- E <--- F  master
        \           /
         C <------ D  feature

结合上面的那个问题,从图上(合并的箭头方向)可以看到,你是站在 master 分支上,把 feature 分支合并了 “进来” 。

回顾一下

如果合并后想反悔,那么可以执行之前所学的 git reset --soft HEAD~1,回到提交前夜,然后放弃提交。

合并所产生的节点和普通节点有一个很重要的区别:

  • 普通节点只有唯一的父节点,表示为 HEAD^1
  • 合并节点有 2 个父节点,分别表示为 HEAD^1HEAD^2

# 4. 冲突

Git 的设计目标之一就是为了能够让开发者之间的分布式协作变得尽可能容易一些。因此从很大程度上来说,merge 命令能自动对分支进行合并,完全不需要用户交互。

当两个分支对于同一个文件做出了不同的操作时,可能会出现冲突,而且此时,Git 无法自动 “帮” 你合并。最常见的冲突情况有 2 种:

  1. 两个分支对于同一个文件的同一个位置做出了修改。

    那么你在合并这两个分支时,Git 无法自己决定 “合并后保留的是哪个分支上的内容(而废弃另一个),亦或者是两个分支上的都要保留” ?这种情况,就需要人来裁决。

  2. 一个分支对一个文件执行了删除操作,而另一个分支上这个文件还在,仅仅只是内容有变动。

    那么你在合并这两个分支时,Git 无法自己决定 “合并后这个文件到底是应该删除,还是留着” ?这种情况也需要人来裁决。

补充一点,并非对于同一个文件的修改,都会造成冲突。比如,如果两个分支是对同一个文件的不同位置做出了修改(注意和上述第一种情况的区别),Git 自己也会自动合并:合并后的内容会同时采纳两个分支上的操作。

当 Git 遇到了自身无法解决的冲突时,就会显示以下错误信息。

$ git merge a-branch

Auto-merging foo.txt
CONFLICT (content): Merge conflict in foo.txt
Automatic merge failed; fix conflicts and then commit the result.

此时,如果执行 git status 命令,会看到 git 提醒你,它无法完成自动合并,需要你手动进行编辑,并且要求你在编辑之后,执行 git commit

$ git status

On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)
 
both modified:   foo.txt
 
no changes added to commit (use "git add" and/or "git commit -a")

冲突标志通常会描述两组修改。首先这些被修改的行在当前分支(HEAD)中的内容。接下来又列出了他们在另外一个分支的内容,例如:

第一次修改
&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
第二次修改
=======
在a-branch上进行的修改
>>>>>>> a-branch

手动解决完冲突以后,运行 git add 命令把相关文件添加到暂存区。继续执行 git merge --continue 命令编辑新生成的提交的 log 信息。然后 merge 完成。

冲突和解决冲突示意图:

git-top-10-04

# 5. 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

# 6. 快速合并:git merge --ff

# 关于快速合并

所谓的快速合并指的是在执行 git merge 命令时多带一个 --ff 的参数,它是 Fast Forward 这两个单词的首字母。

有 “快速” 合并自然就有 “非快速” 合并:--no-ff

Git 在合并时的默认行为是:先看当前的情况是否符合快速合并的条件,如果符合就进行快速合并,否则就进行非快速合并。

当然,你可以通过强行指定 --no-ff 告诉 Git ,在合并时一定是按 “非快速合并” 进行合并。

# 快速合并

我们以将 feature 分支合并进 master 分支为例,如果 master 分支相比 feature 分支而言,没有额外的提交,这种情况下 Git 的合并行为就是快速合并。

先观察下面的这种情况:

git-merge-1

在上图中,feature 分支上的内容比 master 分支的内容只多不少,即 feature 分支完全 “涵盖” 了 master 分支的内容。

注意

通常 feature 分支的 C3 和 master 分支的 C2 之间的 “连线” 歪着画效果会更显著一些,只不过 GitKraken 将它们画成了一条直线。

这种情况下,执行 git merge --ff 过程效果如下:

git-merge-ff

git-top-10-01

对比一下,在同样的情况下,执行 git merge --no-ff 的效果:

git-merge-no-ff

git-top-10-02

“快速合并” 和 “非快速合并” 的最大区别在于:普通合并/非快速合并,会产生一个新的提交节点,稍微思索一下,你就会发现,而这个新的提交节点实际上并没有必要存在,因为它(C4)和 feature 分支的 C3 的内容是一样的!

# 使用场景

先说结论:

  • 非快速提交( --no-ff )具有普适性,无论什么情况都能用,而快速提交( --ff )只在某些情况下能用;

  • 在既可以使用 --ff ,又可以使用 --no-ff 的时候,使用 --ff 之后的提交记录更合理一些。即,不用生成一个没有必要的新 “结点” 。

git-merge-2

上图中,左图只能使用 --no-ff 方式合并,而右图则在 --no-ff 之外,还可以使用 --ff 方式合并。左右两图的差异在于:feature 分支是否是基于 master 分支的末端(即,master 最新节点)

如果是,那么就可以使用 --ff 方式合并(以减少生成一个逻辑上不必要的节点)。如下图:

git-merge-ff

否则,就只能以 --no-ff 方式合并:

git-merge-no-ff

# git merge 默认行为

在你使用 git merge 命令没有指定合并方式时,Git 总是先判断当前是否能进行快速合并,如果不行,它再执行非快速合并。

  • 快速合并只适用于部分合并情况,而非快速合并适用于所有的合并情况;

  • 快速合并会让历史记录中 “省” 一个合并节点。