[Git使用手册]-5-Git分支管理

引言

本文将介绍git中一个很重要的特性——分支;如今,几乎所有的版本控制系统都支持"分支"这种特性,但git的实现方式与其他的版本控制系统有明显的区别,它不会拷贝整个项目的副本作为"分支"的基础,而是使用类似"快照"的功能,这样做的好处是,节约了创建"分支"的时间成本;所以在git中,可以随时创建大量"分支"来应对不同方向的研发需求。

文章目录

0×1.分支模型图

当使用"git commit"提交修改时,Git计算每一个子目录的校验和,然后在Git仓库中将这些目录保存为树(tree)对象。之后Git创建一个新的历史版本,除了包含相关提交信息以外,还包含着指向这个树对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容。这在上一篇文章历史版本模型图中已经介绍过。

创建分支:

创建一个新的分支,实际上就是创建当前最新历史版本的一个快照,在此分支上的修改和提交不会影响到主分支,直到将这个分支合并到主分支为止,如下图所示:

git-branch-1

假设一个仓库目录进行了两次提交,创建了"C1","C2"两个历史版本,现在突然接到任务要开发一个新的功能,但不确定这个功能是否能够实现,为了不影响主分支的开发,在"C2"的基础上新建了一个分支,并提交了一个新的版本"C4"作为这个新功能的实现版本,在"C4"的基础上又进行修改并提交创建了"C5",此时,分支"C5"和主分支"C2"互相之间没有影响,他们是两个平行的项目分支,我们可以在这两个分支之间切换,比如切换回"C2"分支,然后提交创建"C3",现在,主分支照样开发,而新分支如果实现不了,可以直接删除,并不会影响主分支,如果分支开发完成并可以投入使用,那么再将分支合并到主分支即可。

在上图中,"HEAD"对象指向了新分支最后一次提交的版本,实际上"HEAD"对象永远指向正在使用的分支的最新历史版本,如果我们切换回主分支,那么"HEAD"对象将指向"C3"。

合并分支:

合并分支有两种方式:"Fast forward"与"3-way";

"Fast forward"合并如下图所示,在主分支"master"的"C2"新建了一个分支"A",在分支"A"中进行了两次修改提交,分别创建了"C4"和"C5",如果此时主分支没有变动,将分支"A"合并到"master"分支时,只需要将"HEAD"指向"C5"即可,在主分支的历史版本链中,"C2"后面直接变成了"C5":

git-branch-2

"3-way"的合并方式就显得复杂的多,如下图所示,在主分支的"C2"处创建了一个新的分支,之后新分支和主分支都做出了修改,新分支更新版本到"C5",主分支更新版本到"C3",此时如果想将新分支合并到主分支,需要执行三步操作:首先用"C5"与"C2"比较,得到一张修改列表;再用"C3"与"C2"比较,同样得到一张修改列表;最后,将这两张修改列表进行比较,根据比较结果创建一个新的历史版本"C6"来包含两个分支所有的变化,在主分支历史版本链中将会增加两个历史版本"C5"与"C6","C5"将指向"C3","C6"指向"C5"完成合并:

git-branch-3

0×2.分支管理实例

a.创建分支

					#新建一个目录并将它初始化成git仓库目录
					www@qingsword.com:~$ mkdir BranchDemo
					www@qingsword.com:~$ cd BranchDemo/
					www@qingsword.com:~/BranchDemo$ git init
					初始化空的 Git 版本库于 /home/qing/BranchDemo/.git/

					#在这个仓库中创建两个文件,写入数据并提交
					www@qingsword.com:~/BranchDemo$ touch file1
					www@qingsword.com:~/BranchDemo$ echo aaa >> file1
					www@qingsword.com:~/BranchDemo$ touch file2 
					www@qingsword.com:~/BranchDemo$ echo bbb >> file2
					www@qingsword.com:~/BranchDemo$ git add file1 file2
					www@qingsword.com:~/BranchDemo$ git commit -m 'first commit'

					#修改其中一个文件的内容,第二次提交
					www@qingsword.com:~/BranchDemo$ echo ccc >> file1
					www@qingsword.com:~/BranchDemo$ git commit -am 'second commit'

					#现在,主分支"master"下有两个历史版本
					www@qingsword.com:~/BranchDemo$ git log --oneline
					c9fb17d second commit
					aca49e1 first commit

					#以最新的历史版本为基础创建一个分支"branch_A"
					www@qingsword.com:~/BranchDemo$ git branch branch_A

					#查看分支列表,分支名称前带"*"号,表示当前所在分支
					www@qingsword.com:~/BranchDemo$ git branch
					  branch_A
					* master

					#切换到分支"branch_A",切换分支命令与文件版本回滚命令最大的区别是,文件版本回滚命令在checkout后面有两个"-"符号
					www@qingsword.com:~/BranchDemo$ git checkout branch_A 
					切换到分支 'branch_A'

					www@qingsword.com:~/BranchDemo$ git branch
					* branch_A
					  master

					#切换回"master"分支
					www@qingsword.com:~/BranchDemo$ git checkout master 
					切换到分支 'master'

					#使用"-b"参数,将"创建分支"和"切换分支"合并成一条指令,即创建后自动切换到该分支下
					www@qingsword.com:~/BranchDemo$ git checkout -b branch_B
					切换到一个新分支 'branch_B'

					www@qingsword.com:~/BranchDemo$ git branch
					  branch_A
					* branch_B
					  master
					

