Git之旅(7):分支是啥?

  • A+
所属分类:Git  运维技术

  

博主会将与Git有关的知识点总结到"通俗易懂Git入门系列"文章中,如果你对Git不是特别了解,请按照顺序阅读"Git系列",以便站在前文的基础上理解新的知识点。

 

题外话:由于孩子出生,生活节奏与之前截然不同,所以停更了一段时间,最近刚刚适应一些,所以抽空写完了这篇文章,这篇文章其实写了好久,因为没空,所以总是断断续续的去写,每次写又要从头整理思绪,所以也修改了好几次,不过今天总算出了结果,也算完成了一个阶段性的任务吧,希望这篇文章能够帮助到你,加油~

 

直到现在,我们一直都偏重概念,通常是在介绍清楚相关概念以后,才会结合相关的命令进行练习,所以,如果你同时也在看别的教程,你会发现很多不同之处,你甚至会觉得我们的进度太慢了,如果你看别的教程,在看到第7篇的时候,应该已经介绍了很多命令,但是你可能并不理解为什么要那样做,我们力求在使用命令的同时能够理解相关的原理,所以,不要着急,厚积薄发,搞定命令简直不要太轻松。

 

前文中,我们一直在创建提交,但是一直没有回退到过任何一个版本,主要执行的命令就是,查看状态、暂存修改、创建提交,一直在重复这三件事,这样做是为了更好的理解工作区、暂存区、提交之间的关系,不如现在,我们来尝试一下,回退到某个版本,看一看利用git进行版本回退的效果,同时,通过版本回退的操作引出"分支"的概念。

 

为了我们的思路能够同步,我决定重新创建一个测试仓库,一步一步的创建提交,并且回退到特定的版本,过程如下:

 

创建测试仓库

 
 

进入仓库,创建两个测试文件,f1和f2

如上述命令所示,f1的内容为1,f2的内容为A。

 

测试文件已经初始化完成了,我们来创建第一个提交吧。

使用"git add ."命令将当期目录中的所有变更状态加入到暂存区,然后使用如下命令创建第一个提交,注释信息为1A

 

多次修改f1和f2的文件内容,并且创建对应的提交,为一会儿的回退测试做准备,过程如下,不再详细描述

我又一口气创建了4个提交,每次修改都对两个文件加入一行新文本,到目前为止,我们一共已经有了5个提交,最新的提交是注释信息为"5E"的提交,当前状态下,两个文件的内容如下:

现在,我后悔了,我想要回到"3C"时的状态,该怎么办呢?

如果想要回到"3C"时的状态,首先要找到"3C"提交的哈希码,我们先通过git log命令看看3C状态对应的哈希码吧。

从上述信息可以看出,"3C"提交对应的ID是aa81e63 ,好了,状态对应的哈希码已经找到了,我们可以使用如下命令,回到"3C"提交对应的状态

 

执行完上述命令后,再次查看工作区内两个文件的内容,如下:

可以看出来,我们已经回到过去了,轻松的实现了版本回退,我们只是执行了一条"git reset --hard"命令而已,我们先不纠结这条命令的参数都是什么含义,之后我们再去详细的了解它。

 

如果,我回到"3C"状态以后又反悔了,想要再次回到"5E"时的状态,该怎么办呢?聪明如你一定想到了,仍然使用刚才的命令啊,我们只要找到"5E"状态的哈希码,就能够通过"git reset --hard"命令再次回到"5E"的状态了,没错,思路完全正确,于是,我们可能会想要尝试使用"git log --oneline"命令查找"5E"状态的哈希码,但是当我们执行"git log"命令以后,发现"5E"状态的哈希码不见了,如下:

由于我们之前已经使用"git reset --hard"命令回退到了"3C"时的状态,所以,"git log"命令执行后显示的最近的"commit id"只会显示到"3C",别慌,肯定还有办法能够找到我们想要的哈希码,没错,还有一条命令,那就是"git reflog"命令,执行"git reflog"命令,可以看到如下内容:

注:我们暂且不用纠结git log命令和git reflog命令的区别,因为如果想要彻底理解git reflog命令,可能还需要理解一些其他的概念,所以先往下看。

从上述信息可以看出,"5E"对应的哈希码为"526ff66",所以,我们仍然可以通过这个哈希码再次回到"5E"状态对应的提交,执行如下命令即可

 

再次查看文件的内容,发现真的回到了"5E"的状态

此时,使用"git log"命令,可以看到,最近的一次提交又回到了"5E"了,如下:

 

你看,使用git进行版本回退是不是非常的方便呢?

细心如你一定已经发现了,无论是回退操作,还是使用git log命令查看日志,都会在命令执行后的返回信息中看到一个词,这个词就是"HEAD ",那么"HEAD "是什么意思呢?你可以把"HEAD "理解成一个指针,这个指针通常指向了当前"分支"的最新的提交,你肯定会有疑问,"分支"又是个什么东西呢?为什么"HEAD "这个概念还没说明白,就又多出了一个"分支"的概念呢?不如我们先来搞明白分支是个什么东西吧。

 

如果想要搞明白分支的概念,不如先来看一个实际的问题,从解决问题的思路去理解一个概念,似乎更加容易一些,为了能够更加轻松的、清晰的描述问题,我又创建了一个新的测试仓库,并且创建了两个用于测试的文件,m1和m2

如上例所示,我创建了两个测试文件,m1和m2,假设这两个文件分别代表了两个模块,这两个模块共同组成了我想要的项目,并且假设这两个模块之间的代码在业务逻辑上是没有关系的,是相互独立的,现在,我来对这两个文件进行一些修改,模拟在实际工作中分别在两个模块上进行开发的工作,操作如下:

我再把上述模拟工作的过程用文字大概的描述一遍,我先是只修改了m1文件,然后针对这个修改创建了一个提交(模拟开发模块一),然后我又修改了m2文件,针对这个修改又创建了一个提交(模拟开发模块二),然后我又重复了上述过程,分别对m1和m2进行了修改,并且为各自的修改创建了提交。

 

假设,我现在后悔了,我想让m2文件回到"add B in m2"的状态,我该怎么办呢?没错,我们只要回退就行了,就用刚才总结的命令回退,我们来试试,执行如下命令:

可以发现,我们已经成功的将m2文件变成了"add B in m2"的状态,但是,你会发现一个问题,问题就是,如果你此时查看m1文件的内容,你会发现,m1文件的状态也跟着回退了,如下

我们的初衷是为了让m2回到"add B in m2"的状态,但是并没有想要修改m1的状态,因为之前说过,两个模块在业务逻辑上是独立的,我们并不想为了回退某个模块,而影响另一个模块的代码。

 

那么我们该怎样解决这样的问题呢?如果仍然按照上面的操作方式,我们没有任何办法能够解决这个问题,因为问题的根本原因在于,针对模块一的提交和针对模块二的提交是交错的,这些提交交错在同一条'逻辑线'上,很有可能,针对模块一的某个修改就是针对模块二的某个修改的父提交,而针对模块二的某个修改又是针对模块一的某个修改的父提交,所以在这种情况下,如果你想要针对某个模块的代码进行回退,势必会影响到另外一个模块。换句话说就是,你想要回退的提交之后的提交很有可能包含其他模块的代码改动。

其实,造成上述问题的根本原因就在于,针对两个模块的提交混用了一条"逻辑线",你可以把这条逻辑线理解成一条所谓的"分支",默认情况下,git仓库会为我们创建一条名为master的分支,我们创建的所有提交默认都会在master分支上,这样说不够明了,不如来直观的看一下,在上例中的测试仓库中执行gitk命令,打开图形化工具,因为图形化工具能让我们更加直观的理解"分支"的概念,执行gitk命令后,如下图所示:

注:为了能显示更多的提交,我已经回退到了之前最新提交,此处省略命令

Git之旅(7):分支是啥?

