彻底理解git本地与远程、push、pull、fetch

Aug 5, 2016


git用起来倒是很方便,但基础不牢的话用着用着就会出问题的。各方面都了解一点,还不足以拼凑起来形成能力。时间一长就会积累各种问题,克隆下来的分支怎么推不上去了啊,github上建的分支怎么本地看不到啊,远程分支怎么删除不了啊,乱七八糟的。

首先来明确本地仓库和远程仓库的概念。我们都知道git其实是分布式的版本控制系统,分布式的嘛,哪个是远程哪个是本地是相对而言的。你如果从我的仓库里clone了代码,我的仓库对你而言就是远程库,但是我有时候也要克隆他人的代码来学习一个,那我的仓库对他人来说又是个本地库。仓库(主机)就这一个,你说是远程库还是本地库?所以说这两个概念不是绝对的,有的时候要变通一下子。

首先说明一点,文章提到的远程主机远程仓库名,都是同一个东西,就是所有命令里面的origin。这个origin是git默认为远程的git服务器(git主机)取的别名,我们自己搭git服务器的时候可以不用这个名字的。我一般只用远程主机名来表示这个origin以免混淆。当然这个概念有的人就喜欢叫远程仓库,不喜欢叫远程主机,见仁见智了。真正统一名称的办法,还是要用洋文,人家是叫远程仓库的(remote repository)。

在仓库里进到某套代码的主分支去,新建分支,在新分支上开发,切回主分支,把项目进展从新分支合并进主分支…这些流程初学者也应该很熟练了。这篇文章主要想讲清楚远程分支和本地分支的一些东西。文章清晰可读是不够的,要清晰且可实践,所以还是得举个例子。

首先我在github上新建一个仓库learnrepo,现在这个仓库是空的:

图片

图片里已经说到了,我们可以直接克隆到本地,一行代码就搞定,但是为了提高大家的姿势水平这次用一种矫情的方式去给这个远程库添加文件。首先我再本地随意建了一个文件夹,创建了一个txt,随意写了点东西:

图片

这样,在打开git bash并切换到这个文件夹下,将其初始化为git控制下的目录,就是要用git init

图片

可以看到,.git文件夹已经出现了。现在这个目录正处于版本控制系统下了,注意目录名我换了一下变成repolearn,目录名并不一定要和远程库里面的一样。然后git add并commit,将刚才新建的文件加入git并提交,这里省略了。那么我现在就要把这个文件夹和远程库关联起来,这样我才能push得上去。先用remote add命令可以添加远程库。

图片

直接就添加好了。现在就把新的文件推上去。再次注意,git仓库建立的时候实际上已经有了自动建立的master分支,我们只要在首次push时把本地自动建立的master分支和远程的自动建立的master分支关联起来,这个关联用-u参数实现,以后再push就不用加-u了,因为关联已建立。

图片

看上图,set up to track就是建立了关联。这里顺带说一下push的格式是git push <远程主机名> <源分支地址>:<目的分支地址>,如果源分支与目的分支已经关联或者没有对应的目的分支可省略冒号及冒号后的部分(例如git push origin 分支名不存在的远程分支会自动新建),如果源分支就是当前分支可省略源分支地址部分(例如git push origin),如果远程主机只有一个,那么连远程主机名都可以省略(直接git push)。这三种再加上完整格式共四种,就个人而言我比较喜欢第三种,最简单,而且在目前的实际情况下最常见。记得前面说的相对的概念,push是从本地推到远程,所以源分支是个本地分支,目的分支是个远程分支。

push成功后,现在github上就有了这个新文件。

图片

现在就来搞分支操作了,首先我在github的仓库主页面上,新建了一个分支名叫develop的:

图片

