当我们初始化Git仓库的时候,Git会默认创建一个名为master的主分支。在实际工作中,主分支要求是一个稳定、健壮、安全的主线,一般不允许在主分支上直接进行开发,而是拉取一个新的分支,开发、测试完成后,再将分支合并到主分支上。

使用分支意味着你可以从开发主线上分离开来,然后在不影响主线的同时继续工作。在很多版本控制系统中,这是个昂贵的过程,常常需要创建一个源代码目录的完整副本,对大型项目来说会花费很长时间。

Git 的分支模型可称为“必杀技特性”,而正是因为该特性将 Git 从版本控制系统家族里区分出来,鹤立鸡群。其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。但Git的分支是与众不同的,无论创建、切换和删除分支,Git能在瞬间完成!无论你的版本库是1个文件还是1万个文件。

Git 鼓励在工作流程中频繁使用分支与合并,哪怕一天之内进行许多次都没有关系。在实际工作中,往往修复一个bug都会使用一个分支来完成。

理解分支的概念并熟练运用后,你才会意识到为什么 Git 是一个如此强大而独特的工具,并从此真正改变你的开发方式。

分支实现原理

在第一篇文章中提到过,Git 保存的不是文件差异或者变化量,而只是一系列文件快照。

在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。

为直观起见,我们假设在工作目录中有三个文件,准备将它们暂存后提交。暂存操作会对每一个文件计算校验和(即SHA-1 哈希字串),然后把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 类型的对象存储这些快照),并将校验和加入暂存区域。

当使用 Git commit新建一个提交对象前,Git 会先计算每一个子目录(本例中就是项目根目录)的校验和,然后在 Git 仓库中将这些目录保存为树(tree)对象。之后 Git 创建的提交对象,除了包含相关提交信息以外,还包含着指向这个树对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。

现在,Git 仓库中有五个对象:三个表示文件快照内容的 blob 对象;一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象;以及一个包含指向 tree 对象(根目录)的索引和其他提交信息元数据的 commit 对象:

作些修改后再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针(即下图中的 parent 对象)。两次提交后,仓库历史会这个样子:

Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 作为分支的默认名字。在若干次提交后,其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。

创建分支

创建一个新的名为“testing”分支,可以使用“git branch<branchName>”命令:

git branch testing

该命令会在当前 commit 对象上新建一个指针:

那么,Git 是如何知道你当前在哪个分支上工作的呢?其实答案也很简单,它保存着一个名为HEAD 的特别指针。在 Git 中,它是一个指向正在工作中的本地分支的指针(可以将 HEAD 想象为当前分支的别名)。运行Git branch 命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去,所以在这个例子中,我们依然还在 master 分支里工作。

使用不带任何参数的“git branch”命令可以查看当前的分支情况:

* master
  testing

Git显示,共有两个分支,当前工作分支为master,分支列表中的星号“*”相当于HEAD指针,标注了当前工作分支。

切换分支

命令“git checkout <branchName>”可以将当前工作分支切换到名为branchName的分支。比如,运行命令:

git checkout testing

Git会提示:

Switched to branch 'testing'

这样 HEAD 就指向了 testing 分支:

现在我们如果修改了工作区的文件,所有commit操作都是提交到testing分支,而非master。

现在 testing 分支向前移动了一步,而 master 分支仍然指向原先 git checkout 时所在的 commit 对象。现在重新切换到master分支:

git checkout master


这条命令做了两件事。它把 HEAD 指针移回到 master 分支,并把工作目区的文件换成了 master 分支所指向的快照内容。也就是说,现在开始所做的改动,将始于本项目中一个较老的版本。它的主要作用是将 testing 分支里作出的修改暂时取消,这样你就可以向另一个方向进行开发。

在mast分支上再做些修改,然后提交。现在我们的项目提交历史产生了分叉,因为刚才我们创建了一个分支testing,转换到其中进行了一些工作,然后又回到原来的master主分支进行了另外一些工作。

这些改变分别孤立在不同的分支里。我们可以在不同分支里反复切换,并在时机成熟时把它们合并到一起。

由于 Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,所以创建和销毁一个分支就变得非常廉价。说白了,新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)那么简单,当然也就很快。

合并分支

模拟这样的一个场景,早上到了公司接到新任务,新建一个名为“iss53”的分支来进行开发工作。要新建并切换到该分支,运行git checkout 并加上 -b 参数:

git checkout -b iss53

这相当于执行下面这两条命令:

git branch iss53
git checkout iss53


然后不断地写代码,提交代码:

突然,接到通知,需要立即修复master分支上的一个严重bug。

第一步肯定需要切换到master。如果当前工作区与暂存区都是干净的,OK,直接切换回master即可。但是如果iss53分支上的开发还没有完成,并且不便于commit到版本库,怎么办?一旦切回到其他分支,工作区与暂存区就会被清空、覆盖。实际上,如果工作区或暂存区不是干净的,存在没有提交到版本库的更改,Git是不允许切换分支的,会提示:

error: Your local changes to the following files wouldbe overwritten by checkout:
      readme.txt
Please, commit your changes or stash them before you can switch branches.

解决这个问题的办法就是git stash命令。

该命令可以获取工作目录的中间状态——也就是修改过的被追踪的文件和暂存的变更——并将它保存到一个未完结变更的堆栈中,随时可以重新应用。

运行“git stash”命令之后,iss53分支上未commit得变更就会被“储藏”起来,可以顺利地切换到master分支了。要查看现有的储藏,你可以使用 git stash list,会的到这样的一个列表:

stash@{0}: WIP on testing: 049d078 …
stash@{1}: WIP on testing: c264051 …
stash@{2}: WIP on testing: 21d80a5 …

列出的是该分支上所有被stash过的编号,使用命令“git stash apply”即可恢复到最新stash过的场景。如果想应用更早的储藏,可以通过名字指定它,像这样:git stash apply stash@{2}。如果不指明编号,Git 默认使用最近的储藏并尝试应用它。

题归正转,我们切换到master分支,拉去一个名为“hotfix”的分支来紧急修复bug。

git checkout -b 'hotfix'

修复好之后,commit到版本库,则现在Git的分支结构如下图所示:

经测试之后,该bug成功修复,然后需要将该分支合并到master,首先依然要切换到master,然后使用命令“git merge”合并分支:

git checkout master
git merge hotfix

Git提示:

Updating 771f6de..adea62a
Fast-forward
 …

请注意,合并时出现了“Fast forward”的提示。由于当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把master 分支指针直接右移。换句话说,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)

现在最新的修改已经在当前master 分支所指向的提交对象中了:

在那个超级重要的修补发布以后,就可以回继续之前未完成的工作。由于当前 hotfix 分支和 master 都指向相同的提交对象,所以hotfix 已经完成了历史使命,可以删掉了。使用 git branch 的 -d 选项执行删除操作:

git branch -d hotfix

不用担心之前 hotfix 分支的修改内容尚未包含到 iss53 中来。如果确实需要纳入此次修补,可以用git merge master 把 master 分支合并到 iss53;或者等 iss53 完成之后,再将iss53 分支中的更新并入 master。

现在回到之前未完成的 iss53分支上继续工作,完成后commit到版本库。

在问iss53 分支上的工作完成之后,可以合并回 master 分支。实际操作同前面合并 hotfix 分支差不多,只需回到master分支,运行 git merge 命令指定要合并进来的分支。

请注意,这次合并操作的底层实现,并不同于之前 hotfix 的并入方式。因为这次开发历史是从更早的地方开始分叉的。由于当前master 分支所指向的提交对象(C4)并不是 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算。下图用红框标出了Git 用于合并的三个提交对象:

这次,Git 没有简单地把分支指针右移,而是对三方合并后的结果重新做一个新的快照,并自动创建一个指向它的提交对象(C6)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。

值得一提的是 Git 可以自己裁决哪个共同祖先才是最佳合并基础,不需要开发者手工指定合并基础。此特性让Git 的合并操作比其他系统都要简单不少。

解决冲突

有时候合并操作并不会如此顺利。如果在不同的分支中都修改了同一个文件的同一部分,Git 就无法干净地把两者合到一起,逻辑上说,这种问题只能由人来裁决。这时候如果合并分支就会出现下面的结果:

Auto-merging readme.txt
CONFLICT (content): Merge conflictin readme.txt
Automatic merge failed; fix conflicts and then committhe result.

Git 作了合并,但没有提交,它会停下来等你解决冲突。要看看哪些文件在合并时发生冲突,可以用git status 查阅:

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

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

任何包含未解决冲突的文件都会以未合并(unmerged)的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记,可以通过它们来手工定位并解决这些冲突。可以看到此文件包含类似下面这样的部分:

this is my first git project
<<<<<<<HEAD
add row on master branch
add annother row on master branch
=======
add row on testing branch
add another row on testing branch
>>>>>>>testing

可以看到 ======= 隔开的上半部分,是 HEAD(即 master 分支,在运行merge 命令时所切换到的分支)中的内容,下半部分是在 iss53 分支中的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。当然,Git插入的额外标记行也需要删除。

在解决了所有文件里的所有冲突后,运行 git add将把它们标记为已解决状态(实际上就是来一次快照保存到暂存区域)。因为一旦暂存,就表示冲突已经解决。

删除分支

分支合并到master之后,如果无特殊用途,应该及时删除分支。

要从该清单中筛选出已经(或尚未)与当前分支合并的分支,可以用 “--merged”和“--no-merged” 选项。

比如用“git branch --merged” 查看哪些分支已被并入当前分支,也就是说哪些分支是当前分支的直接上游:

* master
  testing

证明testing分支已经合并到master分支当中了,可以删除:

git branch –d testing

使用“git branch --no-merge”查看还没有合并的分支:

 newTesting

如果使用命令“git branch–d newTesting”删除该分支,Git会提示:

