# 第七讲:Git refspec与远程分支的重要操作
# 前言
这一节来介绍本地仓库与远程仓库的分支映射关系:git refspec。彻底弄清楚本地仓库到底是如何与远程仓库进行联系的。
# 一、Git refspec
refspec是Reference Specification的缩写,字面意思就是具体的引用。它其实是一种格式,git通过这种格式来表示本地分支与远程分支的映射关系;
在本地仓库创建master分支外的其他两个分支develop和test:

在develop分支上执行git push命令,出现如下错误:

这是由于本地分支develop没有与任何的远程分支建立联系导致的。通过git branch -vv查看本地与远程分支的关联情况,可见并没有建立任何联系:

# 二、本地远程分支
在讲解如何建立与本地分支关联的远程分支之前,首先我们来介绍期待已久的本地远程分支:
git中其实有三种分支:本地分支、本地远程分支、远程分支;- 可以这样理解:本地远程分支是远程分支的一个镜像,并且在本地仓库与远程仓库之间起到一个桥梁的作用;
- 在没有办法直接查看远程仓库的时候,可以通过本地远程分支观察远程分支的变化情况。比如本地远程分支
origin/develop就对应着远程分支develop。
# 1.三分支关系
当本地master分支建立了与之关联的远程分支master后,查看当前分支状态:

图中的origin/master为本地远程分支,代表的是远程仓库的master分支,而这个分支是在本地的;也就是说加上远程仓库的master分支,一共有三个master分支:

并且,当本地仓库中的每一个分支都有与之关联的远程分支之后,本地仓库都会创建对应的本地远程分支,它们所处的位置和关系如下图所示:

可以这样理解:本地远程分支
origin/master为远程分支master的本地化形式;
假设远程仓库和本地仓库文件内容是一样的,都只有两次提交,此时三个分支的状态如下图所示:

然后,在本地的master分支中新增了提交3rd,本地仓库的分支情况变为:

上图中的
git dog为指令:git log --all --decorate --oneline --graph的别名,有关内容将在下一节讲解。
分支的示意图如下:

可见本地master分支比本地远程分支origin/master多了一次提交。这是因为本地远程分支是为了追踪远程分支而存在的,只有在执行pull或push操作时它的指向才会更新。比如在执行推送(push)指令时:
首先,本地
master分支对应的本地远程分支(origin/master)会指向本地master分支最新的提交(向前走了几步);然后,本地
master分支再将文件推送到远程master分支中。完成推送后,三分支的状态为:
回到终端,我们将刚才新增的提交3rd推送到远程分支,成功后查看本地分支以及本地远程分支的提交历史:

可见,本地远程分支的指向得到了更新,指向了最新的提交3rd,由此验证了上述说法。
查看分支关联
可以通过以下指令查看本地分支与本地远程分支的关联情况:
git branch -vv

可以看到:本地的master分支有本地远程分支origin/master关联,说明本地master分支已经和远程master分支建立了关联;
其余两个本地分支pop和develop并没有与之关联的本地远程分支,所以它们并没有与远程分支建立联系。
简单点说:只要本地分支有与之对应的本地远程分支,就有与之对应的远程分支。
总结:origin/master作用:追踪远程分支。当执行git push/pull操作时,该分支的指向都会相应地发生变化,用于与远程仓库保持同步;比如:本地仓库在执行git push操作的时候,不仅会把本地的修改推送到远程;还会同时修改origin/master分支的指向;
# 2.实战演示
可通过该指令查看本地的所有分支及其最新的提交信息:
git branch -av

首先,在master分支上进行三次提交,并将它推送到与之关联的远程master分支,此时各分支的提交历史为:

三个分支的状态为:

在此基础上,在master分支上进行一次提交4th,然后查看状态git status:

图中提示信息表明,当前分支(master)已经领先于origin/master分支一次提交。为了看得更清楚,我们查看本地各分支的提交历史:

从图中可看出,origin/master分支确实落后了一次提交,表示远程master分支落后了一次提交。此时可以使用git push将新增的提交推送到远程master分支,在这个过程中会将本地远程分支origin/master指向最新的提交4th。成功推送之后,再次查看本地各分支的提交历史:

可见,通过git push操作本地远程分支确实发生了更新,指向了最新提交4th。这就验证了执行git push时进行了两步操作:
- 将本地
master分支的新提交推送到与之关联的远程master分支; - 将本地远程分支
origin/master指向本地master分支的最新提交;
git pull操作同理,也会更新本地远程分支的指向;
也就是说:每次执行push或pull操作后,本地分支、本地远程分支、远程分支三个分支的指向都会达到同步。
当切换到origin/master分支上时,如下图所示:

git并不会直接将分支切换到origin/master上,而是切换到最新的一次提交上,即一个游离的提交。这从侧面说明了:git是禁止我们直接修改origin/master分支的,只允许我们切换到最新的提交上;
也就是说本地远程分支(如origin/master)是只读的,只能由git来改变,这就解释了为何使用git branch无法查看本地远程分支。

# 三、设置远程分支
弄清楚了什么是本地远程分支,就能更好地理解接下来所要介绍的,如何建立本地分支与远程分支的联系了。
# 1.设置同名远程分支
上图提示信息中的:upstream branch表示上游分支,即远程仓库的分支。当前的本地分支develop并没有一个远程仓库的develop分支与之对应;要想推送develop分支到远程仓库的同名分支,首先要创建对应的远程分支,有以下两种类型四种方法:
**类型一:**建立本地与远程分支追踪关系的。
git push --set-upstream origin <branch> git push -u origin <branch>1
2使用该类型方法,只需设置一次,之后就可以使用简写形式
git push进行推送。**类型二:**不建立本地与远程分支追踪关系的。
git push origin HEAD git push origin <branch_name>1
2使用该类型方法,每次推送都需要采用上述的完整写法。
下面就来详细介绍这四种方法:
# git push --set-upstream origin <branch>
**方法一:**采用下述指令为本地仓库mygit的develop分支创建远程分支:
git push --set-upstream origin develop
该命令的作用为:在远程仓库创键一个与本地分支develop关联的同名分支develop,并将本地分支develop的文件推送到该远程分支上。
也就是将本地分支develop的上游分支设置为远程仓库的develop分支,并进行文件同步。
执行完上述命令后会有这样的提示:

表示本地的develop分支已与远程的develop分支建立联系;此时查看本地分支,会发现多了一个本地远程分支origin/develop,并且已与本地develop分支建立了联系:

随后再次执行git push就不会出现问题了:

此时在github上查看对应的远程仓库,就能查看到新增的远程分支develop了:

上图中的
master分支是远程仓库创建时默认创建的,并没有与本地master分支建立联系。
随后点开branch可以看到:

当前一共有两个分支,master分支是default(默认)分支,是不能够被删除的;活跃的分支为deavelop;
# git push -u origin <branch>
**方法二:**先切换到test分支,再执行以下命令,为本地仓库mygit的test分支创建对应的远程分支:
git push -u origin test

-u与--set-upstream作用是类似的,都是在远程仓库新建一个新的分支,并与本地分支建立联系。
执行完上述指令后,再次查看本地分支的详细情况,以及分支对应关系,可以发现test分支已与远程test分支建立联系:

# git push origin HEAD
方法三:
如下图所示,通过该指令成功设置了本地develop分支对应的远程develop分支。但没有显示追踪信息,之后不能使用git push推送。

# git push origin <branch>
方法四:
如下图所示,该方法实质上与方法三相同,因为HEAD指向的就是当前分支。同样没有显示追踪信息,之后也不能使用git push推送。

**总结:**当本地分支与远程分支同名时,一旦手动建立了它们之间的联系。之后推送本地分支的文件到对应的远程分支时可以采用简写形式:
git push。这是因为在已经建立三个分支的对应关系并后,再执行
git push,git会自动地将同名的本地分支与远程分支进行匹配;而其他情况则要采用完整写法进行推送。关于这些结论,将在第三大点
-u参数的作用中详细介绍。
# 2.设置不同名远程分支
主要有以下四种方法,注意:使用每种方法前都需要先切换到对应分支上。
# git push --set-upstream origin <branch1>:<branch2>
方法一: 比如当前位于develop分支,如果采用的是以下简写命令:
git push --set-upstream origin develop
则会创建一个同名的远程分支develop。而如果采用该命令的完整写法,就可以自定义远程分支的名字了,比如设为develop2:
git push --set-upstream origin develop:develop2

