30 天精通 Git 版本控管

 主页   资讯   文章   代码   电子书 

第 08 天:关于分支的基本观念与使用方式

在 Git 裡面 分支 (Branch) 是个非常重要的机制,使用上也必须特别小心,因为专案总不能无限制的「分支」下去,最终总是要合併的,但合併是日后的议题,这篇文章将会先带大家学会 Git 分支基本观念与使用方式。

关于分支的基本观念

在版本控管中使用「分支」机制,最主要的目的就是用来解决开发过程中版本衝突的问题。笔者认为,有许多曾经用过任何版本控管机制的人,都会认为「分支」是个「产生版本衝突」的元凶,因为当你开始分支之后,一定就会想到合併的议题,而当分支之后,若是有人跟你一样修改到相同档案的相同一行时,就会引发「版本衝突」,而只要发生衝突,就必须费心解决。

当衝突发生时,有时可以很轻易的决定要用自己的版本或是对方的版本,但有时却没那麽容易,複杂的时候还要依据衝突的片段,找到当初改过这几行的人出来,协调出彼此的变更对系统的影响,最后决定要怎样合併,诸如此类的问题非常繁琐,也因此很多人会尽力避免「分支」的情况发生,以免发生「衝突」。

不过,若是开发团队越来越大,系统功能越来越多,就算你不对版本做分支,大家的衝突情况一样也会层出不穷,有时候还不是衝突的问题,而是 A 写好一个功能,但被 B 的后续版本给盖掉了,然后没有任何衝突发生,这也不是大家所乐见的。然而,这也是一种「无形的衝突」状况。

以前在集中管理的 Subversion 版控机制中,也有分支的概念,也可以运作的很好。当然,如果你的软体架构不够好,如果你对分支的概念、工具的使用也不是很清楚,我相信使用「分支」时也不会多顺利,这是个必然的结果,这世界绝不会有「免学、无痛、自然学会分支」的这种版本控管工具出现,事在人为,人的观念不对,用什麽工具都不会顺的。

由于 Git 属于「分散式版本控管机制」,在分散式版本管理的使用情境中,最不想做的事情就是「管理」,所以 Git 很少有所谓的管理机制或权限控管机制,它唯一想做的仅仅是让大家可以顺利的「分支」与「合併」而已。

我们以【第 03 天:建立储存库】这篇文章提到的「远端的储存库 (remote repository)」为例,你可以这样想像:从我们使用 git clone 指令开始,其实就是「分支」的开始,你从远端储存库複製一份完整的储存库下来,然后开始在自己的本地端建立版本,等软体修订到一定程度后再「合併」回去,只是这时合併的指令叫做 git push 而已。

这种分支与合併的情形,在 Git 版本控管的过程中无所不在,远端的储存库可以有分支,本地的储存库可以有分支,你可以从远端任何一个分支合併(pull)到本地分支,也可以将本地的分支推向(push)远端的分支,你当然也可以从本地任何一个分支合併(merge)到本地的另一个分支。可以想见,如果「分支」没有一套良好的控管逻辑,最后可以组合出各种极其複杂的版本控管使用情境,这也不是大家所乐见的。因此,好好学会「分支」与「合併」真的非常重要。例如 git-flow 就是一套广受欢迎的分支管理模式,这不是一套工具,而是一种管理分支的逻辑,这部分在我未来的文章中将会加以说明。

Linux kernel 发展的过程,在全世界有成千上万的开发人员共同参与,为了管理这麽大量的开发团队,Git 俨然而生,这是套分散式的版控机制,每个人都有完整的版本,版本散出去之后,大家必须管好自己的版本,然后遵照团队的要求合併回来。然而,在合併回来之前,这套机制确保每个人都能够顺利的开发,不受任何其他开发人员的版本而影响,而 Git 确实做到了这点,同时又降低了版本控管的複杂度。

当然,我也必须讲,如果参与软体开发的团队只有两三人,而且这些人还都聚在一起,那确实不一定要使用 Git 版本控管,使用 Subversion 也是个很好的选择,简单又直觉,开发的过程中若遇到问题,前后左右协调一下就能解决,这比让整个团队都来了解 Git 来的方便很多。

如果你的团队有点规模,或大家并没有坐在一起工作,又要做版本控管的话,或许 Git 是个不错的选择,但工作团队之间拥有一致的版控观念或习惯,也是非常重要的一件事。