现在在本地是看不到这个新建的分支的,不管你是git branch -a看所有还是-r,都没有develop分支。这没有任何问题,因为本地库并不知道远程库加了新分支。有的人会说,他们不是关联了吗怎么没有?这个仓库是你自己的啊,你把他们关联了,又没叫本地库去取回远程的更新,本地库怎么会自己取呢?本地库又没有智能,根本不知道远程更新了,也没有办法自己去访问网络。而且我们git branch -r看到的其实不是真的远程库上的远程分支,而是一种本地的远程分支,那怎么证明远程分支真的有呢?show一下,显示远程主机的情况,就知道了:

图片

看到显示远程有新分支了,而且还提示了下一次fetch时新分支将要存放的路径

有的人就会问了,我们在本地看不见新的develop分支,那要想删了怎么办?按照通常的情况,本地和远程的分支都有关联,我们所谓的删除远程分支,通常用git branch -r -d origin/develop删除本地的远程分支(真正远程分支其实还在那里的,并没有被删,这不是当前的重点),之后再git branch -r就看不见那个本地的远程分支,也就没法push到其对应的远程分支了,但现在远程develop分支是我们直接在远程建的,在本地无法访问,想删了怎么办呢?比较简易的办法是去github网页上找仓库的branches列表点删除按钮,程序员的办法当然是别的。还记得前面讲的push四种用法吧,其中最后一种就可以用来删除未关联的远程仓库,怎么办到的呢?看图:

图片

冒号前面什么都没写,什么意思呢?意思就是我推了个空的东西到远程的develop去?什么意思呢?就是把develop删了的意思!这样远程的分支会真的被删除而不是本地的远程分支被删除!git命令的设计机智不!这种push写法啊还不止可以删远程分支,还可以新建远程分支————push时如果该远程分支不存在的话就会自动新建,且自动关联上。

现在的话我要回退一下,假设我们没有删远程的develop,回到在github网页上新建完develop分支的时候,当时我们正想把develop分支弄到本地,然后…

这个时候就需要我们fetch一下子。fetch的主要作用是把远程的commit更新拿到本地来。fetch也有几种写法,完整的格式是git fetch <远程主机名> <源分支地址>:<目的分支地址>这样,同样是四种写法。git fetch会把所有的远程分支都拿下来创建成本地的远程分支,并将当前分支的FETCH_HEAD指向远程库的master,这个就搞得比较大了。FETCH_HEAD表示的是当前分支在远程库上的最新状态,每个本地分支都有FETCH_HEAD。fetch命令加上远程主机名就是把该主机的所有commit抓下来,加上源分支地址的话,注意fetch是从远程拿到本地,前面说过这个相对的概念吧,这时候源分支地址就是个远程分支(push的时候源分支地址是本地分支),如果我们fetch时候加上源分支地址,那我们就会把当前分支的FETCH_HEAD指向那个源分支地址的远程分支。为什么说指向呢,因为git分支就是基于指针实现,当然你也可以理解为这时候当前分支的FETCH_HEAD就是指定的远程分支,也没关系。第四种用法加上了目的分支地址,也就是加了一个本地分支的地址,把某个远程分支的commit拿到某个本地分支来(如果能合并这两个分支,他们就会自动合并),这个本地分支如果不存在会自动创建。

讲了那么多,那我们用第几种方式去fetch呢?建议四种都试试,不过篇幅所限这里只列举第三种,把远程的develop分支拿下来:

图片

看到了吧,现在显示当前分支也就是master的FETCH_HEAD被指向了远程的develop,git发现远程有了新分支develop,所以把它拿下来作为一个本地的远程分支,这个名字是我自己起的,就是用来表示它和普通的本地分支、远程分支都不一样。fetch之后再列出所有分支,就能看到新的develop分支了:

图片

那现在我们已经把远程的develop分支拿下来了,迫不及待地切进去,结果会收到这样的提示:

图片

这是什么意思呢?我前面说了,这个fetch只是把远程的commit拿下来,所以我们切到的这个origin/develop分支,也就是我前面说的本地的远程分支,它就是个commit,你可以看到目录右边蓝色字还有commit编号。这种当前分支指向一个commit id的情况,就叫detached HEAD。这个概念不是今天的重点。反正我们现在知道,这个从远程fetch下来的origin/develop分支不能像我们通常自己在本地创建的分支一样使用。那怎么办呢?