执行上述指令后,成功创建了对应的,不同名的本地远程分支origin/develop2。表示本地develop分支已与远程develop2分支建立联系(因为远程分支与本地远程分支是一一对应的关系):

在github上查看本地仓库关联的远程仓库MY,可以看到顺利创建了develop2分支:

可以发现这么一个规律:在创建远程分支的同时会创建同名的本地远程分支。
# git push -u origin <branch1>:<branch2>
方法二:
如下图所示,使用-u参数也能将本地develop分支的远程分支自定义为develop2。

# git push origin HEAD:<branch>
方法三:
通过该方法也能成功设置与本地分支关联的,不同名的远程分支develop2:

# git push origin <branch1>:<branch2>
**方法四:**该方法与方法二实质上是一样的,因为方法二中的HEAD指针指向的就是当前所在的分支,也就是develop分支。过程与方法二类似:

上面这四种设置不同名远程分支的方法,都有一个共同特点:不能使用
git push进行推送。
若使用git push都会出现找不到对应远程分支的错误:

原因在下面第三点的-u参数作用中会详细讲解。
既然是-u参数追踪问题,那我加上-u参数不就行了么?其实这样也行不通:

解决方案:每次推送的时候,指明本地分支与远程分支的对应关系,即采用上述命令的完整写法,比如:
git push --set-upstream origin develop:develop2
git push -u origin develop:develop2
git push origin develop:develop2
git push origin HEAD:develop2
2
3
4
采用了完整写法后,成功地进行了推送,如下图所示:

注意:虽然可以自定义远程分支与本地远程分支的名字,但是十分不推荐,因为容易出错。所以,建议本地远程分支和远程分支都使用默认的,与本地分支相同的名字。
# 3.总结
以本地分支develop为例,不难发现:
使用下列简写命令时,远程分支和本地远程分支都会采用默认的,与本地分支相同的名字:
git push --set-upstream origin develop git push -u origin develop1
2而使用下列命令的完整写法时,就可以自定义远程分支与本地远程分支的名字:
git push --set-upstream origin develop:develop2
git push -u origin develop:develop2
git push origin develop:develop2
git push origin HEAD:develop2
2
3
4
# 四、git push origin master与git push -u origin master的区别
第一次将本地仓库的master分支推送到远程仓库的master分支上时,使用前者和后者都可以顺利推送,区别在于是否使用了-u参数:
推送时不使用
-u参数:
推送时使用
-u参数:
注意到推送时使用-u参数会打印下列提示信息:
Branch 'master' set up to track remote branch 'master' from 'origin'.
表示本地的master分支被设置去追踪远程的master分支,在第2~n次推送中,只需要使用git push这样的简写命令(当然,完整写法效果等同)。git就会自动将本地的master分支与远程的master分支进行匹配,完成推送:

而不使用-u参数时,没有上述的分支追踪信息。此时使用简写git push进行推送会出现错误:

错误信息显示:当前分支没有与之对应的远程分支。这个时候想要成功推送,必须采用指明对应关系的完整写法,比如:
git push origin master

这就是推送时使不使用-u参数的区别。并且,根据上面的介绍,使用如下指令进行推送也能达到-u参数的效果:
git push --set-upstream origin develop

之后也可以使用简写的git pull指令进行推送:

细心的你一定发现了,以上都只是本地分支与远程分支同名的情况。不同名的情况下,上面的两个方法还好使吗?
首先验证方法一:-u参数:

设置不同名的远程分支时要注意写成完整形式:
pop:pop2
可以看到,即使创建不同名的远程分支,-u参数也一样能够设置追踪关系;但是,奇怪的是git push却不好使了:

还是和没使用-u参数时一样,找不到对应的远程分支,需要采用指明对应关系的完整写法,比如:
git push origin pop:pop2

其次验证方法二:--set-upstream:

同样设置分支对应关系时要使用完整写法。可以看到,该方法也设置了追踪关系。奇怪的是git push同样不管用:

同样找不到对应的远程分支,需要采用指明对应关系的完整写法,比如:
git push origin bob:bob2