准备工作目录

我们透过以下指令快速建立一个拥有两个版本的 Git 储存库与工作目录:

mkdir git-branch-demo
cd git-branch-demo
git init

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

echo 1 > a.txt
git add .
git commit -m "a.txt: set 1 as content"

接著使用 git log 取得版本资讯如下:

C:\demo\git-branch-demo>git log
commit b917758c0f2f347a895ee5bbb5e5c8228f66335a
Author: Will <doggy.huang@gmail.com>
Date:   Fri Oct 4 20:58:16 2013 +0800

    a.txt: set 1 as content

commit aa3e4fe2ee065b046e00a74b1f12a0b0f8154003
Author: Will <doggy.huang@gmail.com>
Date:   Fri Oct 4 20:04:39 2013 +0800

    Initial commit

接著我们透过 git branch 指令得知我们已经拥有一个名为 master 的分支,这是在 Git 储存库中的预设分支。如果你尝试透过 git branch -d master 删除这个分支,将会得到 error: Cannot delete the branch 'master' which you are currently on. 的错误讯息,这意思是「当你目前工作目录分支设定为 master 时,不能删除目前这个分支」,也就是说,你必须先切换到「其他分支」才能删除这个分支。

image

当然,我们现在只有一个分支,自然无法删除自己。

建立分支

建立分支最常见有两种方法,分别是:

  1. 建立分支,但目前工作目录维持在自己的分支: git branch [BranchName]

    当我执行 git branch newbranch1 指令,这会建立一个新的 newbranch1 分支,我们接著用 git branch 查看目前有多少分支,你会看到两个,但目前工作目录还会停留在 master 分支上,如下图示:

    image

    如果这时你在目前的工作目录建立版本,这时会建立在 master 分支裡面,我们这时建立一个新档案,并且透过 git commit 建立版本,指令如下:

     echo master > b.txt
     git add .
     git commit -m "Create b.txt with content 'master' in the master branch"

    请先记得:我们先在预设的 master 分支建立两个版本,然后建立一个分支,然后在 master 分支又建立了一个版本。

  2. 建立分支,并将目前工作目录切换到新的分支: git checkout -b [BranchName]

    接下来,我们用第二种方法建立分支,当我执行 git checkout -b newbranch2 指令,不但会建立一个新分支,还会将目前工作目录切换到另一个分支,最后用 git branch 查看目前有多少分支,你会看到已经有三个,而且目前工作目录已经切换到刚刚建立的 newbranch2 分支上,如下图示:

    image

    如果这时你在目前的工作目录建立版本,这时会建立在 newbranch2 分支裡面,我们这时建立一个新档案,并且透过 git commit 建立版本,指令如下:

     echo newbranch2 > b.txt
     git add .
     git commit -m "Modify b.txt with content 'newbranch2' in the newbranch2 branch"

    请记得:我们先在预设的 master 分支建立两个版本,然后建立一个分支,然后在 master 分支又建立了一个版本,接著又把当下这份 master 分支的状态建立一个新的 newbranch2 分支,并将工作目录到切换到 newbranch2 分支,然后再建立一个版本。我们这时如果执行 git log 会显示出 4 个版本纪录,因为分支会自动继承来源分支的完整历史。

: 详细的指令与参数说明,可以输入 git help branch 查询完整的文件。

git branch [branch_name]

切换分支

如果你想将工作目录切换到其他分支,你可以输入以下指令 (不含 -b 参数):

git checkout [branch_name]

假设我们想把工作目录切换到 newbranch1 分支,这时可以输入 git checkout newbranch1 切换过去,然后你可以立刻使用 git branch 检查目前工作目录是否切换过去,然后再用 git log 检查当下 newbranch1 分支的历史纪录。因为这是我们第一次建立的分支,照理说这个分支状态应该只会有两笔历史纪录而已,如下图示:

image

: 详细的指令与参数说明,可以输入 git help checkout 查询完整的文件。

git checkout [branch_name]

删除分支

如果你想删除现有的分支,就如同我们在准备工作目录有提到过的指令,如下:

git branch -d [branch_name]

先前也有提到,你不能删除目前工作目录所指定的分支,必须先切换到其他分支后,再删除你目前这个分支。举个例子来说,如果你想删除当下这个 newbranch1 分支,那麽你必须先切换到其他分支,例如 master 分支,然后再下达 git branch -d newbranch1 指令,即可删除分支,如下图示:

image

查看工作目录在哪个分支

你可以透过 git branch 命令,查看目前所在分支,如下图示:

image

查看 Git 储存库的完整分支图

最后,我用 SourceTree 工具来显示目前 Git 储存库的分支图。目前我们只有两个分支,一个是 master 分支,另一个是 newbranch2 分支,因为 newbranch1 分支已经在练习的过程中被删除了。为了要让我们的分支有「树状」的感觉,接下来我要示范如何重新建立一个与先前 newbranch1 一样状态的分支,并且在这个分支下加入一个新版本。

不知道各位还记不记得,我们是在 master 分支建立两个版本后才建立 newbranch1 分支的,现在我们就先找到到这个版本的 commit 物件 id,透过 git log 即可取得,如下图示:

image

所以我的 commit 物件 id 为: b917758c0f2f347a895ee5bbb5e5c8228f66335a

接著我先把工作目录切换到这个版本,透过 git checkout [commit_id] 即可完成这个任务:

git checkout b917758c0f2f347a895ee5bbb5e5c8228f66335a

这时你用 git log 应该只会看到两个版本纪录而已,因为我们已经把工作目录的状态切换成这个版本了。从下图可以看到我们执行 git checkout b917758c0f2f347a895ee5bbb5e5c8228f66335a 时会出现一对讯息,这些讯息很重要,必须了解一下,如下图示:

image

首先,由于你将工作目录的版本切换到「旧的」版本,所以你会被提示这个工作目录已经进入了所谓的 detached HEAD 状态,这是一种「目前工作目录不在最新版」的提示,你可以随时切换到 Git 储存库的任意版本,但是由于这个版本已经有「下一版」,所以如果你在目前的「旧版」执行 git commit 的话,就会导致这个新版本无法被追踪变更,所以建议不要这麽做。

若你要在 detached HEAD 状态建立一个可被追踪的版本,那麽正确的方法则是透过「建立分支」的方式来追踪,现在我们就要在这个版本建立一个新的 newbranch1 分支,并将工作目录切换过去,指令如下:

git checkout -b newbranch1

image

然后我们再建立一个新档案 b.txt,内容为 newbranch1,并建立一个新版本,指令如下:

echo newbranch1 > b.txt
git add .
git commit -m "Add b.txt in newbranch1"

好了,我们现在有了 master 以外的两个分支,而且两个分支都有自己的版本,你先在脑中思考一下这棵树长怎样!

接著我们开启 SourceTree 工具,并将这个工作目录加入到 SourceTree 的管理工具中:

image

image

加入后,我们切换到这个 Git 储存库的分支,总共有三个,我们分别切换过去看看:

image

image

image

最早的版本在最下面,最新版在最上面,当我们切换到不同的分支,你可看到这三个个分支图示都一样,只有预设停留的「光棒」不一样。首先,从图片来看,你看到的是「整份 Git 储存库」中的所有版本、所有分支,以及该分支是从哪个版本开始建立分支的。而「光棒」则是该分支的「最新版」位于整个 Git 版本库的哪个版本。

今日小结

其实在 Git 裡面使用分支是很容易的事,难的地方在于让大家都知道「分支」到底在做什麽,还有大家对「分支」的想像是否是一致的,只要大家对分支的想像是一致的,在团队版控上就不会有太大的落差。

对我来说,分支我会把它想像成一种「快照」功能,把某个 commit 版本与其历史版本建立出一个快照,然后複製一份出来,并给予一个分支名称,你可以在这些分支上建立版本,等待日后进行合併。

而整份 Git 储存库,则会保留所有的分支与版本,最终呈现出一个树状架构的分支图,我们最后透过 SourceTree 工具可以清楚的看到 Git 储存库中的分支状况与版本变化。这张图,我很早就看过,但第一次完全看不懂,只觉得是「一张图」,没有感觉,但自从越来越了解 Git 之后,这张分支图可以让我一目了然的理解整个 Git 储存库的变化情形,也更容易掌握 Git 的版本变化。

希望可以透过我的文字与指令搭配图片示范,让大家在自己脑中勾勒出一个分支架构,对 Git 分支结构有更深层的理解。

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

  • git branch
  • git branch [branch_name]
  • git checkout -b [branch_name]
  • git checkout [branch_name]
  • git branch -d [branch_name]
  • git log

参考链接