[Git使用手册]-5-Git分支管理
引言
本文将介绍git中一个很重要的特性——分支;如今,几乎所有的版本控制系统都支持"分支"这种特性,但git的实现方式与其他的版本控制系统有明显的区别,它不会拷贝整个项目的副本作为"分支"的基础,而是使用类似"快照"的功能,这样做的好处是,节约了创建"分支"的时间成本;所以在git中,可以随时创建大量"分支"来应对不同方向的研发需求。
文章目录
0×1.分支模型图
当使用"git commit"提交修改时,Git计算每一个子目录的校验和,然后在Git仓库中将这些目录保存为树(tree)对象。之后Git创建一个新的历史版本,除了包含相关提交信息以外,还包含着指向这个树对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容。这在上一篇文章历史版本模型图中已经介绍过。
创建分支:
创建一个新的分支,实际上就是创建当前最新历史版本的一个快照,在此分支上的修改和提交不会影响到主分支,直到将这个分支合并到主分支为止,如下图所示:
假设一个仓库目录进行了两次提交,创建了"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":
"3-way"的合并方式就显得复杂的多,如下图所示,在主分支的"C2"处创建了一个新的分支,之后新分支和主分支都做出了修改,新分支更新版本到"C5",主分支更新版本到"C3",此时如果想将新分支合并到主分支,需要执行三步操作:首先用"C5"与"C2"比较,得到一张修改列表;再用"C3"与"C2"比较,同样得到一张修改列表;最后,将这两张修改列表进行比较,根据比较结果创建一个新的历史版本"C6"来包含两个分支所有的变化,在主分支历史版本链中将会增加两个历史版本"C5"与"C6","C5"将指向"C3","C6"指向"C5"完成合并:
0×2.分支管理实例
a.创建分支
#新建一个目录并将它初始化成git仓库目录
987@hk987.xyz:~$ mkdir BranchDemo
987@hk987.xyz:~$ cd BranchDemo/
987@hk987.xyz:~/BranchDemo$ git init
初始化空的 Git 版本库于 /home/987/BranchDemo/.git/
#在这个仓库中创建两个文件,写入数据并提交
987@hk987.xyz:~/BranchDemo$ touch file1
987@hk987.xyz:~/BranchDemo$ echo aaa >> file1
987@hk987.xyz:~/BranchDemo$ touch file2
987@hk987.xyz:~/BranchDemo$ echo bbb >> file2
987@hk987.xyz:~/BranchDemo$ git add file1 file2
987@hk987.xyz:~/BranchDemo$ git commit -m 'first commit'
#修改其中一个文件的内容,第二次提交
987@hk987.xyz:~/BranchDemo$ echo ccc >> file1
987@hk987.xyz:~/BranchDemo$ git commit -am 'second commit'
#现在,主分支"master"下有两个历史版本
987@hk987.xyz:~/BranchDemo$ git log --oneline
c9fb17d second commit
aca49e1 first commit
#以最新的历史版本为基础创建一个分支"branch_A"
987@hk987.xyz:~/BranchDemo$ git branch branch_A
#查看分支列表,分支名称前带"*"号,表示当前所在分支
987@hk987.xyz:~/BranchDemo$ git branch
branch_A
* master
#切换到分支"branch_A",切换分支命令与文件版本回滚命令最大的区别是,文件版本回滚命令在checkout后面有两个"-"符号
987@hk987.xyz:~/BranchDemo$ git checkout branch_A
切换到分支 'branch_A'
987@hk987.xyz:~/BranchDemo$ git branch
* branch_A
master
#切换回"master"分支
987@hk987.xyz:~/BranchDemo$ git checkout master
切换到分支 'master'
#使用"-b"参数,将"创建分支"和"切换分支"合并成一条指令,即创建后自动切换到该分支下
987@hk987.xyz:~/BranchDemo$ git checkout -b branch_B
切换到一个新分支 'branch_B'
987@hk987.xyz:~/BranchDemo$ git branch
branch_A
* branch_B
master
b.合并分支
首先使用"branch_B"这个分支来演示一次"Fast forward"合并:
#接着上面的实验,当前处于"branch_B"分支下,给"file2"添加几个字符,并且提交这次修改
987@hk987.xyz:~/BranchDemo$ echo ddd >> file2
987@hk987.xyz:~/BranchDemo$ git add file2
987@hk987.xyz:~/BranchDemo$ git commit -m 'branch_B first commit'
[branch_B bec5d00] branch_B first commit
#在"branch_B"分支下查看历史版本列表
987@hk987.xyz:~/BranchDemo$ git log --oneline
bec5d00 branch_B first commit
c9fb17d second commit
aca49e1 first commit
#切换回"master"分支
987@hk987.xyz:~/BranchDemo$ git checkout master
切换到分支 'master'
#查看"master"分支历史版本,并没有"bec5d00 branch_B first commit"这条记录,根据第1小节的说明也不难理解,在没有合并前,主分支和其他分支是并行存在的
987@hk987.xyz:~/BranchDemo$ git log --oneline
c9fb17d second commit
aca49e1 first commit
#将"branch_B"分支与主分支合并,因为主分支并没有做出修改,所以git选择"Fast-forward"方式合并,注意输出中显示"更新 c9fb17d..bec5d00",也就是直接将"branch_B"中的最新历史版本保存为主分支的最新历史版本
987@hk987.xyz:~/BranchDemo$ git merge branch_B
更新 c9fb17d..bec5d00
Fast-forward
file2 | 1 +
1 file changed, 1 insertion(+)
#再次查看主分支的历史版本列表,多出来一个历史记录
987@hk987.xyz:~/BranchDemo$ git log --oneline
bec5d00 branch_B first commit
c9fb17d second commit
aca49e1 first commit
#合并分支后本不代表"branch_B"分支就不存在了,它还是存在的,直到我们手动删除为止,在实际环境中,也应该删除这些被合并了的分支,分支删除操作稍后会介绍
987@hk987.xyz:~/BranchDemo$ git branch
branch_A
branch_B
* master
下面使用"branch_A"分支来演示"3-way"这种合并方式,经过上面的操作,现在主分支已经发生了变化,多出一个历史版本,而"branch_A"分支还是基于"c9fb17d second commit"这个历史版本:
#切换到"branch_A"分支,修改"file1"并提交一个新的版本
987@hk987.xyz:~/BranchDemo$ git checkout branch_A
切换到分支 'branch_A'
987@hk987.xyz:~/BranchDemo$ echo eee >> file1
987@hk987.xyz:~/BranchDemo$ git add file1
987@hk987.xyz:~/BranchDemo$ git commit -m 'branch_A first commit'
[branch_A cfe93e4] branch_A first commit
#"branch_A"分支的历史版本链
987@hk987.xyz:~/BranchDemo$ git log --oneline
cfe93e4 branch_A first commit
c9fb17d second commit
aca49e1 first commit
#切换回"master"分支
987@hk987.xyz:~/BranchDemo$ git checkout master
切换到分支 'master'
#将"branch_A"分支合并到主分支中
987@hk987.xyz:~/BranchDemo$ git merge branch_A
#这一次使用的是"3-way"合并方式,最明显的特点是,会弹出一个vim界面,让你输入一段描述文本,作为合并后新创建的历史版本的说明
Merge made by the 'recursive' strategy.
file1 | 1 +
1 file changed, 1 insertion(+)
#查看合并后主分支的历史版本链,根据第一小节的分析不难理解,为什么多出两个历史版本
987@hk987.xyz:~/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"分支
987@hk987.xyz:~/BranchDemo$ git checkout -b branch_C
切换到一个新分支 'branch_C'
#在这个分支下,修改"file2"追加一行"xxx",并提交这次改动
987@hk987.xyz:~/BranchDemo$ echo xxx >> file2
987@hk987.xyz:~/BranchDemo$ git add file2
987@hk987.xyz:~/BranchDemo$ git commit -m 'branch_C first commit'
[branch_C 664a102] branch_C first commit
#此时"branch_C"分支的历史版本链如下
987@hk987.xyz:~/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"分支
987@hk987.xyz:~/BranchDemo$ git checkout master
#对同一个文件的同一行做出不同的修改,在"master"分支中,修改"file2"追加一行"ooo",并提交
987@hk987.xyz:~/BranchDemo$ echo ooo >> file2
987@hk987.xyz:~/BranchDemo$ git commit -am 'master update file2'
[master 243ad07] master update file2
#此时"master"分支的历史版本链如下
987@hk987.xyz:~/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"存在冲突,一般这种代码冲突都需要手动合并并且再次提交
987@hk987.xyz:~/BranchDemo$ git merge branch_C
自动合并 file2
冲突(内容):合并冲突于 file2
自动合并失败,修正冲突然后提交修正的结果。
#查看file2的内容,可以看到"======="隔开的上半部分,是HEAD(即"master"分支,在运行merge命令时检出的分支)中的内容,下半部分是在"branch_C"分支中的内容
987@hk987.xyz:~/BranchDemo$ more file2
bbb
ddd
<<<<<<< HEAD
ooo
=======
xxx
>>>>>>> branch_C
#需要使用编辑器手动修改这几行的内容
987@hk987.xyz:~/BranchDemo$ vim file2
#例如改成下面的样子,需要手动将"冲突说明"信息删除(就是文件中"<<<<<<<HEAD"这些信息)
987@hk987.xyz:~/BranchDemo$ more file2
bbb
ddd
ooo
xxx
#手动修改完这些冲突后,提交这些修改,就完成了与"branch_C"分支的合并,有些朋友可能会觉得很奇怪,合并的时候不是报错了吗,为什么这里说完成了合并?实际上,两个分之间没有冲突的地方已经被自动合并了,只有产生冲突的地方没有完全合并,而现在手动修改了这些冲突后再次提交,也是就完成了全部的合并
987@hk987.xyz:~/BranchDemo$ git commit -am 'merge branch_C'
d.删除分支
上面我们创建了三个分支,现在演示如何删除它们:
#当前在"master"分支中
987@hk987.xyz:~/BranchDemo$ git branch
branch_A
branch_B
branch_C
* master
#分别删除前面的三个分支,只需要使用"-d"参数
987@hk987.xyz:~/BranchDemo$ git branch -d branch_A
已删除分支 branch_A(曾为 cfe93e4)。
987@hk987.xyz:~/BranchDemo$ git branch -d branch_B
已删除分支 branch_B(曾为 bec5d00)。
987@hk987.xyz:~/BranchDemo$ git branch -d branch_C
已删除分支 branch_C(曾为 664a102)。
#只有当分支被正常合并到主分支"master"中后,才能使用这种删除方式,新建一个分支"branch_D",并在其中完成一次文件修改提交,在合并之前尝试删除它
987@hk987.xyz:~/BranchDemo$ git checkout -b branch_D
切换到一个新分支 'branch_D'
987@hk987.xyz:~/BranchDemo$ echo qqq >> file2
987@hk987.xyz:~/BranchDemo$ git commit -am 'branch_D first commit'
#切换回"master"分支,尝试删除"branch_D",结果报错了,提示"branch_D"分支并没有被合并
987@hk987.xyz:~/BranchDemo$ git checkout master
切换到分支 'master'
987@hk987.xyz:~/BranchDemo$ git branch -d branch_D
error: 分支 'branch_D' 没有完全合并。
如果您确认要删除它,执行 'git branch -D branch_D'。
#如果想删除还没有被合并的分支,请使用大写的D参数,这将强制删除这个分支,如下
987@hk987.xyz:~/BranchDemo$ git branch -D branch_D
已删除分支 branch_D(曾为 4c07a04)。