git分支
1、开始使用分支
在git中使用分支很简单,只要使用git branch
命令即可:
git branch
如果git branch
后面没有接任何参数,它仅会输出当前在这个项目中有哪些分支。git默认会设置一个名为master的分支,前面的星号(*)表示现在正在这个分支上。
1.1、新增分支
要增加一个分支,可在执行git branch命令时,在后面加上想要的分支的名称:
git branch cat
这样就新增了一个cat分支。再查看一下:
git branch
可以看出,的确多了一个分支,但当前分支还是在master上。
1.2、更改分支名称
如果觉得分支名取得不够响亮,可以随时更改,而且不会影响文件或目录。假设现在的分支有3个:
git branch
如果把cat分支改成tiger分支,使用的是-m
参数:
git branch -m cat tiger
看一下当前的分支:
git branch
即使是master分支也可以改,如把master改成slave:
git branch -m master slave
1.3、删除分支
当前共有3个分支,如果其中的dog分支不需要了,可以使用-d
参数来删除:
git branch -d dog
如果要删除的分支还没有被完全合并,git会有贴心小提示:
git branch -d tiger
因为tiger的内容还没有被合并,所以使用-d
参数无法将其删除。这时只需改用-d
参数即可将其强制删除:
git branch -d tiger
摘录来自: 高见龙. “git从入门到精通。” apple books.
1.4、切换分支
要切换分支,就是git checkout
:
git checkout tiger
看一下当前的分支状态:
git branch
可以看到,前面的那个星号已经移到tiger分支上了。
1.5、切换分支时
例如,上文已经切换到tiger分支,然后新增tiger1.txt和tiger2.txt文件,并commit:
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记录:
git log --oneline
可以看出,tiger分支的确比master分支多前进了两次commit。
再看一下文件列表:
ls -al
这时如果切换回原本的master分支:
git checkout master
再看一下文件列表:
刚才那两个文件不见了!别担心,其实它们都还在,只是在不同的分支而已,只要切换回cat分支,文件就会出现了。
1.6、要切换到哪个分支,首先要有那个分支
如果要切换到某个分支,这个分支必须要先存在,不然会发生错误:
如果没有这个分支,只要在给git checkout
分支命名的时候加上-b
参数就没问题了。如果这个分支本来就存在,git就会直接切换过去;如果不存在,git就会帮你创建一个,然后再切换过去:
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分支:
git checkout master
接下来,git会用master分支指向的那个commit的内容来更新暂存区及工作目录。因为master此时指向的commit并没有cat1.html和cat2.html这两个文件,所以“更新”之后,不管在暂存区还是在工作目录中都不会有这两个文件。同理,再次切换回cat分支时:
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)。但在切换分支之前所做的改动则会留在工作目录中,不受影响”。其中“在切换分支之前所做的改动则会留在工作目录中,不受影响”说的就是这件事。直接操作看看:
git checkout master
查看一下文件列表:
可以看到,刚刚新增的cat3.html文件还在。再看一下git的状态:
可以看到,git的状态与刚刚在cat分支时的状态是一样的,也就是说,切换分支并不会影响已经在工作目录中的那些改动。
3、合并分支
3.1、合并示例
在前面的例子中,从master分支开了一个cat分支,随后修改了index.html和新增了cat3.html文件,我们进行两次commit,现在大概是这个样子:
任务执行得差不多了,就要准备合并回来了。如果想要用master分支来合并cat分支,就要先切换回master分支:
git checkout master
接下来,使用git merge
命令合并分支:
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分支,可用如下命令:
git merge dog
执行这个命令时会弹出一个vim编辑器窗口,为了进行这次合并,git做出了这个额外的commit对象,这个commit会分别指向cat分支和dog分支,head随着cat分支往前,而dog分支停留在原地:
如果改由dog分支来合并cat分支,则使用如下命令:
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分支,它会提醒:“这个分支还没全部合并哦”。
$ 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这么贴心地提醒了,但这里仍然把它删除了:
$ git branch -d cat
deleted branch cat (was b174a5a).
这时的关联图如图:
上面不是已经删除cat分支了吗,怎么还在?这里再次跟大家说明一下分支的概念:分支只是一个指向某个commit的指标,删除这个指标并不会使那些commit消失。
所以,删掉分支后那些commit还在,只是因为你不知道或没有记下那些commit的sha-1值,所以不容易再拿来利用。原本领先master分支的那两个commit现在就像空气一样,虽然看不到,但它们是存在的。既然存在,那就把它们“接回来”吧:
$ git branch new_cat b174a5a
这个命令是“请帮我创建一个叫作new_cat的分支,让它指向b174a5a这个commit”,也就是再拿一张新的贴纸贴回去。看一下现在的分支:
$ git branch
* master
new_cat
切换过去试试看:
$ git checkout new_cat
switched to branch 'new_cat'
确认一下文件列表,失去的文件都回来了。
4.2、还没有把刚刚删除的那个cat分支的sha-1值记下来怎么办?查得到吗
可以用git reflog命令去查找,reflog默认会保留30天,所以30天内还找得到。
4.3、其实所谓的“合并分支”,合并的并不是“分支”
如果你对git分支的认识是正确的,就可以猜到下面这个命令在做什么:
$ 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”容易理解。
5、使用rebase合并
5.1、使用rebase合并分支
准备环境:
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这两个分支:
git rebase dog
用通俗的话来说,上述命令的含义大致就是“我(即cat分支)现在要重新定义我的参考基准,并且将使用dog分支作为我新的参考基准”。
rebase合并方式和一般的合并方式的一个很明显的区别,就是使用rebase方式合并分支,git不会做出一个专门用来合并的commit。
5.2、rebase原理
rebase的过程大概是这样的:
- 先将c68537这个commit接到053fb2这个commit上。因为c68537的上一层commit原本是e12d8e,现在要接到053fb2上,所以需要重新计算这个commit的sha-1值,重新做出一个新的commit对象35bc96。
- 再拿b174a5这个commit接到刚刚做出来的commit对象35bc96上。同理,因为b174a5这个commit要接到新的commit上,所以它会重新计算sha-1值,得到一个新的commit对象28a76d。
- 最后,原本的cat是指向b174a5这个commit的,现在转而指向最后做出来的那个新的commit对象28a76d。
- 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命令硬切回去:
git reset a9aa5bf --hard
这样一来,就会回到rebase前的状态了。
2、使用orig_head
在git中有另一个特别的记录点——orig_head。orig_head会记录“危险操作”之前head的位置。例如,分支合并或reset之类的都算是所谓的“危险操作”。通过这个记录点来取消这次rebase相对来说更简单:
git rebase dog
git reset orig_head --hard
实际也是回到a9aa5bf位置的commit。
6、合并冲突
6.1、发生冲突
git可以检查出简单的冲突,所以并不是改到同一个文件就一定会发生冲突,但改到同一行就会出现冲突。假设在cat分支改动了index.html文件的内容:
git add .
git commit index.html -m "cat branch update"
然后在dog分支也改动了index.html文件的内容:
git add .
git commit index.html -m "dog branch update"
这时进行合并,不管是一般的合并还是使用rebase进行合并,都会出现冲突。下面先使用一般的合并:
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分支的内容,顺便把那些标记修掉。最后内容如下:
改完后,切记把这个文件加回暂存区:
git add index.html
然后就可以commit并完成这一回合了:
git commit -m "conflict fixed"
6.3、处理非文本文件的冲突
因为上面的index.html文件是文本文件,所以git可以标记出发生冲突的点在哪里,用肉眼就能看得出来大概该怎样解决,但如果是图像文件之类的二进制文件该怎么办?例如,在cat分支和dog分支同时加了一张叫作cute_animal.jpg(可爱的动物)的图片,则合并时出现冲突的信息为:
$ 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分支的文件:
git checkout --ours cute_animal.jpg
如果要用dog分支,则使用--theirs
参数:
git checkout --theirs cute_animl.jpg
决定之后,像前面一样将内容加到暂存区,准备commit,然后结束这一回合。
7、从过去某个commit创建分支
历史记录如下:
要从add sql.的commit(62fb48e)中再做出新的分支,首先要回到那个commit的状态,这时使用的是git checkout
命令:
git checkout 62fb48e
从这段信息中可以看出,当前正处于detached head
状态。可以从现在这个commit开出新的分支。使用checkout命令的-b
参数直接创建分支并自动切换过去:
git checkout -b xxx
如果不想两步操作,也可以一行直接搞定:
git branch xxx 62fb48e
意思就是“请帮我在62fb48e这个commit上开一个xxx分支”。
发表评论