Git总结(二)

摘要

Git是一个免费的开源分布式版本控制系统,旨在快速高效的处理从小到大的所有项目。
本篇文章介绍Git中的分支概念和相关操作,主要依托网站https://learngitbranching.js.org/?demo=&locale=zh_CN,这个网站以动画的形式形象的介绍了分支的操作。此外,还将介绍远程仓库相关的概念,实际上,上一篇中就已经涉及到远程仓库的常见操作,这里将更详细的介绍。

分支和创建分支

我们知道,在Git中不断提交的记录形成一条版本时间线,例如在master分支中提交了若干次之后,形成了下图的时间线(提交的时候我把备注直接写成C1C2这种形式,相比于对比哈希值更便于观察版本关系)。
alt 版本图1
alt 版本图1

这样在master分支下提交了三次,现在我们要创建一个新的分支dev, 使用命令git branch dev:

1
git branch <newBranch>

这样新创建的分支dev和当前分支指向的提交记录一样,都是指向C2,就是这么简单,只是添加了一个指向当前最新提交的指针罢了。因此在Git中,分支的本质不过是一个指向提交对象的指针,这也是Git在版本控制系统中脱颖而出的特性之一,有些版本控制器创建分支需要将项目文件进行拷贝放置单独的目录下,这是很慢的。而在Git中仅仅是进行指针的操作。
需要注意的是图中带*号的是当前所处的分支,使用上述命令创建分支并不会改变当前所处的分支。
alt 版本图2
alt 版本图2

下列命令可以查看所有分支和切换分支:

1
2
git branch   //查看分支
git checkout <branchName> //切换至分支

还有一个常用的命令是创建分支的同时切换过去:

1
git checkout -b <newBranch>  //创建并切换至newBranch分支

对不需要的分支进行删除:

1
git branch -d <branchName>

分支合并

现在,我们在新分支下提交一次,先使用git checkout dev切换到dev分支,然后进行一次提交(提交前对文件进行些修改),提交之后的版本图如下:
alt 版本图3
alt 版本图3

我们看到master分支并没有前进,只有dev分支前进了一次提交,这是显然的,因为我们现在处在dev分支上进行了一次提交。注意这时我们切换回master分支进行一次提交,现在的情况是,两个分支各有一次新的提交,也就是说版本图发生了“分叉”(注意:两个分支进行的两个提交不能修改同一个文件,不然分支合并时会引起冲突):
alt 版本图4
alt 版本图4
alt 版本图4
dev分支来看,提交记录是从C2C3,而从master分支来看,提交记录为从C2C4;若两个分支做的工作为项目的不同部分(即未修改同一个文件),我们可以用以下命令将分支进行合并:

1
git merge <branchName>  //将branchName分支的内容合并至当前所处的分支

如使用在master分支用git merge devdev分支的内容合并过来:
alt 版本图5

这样master分支就包含了所有的修改了,但是dev分支依旧处在C3提交处没有改变。若想将dev分支也获得所有的修改,我们只需再切换回dev分支,然后把master分支合并过来即可git checkout devgit merge master
alt 版本图5

因为master分支继承自dev分支,所以合并的时候,Git什么都不用做,只需要将dev指针移动到master分支所指的提交记录就行了!
实际上,使用git merge命令只是分支合并的一种方式,常用的还有另外一种合并分支的方法:

1
git rebase <branchName>

rebase操作实际上是取出一系列的提交记录,“复制它们”,然后在另一个地方逐个放下去,使用rebase的优势是可以创建更加线性的提交历史。
还是用之前这张图举例:
alt 版本图4

master分支上使用git rebase dev,就把master分支的工作直接移到了dev的下面,就好像两个分支是顺序开发的一样,但实际上是并行开发的。
alt 版本图5
注意C4分支的提交记录依然是存在的(图中的透明节点),C4'rebasedev分支上的一个副本,现在的唯一问题是dev分支还没有更新,同样在dev分支上使用命令git rebase master更新:
alt 版本图6

由于master继承自dev,Git其实什么都不用做,只需要把dev分支引用移动到master指向的提交处!总的来说,使用rebase也可以达到合并分支的作用,它的优点就是提交历史很干净,都是线性的。当然它的缺点就是掩盖了真实的开发情况,把并行的分支开发记录成了线性的顺序开发。至于用什么方式进行合并,看个人习惯。

HEAD指针和相对引用

HEAD是对当前工作分支的一个别名指针,它总是指向当前分支上最近一次提交记录,也就是通常情况下HEAD是指向分支名的。如最开始使用master提交三次时,HEAD指针会一直跟着master移动,始终指向master
alt HEAD

但实际上,HEAD指针并不一定总是指向分支名,只是大多时候是这样,可以使用分支切换命令git checkoutHEAD指针进行移动,例如移动至某个提交记录上,这时候进入了HEAD分离状态。如将HEAD移动至C1上:git checkout [C1哈希值]
alt HEAD分离
即通常情况下是HEAD-->branch-->提交记录
进入分离状态之后就是HEAD-->提交记录

可以使用以下reset命令进行版本的回退:

1
git reset <pos>  //将当前分支指针置于pos处,这里的pos可以是提交哈希值或相对引用

