分支管理是Git最强大的功能之一。
每一次提交,Git都把他们串成一条时间线,这条时间线就是一个分支。在之前的例子中,我们都只有一条时间线。在Git中,这个分支叫做主分支,也就是master分支。
对于先从github上创建再clone到本地的仓库,主分支被称为main分支。
对于先从本地创建,再push到远程仓库,主分支称为master分支。
一、创建与合并分支
一开始的时候,master 分支是一条线,Git 用 master 指向最新的提交,再用 HEAD 指向 master,就能确定当前分支,以及当前分支的提交点。
上图的圆圈表示的是每一次提交,每次提交,master都会向前移动一步。
分支的简单操作
我们分步对分支进行学习,先理解概念,再说指令:
1.当我们新建一个分支,比如dev分支时,Git将会新建一个指针叫dev,指向当前提交,也就是目前master所指向的提交。
2.如果我们使用指令将HEAD指向dev指针,则可以实现分支的切换。
3.HEAD指向dev分支之后,我们所做的提交,都将指向保存在dev分支,也就是说dev指针会在原来的基础上不断前进,然而master分支却止步不前。如下图所示:
4.当我们在dev分支上的工作完成之后,就可以将dev合并到master分支。最简单的方法就是让master也指向dev分支下的最新提交,就完成了dev和master的分支合并:
5.合并完成后就可以将dev分支删除:
实际指令:
1.新建一个分支dev,并且切换到新的分支:
git checkout -b dev
这里边-b就是 branch,分支的意思。
为了更明显地看到master,所以我们用了第一个项目git_study。
2.上述指令完成了创建分支、并切换的操作,也可以拆分为先创建,后切换两条指令:
git branch dev
git checkout dev
可以用以下指令查看当前分支状态:
git branch
从图中的星号可以看出,当前所在的分支是dev分支。
3.然后,我们就可以在 dev 分支上正常提交,比如对 readme.txt 做个修改,加上一行:
然后在当前dev分支,执行git add 和git commit操作进行提交。
4.切换到master分支,合并dev上所做的更改操作:
git checkout master
切换到master分支后,可以看到master分支下的readme.txt还是上一次提交前的内容。
现在让master也指向最新的提交,将dev分支下的操作合并到master分支下。
git merge dev
融合之后再来看readme.txt这个文件,发现dev分支下所做的更改已经合并到了master分支下:
注意到上面的 Fast-forward 信息,Git 告诉我们,这次合并是快进模式,也就是直接把master指向 dev 的当前提交,所以合并速度非常快。但是如果说我们在master分支下也做了更改,并且也提交了。那么需要有另外的方式进行合并。
5.合并完成后,dev已经完成了它的使命,就可以将dev分支删除:
Git鼓励使用大量分支:
查看分支:git branch
创建分支:git branch dev
切换分支:git checkout dev
创建的同时切换分支:git checkout -b dev
合并某分支到当前分支:git merge dev
删除分支:git branch -d dev
二、解决冲突
如果说我们在dev分支和master分支上都做了更改和提交,那么在合并更改时就无法使用Fast-forward 快进合并模式。
创建新的分支feature1来进行上述操作。
git checkout -b feature1
在该分支下,我们将readme.txt的最后一句话增加一些内容,然后添加并提交到feature1分支。
我们再回到master分支下,在readme.txt的最后增加一些内容,然后添加并提交到master分支。
显而易见的是,当前master分支和feature1分支下的readme.txt有“AND”和“&”的差别。
现在两个分支都有了各自的提交,时间线变成了这样:
如果我们直接进行合并:
git merge feature1
将会提示自动合并失败,需要修正冲突然后提交修正的结果:
查看git status也会告诉我们有冲突的文件:
此时的文件中,已经自动有两个选择,可以选择接受哪个版本的传入。也可以使用VScode中的分支管理插件,选择接受哪个版本的传入。
我们不接受上述的两个输入,在txt文档中更改为 and。然后再在master分支下添加并提交:
此时,时间线变成如下图所示:
使用如下指令也可以查看分支合并情况:
git log --graph --pretty=oneline --abbrev-commit
上述指令 oneline是让每次提交都保持一行,abbrev是让commit-id缩写。
git log是从往上看的,可以看出时间线收束了。同时也可以看出本地的master分支要比远程(origin/master)分支超前两个提交。
最后删除feature1分支:
git branch -d feature1
三、分支管理策略
上边的一和二两种分支合并方式,第一种是不存在冲突,第二种是存在冲突。
当两个分支不存在冲突时,Git会默认采用Fast forward的合并模式(简称ff)。合并后我们再删除分支。
当两个分支存在冲突时,输入git merge会显示存在冲突,我们需要根据提示手动解决冲突后再提交。提交后删除分支。
细心观察上述的两个合并模式,会发现,第一种合并过程虽然快,但是会丢失分支上的提交信息。第二种合并过程能够保留分支上的提交信息,即使删除了feature1这个分支指针。提交信息可以通过图上的○看到。
依据提交信息,我们就可以使用git reset --hard 指令切换到对应的提交版本(详见文件1)。很多时候,在第一种情况下,我们也不想丢失dev分支下的提交。
解决方案
对此我们的解决方案是通过强制禁用Fast forward的工作模式,这样Git在merge时就会生成一个新的commit,这样就可以在分支历史上查看到分支信息。
要想强制禁用ff的工作模式,需要在git merge时添加 –no-ff 的参数。同时因为会新建一个commit,所以要加上 -m为提交添加注释。
下面来进行测试,首先新建并切换到分支dev,在dev下对readme.txt文件进行更改,然后添加并提交。
切换到master分支,然后使用如下指令强制禁用Fast forward合并模式:
git merge --no-ff -m "merge with no-ff" dev
使用上述指令,我们在进行合并的时候,将dev分支下的提交保存下来。查看目前的分支如下图所示:
完成上述合并后,使用git checkout master切换到 master分支。然后使用:git branch -d dev指令删除dev分支后,可以看到commit-id为 059b772的提交仍然存在。
禁用快速合并后,分支上的提交得以保存。这样就能看出来曾经做过合并,而快速合并之后删掉分支,就看不出来曾经做过合并。
在实际的开发过程中,master分支是主分支,应当是非常稳定的,也就是仅仅用来发布新版本,平时不能在上面干活。所有的开发活动都是在dev分支上进行的。也就是说dev分支是不稳定的。到某个时候,比如dev上的v1.0版本开发出来了,再把dev合并到master上,在master上发布1.0版本。在项目开发时,不同的开发人员都在dev分支上开发,每个人都有自己的分支,根据开发情况往dev上合并。等dev到了某一个版本,再合并到master上进行发布。团队合作的分支看起来是这样的:
四、Bug分支
当程序开发人员在各自的dev分支上进行工作时,突然发现master上有一个bug急需解决,想要创建一个issue-1分支来排bug。但是dev上的开发还没有完成,还不想提交。比如我们对dev分支下的readme.txt做出如下更改:
如果我们不想提交,而只是切换到master分支下,来创建issue-101分支,就会发现master分支下的readme.txt也有这句话。
我们通过如下指令对比(详见文件一)当前的readme.txt和版本库中最新的readme有何不同。
这样就会混乱。切换分支前需要提交
这个时候,才是正常的master状态。也就是说如果从dev切换到master前不提交,那么dev分支下的更改将直接在master中保留,如果是这样的话,git就失去了它的意义。
但是为了让dev和master两个分支分离开,我们也做出了牺牲:
在dev还没有开发完成的时候,提前进行了提交。
那么有没有什么办法可以将dev分支上进行的操作先藏起来,等我们在主分支上新建issue-1分支解决bug后,再展开dev分支上的操作?答案是有的:
git stash
stash就是储藏的意思,将当前分支下的活动先藏起来,让工作区保持干净。
下图可以看出,我们在dev分支下的工作内容被收起来了。
通过如下指令,可以查看当前分支下有多少个储藏室:
git stash list
在收起来dev分支下所做的更改后,切换到想要解决bug的分支,比如master分支。
新建分支issue-01,解决bug后,添加并提交到issue-01分支,将该分支与想要解决bug的分支进行合并。完成bug的排查。
修复bug后,删除issue-1分支
删除bug分支后,再切换到dev分支,展开之前的储藏,继续进行开发工作。
展开储藏的工作
对于同一个分支,可以进行多次储藏,通过如下指令可以查看储藏列表:
git stash list
展开储藏的工作有两种方式
第一种是:
gut stash apply stash@{}
这种恢复并不删除stash的内容,需要使用如下指令进行删除
git stash drop stash@{}
第二种是:恢复最近stash的同时也删除stash的内容
git stash pop
可以看出工作区回到了开发状态。
综上所述,当手头工作没有完成时,先把工作现场 git stash 一下,然后去修复 bug,修复后,再 git stash pop,回到工作现场。
五、Feature分支
为了不扰乱主分支,当开发实验性质的代码时,最好新建一个feature分支,在上面开发,然后合并,最后删除该分支。
但是有的时候,原本设想的feature分支后来又不需要了,这个时候,需要切换到master分支,然后删除feature分支,注意此时的feature分支还没有合并到master分支,如果我们直接删除该分支,将如下图所示:
Git将会提示无法删除没有合并的git分支,这个时候需要按照提示中的指令:
git branch -D feature1
强制删除feature1分支,而不让它和master合并。结果如下图所示:
六、分支抓取与推送
现在我们换一个文件夹并且将git_study项目克隆到该文件夹下,来模拟别人或者自己在另一台电脑上,对项目的更改和推送操作。
我们新建了文件夹git_study_2,在该文件夹下使用git clone,这样git_study项目被克隆到了本地。
当我们从远程仓库克隆时,实际上 Git 自动把本地的 master 分支和远程的 master 分支对应起来了,并且,远程仓库的默认名称是 origin。
使用指令:
git remote -v
可以查看当前项目抓取和推送的 origin地址:
如果没有推送权限,则只能看到抓取地址。
6.1 推送分支
推送分支,就是将本地的分支推送到远程仓库。如果要推送master分支,就使用:
git push origin master
如果要推送dev分支,就使用:
git push origin dev
6.2抓取分支
多个开发人员对同一个项目进行开发时,都会往master和dev分支上推送自己的修改。
这就可能遇到类似于在本地master分支和dev分支出现合并冲突的地方,只不过这种情况下是远程的master或dev分支与本地的某个分支发生了冲突。
当2号人员,将我们的git_study项目克隆到他的电脑上后,git checkout -b dev新建了一个名为dev的分支,并提交了一些更改。
在更改完成后,2号人员想要将自己的更改也推送到远程仓库(前提是git remote -v有推送权限)。
git push origin dev
然后再将远程分支dev和本地dev关联起来:
git branch --set-upstream-to=origin/dev dev
(注意这个推送可能会失效,最好是在本地新建dev分支时,使用如下指令,直接创建远程origin仓库的dev分支到本地
git checkout -b dev origin/dev
)
这个时候,使用我们第一个git_study还是这状态:
假设1号人员不知道2号人员新建了dev分支,并且添加了一个hello.py文件。这样的话1号人员的项目就有两处与远程仓库不同,一处是readme.txt不同,一处是多了一个python文件。
当1号人员想要推送本地dev到远程仓库origin时,会遇到如下错误:
git拒绝了1号人员的提交,并且说明了原因:
另外一个仓库已经向该引用进行了推送,再次推送前,需要县整合远程变更。
那么我们就需要按照git的提示,使用如下指令将最新的代码从远程仓库origin/dev上抓下来,然后先在本地进行合并,再去推送。
git pull
可以看到git pull也失败了,原因是当前分支没有跟踪信息,这里的原因与文件3中将远程分支和本地分支关联的原因相同。只有关联起来,本地的dev才能上传到远程的dev。
按照Git的提示:如果您想要为此分支创建跟踪信息,您可以执行:
git branch --set-upstream-to=origin/<分支> dev
实际输入:
git branch --set-upstream-to=origin/dev dev
显示已经设置好了跟踪,接下里再使用:
git pull
拉取远程仓库origin的dev仓库进行合并。
git提示说明:致命错误,需要制定如何调和偏离的分支。
根据上述提示我们使用合并策略来进行git pull:
git pull --no-rebase
输入如下指令查看当前分支状态:
git log --graph --pretty=oneline --abbrev-commit
也可以通过VScode查看分支状态:
可以看到经过:
git branch --set-upstream-to=origin/dev dev
设置关联。再经过:
git pull --no-rebase
git pull --rebase
使用–no-rebase会产生额外的合并提交。
之后,远程库已经合并到了本地。
此时再进行提交,就可以顺利地上传到远程库:
git push origin dev
至此1号人员已经让自己的本地仓库保持了最新,再到2号人员的仓库下拉取最新代码:
这样远程仓库、git_study、git_study_2的代码都是最新状态。
对于1号人员,我们对本地的git_study仓库进行分支合并merge,并删除dev分支,然后推送到远程代码仓库,再通过如下指令删除远程代码仓库的dev分支,完成git_study项目的开发。
git push origin --delete dev
对于2号人员,因为远程仓库已经删除了dev分支,所以再进行git pull将会提示找不到远程分支,这时候切换到master分支,然后git pull更新当前master分支,再删除dev分支,这样也完成了git_study项目的同步。
总之:
1. 本地新建的分支如果不推送到远程,对于其他人是不可见的;
2. 从本地新建分支如果要推送到远程,使用 git push origin branch-name;
2.1 如果远程没有这个分支,则会新建(注意网络是否通常)新建完后要使用git branch --set-upstream branch-name origin/branch-name将这两个分支关联(即使是上传了,也不一定关联到了);
2.2 如果远程有这个分支,且比本地的新(别的成员上传的),则使用git pull,如果有冲突,就先处理冲突,然后上传。