如你所见,上图中有5个提交,对于这5个提交来说 ,每个提交都是下一个提交的父提交,它们组成了一条所谓的"逻辑线",这些提交所连成的线就是所谓的分支,只不过默认情况下,所有提交会连成一条名为master的分支,从字面上理解,master分支的意思是主分支,从上图可以看出,绿色的master方形总是指向master分支上最新的提交,我们可以把上图中绿色的master方形称之为分支指针,分支指针总是指向当前分支的最新提交。

 

那么回到刚才的问题,上例中,当想要回退某个模块时,会影响另一个模块,这是因为默认情况下,所有提交交错在一条分支上,那么你肯定想到了解决方法,我们只要将两个模块的代码分别提交到两个分支不就能解决问题了么,没错,我们可以再创建一条分支,然后就可以将两个模块的代码分别提交到不同的分支上,这样,处于两个分支的提交在进行版本回退的操作时,就不会相互影响了,大致思路如下图所示

Git之旅(7):分支是啥?

如上图所示,橘色代表m1相关的代码,蓝色代表m2相关的代码,我们根据当前的状态创建出一个新的分支,然后将之后的代码修改按照模块逻辑进行区分,分别提交到不同的分支上,这样就能达到我们的目的,在之后的工作中,让两个模块互不影响了。

当我们需要一个完整的项目时,则需要所有模块的代码,这时,我们只需要将两个分支合并在一起就好了,我们只要知道分支从本质上就是一些提交所连成的线,当我们需要某些提交与另一些提交之间不会相互影响时,就可以利用分支将它们分开。

当然,在实际工作中,即使使用分支,也不一定能将代码完全从业务逻辑上分开,比如,一个团队中有三个开发,A、B、C,他们为了自己的工作不影响别人,分别创建了A、B、C三条分支,但是并不代表他们不会同时修改同一个业务模块的代码,很有可能他们三个人的开发工作所对应的代码是处于同一个模块的,所以,在这种情况下,分支的作用只是为了暂时的将每个人的工作隔离开,以便不影响别人,并不是为了从业务逻辑上分开代码,但是本质上,仍然利用了分支之间的提交互不影响的特性,当然,如果A、B、C三个人同时修改了同一模块的某一段代码,当我们需要将三个人的代码整合到一起时,很有可能出现所谓的"冲突",这时候,就需要人为介入,解决冲突。

 

此时我们还没有完全使用过分支,所以不理解"合并分支"以及"解决冲突"这些操作是完全正常的,我们先别在意它们,不如先通过实际操作来更加具象化的认识一下分支吧。

 

现在,我们通过实际操作来创建一个分支,为了方便,我们继续使用上文中的示例仓库,在仓库目录中打开git bash,可以从git bash的命令提示符中看到当前工作目录处于哪个分支,如下图所示

Git之旅(7):分支是啥?

可以看到,我们当前处于默认的分支master分支,除了在命令提示符中能够看到当前处于哪个分支以外,使用" git status"命令也可以,如下:

从上述返回信息可以看出,我们处于master分支。

使用"git branch"命令能够查看现在都有哪些分支

如上述命令所示,我们当前只有一个分支,这个分支的名字是master,当有多个分支时,会显示所有分支的名字,我们当前所处的分支前面会有一个"*"(星号)

使用"git branch"命令时还可以加上"-v"参数或"-vv"参数,使用"-v"参数或"-vv"参数可以查看更加详细的分支信息。

可以看到,目前我们处于master分支,master分支的最新的提交的哈希值为89b7282,最新的提交的注释信息为"add C in m2"

 

现在,我们就来创建一个新的分支,创建新分支时,默认是以当前所在的分支作为基础创建的,你可以这样理解,当我们创建新分支时,是将当前所在的分支'复制'了一份(并不是真正的复制,只是将新的分支指针指向所基于的分支对应的提交,后面会有解释,此处不用纠结),我们当前处于默认的master分支,假如我们想要创建一个用于测试的名为test的分支,那么可以使用如下命令创建

