Git 真正理解 git fetch, git pull 以及 FETCH_HEAD

zw963 · 2012年08月06日 · 最后由 zw963 回复于 2013年07月09日 · 182941 次阅读

====> 注意: 因为发这个帖子时 (2012 年) 本人 git 仍处在初级阶段, 纯粹靠臆测写下以下内容, 所以存在不少错误.(已经被证实), 新人同学千万别被误导呀. 有空我会重新改之, 并完善下.

之前使用 git 主要还是限于本地项目使用, 所以接触 fetch, pull , push 比较少. 最近在 github 上用到了这些命令, 对其中的一些细节不甚了解, 今天花时间专门研究了下. 整理了一些笔记, 在这里分享给大家.

因为是纯粹靠实践以及系统 man 帮助来 臆测 有关的功能, 可能有不对的地方, 大家一定要指正.


git push.


这个很简单, 其实和后面的差不多, 这里就不讲了.

唯一需要注意的地方是:

git push origin :branch2, 表示将一个内容为空的同名分支推送到远程的分支.(说白了, 即删除远程主机的 branch2 分支), 但是这并不会消除之前的 comment 内容, 而且你一旦提交了一些大的文件 (例如: 图片之类的), 通过这个操作, 是不会将这些文件占用的空间消除的. 如果要真正的删除一个文件, 除了删除整个项目, Github 网站也有提供办法, 不过还没看懂.

git fetch, 理解 fetch 的含义, 是远程协作的关键.


而理解 fetch 的关键, 是理解 FETCH_HEAD.

这里需要解释下什么是 FETCH_HEAD??

FETCH_HEAD 指的是: 某个branch在服务器上的最新状态'. 每一个执行过 fetch 操作的项目'都会存在一个 FETCH_HEAD 列表, 这个列表保存在 .git/FETCH_HEAD 文件中, 其中每一行对应于远程服务器的一个分支. 当前分支指向的 FETCH_HEAD, 就是这个文件第一行对应的那个分支.

一般来说, 存在两种情况:

  • 如果没有显式的指定远程分支, 则远程分支的master将作为默认的 FETCH_HEAD.

  • 如果指定了远程分支, 就将这个远程分支作为 FETCH_HEAD.

常见的 git fetch 使用方式包含以下四种:

  • git fetch

这一步其实是执行了两个关键操作:

  • 创建并更新所有远程分支的本地远程分支.
  • 设定当前分支的FETCH_HEAD远程服务器的master分支 (上面说的第一种情况)

需要注意的是: 和 push 不同, fetch 会自动获取远程`新加入'的分支.

  • git fetch origin

同上, 只不过手动指定了 remote.

  • git fetch origin branch1

设定当前分支的 FETCH_HEAD' 为远程服务器的 branch1 分支`.

注意: 在这种情况下, 不会在本地创建本地远程分支, 这是因为:

这个操作是git pull origin branch1的第一步, 而对应的 pull 操作,并不会在本地创建新的 branch.

一个附加效果是:

这个命令可以用来测试远程主机的远程分支 branch1 是否存在, 如果存在, 返回 0, 如果不存在, 返回 128, 抛出一个异常.

  • git fetch origin branch1:branch2

