Pro Git03-Git分支
Pro Git03-Git分支
Git分支简介
几乎所有版本控制系统都以某种形式支持分支。在git中以一种特殊的方式实现了分支功能,git处理分支的方式可谓是难以置信地轻,创建新分支这一操作几乎瞬间完成,并且在不同分支直接切换也很便捷。Git鼓励在工作流程中频繁使用分支与合并。在很多版本控制系统中,这是一个略微低效的过程—因为可能需要完全创建一个源代码目录的副本。
git是如何保存数据的
从之前的文章你可能也大概了解到了,git存储的是一系列不同时刻的文件快照。回顾之前的文章,你会知道,我们使用git的普遍流程是修改了文件,然后运行git add
命令添加修改到暂存区,然后运行git commit
提交文件的变化。
当使用git commit
进行提交操作时,GIt会先计算每一个子目录的校验和,然后再Git仓库中将这些校验和保存为树对象。随后git便会创建一个提交对象,它除了包含上面提到的这些信息之外,还包含指向这个树对象的指针。如此依赖,Git就可以在需要的时候重现这次保存的快照了。
截止到现在,Git仓库中有5个对象:三个blob对象(保存着文件快照)、一个树对象(记录着目录结构和blob对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。
你可以看到98ca9这个就代表着提交对象,里面存储了指向92ec2的指针,还存了当前提交的作者信息。92ec2这个树对象存储了指向了5b1d3和911e7和cba0a这三个blob块的指针。
如果你做些修改以后再次提交,那么这次产生的对象会包含一个指向上一个提交对象的指针。
你可以看到每一个提交对象中都存储了一个名为parent的属性,这个属性就代表上一个提交目录的对象。
git的分支,其实本质上仅仅是指向提交对象的可变指针。git的默认分支是master。master分支并不是一个特殊的分支,它与其他分支并无区别。之所以每个仓库都有master分支,是因为git init
命令默认创建它,并且大多数人都懒得区改动它。最后还有一个HEAD指针,这个指针指向git的分支。它意味着你当前本地的环境是哪个分支。
你可能看到这里,头有点昏,git咋这么多指针指来指去的。没事,我们以上图为例最后总结一下。
上图中所有对象以某种角度来说都是指针,他们都没有实际保存对象的文件。其中深绿色方块的对象是树对象,它是直接指向文件快照的。灰色方块的对象是提交对象,它是直接树对象和上一个提交对象的。红色方块是分支,它是指向提交对象的。创建分支其实也就是创建一个指向提交对象的指针。黄色的方块是HEAD对象,它指向分支的。
长期分支
随着你的提交不停右移的指针。不同的分支总是在提交历史中落后一大截。
或者以另一种视角来看
你可以用这种方法维护不同层次的稳定性。当某些分支测试以后且具有成熟的稳定性之后可以并入master分支。你不用担心出现这样的分支的合并难度。git很容易处理这样的分支。
远程分支
远程引用是对远程仓库的引用,包括分支、标签等等。
1 |
|
远程跟踪分支是远程分支的引用。他们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。它们以 (remote)/(branch)
形式命名。就类似上面的remotes/origin/master
。后面的章节会对线上分支的合并进行消息地介绍。
通过文件变化来讲解分支
我们来设想一下工作场景,假设你拉下了项目代码,需要新建一个属于自己的分支并且在自己的分支上开发,这是比较常见的。
新建分支
假设现在的分支是这样的一个简单提交历史。
现在你需要在master分支上新建一个分支并同时切换到对应分支上,你可以运行一个git checkout -b
命令。该命令是git branch iss53
和git checkout iss53
命令的简写。前者创建分支,后者切换分支。
1 |
|
不能直接使用git checkout
命令,因为没有分支。
现在我们就创建了一个新的分支,注意分支实际操作相当于我们只是新增了一个新的分支指针。
你可能会发生在上面终端的输出中有这么一个命令git branch
。该命令会显示当前总共有哪几个分支。-a
参数会把远程分支打印出来。**git branch
命令后跟分支名是创建分支,直接使用这个命令是查看分支**
1 |
|
随着你在该分支上的修改提交,目前你的分支情况可能会变成以下图形。如果你一直提交,那么iss53这个分支指针就会一直向前进。
切换分支
假如现在主项目突然出现了一个bug,需要你马上去处理。你需要更改master分支的代码。注意这时候最好的选择是切换回master分支,然后新建一个对应处理的分支,而不是直接更改master分支的代码。git checkout 分支名
该命令会切换为对应的分支。
1 |
|
然后你在hotfix分支上进行了一些修改,截止目前为止,你的分支应该是这样的。
分支合并
没有冲突的分支合并
接下来我们假定已经通过了测试,你现在需要把master分支和hotfix合并来形成新的稳定版本。你需要先回到master分支上,你可以使用git merge
命令来合并。
1 |
|
看上面终端打印的信息:你可能注意到了Fast-forward这个词。由于master分支是你当前提交hotfix的直接上游,所以git只是简单地将指针移动。也就是说,当你合并两个分支时,如果顺着一条支线走下去能到另一个分支,那么git在合并两者的时候,只会简单的将指针向前推进,这种情况下的合并操作没有需要解决的冲突,这就叫做快进(Fast-forward)
现在最新的样子是这样的。
现在hotfix和master都指向同一个地方了,你应该先删除hotfix分支,因为你已经不需要它了,使用git branch -d
命令来删除分支:
1 |
|
没有冲突的合并提交
书接上会,你修复了bug之后又继续在iss53上继续开发,继续提交了一些分支。看起来你与master分支好像越走越远了。但是不用担心,你不需要考虑那么多,直接合并就行。假如你的分支和master分支没有修改同一个文件的地方,那么一切都会自动进行。
1 |
|
因为合并的分支产生了分叉,所以git需要做一些额外的工作。出现这种情况时,git会使用两个分支末端的快照(C4和C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并,git会将三方合并的结果做一个新的快照并自动创建一个新的提交指针指向它。git会自行决定选取哪一个提交作为最优的共同祖先。
有冲突的合并提交
但是假如两个同时修改了同一个文件呢?Git就没办法直接合并,需要你去解决冲突了。
1 |
|
此时git做了合并,但是没有创建新的合并提交。git会等你去解决合并产生的冲突。你可以在任何时刻使用git status
命令来查看哪些处于合并冲突的文件。
1 |
|
打开冲突的文件,你会发现长这样。
1 |
|
上面那部分是master分支的内容,下面的是被合并的分支内容。接下来你就把这个文件的冲突修改一下。(把那些特殊的符号改了就行),然后对有冲突的文件使用git add
命令来标记为冲突已解决。一旦暂存了这些有冲突的文件,git就会标记为冲突已解决。
1 |
|
接下来就只需git commit
提交就行,默认会打开记事本或者其他可以编辑文本的编辑器。默认的提交记录长这样。
1 |
|
接下来就完成了合并,至于分支,你想删除就删除,不想删就不删。
远程分支的合并
开头说了,远程分支和普通分支其实区别就是,远程分支是一个本地无法编辑的分支。我来假设一个场景,假如正常工作时,其他同事已经使用git push
命令提交了代码。现在你需要同步线上的代码。我们之前说过git pull
命令其实是git fetch
和git merge
命令的组合。
当我们输入git fetch
命令以后分支是图下这样的。
你看远程分支origin/master,其实可以看作是另一个本地分支而已,只是这个分支只会随着网络变化而变化,你无法编辑。那么接下来就和之前的一样了。合并master分支和origin/master分支就行了。
那么假如是多个远程仓库的情况呢?其实也是一样的,你只用当成两个远程分支就好了。
变基
那么什么是变基呢?这个平时用得很少,而且也不建议用。但是我们要知道概念是什么。
回到我们之前合并分支的例子,正常的合并如下图所示。
其实还有一种合并方法,你可以提取C4中引入的修改,然后在C3的基础上应用一次。在git中,这种操作就叫做变基。你可以使用git rebase
命令将提交到某一个分支上的所有修改都移至另一分支上。
实际上变基的原理是找到这两个分支的最近共同祖先,在这里例子中是C2,然后修改另一个提交,使其指向另一个目标C3,最后以此将更改之前的提交路径。
最后快进合并就行。一般我们这样做是为了确保向远程分支推送时就能保持提交历史的简洁。你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master
上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。无论是通过变基还是三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。
还有一件很重要的事情:不要对在你的仓库外有副本的分支执行变基。
变基实质是丢弃一些现有的提交,然后相应地新建了一些内容一样但实际上不同地提交。如果你已经推送到了线上仓库,其他人也可以根据这个仓库拉取了代码,此时,你使用变基会改变线上仓库的提交历史,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。
变基与合并
有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用_谎言_掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。
另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。
现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!