我大概可以举出三种方式:

第一种,在fetch的时候就不要偷懒,把命令写完整。在这个例子下的话就写git fetch origin develop:develop。这样写之后因为本地没有develop分支,git在本地会自动创建一个develop分支,这个分支是新的,当然可以和远程的develop分支合并,它们就会自动合并,关联起来,什么事都省了。但是我为什么之前没这样写呢,就是想搞点事出来,然后就有机会把fetch讲清楚了。把命令写完整还有一个微小的用途,就是把名字输到源地址的地方去,用来检查这个名字的远程分支是否存在,不存在会报错的。

第二种,在本地创建新的分支,把fetch下来的分支合并进去。这样我们首先建一个名为develop的本地分支并切换进去,然后用merge命令合并,也是很快的。合并完用branch -a可以看到远程和本地的分支都可见了,多么和谐:

图片

图片

注意建新分支要加上origin/develop(第一条命令的最后部分),这样才能保证新分支和远程分支的关联,关联了才能合并。至于本地分支叫develop,和远程的同名,不是必须的,只是一个好习惯。merge命令则没有什么好说的,因为这个情况下merge不可能出现冲突。

第三种方法,就不是fetch了,就要介绍pull了。我既然把这种方法和第二种并列,那么就应当知道,pull的作用就相当于fetch再merge。一样的格式,一样的四种用法,如下:

$ git pull <远程主机名> <源分支地址>:<目的分支地址>

再重提一次,pull的话是从远程拉到本地,源分支应是远程分支,目的分支是本地分支。

第一种用法,写全,把特定的远程分支和特定的本地分支合并$ git pull <远程主机名> <源分支地址>:<目的分支地址>

第二种用法,把特定的本地分支和当前分支合并$ git pull <远程主机名> <源分支地址>

第三种用法,把与当前分支关联的远程分支合并进当前分支$ git pull <远程主机名>

第四种用法,远程主机唯一时这么写就可以把当前分支和唯一的关联的远程分支合并$ git pull

在这里我们随便选用第一种方法来完成合并:

图片

push、pull、fetch都是这四种写法,类比一下就很好记了。

前面讲看很多分支间的关联都是自动关联,还讲到git branch -r -d xxxx删除“远程分支”只是删了本地的远程分支,这两个坑讲到现在才逐渐合并到一起,可以说明清楚了。

我们已经知道本地仓库有本地分支,远程库有远程分支,那么前面我自己取名的本地的远程分支是什么呢?这个东西洋文叫remote tracking branch,通常我们翻译叫追踪分支,这个追踪分支就是用来把本地分支和远程分支联系起来的,追踪分支本身是保存在本地。前面我们git branch -r显示的就是追踪分支而不是远程仓库里的远程分支,git branch -r -d origin/develop删的也只是追踪分支,我们做push的时候实际上是push到追踪分支,追踪分支自动合并进所追踪的远程分支,实现远程代码的更新的。

所以,对于远程刚新建的分支,本地没有建立关联,也就是没有追踪的时候,我们fetch,会把对应的远程拿来变成追踪分支,这个追踪分支和远程分支当然是同步的(刚拿来的),然后我们再建本地分支去和追踪分支合并,就达到了把远程分支拿下来的效果。也可以一步到位用pull,也是一样的,就如前面所说。总之每个本地分支一定通过追踪分支才能关联远程分支。

而对于刚新建的本地分支,由于没有追踪关系,远程分支也不知道其存在,我们push的时候会自动建立这个追踪分支并建立新的远程分支,实现关联。

说了这么多最准确的其实还是要洋文来表述。本地的分支通过追踪分支(tracking branch)关联到被追踪的远程分支(remote branch),这个远程分支又叫做这个本地分支的上游分支(upstream branch)。

还有什么没说清楚的吗?