只要明白了上面的含义, 这个就很简单了,

  • 首先执行上面的 fetch 操作
  • 使用远程 branch1 分支在本地创建 branch2(但不会切换到该分支), 如果本地不存在 branch2 分支, 则会自动创建一个新的 branch2 分支, 如果本地存在 branch2 分支, 并且是`fast forward', 则自动合并两个分支, 否则, 会阻止以上操作.

  • git fetch origin :branch2

等价于: git fetch origin master:branch2

git pull


只要理解了 git fetch, git pull 就太简单了.

git pull 等价于以下两步:

  • 经命令中的 pull 换成 fetch, 执行之...
  • git merge FETCH_HEAD

唯一需要提及的一点是:

我认为 pull 操作, 不应该涉及三方合并衍合 操作 换个说法: pull 应该总是 fast forward 的. 为了达到这样一个效果, 在真正 push 操作之前, 我倾向于使用衍合, 在本地对代码执行合并操作.

坑爹的 markdown, 我怎么感觉越改越难看呢?

怎么我写的帖子, 除了自己, 一个回复都没有呢?

太小白??

#2 楼 @zw963 我觉得可以写成 blog 啊

#3 楼 @fsword

下个月打算自己写个博客, 然后上线, 现在还在复习一些 Ruby 的知识...

不过我觉得 Ruby-china 就是一个所有 Rubyist 的 blog...

#3 楼 @fsword

还有就是, 很多时候, 我并不确定自己讲的对不对, 所以发出来让大家帮我指正.

如果你觉得有问题, 咱们可以讨论下. 可别不说呀.

#5 楼 @zw963 git 我还很浅的,也许 @Saito 能做一些补充

#5 楼 @zw963 filter-branch 可以做删除单个文件.

Git 仓库本身也是可以 force push 改变历史的.

好贴,格式很清楚,lz 排版辛苦了。

换个说法: pull 应该总是 fast forward 的.

以前我总是千方百计的保证 pull 是 fast forward,后来发现挺碍手碍脚的,最近钟情于

git pull --rebase

#7 楼 @Saito

喔~ filter-branch, 那时 git 里面的核弹级功能, 我除了在网上找的脚本, 删除大文件以外, 貌似其他功能完全用不着. 呵呵

@zw963 支持一下。

#8 楼 @happypeter

我刚刚看了下 pull --rebase 的 man 帮助, 就是先 fetch, 然后和本地当前分支 rebase, 我感觉用这个参数会造成混乱.

你平常怎么用这个参数? 先本地 rebase, pull 的时候再 rebase ? 要不就是你直接在 master 下工作, 从来不会创建分支, 工作完后, 直接 fetch, 然后 rebase.

其实你自己完全可以一直保证 pull 是 fast forward, 只要保证 不要在master下直接工作,就可以了. 之前就知道 git 鼓励多创建分支, 并不明白为什么, 但是等明白了 rebase 的含义之后, 就理解了.

#11 楼 @zw963 即使你是在一个单独的分支上来做一个 new feature,也总不免去 master 上去做一些 hotfix 吧。所以本地 master 和远端 master 出现分歧是很正常的事情。这时候就有两个选择:

  1. git pull,也就是 fetch + merge
  2. git pull --rebase,也就是 fetch + rebase

这两种方式都是很自然的,如果这都叫混乱的话,那就干脆不要开新分支了。

#12 楼 @happypeter

假设你首先 pull master 下来, 然后在 master 上建立 开发分支 开始工作, 当你确信工作已经完成, 打算要合并到 master 的时候, 这个时候首先切换到 master, 然后再次 pull master, 因为这个时候, 你的本地 master 并没有执行任何更改, 当然是 fast-forward, 然后, 下一步在本地 rebase 开发分支 到 最新的 master, 解决冲突后, push 即可.

我上面这个过程, 你说是不是 pull 总是 fast-forwrd 呢?

既然 pull 总是 fast-forward, 干嘛还要加个--rebase, 搞的那么复杂...

#13 楼 @zw963 你这思路也挺简单清晰的,不过我们如果遇到这样的情况呢:

我们正在我们的 new-feature 分支上写代码,写到一半忽然发现有一个和当前工作不相关的小 bug,那这时你会怎么办呢?

#14 楼 @happypeter 这是普遍情况,你可以用 git-stash 解决。 首先,在这个 new-feature 分之上执行 git stash,这时所有未提交的修改都会被零时保存起来。这个时候你就可以方便的 git checkout master 切换到主干分钟,然后 branch 新的分之并解决 bug。等 bug 解决了以后,你可以回到 new-feature 分之上,用 git stash apply 将之前保存的变更全部还原出来。进一步关于 git-stash 的信息可以看这里: http://www.kernel.org/pub/software/scm/git/docs/git-stash.html

#14 楼 @happypeter

我是站在作者编写 git 实用的角度, 觉得按照这种方式来使用 git 才是正确的. 事实上我也可以理解为什么提供 pull --rebase, 如果这个项目很大, 例如: linux kernel, 可能在你处理本地 rebase 的时候, 又有其他人向 master push 了新的 commit, 而且这个 commit 碰巧就和你要提交的 commit 发生冲突, 这个时候, 就需要 pull --rebase 了. 除非项目很大很大, 否则这样的几率还是很低的.

我的理解, 是 rebase 应该总是在本地的不同分支间被执行, 这样一步一步才条理, 直接在 master 上用 pull --rebase, 我觉得这样是曲解了这个功能的本意 (有点滥用). 相比较而言, pull 的时候如果存在冲突, merge 反倒更靠谱一些.

#15 楼 @lgn21st

讲的很好呀, 受教了, 这的确是使用 stash 很好的场景.~~

不过..., 你是用手机回复的吗? 这不像你的风格呀.

#14 楼 @happypeter 这是普遍情况,你可以用 git-stash 解决。 首先,在这个 new-feature 分之 上执行 git stash,这时所有未提交的修改都会被 零时 保存起来。这个时候你就可以方便的 git checkout master 切换到主干分钟,然后 branch 新的 分之 并解决 bug。等 bug 解决了以后,你可以回到 new-feature 分之 上,用 git stash apply 将之前保存的变更全部还原出来。进一步关于 git-stash 的信息可以看这里.

我补充一点: 好像还有一个 git pop, 取出来后, 顺便清空 stash-list.

#17 楼 @zw963 最近事情有点多,太忙了,所以随手回复之前,忘记好好检查一下了。

@zw963 我发现 zw963 是 ruby-china 属一,属二的 “建筑开发商 “

#19 楼 @hysios

嘛意思... 褒义还是贬义啊? 呵呵.

#14 楼 @happypeter 我最近恰好做了个 screencast 关于这个的

顺路推荐一下 git flow 比如#14 楼 @happypeter 的情况,就可以: git flow hotfix start xxx

git flow 就是通过命令将 hotfix/feature/release 等流程规范化,很适合团队开发。

#15 楼 @lgn21st #17 楼 @zw963

对于 @lgn21st 的 stash 用法我是完全同意的,一个 new-feature 如果代码较少的话,这样就可以了。一般对于较大的 feature, 我会在 new-feature 分支上打十几甚至几十个 commit(碎片无极限),然后往 master 上 merge 的时候用 rebase -i squash 成一个或两个 commit.

这时如果有 hotfix 的话,当然也会用到 stash, 然后切回 master 不过这种小 bug 一般我不会再开新分支了,直接在 master 上 change + commit。只是一两行的小修改,为何要开新分支呢?!

#16 楼 @zw963 你的意思是:本地 master 和 new-feature 之间 rebase,OK. 远端 master 和 本地 master 之间 rebase,不 OK。

我是 100% 反对的 😄 因为这两者根本就没有区别。

#22 楼 @knwang #23 楼 @quakewang

果断收藏,并学习中。。。

#24 楼 @happypeter 没错,如果只是小修改是不需要开分支的。 如果修 bug 是不知道要多久,bug 修好可可能 remote master 已经前移了。而 remote 的新代码又很可能和修 bug 的代码有冲突。这样的话开分支就比较好,并且在 commit back to master 之前把当前远程的最新代码抓下来一起 integration test.

#22 楼 @knwang Nice cast, really enjoyed it.

There are two things I sometimes do a bit differently though:

  1. I always consider my local master branch is just another branch from origin/master.

    sometimes if the bugfix is really small(say, a typo), that I do not need to test, then I will just do it on my local master. master shall always be deployable? Yes, but only the public/remote one.

  2. I usually do a lot of "WIP" commits on new-feature branch, then rebase -i master to squash them into logical one or two patches.

    I see in the cast you do a "WIP" commit and laster reset it to continue where you left off, I never do it this way. I will either stash the changes, or in most cases, I will do 20 WIPs on this branch, for features big enough, this brings me the freedom to go back and forth in the history, free as in air.

#28 楼 @happypeter

  1. 原理上是这样, 如果把本地的 master 看成是远程的镜像可以把流程简化很多。也就是说, 这要本地的 master 测试都通过就立刻 push remote master.这样别人可以立刻拿到你的 code,可以 integration test 或者 reconcile 等等,不至于要一次集成太多代码。还有,你可能有多个 remote, 比如一个 Github, 一个 staging server, 一个 production server, 等等, 如果你的 deploy 用 git 的 workflow, 比如 Heroku。 如果保持本地 master deployable, 就可以很方便的选择 deploy 到哪个上面

  2. 如果你的 feature 需要 20 个 wip, 说明你 feature 本身太大,应该掰开成更细颗粒的 feature. reset -i 是很有用的, 不过我一般最多是五六个 commit, 没用过 20 多个这么多。 有时候我会需要在一个 branch 上面工作很久,但一般我都会很经常的 rebase master, 跟上 master。 不然到最后一个大的 merge/rebase 太头痛

有时只是预期一个小提交,就不想开新分支,直接在主干分支(如 master, dev)上直接提交,但 push 的时候发现远程分支有新 push,这时 pull --rebase 就很适用

#31 楼 @HungYuHei 是的,我从来不用 git pull, 一律 git pull --rebase (alias 成 gpr)

#30 楼 @knwang With you. Merge often and merge early.

#22 楼 @knwang

视频翻墙也打不开, 郁闷.

#23 楼 @quakewang

等下找 man 看看, 我甚至都没有听说过...

#25 楼 @happypeter

这怎么能没有区别呢? 在执行 git pull --rebase 之前, 你甚至都不知道 remote 上做了那些更改, 如果你本地 master 进行了大量更改 (例如, 好长时间才 push 一次), 那绝对是一个噩梦.

在什么时候该使用 pull rebase 参数, 我和@HungYuHei 的想法是一致的. 见 #16

我不反对用 pull---rebase , 事实上, 看了楼上 @knwang 的回帖, 刚刚我又试了试, 果断的把 pull 的 alias 加上了 --rebase 参数 (反正真要冲突了, 肯定要 rebase, 如果不冲突, 直接 fast-forword 挺好), 但是关键一点是: 目的和你不一样.

也可能是我误会你了, 你貌似倾向于使用 pull --rebase 直接代替本地的rebase, 好像一步到位那样的.

#30 楼 @knwang 受教了~ 从更深层次, 多角度 理解 git. 👍

#35 楼 @HungYuHei

讲的很好哇~~ 不过好像那个图画错了...

git pull --rebase 之后, 应该是

D---E---A---B---C--F'--G'  master, remotes/origin/master

才对吧?

#34 楼 @zw963

也可能是我误会你了, 你貌似倾向于使用 pull --rebase 直接代替本地的 rebase, 好像一步到位那样的. 这个没有误会,我确实经常这么做,但是本地 rebase 用的更多些。这两种方法都没问题啊。

#36 楼 @zw963 噢……对……马上更正

#30 楼 @knwang

嗨~ 下午那会儿突然想起了你的这句话: 有时候我会需要在一个branch上面工作很久,但一般我都会很经常的rebase master, 跟上master

因为看不到你的视频, 所以不知道你是如何操作的. 你这个所谓的经常的rebase master,是指的什么意思? 这里面所说的 master 是remote的 master 还是特指本地的 master. 你的意思是指你的这个 rebase, 是指 push 之前的 rebase, 还是只是在本地 rebase, 并不 push 呢

如果push, 你本地分支的 feature 根本没有完成, 就 push 到远程的 master, 这好像不合理吧?

如果不push, 那你 rebase 之前, 会不会先 pull 呢?

如果每次都 pull, 那样会使你当前分支的 commit 呈现一个不连续的状态.

如果一直不 pull, 那么等你 push 之前, 不是还得重新上演一次之前同样的 rebase ?? 同样的 rebase 过程上演两遍, 这不是反而复杂了吗?

所以, 怎么想都觉得这句话不理解, 还望指点一二.

楼上的太高深了,看不懂,还是来学习的好

需要在 branch 上很久的时候多半是要完成一整块的 feature set 才能整合在 master 里面, 比如,凡是涉及到付费的时候就要把很多情形考虑到做完才能一起 deploy。可以每做完一小块测试通过后就 git checkout master; git pull --rebase origin master; git checkout my_branch; git rebase master

这样先把远程 master 别人的代码拿下来,再 rebase 到自己的 branch 上面。再跑测试。 通过了之后 git push origin my_branch, 这样就不用担心比如电脑掉到水坑里,或者需要别人接着做这个大 feature set. 再继续做下一小块。原因还是每次集成少量代码,有问题早解决。而 remote branch 伤得的 commit message 也可以作为一种和同事的交流工具,让别人知道你做到哪里。

等到 feature set 做完, 还是一样 git checkout master; git pull --rebase origin master; git checkout my_branch, git rebase master; run test; git checkout master; git merge my_branch (fast forward); git push origin master; deploy

总结下,

1) 所有 developer 的本地 master 都紧跟 remote master,而且都是 working code, 随时可以 deploy 2) 本地上推前先抓 remote master, 整合测试通过后从 local master push remote master, no conflict. 3) push/deploy often, push/deploy early 4) remote branches 做备份和交流

要是出现了几个 long living branch 又都不能很快 deploy 的时候就比较复杂,可以考虑再开一个非 master 的 integration branch. 但这种情况一旦出现更多的是整个项目的筹划出了问题。

#39 楼 @zw963

我的做法也跟 @knwang 一样,每次提交前都要 rebase 一次同步过的主干分支,不过我合并时通常会用 merge --no-ff

#41 楼 @knwang

明白了, 谢谢! 让我收益非浅呀. 大体和我想的差不多, 就是 remote 多了一个分支, 用来跟踪本地的最新演进, 想了想就像一棵树的分支, 刚分叉不久, 又合并(rebase)', 而开发分支始终保持在最新的 master`下游不远的地方. 太酷了!

