Git——分支详解

目录

Git分支

1、开始使用分支

在Git中使用分支很简单,只要使用git branch命令即可:

shll 复制代码
git branch

如果git branch后面没有接任何参数,它仅会输出当前在这个项目中有哪些分支。Git默认会设置一个名为master的分支,前面的星号(*)表示现在正在这个分支上。

1.1、新增分支

要增加一个分支,可在执行git branch命令时,在后面加上想要的分支的名称:

shell 复制代码
git branch cat

这样就新增了一个cat分支。再查看一下:

shell 复制代码
git branch

可以看出,的确多了一个分支,但当前分支还是在master上。

1.2、更改分支名称

如果觉得分支名取得不够响亮,可以随时更改,而且不会影响文件或目录。假设现在的分支有3个:

shell 复制代码
git branch

如果把cat分支改成tiger分支,使用的是-m参数:

shell 复制代码
git branch -m cat tiger

看一下当前的分支:

shell 复制代码
git branch

即使是master分支也可以改,如把master改成slave:

shell 复制代码
git branch -m master slave

1.3、删除分支

当前共有3个分支,如果其中的dog分支不需要了,可以使用-d参数来删除:

shell 复制代码
git branch -d dog

如果要删除的分支还没有被完全合并,Git会有贴心小提示:

shell 复制代码
git branch -d tiger

因为tiger的内容还没有被合并,所以使用-d参数无法将其删除。这时只需改用-D参数即可将其强制删除:

shell 复制代码
git branch -D tiger

在Git中什么分支都可以删,包括默认的master分支(master只是默认的分支,并没有其他特别之处)。如果非要说哪个分支是不能删的,只有"当前所在的分支"不能删,不过只要先切换到其他分支就可以将其删除。

摘录来自: 高见龙. "Git从入门到精通。" Apple Books.

1.4、切换分支

要切换分支,就是git checkout

shell 复制代码
git checkout tiger

看一下当前的分支状态:

shell 复制代码
git branch

可以看到,前面的那个星号已经移到tiger分支上了。

1.5、切换分支时

例如,上文已经切换到tiger分支,然后新增tiger1.txt和tiger2.txt文件,并Commit:

shell 复制代码
echo "tiger1" > tiger1.txt
echo "tiger2" > tiger2.txt
git add .
git commit tiger1.txt -m "add tiger1"
git commit tiger2.txt -m "add tiger2"

查看一下Git记录:

shell 复制代码
git log --oneline 

可以看出,tiger分支的确比master分支多前进了两次Commit。

再看一下文件列表:

shell 复制代码
ls -al

这时如果切换回原本的master分支:

shell 复制代码
git checkout master

再看一下文件列表:

刚才那两个文件不见了!别担心,其实它们都还在,只是在不同的分支而已,只要切换回cat分支,文件就会出现了。

1.6、要切换到哪个分支,首先要有那个分支

如果要切换到某个分支,这个分支必须要先存在,不然会发生错误:

如果没有这个分支,只要在给git checkout分支命名的时候加上-b参数就没问题了。如果这个分支本来就存在,Git就会直接切换过去;如果不存在,Git就会帮你创建一个,然后再切换过去:

shell 复制代码
git checkout -b aaa

2、分支原理

2.1、单个分支

可以把分支想象成一张贴纸,贴在某一个Commit上面:

当做了一次新的Commit之后,这个新的Commit会指向它的前一个Commit:

而接下来"当前的分支",也就是HEAD所指的这个分支,会贴到刚刚做的那个Commit上,同时HEAD也会跟着前进:

2.2、多个分支

如果一个分支不够说明,那就来两个。通过git branch cat命令创建一个新的分支。它就像一张贴纸,与master贴在同一个地方:

接下来执行git checkout cat命令,切换到cat分支。此时,HEAD转而指向cat分支,表示它是"当前的分支":

接着进行一次新的Commit,这个新的Commit会指向前一次Commit:

然后,cat分支上的"贴纸"就会被撕下来,转而贴到最新的那个Commit上;当然HEAD也是一样:

2.3、切换分支时的逻辑

Git在切换分支时主要做了以下两件事:

1、更新暂存区和工作目录

Git在切换分支时,会用该分支指向的那个Commit的内容来"更新"暂存区(Staging Area)及工作目录(Working Directory)。但在切换分支之前所做的改动则会留在工作目录中,不受影响。

这段话有点难理解,先来看看前半段的意思。假设原本正处于cat分支,执行下面的命令后,就会由cat分支切换到master分支:

shell 复制代码
git checkout master

接下来,Git会用master分支指向的那个Commit的内容来更新暂存区及工作目录。因为master此时指向的Commit并没有cat1.html和cat2.html这两个文件,所以"更新"之后,不管在暂存区还是在工作目录中都不会有这两个文件。同理,再次切换回cat分支时:

shell 复制代码
git checkout cat

Git会用cat分支指向的那个Commit的内容来"更新"暂存区及工作目录,所以这两个文件就又出现了。

在Git的世界中,每一次的Commit都是一个对象,它会指向某一个Tree对象(目录),而这些Tree对象会指向其他的Tree对象(子目录)或Blob对象(文件)。这种结构有点像葡萄。只要伸手把源头的Commit对象拎起来,整串内容都可以被拿出来。

2、变更HEAD的位置

除了更新暂存区及工作目录的内容外,HEAD也会指向刚刚切换过去的那个分支,也就是说,.git/HEAD文件会一起被改动。

2.4、如果将文件改动了一半就切换分支

假设现在还在cat分支,在切换到master分支之前,新增了一个cat3.html文件,同时也改动了index.html文件的内容,这时的状态如下:

当前index.html是modified状态,而cat3.html是Untracked状态。如果这时就直接切换到master分支,也就是还没Commit就切换分支,会发生什么事?

前文提到过,"Git在切换分支时,会用该分支指向的那个Commit的内容来'更新'暂存区(Staging Area)及工作目录(Working Directory)。但在切换分支之前所做的改动则会留在工作目录中,不受影响"。其中"在切换分支之前所做的改动则会留在工作目录中,不受影响"说的就是这件事。直接操作看看:

shell 复制代码
git checkout master

查看一下文件列表:

可以看到,刚刚新增的cat3.html文件还在。再看一下Git的状态:

可以看到,Git的状态与刚刚在cat分支时的状态是一样的,也就是说,切换分支并不会影响已经在工作目录中的那些改动。

3、合并分支

3.1、合并示例

在前面的例子中,从master分支开了一个cat分支,随后修改了index.html和新增了cat3.html文件,我们进行两次Commit,现在大概是这个样子:

任务执行得差不多了,就要准备合并回来了。如果想要用master分支来合并cat分支,就要先切换回master分支:

shell 复制代码
git checkout master

接下来,使用git merge命令合并分支:

shell 复制代码
git merge cat

查看一下文件列表:

因为master现在已经合并了cat分支,所以在cat分支新增的cat3.html文件在master分支也出现了。

3.2、A合并B与B合并A区别

在此假设从master分支创建了cat和dog两个分支,并且当前正在cat分支:

cat分支与dog分支都是来自master分支,所以不管master是要合并cat分支还是dog分支,Git都会直接使用快转模式(Fast Forward)进行合并,也就是master直接"收割"cat或dog的成果。

但如果是cat与dog这两个分支要互相合并就不一样了,虽然它们有同样的来源,但要合并就不会这么顺利了。在这种情况下,Git会生成一个额外的Commit来处理这件事。一般的Commit只会指向某一个Commit,但这个Commit会指向两个Commit,明确地标记是来自哪两个分支。

来看看会怎么演变。假设想用cat分支来合并dog分支,可用如下命令:

shell 复制代码
git merge dog

执行这个命令时会弹出一个Vim编辑器窗口,为了进行这次合并,Git做出了这个额外的Commit对象,这个Commit会分别指向cat分支和dog分支,HEAD随着cat分支往前,而dog分支停留在原地:

如果改由dog分支来合并cat分支,则使用如下命令:

shell 复制代码
git merge cat

程序上与刚才几乎是一样的。这时的状态如图:

其实就结果来看,不管是谁合并谁,这两个分支的文件最后都得到了。

事实上不管是谁合并谁,这两个分支上的Commit都是对等的。如果非要说哪里不一样,就是cat分支合并dog分支时,cat分支会往前移动,反之亦然。不过前面提到过,分支就像贴纸一样,删除或改名都不会影响现在已经存在的Commit。

不一样的地方还有一处,就是这个为了合并而生成的额外的Commit对象,里面会记录两个"老爸",谁合并谁就会有"谁在前面"的区别,不过这就有点太过细节了。

3.3、合并过的分支要保留吗

结论:都可以,看自己的需要。

基本上,分支就是一个只有40字节的文件而已。这个分支,也就是这40个字节,会标记出它当前指向哪一个Commit。不管是上节提到的快转模式(Fast Forward)还是非快转模式,只要该分支被合并过,就代表"这些内容本来只有你有,现在我也有了"。

既然合并后,原本没有的内容都有了,而分支本身又像一张贴纸一样"地位低微",那么它也就没有利用价值了。所以,合并过的分支想删就删吧。删除分支就只是把一张贴纸撕下来而已,原来被这张贴纸贴着的东西并不会因此而不见。

当然,没有被合并过的分支就是另一回事了。回到原来的问题------合并过的分支要留着吗?都可以,如果想要删掉,或者已经和这个分支建立"感情"了,想留着做纪念也可以。

4、不小心把还没合并的分支删除了,救得回来吗

4.1、恢复已被删除的还没合并过的分支

合并过的分支想留就留、想删就删,Git的分支并不是复制文件到某个目录,所以不会因为删掉分支文件就不见了。

但如果删除的是还未合并的分支就不一样了。先想象一下这个画面:

cat分支是从master分支出去的,当前领先master分支两次Commit,而且还没有合并过。这时如果试着删掉cat分支,它会提醒:"这个分支还没全部合并哦"。

shell 复制代码
$ git branch -d cat 
error: The branch 'cat' is not fully merged. If you are sure you want to delete it, run 'git branch -D cat'.

虽然Git这么贴心地提醒了,但这里仍然把它删除了:

shell 复制代码
$ git branch -D cat 
Deleted branch cat (was b174a5a).

这时的关联图如图:

上面不是已经删除cat分支了吗,怎么还在?这里再次跟大家说明一下分支的概念:分支只是一个指向某个Commit的指标,删除这个指标并不会使那些Commit消失。

所以,删掉分支后那些Commit还在,只是因为你不知道或没有记下那些Commit的SHA-1值,所以不容易再拿来利用。原本领先master分支的那两个Commit现在就像空气一样,虽然看不到,但它们是存在的。既然存在,那就把它们"接回来"吧:

shell 复制代码
$ git branch new_cat b174a5a

这个命令是"请帮我创建一个叫作new_cat的分支,让它指向b174a5a这个Commit",也就是再拿一张新的贴纸贴回去。看一下现在的分支:

shell 复制代码
$ git branch 
* master
new_cat

切换过去试试看:

shell 复制代码
$ git checkout new_cat 
Switched to branch 'new_cat'

确认一下文件列表,失去的文件都回来了。

4.2、还没有把刚刚删除的那个cat分支的SHA-1值记下来怎么办?查得到吗

可以用git reflog命令去查找,Reflog默认会保留30天,所以30天内还找得到。

4.3、其实所谓的"合并分支",合并的并不是"分支"

如果你对Git分支的认识是正确的,就可以猜到下面这个命令在做什么:

shell 复制代码
$ git merge b174a5a 
Updating e12d8ef..b174a5a Fast-forward cat1.html | 0 cat2.html | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 cat1.html create mode 100644 cat2.html

这不是在合并分支时弹出来的信息吗?是的,所谓的"合并分支",其实是合并"分支指向的那个Commit"。分支只是一张贴纸,它是没有办法被合并的,只是大家会用"合并分支"这个说法,毕竟它比"合并Commit"容易理解。