所以可以得出结论:
- 本地/远程分支同名时:
-u参数的作用是设置本地分支与远程分支的追踪关系,设置了追踪关系后,之后的推送可使用简写git push,git内部会自动进行匹配;--set-upstream参数与-u参数效果等同;
- 本地/远程分支不同名时:
--set-upstream参数与-u参数依然可以设置分支的追踪关系,但是,之后的推送不能使用简写git push,只能使用指定分支对应关系的完整写法;
总结:十分建议将所有的本地分支与对应的远程分支设为同名,并且第一次推送使用--set-upstream或-u参数建立分支追踪关系,之后就可以使用简写git push进行推送了!
# 五、git push -f
该命令的完整写法为:
git push -f origin master
意思为强制推送:直接跳过与远程仓库的master分支合并的环节,强制覆盖远程仓库上master分支的内容,即以本地的master分支内容为准。应慎用该命令,否则将覆盖远程仓库中master分支上其他人推送的文件(一星期的成果没了)。
# 1.应用场合
- 当远程仓库的历史提交记录太乱了,想要重新整理时。**注意:**一定要与其他人协商好再用本地分支强制覆盖远程分支。
- 只有一个人开发时,代码以本地为准。为了避免推送时繁琐的合并,可以使用
-f强制推送,直接覆盖远程分支上的内容;
分两种写法:
**第一种:**已经通过
-u参数等方式,设置了本地分支与远程分支的追踪关系时,采用:git push -f1
**第二种:**还未设置追踪关系,采用:
git push -u origin master -f1
# 2.预防措施
Github提供了相应的分支保护机制,可以在Settings选项中进行设置:

可以看到Github默认是保护分支的:

# 3.补救措施
让有进度的人,再次对被强制覆盖的远程分支执行一次 git push -f 指令,把正确的内容强制推送上去,覆盖前一次 git push -f 所造成的灾难。
# 六、设置远程分支对应的本地分支
假如远程仓库M3Y中有master和develop两个分支,此时新建一个空的本地仓库mygit,通过以下指令将它的远程仓库地址origin设置为M3Y的地址:
git remote add origin git@github.com:AhuntSun/M3Y.git
此时两仓库的状态为:

由于mygit是空仓库与远程仓库M3Y没有任何公共提交历史,所以在执行git pull时会出现下图所示的不同源冲突(上一节中详细介绍过该冲突):

虽然git pull操作失败了,但是也成功地将远程仓库M3Y的分支拉取了下来。但是,通过git branch -vv查看分支追踪关系,发现并没有本地分支与这两个远程分支建立了联系:

如何建立这两个本地远程分支对应的本地分支?可以通过以下两种方法:
# 1.git checkout -b <branch> origin/<branch>
比如可以通过以下命令,设置本地远程分支origin/master与本地master分支的追踪关系:
git checkout -b master origin/master

以上为本地master分支已存在的情况,如果本地分支develop未创建,可以采用下述命令创建并切换到develop分支,并且设置origin/develop与develop的追踪关系:
git checkout -b develop origin/develop

设置了本地分支与远程分支的追踪关系,接下来就可以在本地仓库执行git push进行推送了:

# 2.git checkout --track origin/<branch>
重置条件,新创建一个空的本地仓库mygit2,同样将其远程地址origin设置为远程仓库M3Y的地址。随后在本地仓库mygit2中执行git pull操作,将远程仓库M3Y中的两个分支拉取到本地:

与上次一样,拉取到本地的两个本地远程分支没有与任何本地分支建立追踪关系。这次可以采用另外一种方法:
git checkout --track origin/test
创建并切换到develop分支,并且设置该分支与origin/develop分支的追踪关系:

可以说该方法是方法一的特殊情况,因为该方法没有指明创建的本地分支的名字,所以默认采用与远程分支一样的名字develop来命名;
如果想在本地建立一个develop2(不同名)的分支与本地远程分支origin/test建立追踪关系,则应采用第一种方法。
# 七、远程分支信息
可以进入.git目录,查看储存远程分支信息的文件:

# 1.查看config文件
使用vim编辑器打开该文件,可以查看到关于远程分支的信息:

可以看到remote这一栏中有两个信息,第一个是远程仓库的url,第二个是fetch信息,这两个信息尤为重要:
refs/heads/*表示远程仓库的refs/heads目录下的所有引用都会写入到本地的refs/remotes/origin目录中;- 其中的
+号是可选的,加了表示无论是否能够自动合并,即是否为Fast Forward方式,都将远程仓库所有文件拉取到本地。 - 而不加
+则表示如果不是Fast Forward方式就不拉取。一般情况下都是加上+号的,先把文件拉取到本地,不是Fast Forward方式就手动合并;
# 2.查看refs文件
refs文件夹存储着refspec的文件,里面维护着三个目录:

第一个目录
heads:存储的是本地仓库的分支信息:
可以查看其中一个分支:

是一个
SHA1值,表示分支就是一个指针,指向当前提交。第二个目录
remotes:里面存放着远程分支信息,远程仓库中也存在这样的目录与文件;
从上图可以看到,远程分支只有
master,没有develop(因为之前被删除了)。并且它们本质上也是一个代表提交的SHA1值:
建立
refspec映射(即本地分支、本地远程分支、远程分支三者间对应关系)后,git会获取远端上refs/heads下的所有引用,并将它们写入本地的refs/remotes/origin目录下。所以,可以通过查看本地远程分支(如origin/master)的方式查看本地仓库最后一次访问远程仓库时,远程仓库master分支上的历史提交记录://完整写法 git log refs/remotes/origin/master //进一步简写 git log remotes/origin/master //继续简写 git log origin/master1
2
3
4
5
6上述两种省略的写法最终都会转换为完整的写法:

第三个目录
tags:存放标签信息,也是一个SHA1值:
详细内容将在下一节介绍。
# 八、删除远程分支
如下图所示,远程仓库有三个分支master、develop和test:

通过前面的学习,我们知道通过下述指令可以删除本地develop分支:
git branch -d develop

那么如何删除远程分支呢?
首先我们来看看git push的完整写法:
git push origin srcBranch:destBranch
srcBranch表示本地的分支,destBranch表示对应的远程分支;表示将本地的分支推送到远程分支,这两个分支可以不同;
之所以可以直接使用
git push是因为我们设置的本地分支和远程分支的名字是相同的,并且手动建立了联系,所以git能够自动识别;
明白了这点后,就不难理解下列删除远程分支的两种做法了:
# 1.git push origin :destBranch
将空的分支推送到远程分支,这样就能将该远程分支删除;比如删除本地分支develop的远程分支:
git push origin :develop

可以看到成功删除了远程分支develop以及它所对应的本地远程分支origin/develop。
**注意:**并不需要切换到需要删除远程分支的本地分支
develop上,再执行上述指令。也就是说,可以在任意本地分支上删除任意本地分支对应的远程分支。
# 2.git push origin --delete destBranch
还可以采用更加直观的--delete参数,比如删除远程分支develop:
git push origin --delete develop

这两种方式是等价的,可根据需求选择。
# 3.git remote prune origin
该方法用于删除无效的远程分支对应的本地远程分支,具体场合如下:
如图所示mygit与mygit2共享一个有三个分支的远程仓库:

首先在mygit2中删除远程仓库的develop分支,可以看到mygit2中远程分支develop对应的本地远程分支origin/develop被删除了:

然后在mygit中查看远程分支详细信息:

可以看到提示信息中显示远程分支develop对应的本地远程分支origin/develop处于stale(腐烂,游离)状态,即该分支对于mygit来说已经失效,可以使用:
git remote prune origin
(prune:裁剪)删除mygit上这个无效的本地远程分支:

再次查看分支信息,可发现mygit中的本地远程分支origin/develop已经被删除了:

**注意:**一般本地远程分支设置了保护措施,不能随意删除;
# 九、重命名分支
# 1.本地分支
可以通过以下命令,将本地分支dev重命名为develop:
git branch -m dev develop

# 2.远程分支
无法直接重命名远程分支,只能通过先删除原来的远程分支,再创建重命名后develop分支对应的远程分支,过程为:
//删除远程分支dev
git push origin :dev
//创建重命名后develop分支对应的远程分支
git push -u origin develop
2
3
4
由此间接地完成了远程分支的重命名。