不过, 我觉得这种方式, 其实根本用不到 pull --rebase 参数? 感觉就是两个单独的分支, 在不断的向前推进, 因为在你 merge 当前开发分支 到 master 之前, master 始终是 remote 的 master, 不是应该一直是 fast-forword 吗?

#43 楼 @zw963

是 fast forward, 因为别人如果按照同样的流程也一定集成了你的所有本地 commit,而没有 push 的 commit 又都不在 master 上。

#44 楼 @knwang

完全明白了, 十分感谢~~

#42 楼 @HungYuHei

嗯. 我想问下, 实际应用中, 那样会不会有点乱?

我觉得你的目的是: 希望在将来一眼就可以看出来, 这部分来自于某个分支.

可是分支一旦合并后, 肯定会删除它, 显示之前的某个 commit 来自于某个分支, 真的很重要么?

#45 楼 @zw963 像这样,能够很清晰地让其他人看出一系列的提交都是为实现同一目的,方便 code review

#46 楼 @HungYuHei

很有道理. 谢谢指点呀. 我也添加这个参数试试看效果好了.

#47 楼 @zw963

说归到底,git 是为开发流程服务的, 而开发的流程又跟团队的构成和开放方式密切相关。

比如说,如果团队的程序员都坐在同一个办公室里,并且有对代码交流讨论的习惯,水准都不错而且互相信任;或者经常做结对编程有充分的讨论;这样的话用 rebase 就很好,可以把小块的代码快速集成而且高频率的 deploy, 大部分的 branch 都是很快就会剔除掉