b.合并分支

首先使用"branch_B"这个分支来演示一次"Fast forward"合并:

					#接着上面的实验,当前处于"branch_B"分支下,给"file2"添加几个字符,并且提交这次修改
					www@qingsword.com:~/BranchDemo$ echo ddd >> file2
					www@qingsword.com:~/BranchDemo$ git add file2
					www@qingsword.com:~/BranchDemo$ git commit -m 'branch_B first commit'
					[branch_B bec5d00] branch_B first commit

					#在"branch_B"分支下查看历史版本列表
					www@qingsword.com:~/BranchDemo$ git log --oneline
					bec5d00 branch_B first commit
					c9fb17d second commit
					aca49e1 first commit

					#切换回"master"分支
					www@qingsword.com:~/BranchDemo$ git checkout master
					切换到分支 'master'

					#查看"master"分支历史版本,并没有"bec5d00 branch_B first commit"这条记录,根据第1小节的说明也不难理解,在没有合并前,主分支和其他分支是并行存在的
					www@qingsword.com:~/BranchDemo$ git log --oneline
					c9fb17d second commit
					aca49e1 first commit

					#将"branch_B"分支与主分支合并,因为主分支并没有做出修改,所以git选择"Fast-forward"方式合并,注意输出中显示"更新 c9fb17d..bec5d00",也就是直接将"branch_B"中的最新历史版本保存为主分支的最新历史版本
					www@qingsword.com:~/BranchDemo$ git merge branch_B
					更新 c9fb17d..bec5d00
					Fast-forward
					 file2 | 1 +
					 1 file changed, 1 insertion(+)

					#再次查看主分支的历史版本列表,多出来一个历史记录
					www@qingsword.com:~/BranchDemo$ git log --oneline
					bec5d00 branch_B first commit
					c9fb17d second commit
					aca49e1 first commit

					#合并分支后本不代表"branch_B"分支就不存在了,它还是存在的,直到我们手动删除为止,在实际环境中,也应该删除这些被合并了的分支,分支删除操作稍后会介绍
					www@qingsword.com:~/BranchDemo$ git branch
					  branch_A
					  branch_B
					* master
					

下面使用"branch_A"分支来演示"3-way"这种合并方式,经过上面的操作,现在主分支已经发生了变化,多出一个历史版本,而"branch_A"分支还是基于"c9fb17d second commit"这个历史版本:

					#切换到"branch_A"分支,修改"file1"并提交一个新的版本
					www@qingsword.com:~/BranchDemo$ git checkout branch_A
					切换到分支 'branch_A'
					www@qingsword.com:~/BranchDemo$ echo eee >> file1
					www@qingsword.com:~/BranchDemo$ git add file1
					www@qingsword.com:~/BranchDemo$ git commit -m 'branch_A first commit'
					[branch_A cfe93e4] branch_A first commit

					#"branch_A"分支的历史版本链
					www@qingsword.com:~/BranchDemo$ git log --oneline
					cfe93e4 branch_A first commit
					c9fb17d second commit
					aca49e1 first commit

					#切换回"master"分支
					www@qingsword.com:~/BranchDemo$ git checkout master
					切换到分支 'master'

					#将"branch_A"分支合并到主分支中
					www@qingsword.com:~/BranchDemo$ git merge branch_A 

					#这一次使用的是"3-way"合并方式,最明显的特点是,会弹出一个vim界面,让你输入一段描述文本,作为合并后新创建的历史版本的说明
					Merge made by the 'recursive' strategy.
					 file1 | 1 +
					 1 file changed, 1 insertion(+)

					 #查看合并后主分支的历史版本链,根据第一小节的分析不难理解,为什么多出两个历史版本
					www@qingsword.com:~/BranchDemo$ git log --oneline
					db6deb3 Merge branch 'branch_A'
					cfe93e4 branch_A first commit
					bec5d00 branch_B first commit
					c9fb17d second commit
					aca49e1 first commit
					

c.合并有冲突的分支