上述命令的意思是,根据当前所在分支(master分支)创建一个名为test的分支,但是并不切换到新的分支(test分支),仍然停留在当前分支(master分支)。

执行完上述命令后,再次使用"git branch"命令查看分支信息,你会看到test分支已经被创建了,如下所示

从命令的返回信息可以看出来,test分支已经被创建了,由于test分支是基于master分支创建的,所以目前来说,这两条分支是完全相同的,之前说过,分支指针总是指向当前分支的最新提交,所以test分支的分支指针也会指向test分支上最新的提交,但是由于test分支是基于master分支刚刚创建完成的,所以这两条分支完全相同,test分支上最新的提交与master分支上最新的提交自然是同一个提交。

而且,从上述信息可以看出,目前我们仍然处于master分支,并没有切换到test分支,因为星号仍然处于master分支,也就是说,现在,如果在工作空间中进行改动并且创建提交,新创建的提交仍然属于master分支,因为我们并没有切换到test分支,如果想要在test分支上进行工作,则必须先切换到test分支,但是在切换分支之前,我们先使用gitk命令看一下图形化界面,看看当前的状态到底是个什么样子,执行gitk命令,可以看到当前的提交状态如下:

Git之旅(7):分支是啥?

可以看到, test分支和master分支的分支指针同时指向了同一个提交,刚才提到过," git branch test"命令的意思是基于当前分支创建test分支,但是并不是真正的将当前分支复制一份,什么意思呢?你可以这样理解,在没有创建test分支之前,master分支如下图所示:

Git之旅(7):分支是啥?

如上图所示,上例的测试仓库中一共有5个提交,我们用C1代表第一个提交"init file m1 and m2",用C2代表第二个提交"add 2 in m1",以此类推,每个提交都指向了自己的父提交(前文中有说过,每个提交都会指向自己的父提交,如果忘了请回顾前文),上图中绿色的master方框表示master分支的分支指针,它指向了master分支的最新提交。

当我们执行" git branch test"命令后,会基于master分支创建test分支,你可以把创建test分支的过程理解成"复制"当前的master分支,但不是真的复制,前文中一直在说,git不会像我们一样手动的创建真实的"副本",因为通过"复制操作"创建的副本总是会占用相对较多的磁盘空间,前文中git所体现出的'智慧'在这里同样适用,当我们基于master分支创建test分支时,git也并不会真正的将master分支"复制",git只会创建一个test分支指针,并且让test分支指针指向master分支对应的最新的提交,如下图所示:

Git之旅(7):分支是啥?

聪明如你一定想到了,从"概念"上来说,目前存在两条分支,master分支和test分支,但是从"物理"上来说,其实就是5个提交连成的线,我们根据需要赋予了这条线两个含义,这两个含义就是master分支(默认创建的含义)和test分支(我们所赋予的含义),你如果阅读过前文,肯定能明白,为什么git没有将这个5个提交复制一遍,因为这时,test分支和master分支是完全一样的,所以我们只要引用这5个提交即可,没有必要的复制出5个相同的提交,也就是说,从概念上来说,现在master分支上有5个提交,test分支上同样有5个提交,并且从当前的情况来说,test分支和master分支是完全相同的,但是它们之间却又互不影响,你现在可以在test分支上随意的回退到任何一个提交,都不会影响master分支上的提交,这样说可能不太容易理解,不如先向下看。

 

刚才说过,如果想要使用新创建的test分支,则必须切换到新分支,那么怎样切换分支呢?使用"git checkout test"命令即可从当前分支切换到test分支,如下:

Git之旅(7):分支是啥?

'checkout'的字面意思是'检出',也就是说,我们可以使用上述命令,从当前分支检出(切换)到其他分支,当你切换到test分支时,就表示当前工作空间中的内容已经变为test分支指针所指向的提交所对应的状态了,但是由于当前test指针和master指针指向的是同一个提交,所以即使你切换到test分支,当前工作空间中的内容也不会发生改变。

从返回信息可以看出,执行上述命令后,已经切换到test分支,在