如果团队不在同一个地方希望能加强交流, 或者水平不一有些新手, 这样象@HungYuHei的流程就很好,每一个 merge 的流程都清晰表现出来,而且要 merge 的 commits 也一目了然, 很适合根据这个来进行 code review 和讨论。 这个流程的代表是 Github, 他们的流程包含一个对自己 repo 的 pull request, 并且用 pull request 来作为讨论代码的一个工具。可以参看http://zachholman.com/talk/how-github-uses-github-to-build-github

这种方式需要注意的是因为需要等别人的 code review, 代码不能立刻 deploy 会放慢节奏,而且有可能会因此创建大量的 branches, 导致可能要多方 merge, 而且也可能会让 commit history 杂乱。 但确实对于团队的交流非常有好处。

所以就是根据团队的情况做选择了。

#48 楼 @knwang 是啊,通常都会根据团队情况来定一些公共约定

想請問一下...git fetch 段落中的一句: "需要注意的是: 和 push 不同, fetch 会自动获取远程`新加入'的分支." 感覺應該是筆誤, 應該是和 "git pull" 來比較不同吧? 還是您真的指的是"git push"?

#50 楼 @seagal82

我的文章的确有不少错误, 也有很多内容需要补充, 不过你提的那一点是没错的. 我说的是 git fetch, 如果不加分支, git fetch , 会将远程服务器上的所有分支, 拉到本地的 remote 分支.

你一定要明白一点: 真正将数据从远程服务器 pull 到 本机的是 fetch, 而不是 pull. pull 应该只是调用了 fetch. 然后默认 merge 了一下. 根据我的经验, git pull 和 git fetch 不加参数, 行为是完全一致的. 只有加了 branch 参数, git pull 会帮你自动合并.

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册