30 天精通 Git 版本控管

 主页   资讯   文章   代码   电子书 

第 17 天:关于合併的基本观念与使用方式

我曾在【第 08 天:关于分支的基本观念与使用方式】提过关于「分支」的基本观念与用法,现在则要来讲「合併」如何进行。由于 Git 是一种分散式版本控管系统(DVCS),过程中会不断的进行分支与合併,无论是有意的合併(git merge)或无意的合併(git pull),总之使用 Git 版控「分支」与「合併」的动作确实经常发生。本篇文章将说明「合併」动作的基本观念与使用方式。

关于「合併」的基本观念

当你在 Git 工作目录下建立分支时,可以让你的系统依据不同的需求分别进行开发,又不互相影响。例如你原本稳定的系统可以放在 master 分支中进行开发,而当要修正错误时则额外建立一个 bugfix 分支来改正软体错误,等 Bugs 修正后,在透过「合併」的方式将 bugfix 分支上的变更重新套用到 master 上面,这就是一种主要的使用情境。

一般来说,大家都是以一个主要或预设分支进行开发(master),然后再依据需求建立分支(bugfix),最后则是将两个分支合併成一个。事实上,执行「合併」动作时,是将另一个分支合併回目前分支,然后再手动将另一个分支给移除,这样才符合「两个分支合併成一个」的概念。

实务上,也经常有机会将三个、四个或更多的分支合併到其中一个分支。例如你除了主要分支(master)外,还额外建立了除错用的分支(bugfix)与新增功能(feature)的分支,当开发到一定程度后,你可以决定要不要将这个两个分支一起合併回主要分支(master)。

在 Git 使用合併时,有一个重要的观念是【合併的动作必须发生在同一个储存库中】。请回想一下,在任何一个 Git 储存库中,都必须存在一个 Initial Commit 物件(初始版本),而所有其他版本都会跟这个版本有关系,这个关系我们称为「在分支线上的可追踪物件」(the tracked object on the branch heads),所以你不能将一个储存库的特定分支合併到另一个毫不相干的储存库的某个分支裡。

合併的时候,如果两个分支当中有修改到相同的档案,但只要修改的行数不一样,Git 就会自动帮你套用/合併这两个变更。但如果就这麽刚好,你在两个分支裡面改到「同一个档案」的「同一行」,那麽在合併的时候就会引发衝突事件。当合併衝突发生时,Git 并不会帮你决定任何事情,而是将「解决衝突」的工作交给「你」来负责,且这些发生衝突的档案也都会被标示为 unmerged 状态,合併衝突后你可以用 git status 指令看到这些状态。

体验一场「成功的合併」

简单来说,「成功的合併」就是没有发生「衝突」的合併。我们用一个简单的例子说明合併的过程与指令的用法:

mkdir git-merge-demo
cd git-merge-demo
git init

echo. > a.txt
git add .
git commit -m "Initial commit (a.txt created)"

echo 1 > a.txt
git add .
git commit -m "Update a.txt!"

执行结果如下图示,我们建立了两个版本,而且最新版的 a.txt 内容为 1

image

接著我们透过 git checkout -b feature 建立一个 feature 分支,并同时把工作目录给切换到 feature 分支进行开发,然后建立一个内容为 2b.txt 档案:

image

注意:在切换「分支」之前,请随时查看并保持「工作目录」的状态是「乾淨的」,不要有任何档案异动中的状态。

现在我们回顾一下两个分支的内容:

  • 主要分支:master
    • 拥有 a.txt 其内容为 1
  • 功能分支:feature (目前所在分支)
    • 拥有 a.txt 其内容为 1 (此档案是从 master 分支继承过来的)
    • 拥有 b.txt 其内容为 2

我们现在要做 3 件事:

  1. 将工作目录切换回 master 主要分支
  2. a.txt 的内容新增一行 NEW LINE 在第二行 (第一行不动)
  3. a.txt 的异动加入索引,并 commit 版本

如下图示:

image

现在我们再回顾一次当下两个分支的内容:

  • 主要分支:master (目前所在分支)
    • 拥有 a.txt 其内容有两行,第一行为 1,第二行是 NEW LINE
    • 这裡没有 feature 分支的变更,也就是没有 b.txt 档案。
  • 功能分支:feature
    • 拥有 a.txt 其内容为 1 (这是从 master 分支继承过来的,在 feature 没动过)
    • 拥有 b.txt 其内容为 2 (这是从 feature 才新建立的档案)

我们用 SourceTree 可以查看较为漂亮的 commit graph (版本图):

image

由于两个分支都有在建立 feature 分支后都做过异动,不过你可能会发现到,这两个分支当中,从「分支点」开始,所做的修改并没有互相衝突,只要是这种状况,合併并不会发生问题。接下来我们就来执行「合併」动作 (git merge)。

从下图示中,有许多我想强调的细节:

  1. 合併之前,先看清楚自己在哪个分支
  2. 合併之前,请确保工作目录是乾淨的
  3. 合併时请用 git merge [另一个分支] 来将另一个分支的变更合併回来
  4. 你可以从下图看到我打错字了,但 Git 还会自动帮我执行正确的指令,详情请见【第 14 天: Git for Windows 选项设定】
  5. 合併成功后,你可以利用 git log 查看版本纪录,你可以发现「合併」的过程会自动建立一个新版本

image

