Git应用详解第七讲:Git refspec与远程分支的重要操作

前言

前情提要: Git应用详解第六讲:Git协作与Git pull常见问题

这一节来介绍本地仓库与远程仓库的分支映射关系: git refspec 。彻底弄清楚本地仓库到底是如何与远程仓库进行联系的。

一、 Git refspec

refspecReference Specification 的缩写,字面意思就是 具体的引用 。它其实是 一种格式git 通过这种格式来表示 本地分支远程分支 的映射关系;

在本地仓库创建 master 分支外的其他两个分支 developtest :

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 多了一次提交。这是因为本地远程分支是为了追踪远程分支而存在的,只有在执行 pullpush 操作时它的指向才会更新。比如在执行推送( push )指令时:

  • 首先,本地 master 分支对应的本地远程分支( origin/master )会指向本地 master 分支最新的提交(向前走了几步);

  • 然后,本地 master 分支再将文件推送到远程 master 分支中。完成推送后,三分支的状态为:

回到终端,我们将刚才新增的提交 3rd 推送到远程分支,成功后查看本地分支以及本地远程分支的提交历史:

可见,本地远程分支的指向得到了更新,指向了最新的提交 3rd ,由此验证了上述说法。

查看分支关联

可以通过以下指令查看本地分支与本地远程分支的关联情况:

git branch -vv

可以看到:本地的 master 分支有本地远程分支 origin/master 关联,说明本地 master 分支已经和远程 master 分支建立了关联;

其余两个本地分支 popdevelop 并没有与之关联的本地远程分支,所以它们并没有与远程分支建立联系。

简单点说:只要 本地分支 有与之对应的 本地远程分支 ,就有与之对应的 远程分支

总结: 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 操作同理,也会更新本地远程分支的指向;

也就是说: 每次执行 pushpull 操作后,本地分支、本地远程分支、远程分支三个分支的指向都会达到同步。

当切换到 origin/master 分支上时,如下图所示:

git 并不会直接将分支切换到 origin/master 上,而是切换到最新的一次提交上,即一个游离的提交。这从侧面说明了: git禁止 我们直接修改 origin/master 分支的,只允许我们切换到最新的提交上;

也就是说本地远程分支(如 origin/master )是只读的,只能由 git 来改变,这就解释了为何使用 git branch 无法查看本地远程分支。

三、设置远程分支

弄清楚了什么是本地远程分支,就能更好地理解接下来所要介绍的,如何建立本地分支与远程分支的联系了。

1.设置同名远程分支

上图提示信息中的: upstream branch 表示上游分支,即远程仓库的分支。当前的本地分支 develop 并没有一个远程仓库的 develop 分支与之对应;要想推送 develop 分支到远程仓库的 同名分支 ,首先要创建对应的远程分支,有以下两种类型四种方法:

  • 类型一:建立本地与远程分支追踪关系的。

    git push --set-upstream origin 
    git push -u origin 

    使用该类型方法,只需设置一次,之后就可以使用简写形式 git push 进行推送。

  • 类型二:不建立本地与远程分支追踪关系的。

    git push origin HEAD
    git push origin 

    使用该类型方法,每次推送都需要采用上述的完整写法。

下面就来详细介绍这四种方法:

git push --set-upstream origin

方法一:采用下述指令为本地仓库 mygitdevelop 分支创建远程分支:

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

方法二:先切换到 test 分支,再执行以下命令,为本地仓库 mygittest 分支创建对应的远程分支:

git push -u origin test

-u--set-upstream 作用是类似的,都是在远程仓库新建一个新的分支,并与本地分支建立联系。

执行完上述指令后,再次查看本地分支的详细情况,以及分支对应关系,可以发现 test 分支已与远程 test 分支建立联系:

git push origin HEAD

方法三:

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

git push origin

方法四:

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

总结:当本地分支与远程分支同名时,一旦手动建立了它们之间的联系。之后推送本地分支的文件到对应的远程分支时可以采用简写形式: git push

这是因为在已经建立三个分支的对应关系并后,再执行 git pushgit 会自动地将同名的本地分支与远程分支进行匹配;

而其他情况则要采用完整写法进行推送。关于这些结论,将在第三大点 -u 参数的作用中详细介绍。

2.设置不同名远程分支

主要有以下四种方法,注意:使用每种方法前都需要先切换到对应分支上。

git push --set-upstream origin :

方法一:比如当前位于 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 :

方法二:

如下图所示,使用 -u 参数也能将本地 develop 分支的远程分支自定义为 develop2

git push origin HEAD:

方法三:

通过该方法也能成功设置与本地分支关联的,不同名的远程分支 develop2

git push origin :

方法四:该方法与方法二实质上是一样的,因为方法二中的 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

采用了完整写法后,成功地进行了推送,如下图所示:

注意:虽然可以自定义远程分支与本地远程分支的名字,但是十分不推荐,因为容易出错。所以,建议 本地远程分支和远程分支 都使用默认的,与本地分支相同的名字。

3.总结

以本地分支 develop 为例,不难发现:

  • 使用下列简写命令时,远程分支和本地远程分支都会采用默认的,与本地分支相同的名字:

    git push --set-upstream origin develop
    git push -u origin develop
  • 而使用下列命令的完整写法时,就可以自定义远程分支与本地远程分支的名字:

git push --set-upstream origin develop:develop2
git push -u origin develop:develop2
git push origin develop:develop2
git push origin HEAD:develop2

四、 git push origin mastergit 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 pushgit 内部会自动进行匹配;
    • --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 -f

  • 第二种:还未设置追踪关系,采用:

    git push -u origin master -f

2.预防措施

Github 提供了相应的分支保护机制,可以在 Settings 选项中进行设置:

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

3.补救措施

让有进度的人,再次对被强制覆盖的远程分支执行一次 git push -f 指令,把正确的内容强制推送上去,覆盖前一次 git push -f 所造成的灾难。

六、设置远程分支对应的本地分支

假如远程仓库 M3Y 中有 masterdevelop 两个分支,此时新建一个空的本地仓库 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 origin/

比如可以通过以下命令,设置本地远程分支 origin/master 与本地 master 分支的追踪关系:

git checkout -b master origin/master

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

git checkout -b develop origin/develop

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

2. git checkout --track origin/

重置条件,新创建一个空的本地仓库 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/master

    上述两种省略的写法最终都会转换为完整的写法:

  • 第三个目录 tags :存放标签信息,也是一个 SHA1 值:

    详细内容将在下一节介绍。

八、删除远程分支

如下图所示,远程仓库有三个分支 masterdeveloptest

通过前面的学习,我们知道通过下述指令可以删除本地 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

该方法用于删除无效的 远程分支 对应的 本地远程分支 ,具体场合如下:

如图所示 mygitmygit2 共享一个有三个分支的远程仓库:

首先在 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

由此间接地完成了远程分支的重命名。

以上就是本节的全部内容,相信看到这里的你已经十分熟悉 git refspec 了。下一节将介绍 git 标签与别名。