在上面的例子中,都只是给文件追加文本,所以文本之间并不会出现冲突的情况,接着上面的实验:

					#新建并切换到"branch_C"分支
					www@qingsword.com:~/BranchDemo$ git checkout -b branch_C
					切换到一个新分支 'branch_C'

					#在这个分支下,修改"file2"追加一行"xxx",并提交这次改动
					www@qingsword.com:~/BranchDemo$ echo xxx >> file2
					www@qingsword.com:~/BranchDemo$ git add file2
					www@qingsword.com:~/BranchDemo$ git commit -m 'branch_C first commit'
					[branch_C 664a102] branch_C first commit

					#此时"branch_C"分支的历史版本链如下
					www@qingsword.com:~/BranchDemo$ git log --oneline
					664a102 branch_C first commit
					db6deb3 Merge branch 'branch_A'
					cfe93e4 branch_A first commit
					bec5d00 branch_B first commit
					c9fb17d second commit
					aca49e1 first commit

					#切换回"master"分支
					www@qingsword.com:~/BranchDemo$ git checkout master 

					#对同一个文件的同一行做出不同的修改,在"master"分支中,修改"file2"追加一行"ooo",并提交
					www@qingsword.com:~/BranchDemo$ echo ooo >> file2
					www@qingsword.com:~/BranchDemo$ git commit -am 'master update file2'
					[master 243ad07] master update file2

					#此时"master"分支的历史版本链如下
					www@qingsword.com:~/BranchDemo$ git log --oneline
					243ad07 master update file2
					db6deb3 Merge branch 'branch_A'
					cfe93e4 branch_A first commit
					bec5d00 branch_B first commit
					c9fb17d second commit
					aca49e1 first commit

					#将"branch_C"分支合并到"master"时报错,提示"file2"存在冲突,一般这种代码冲突都需要手动合并并且再次提交
					www@qingsword.com:~/BranchDemo$ git merge branch_C
					自动合并 file2
					冲突(内容):合并冲突于 file2
					自动合并失败,修正冲突然后提交修正的结果。

					#查看file2的内容,可以看到"======="隔开的上半部分,是HEAD(即"master"分支,在运行merge命令时检出的分支)中的内容,下半部分是在"branch_C"分支中的内容
					www@qingsword.com:~/BranchDemo$ more file2
					bbb
					ddd
					<<<<<<< HEAD
					ooo
					=======
					xxx
					>>>>>>> branch_C

					#需要使用编辑器手动修改这几行的内容
					www@qingsword.com:~/BranchDemo$ vim file2

					#例如改成下面的样子,需要手动将"冲突说明"信息删除(就是文件中"<<<<<<<HEAD"这些信息)
					www@qingsword.com:~/BranchDemo$ more file2
					bbb
					ddd
					ooo
					xxx
					
					#手动修改完这些冲突后,提交这些修改,就完成了与"branch_C"分支的合并,有些朋友可能会觉得很奇怪,合并的时候不是报错了吗,为什么这里说完成了合并?实际上,两个分之间没有冲突的地方已经被自动合并了,只有产生冲突的地方没有完全合并,而现在手动修改了这些冲突后再次提交,也是就完成了全部的合并
					www@qingsword.com:~/BranchDemo$ git commit -am 'merge branch_C'
					

d.删除分支

上面我们创建了三个分支,现在演示如何删除它们:

					#当前在"master"分支中
					www@qingsword.com:~/BranchDemo$ git branch
					  branch_A
					  branch_B
					  branch_C
					* master

					#分别删除前面的三个分支,只需要使用"-d"参数
					www@qingsword.com:~/BranchDemo$ git branch -d branch_A
					已删除分支 branch_A(曾为 cfe93e4)。
					www@qingsword.com:~/BranchDemo$ git branch -d branch_B
					已删除分支 branch_B(曾为 bec5d00)。
					www@qingsword.com:~/BranchDemo$ git branch -d branch_C
					已删除分支 branch_C(曾为 664a102)。

					#只有当分支被正常合并到主分支"master"中后,才能使用这种删除方式,新建一个分支"branch_D",并在其中完成一次文件修改提交,在合并之前尝试删除它
					www@qingsword.com:~/BranchDemo$ git checkout -b branch_D
					切换到一个新分支 'branch_D'
					www@qingsword.com:~/BranchDemo$ echo qqq >> file2
					www@qingsword.com:~/BranchDemo$ git commit -am 'branch_D first commit'

					#切换回"master"分支,尝试删除"branch_D",结果报错了,提示"branch_D"分支并没有被合并
					www@qingsword.com:~/BranchDemo$ git checkout master
					切换到分支 'master'
					www@qingsword.com:~/BranchDemo$ git branch -d branch_D 
					error: 分支 'branch_D' 没有完全合并。
					如果您确认要删除它,执行 'git branch -D branch_D'。

					#如果想删除还没有被合并的分支,请使用大写的D参数,这将强制删除这个分支,如下
					www@qingsword.com:~/BranchDemo$ git branch -D branch_D 
					已删除分支 branch_D(曾为 4c07a04)。