分支只是一个指向某个Commit的指向标。

5、使用Rebase合并

5.1、使用Rebase合并分支

准备环境:

shell 复制代码
git branch dog
git checkout dog
echo "dog1" > dog1.txt
echo "dog2" > dog2.txt
git add .
git commit dog1.txt -m "dog1"
git commit dog2.txt -m "dog2"

git branch cat
git checkout cat
echo "cat1" > cat1.txt
echo "cat2" > cat2.txt
git add .
git commit cat1.txt -m "cat1"
git commit cat2.txt -m "cat2"

状态如图:

在Git中还有一个命令叫作git rebase,也可以用来做与git merge命令类似的事情。从字面上来看,rebase是"re"加上"base",其中文含义大致是重新定义分支的参考基准。

所谓"base",就是指"这个分支是从哪里生成的"。以上面这个例子来说,cat与dog两个分支的base都是master。接着使用git rebase命令来"组合"cat和dog这两个分支:

shell 复制代码
git rebase dog

用通俗的话来说,上述命令的含义大致就是"我(即cat分支)现在要重新定义我的参考基准,并且将使用dog分支作为我新的参考基准"。

Rebase合并方式和一般的合并方式的一个很明显的区别,就是使用Rebase方式合并分支,Git不会做出一个专门用来合并的Commit。

5.2、Rebase原理

Rebase的过程大概是这样的:

  1. 先将c68537这个Commit接到053fb2这个Commit上。因为c68537的上一层Commit原本是e12d8e,现在要接到053fb2上,所以需要重新计算这个Commit的SHA-1值,重新做出一个新的Commit对象35bc96。
  2. 再拿b174a5这个Commit接到刚刚做出来的Commit对象35bc96上。同理,因为b174a5这个Commit要接到新的Commit上,所以它会重新计算SHA-1值,得到一个新的Commit对象28a76d。
  3. 最后,原本的cat是指向b174a5这个Commit的,现在转而指向最后做出来的那个新的Commit对象28a76d。
  4. HEAD还是继续指向cat分支。

5.3、原来Commit的去向

原本的那两个Commit(灰色),即c68537和b174a5的去向是哪里?

它们还是在Git的空间中占有一席之地,只是因为已经没有分支指着它们了,所以如果没有特别去记这两个Commit的SHA-1值,它们就会慢慢被边缘化。

但它们并没有马上被删除,只是默默地待在那里,直到有一天被Git的资源回收车拉走。

5.4、谁Rebase谁有区别吗

仅从最后的文件来说并没有什么区别,但就历史记录来说则有区别,谁Rebase谁,会造成历史记录的先后顺序不同。

5.5、取消Rebase

如果是一般的合并,只要git reset HEAD^ --hard一行命令,删除这个合并的Commit后,这些分支就会退回合并前的状态。但是,从上面的结果可知,Rebase并没有做出那个合并专用的Commit,而是整个都串在一起了,与一般的Commit差不多。所以这时如果执行git reset "HEAD^ --hard命令,只会删除最后一个Commit,但并不会回到Rebase前的状态。"

要想取消Rebase,可使用以下三种方式:

1、使用Reflog

其实Reflog会记录很多好用的东西。例如,刚刚把cat分支Rebase到了dog分支上,其状态就会是这样的:

查看一下现在的Reflog:

这个a9aa5bf (cat) HEAD@{5}: commit: cat2就是开始做Rebase前的最后动作,所以就是这个Commit了!下面使用Reset命令硬切回去:

shell 复制代码
git reset a9aa5bf --hard 

这样一来,就会回到Rebase前的状态了。

2、使用ORIG_HEAD

在Git中有另一个特别的记录点------ORIG_HEAD。ORIG_HEAD会记录"危险操作"之前HEAD的位置。例如,分支合并或Reset之类的都算是所谓的"危险操作"。通过这个记录点来取消这次Rebase相对来说更简单:

shell 复制代码
git rebase dog
git reset ORIG_HEAD --hard