我们看看「合併后」的分支状况,用 SourceTree 的 commit graph 来看:

image

最后我们来看看合併后的档案内容,确实如我们预期的把两个分支中的变更都给合併了:

image

这就是一场成功的「合併」! :-)

删除不必要的分支

假设我们 feature 分支还会持续开发新功能,所以可以暂时将它留著,让后续还能继续切换到 feature 分支继续开发。

如果你确定用不到的话,可以用 git branch -d feature 删除该分支。

image

在 Git 裡,只要没有执行过「合併」的分支,都不能用上述指令进行删除,必须改用 git branch -D feature 才能删除该分支。

救回误删的分支

不过,如果你不小心「误删」该分支(feature)的话,则必须用以下步骤救回分支,稍微麻烦一点:

  1. 先利用 git reflog 找出该分支(feature)最后一个版本的 object id (也就是 SHA1 格式的物件绝对名称)
  2. 执行 git branch feature <SHA1> 即可

如下图示,最终还是能把误删的 feature 分支给救回:

image

体验一场「衝突的合併」

现在,我们从 master 分支的最新版(HEAD)建立一个 hotfixes 分支,执行指令:git checkout -b hotfixes

image

我们修改 a.txt 的第一行,把原本的 1 修改为 bugfixed: 1,并建立版本。然后切换回 master 分支:

image

接著,我们在 master 分支修改 a.txt 的第一行,把原本的 1 修改为 bugfixed by Will: 1,并建立版本。

image

现在,我们两个分支都同时修改了 a.txt 的档案内容,而且都在同一行,这样的修改方式,势必引发「衝突」。我们就马上来体验一下:

image

没错,真的衝突了,而且 Git 告诉我要自己修正(fix)这个衝突(conflicts)。我们用 git status 可以看出目前发生衝突的档案有哪些,而且你也可以看到这个档案位于 Unmerged paths 这个区段。

查看衝突的内容

当你发生衝突的时候,切莫慌张,先执行 git diff 自动比对出到底哪些档案的哪几行发生衝突了。

image

从上图你必须注意的是 diff 针对衝突内容的表示法,看的懂,你才容易知道怎样改:

  • <<<<<<< HEAD======= 的内容,代表 HEADa.txt 的内容。HEAD 代表当前 master 分支的最新版。
  • =======>>>>>>> hotfixes 的内容,代表 hotfixes 分支裡 a.txt 的内容

解决衝突状态的方法

我先来说说「解决衝突状态」的方法,其实很简单,直接输入 git add . 把目前工作目录的状态加入到「索引」之中,就可以取消这些 Unmerged 档案的衝突状态。

如果你很懒,遇到衝突什麽都不改,直接执行 git add . 再加上 git commit 的话,确实会「解决衝突」,但所 commit 的版本将会包含这些衝突的表示文字,如果你身处一个开发团队,肯定会被骂翻到臭头,请你千万不要这麽做!

image

做错了?没关系,只要执行 git reset --hard ORIG_HEAD 就可以回复到上一版,然后再重新合併一次引发相同的衝突。

image

找出衝突的档案

当合併发生衝突时的档案数量很少时,或许直接打 git diff 可以看得出差异,但如果是两个比较大的分支发生衝突的话,很有可能会有一大堆档案有衝突的状况。这时你可能会想一个一个档案的来查看衝突的状况,这时你可以用以下两种 Git 指令找出衝突的档案:

  • 执行 git status
  • 执行 git ls-files -u

image

找到之后再用 git diff [filepath] 就可以仅比对其中一个档案了:

image

真正解决衝突

真的要解决衝突,你必须手动把这些衝突的档案改好。不过,如果这时使用 SourceTree 来修复衝突的档案,那生命将会美好许多。

我们开启 SourceTree 并进入 Working Copy 节点,在档案清单中按下滑鼠右键,这时有个 Resolve Conflicts (解决衝突) 的选单,这裡你就能选择你要用我的(Mine)或是用其他人的(Theirs)来解决这个档案的衝突状态。所谓「我的」就是你当前工作目录的那个分支(master),而「他的」就是被我们指定合併进来的那个分支(hotfixes)。

image

如果我选 Resolve Using 'Mine' 的话,他还会出现一个确认合併的视窗,你按下 OK 之后,SourceTree 就会自动将档案中发生衝突的那些段落,修改成「我的」版本:

image

请注意:当你利用 SourceTree 帮你解决衝突后,该档案也会直接加入到索引之中(git add .),但不会帮你执行 git commit 动作,所以你还要自己多做这一步:

image

输入一些版本讯息后,就可以执行 Commit 动作( Commit 按钮在视窗右下角 ):

image

我们来看解决衝突并合併后的 commit graph 如下:

image

今日小结

Git 合併算是比较困难的部分,但重点还是在观念,观念正确了,就算发生衝突也不用害怕。搭配好用的 GUI 工具,也能够更加顺利与快速的解决衝突问题。说真的,若发生衝突时没有 GUI 工具,光是打指令真的会疯掉。

我重新整理一下本日学到的 Git 指令与参数:

  • git merge [other_branchname]
  • git checkout -b [new_branchname]
  • git reflog
  • git branch -d [branchname]
  • git branch -D [branchname]
  • git branch feature
  • git reset --hard ORIG_HEAD
  • git status
  • git ls-files -u
  • git diff [filepath]

参考链接