通过指定版本哈希值比较麻烦,虽然Git只需要你输出哈希值的前几位。此外,Git还提供两种相对引用形式,一种是^,表示向上一个提交记录;另一种是~<num>,如HEAD~2表示HEAD指针的前2个提交记录,如git reset HEAD^:将当前分支回退至HEAD的前一次提交:
alt HEAD分离

但是使用reset进行提交的撤回对于远程版本是无效的,为了将撤销分享给远程版本, 有一个“曲线救国”的命令:

1
git revert <pos>  将当前分支指针置于pos的上一个版本的复制上,体现为提交一次新的更改

例如,git revert HEAD
alt HEAD分离
这里,实际上是提交了一个新的版本,只是这个版本和之前的C1版本一致,相当于回退了版本,这样的好处就是能够将这个回退分享至远程版本。

远程仓库操作

实际上远程仓库并不复杂,其本质上不过是本地仓库在Git服务器上的一份拷贝,上一篇实际上已经涉及到常用的远程仓库操作了,这里深入介绍相关远程操作的原理,各个命令执行时发生了什么。
为了进行远程仓库操作,首先需要执行clone获得远程仓库的一份拷贝:

1
git clone <gitAddress>  //从服务器地址处获取一份远程仓库的拷贝

alt HEAD分离

可以看到执行git clone之后本地仓库多出了一个分支origin/master;这个分支叫做远程分支,它本质上是一个处在本地仓库的分支,但是它有一些特殊的属性。
远程分支反映了远程仓库(在你上次和它通信时)的状态。远程分支有一个特殊的属性,就是当你想要切换至origin/master时,会进入分离HEAD状态,这个状态之前也已经提过;这样设定的目的就是让你无法在远程分支origin/master分支上进行操作,因为这个分支用于反应远程工作,只能通过远程操作更改。倘若想要切换至origin/master分支后进行一次提交,效果如下:

alt HEAD分离
alt HEAD分离

当切换至origin/master分支进行提交,进入了分离HEAD状态,之后进行提交时origin/master分支并不会前进,只是在当前HEAD指针上前进了一次提交,实际上在分离HEAD状态进行提交不会影响到任何一个分支。

接下来介绍一个从远程仓库获取数据的命令:

1
git fetch   //从远程仓库获取本地缺失的提交记录并更新远程分支

fetch命令就是做以下两件事情:

  1. 从远程仓库获取本地缺失的提交
  2. 更新本地仓库中的远程指针,如origin/master

下面是一个执行fetch命令的例子:
alt fetch

可以清楚的看到,执行完fetch命令之后,本地仓库将远程的C2,C3提交下载了下来,并将origin/master移动了;但是需要注意的是:本地master分支并没有移动,且本地工作区的文件也不会发生任何改变,不要误以为执行了fetch命令之后就和远程仓库同步了。
还要说明的一点就是,如果远程有多个分支,fetch命令会将所有分支中本地仓库没有的提交下载下来,并移动相应的远程指针。假如远程仓库还有一个dev分支,那么会将本地的origin/dev分支进行更新。所以,远程分支就是这样在和远程仓库互动的时候更新,因此说远程指针反映了远程仓库(在你上次和它通信时)的状态。

为了将远程仓库的提交更新到我们本地的工作中,可以执行完fetch命令之后再通过分支合并的方法,将远程分支合并。例如在上面的基础上执行git merge origin/master或者git rebase origin/master就会将远程分支的提交合并至master中。在日常操作中先拉取远程仓库提交再合并至本地分支是十分常见的操作,因此Git提供了一个专门的命令来干这件事情:

1
2
git pull  //拉取远程仓库分支并和本地分支合并
git pull --rebase //拉取远程仓库分支并和本地分支合并,合并时使用rebase方法,相当于fetch + rebase

实际上pull命令就是fetch命令和merge命令的组合。下面是这个过程:
alt pull

好了,现在已经知道如何将远程仓库的数据同步至本地了,那么如何将本地的提交分享至远程仓库呢?答案是使用push命令:

1
git push //将本地提交推送至远程仓库,并更新远程分支

push命令会将本地的新提交上传至指定的远程仓库并在远程仓库完成分支的合并,也就是push完成之后,别人就能够下载到你的提交了。
先看最简单的情况:
alt pull

执行完push之后本地仓库和远程仓库同步了,这就是最简单的提交情况。

实际工作时时常发生下面的情况,你在一周前clone了一个仓库,对其中的某个新功能进行开发,现在终于开发完成了,在本地仓库提交完成之后想要push至远程仓库;但不幸的是,你的一个同事在过去的一周里修改了许多代码,甚至包括一些你开发新功能时用的API。这时候Git不知道该怎么合并了,到底是放弃你的工作还是同事的工作呢?
实际上,这时候你直接push会发生错误,Git会强制要求你将现在的远程仓库代码在本地合并之后才能上传代码。因此,需要先执行pull命令,再执行push命令。
alt pull

可以看到,先执行pull的时候,将远程仓库的提交在本地进行了合并,然后再执行push时就直接将本地合并之后的记录一并提交了上去。实际上,为了合并时产生线性的提交记录,可以使用前面提到的pull --rebase命令。

关于远程仓库的操作就介绍到这里了,实际上pullpush还有一些参数可以用于指定拉取或推送的分支,这里就不介绍了。

参考链接

  1. https://learngitbranching.js.org/?demo=&locale=zh_CN
  2. 廖雪峰Git教程