实际也是回到a9aa5bf位置的Commit。

6、合并冲突

6.1、发生冲突

Git可以检查出简单的冲突,所以并不是改到同一个文件就一定会发生冲突,但改到同一行就会出现冲突。假设在cat分支改动了index.html文件的内容:

shell 复制代码
git add .
git commit index.html -m "cat branch update"

然后在dog分支也改动了index.html文件的内容:

shell 复制代码
git add .
git commit index.html -m "dog branch update"

这时进行合并,不管是一般的合并还是使用Rebase进行合并,都会出现冲突。下面先使用一般的合并:

shell 复制代码
git merge dog

这时Git发现那个index.html文件出现问题了。我们先看一下当前的状态:

因为index.html文件的两边都被改动了,所以Git把它标记成both modified状态。

6.2、解决冲突

查看index.html文件的内容:

Git把有冲突的段落标记出来了,上面是HEAD,也就是当前所在的cat分支,中间是分隔线,下面是dog分支的内容。

也就是说,<<<<<<< HEAD=========之间的内容是当前cat分支的修改,===========>>>>>>>>>> dog之间的内容是dog分支的修改。

这个问题看来是沟通不良造成的。要解决问题,就要把两边的人请过来讨论一下,看看到底该用谁的code。最后决定还是采纳cat分支的内容,顺便把那些标记修掉。最后内容如下:

改完后,切记把这个文件加回暂存区:

shell 复制代码
git add index.html

然后就可以Commit并完成这一回合了:

shell 复制代码
git commit -m "conflict fixed"

6.3、处理非文本文件的冲突

因为上面的index.html文件是文本文件,所以Git可以标记出发生冲突的点在哪里,用肉眼就能看得出来大概该怎样解决,但如果是图像文件之类的二进制文件该怎么办?例如,在cat分支和dog分支同时加了一张叫作cute_animal.jpg(可爱的动物)的图片,则合并时出现冲突的信息为:

shell 复制代码
$ git merge dog 
warning: Cannot merge binary files: cute_animal.jpg (HEAD vs. dog) Auto-merging cute_animal.jpg CONFLICT (add/add): Merge conflict in cute_animal.jpg Automatic merge failed; fix conflicts and then commit the result.

这时要把两边的人请过来,讨论到底谁才是最可爱的动物。最后决定猫才是最可爱的动物,所以决定用cat分支的文件:

shell 复制代码
git checkout --ours cute_animal.jpg

如果要用dog分支,则使用--theirs参数:

shell 复制代码
git checkout --theirs cute_animl.jpg

决定之后,像前面一样将内容加到暂存区,准备Commit,然后结束这一回合。

7、从过去某个Commit创建分支

历史记录如下:

要从add sql.的Commit(62fb48e)中再做出新的分支,首先要回到那个Commit的状态,这时使用的是git checkout命令:

shell 复制代码
git checkout 62fb48e

从这段信息中可以看出,当前正处于detached HEAD状态。可以从现在这个Commit开出新的分支。使用Checkout命令的-b参数直接创建分支并自动切换过去:

shell 复制代码
git checkout -b xxx

如果不想两步操作,也可以一行直接搞定:

shell 复制代码
git branch xxx 62fb48e

意思就是"请帮我在62fb48e这个Commit上开一个xxx分支"。

相关推荐
贩卖纯净水.7 小时前
白月光git
git·github
爱吃瓜的猹z11 小时前
git reset 几点疑问
git·源代码管理
悟空201617 小时前
001、Git开发流程规范
git
Li小李同学Li17 小时前
git学习【持续更新中。。。】
git·学习·elasticsearch
晨春计19 小时前
【git】
android·linux·git
念幽20 小时前
Git常用命令
git
神技圈子20 小时前
【git系列】git中的那些迷惑的术语以及概念详解
git
benben04421 小时前
Photoshop使用方法大全
git
ou.cs1 天前
git 删除远程分支的几种写法
git
atlanteep1 天前
Linux·权限与工具-git与gdb
linux·git