error: The branch 'newTesting' is not fully merged.
If you are sure you want to delete it, run 'git branch -D newTesting'.

由于这些分支中还包含着尚未合并进来的工作成果,所以简单地用 Git branch -d 删除该分支会提示错误,因为那样做会丢失数据。

不过,如果你确实想要删除该分支上的改动,可以用大写的删除选项 -D 强制执行,就像上面提示信息中给出的那样。

Git之(四)分支管理的更多相关文章

  1. python使用git进行版本控制-分支管理

    1.远程克隆 最好的方式是先创建远程库,然后,从远程库克隆: 首先在github上创建一个新的仓库,名字叫gitskills 我们勾选Initialize this repository with a ...

  2. Git如何进行分支管理?

    Git如何进行分支管理?     1.创建分支     创建分支很简单:git branch <分支名>     2.切换分支     git checkout <分支名>   ...

  3. Git 远程仓库分支管理

    目录 目录 速查表 关联远程代码仓库 克隆远程仓库 分支管理 创建分支 切换分支 合并分支 删除分支 解决冲突 速查表 指令 作用 git branch 查看分支 git branch newBran ...

  4. 四、git学习之——分支管理、解决冲突

    分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN. 如果两个平行宇宙互不干扰,那对现在的你也没啥影响.不过,在某个时间点,两个平行宇宙合并 ...

  5. 五、git学习之——分支管理策略、Bug分支、feature分支、多人协作

    一.分支管理策略 通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息. 如果要强制禁用Fast forward模式,Git就会在merge时生 ...

  6. Git远程和分支管理

    一.远程       Git是分布式版本控制系统,最重要的优点就是远程仓库托管代码.不用自己搭建一个服务器,在github上面注册一个账户就可免费获取远程仓库.      首先需要先在github上面 ...

  7. Git教程之分支管理之二

    分支管理策略 通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息.如果要强制禁用Fast forward模式,Git就会在merge时生成一个 ...

  8. Git教程之分支管理之一

    分支在实际中有什么用呢? 你创建了一个属于你自己的分支,别人看不到,别人还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又 ...

  9. git入门四(分支创建合并)

    熟悉git分支的原理是掌握了git的精髓,因为git和我们常用的源码管理系统有很大的区别和优点在分支上可以体现出来,一般我们常用的源码管理系统分支都是需要创建新目录,有全新的源码copy,一般都需要创 ...

  10. 史上最浅显易懂的Git教程3 分支管理

    假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了.如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险 ...

随机推荐

  1. JavaScript数据结构与算法(五) 数组基础算法

  2. 前端面试送命题-JS三座大山

    前言 本篇文章比较适合3年以上的前端工作者,JS三座大山分别指:原型与原型链,作用域及闭包,异步和单线程. 原型与原型链 说到原型,就不得不提一下构造函数,首先我们看下面一个简单的例子: functi ...

  3. [LeetCode] Is Graph Bipartite? 是二分图么?

    Given an undirected graph, return true if and only if it is bipartite. Recall that a graph is bipart ...

  4. .NET CORE 2.0之 httpcontext

    HttpContext  在之前的.NET framework 是一个非常常用且强大的类,在.NET CORE2.0中要像以前用是不太方便的了, 要是用sesson 首先需要在startup 的Con ...

  5. [JSOI 2008]星球大战starwar

    Description 题库链接 给你一张 \(n\) 点, \(m\) 条边的无向图,每次摧毁一个点,问你剩下几个联通块. \(1\leq n\leq 2m,1\leq m\leq 200000\) ...

  6. ●洛谷P2934 [USACO09JAN]安全出行Safe Travel

    题链: https://www.luogu.org/problemnew/show/P2934 题解: 最短路(树),可并堆(左偏堆),并查集. 个人感觉很好的一个题. 由于题目已经明确说明:从1点到 ...

  7. ●线段树题之wows

    ●模拟考试的一道似乎是学长出的题,还不错,挺考代码能力的.以此记录. ●题目以被上传(改了改说法),6128 Lence的方块们 ● (像手纸一样长的贴图……) ●题目大意: 有横向排布的n个点,每个 ...

  8. BZOJ4942【noi2017】整数

    题目背景 在人类智慧的山巅,有着一台字长为10485761048576 位(此数字与解题无关)的超级计算机,著名理论计算机科 学家P博士正用它进行各种研究.不幸的是,这天台风切断了电力系统,超级计算机 ...

  9. SparkSQL——用之惜之

    SparkSql作为Spark的结构化数据处理模块,提供了非常强大的API,让分析人员用一次,就会为之倾倒,为之着迷,为之至死不渝.在内部,SparkSQL使用额外结构信息来执行额外的优化.在外部,可 ...

  10. SpringBoot多环境部署,在启动时动态设置相应的配置文件

    项目中,往往在测试环境和正式环境拥有不同的配置,例如数据库连接,第三方库的appkey等.这时候,我们就要在不同的环境启用不同的配置 下面新建三个文件,分别表示开发环境,生产环境和测试环境的配置文件 ...