前言
前情提要: Git 应用详解第六讲: Git 协作与 Git pull 常见问题
这一节来介绍本地仓库与远程仓库的分支映射关系: 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>
使用该类型方法, 只需设置一次, 之后就可以使用简写形式 Git push 进行推送.
类型二: 不建立本地与远程分支追踪关系的.
- Git push origin HEAD
- Git push origin <branch_name>
使用该类型方法, 每次推送都需要采用上述的完整写法.
下面就来详细介绍这四种方法:
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
采用了完整写法后, 成功地进行了推送, 如下图所示:
注意: 虽然可以自定义远程分支与本地远程分支的名字, 但是十分不推荐, 因为容易出错. 所以, 建议本地远程分支和远程分支都使用默认的, 与本地分支相同的名字.
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 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 -f
第二种: 还未设置追踪关系, 采用:
Git push -u origin master -f
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/master
上述两种省略的写法最终都会转换为完整的写法:
第三个目录 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
由此间接地完成了远程分支的重命名.
以上就是本节的全部内容, 相信看到这里的你已经十分熟悉 Git refspec 了. 下一节将介绍 Git 标签与别名.
来源: https://www.cnblogs.com/AhuntSun-blog/p/12721437.html