Git pull with refspec

时间:2018-05-26 16:50:09

标签: git

我读了这个question,现在我怀疑git如何与refpec一起工作:

Step 1 : I am on branchA.

Step 2 : I do `git pull origin branchB:branchC` .

Step 3: I notice : 

a) commits from branchB on remote comes and update `remotes/origin/branchC`

b) Then a merge happened. `branchC` was updated with `remotes/origin/branchC`

c) The `branchC` was merged into `branchA`.

现在,我很困惑,因为git pull = git fetch + git merge,那么2怎么合并发生在这里?步骤b)和步骤c)都合并。

3 个答案:

答案 0 :(得分:2)

第2步不是真正的合并,它是fast-forward merge。快速转发是非当前(即,当前未检出)分支可能的唯一合并类型。如果无法快速转发git将中止fetch/pull;在这种情况下,您可以执行真正的合并(checkout branchC并运行git pull origin branchB)或执行强制更新(git fetch origin +branchB:branchC),从而失去branchC头部的本地提交。

答案 1 :(得分:2)

phd's answer是正确的。将git pull命令分解为两个组件:

  1. git fetch origin branchB:branchC。在同一设置上运行此命令,即设置branchC以指向它在git pull命令之前指向的提交。

  2. git merge <hash-id>。实际的哈希ID取自.git/FETCH_HEADgit fetch离开它。在同一设置上运行此命令,branchA设置为指向它在git pull命令之前指向的提交。

  3. 请注意,第2步git merge对参考branchC没有影响。它确实对当前分支名称有一些影响,即refs/heads/branchA。由于它运行git merge,它可以执行快进合并或真正合并,或者根本不执行任何操作。

    让我们更深入地研究fetch步骤,这个步骤实际上更有趣,或者至少具有挑战性。

    git ls-remote

    在运行git fetch origin branchB:branchC之前,请运行git ls-remote origin。这是我在Git的Git存储库上运行它的东西(有很多位被剪掉):

    $ git ls-remote origin
    e144d126d74f5d2702870ca9423743102eec6fcd        HEAD
    468165c1d8a442994a825f3684528361727cd8c0        refs/heads/maint
    e144d126d74f5d2702870ca9423743102eec6fcd        refs/heads/master
    093e983b058373aa293997e097afdae7373d7d53        refs/heads/next
    005c16f6a19af11b7251a538cd47037bd1500664        refs/heads/pu
    7a516be37f6880caa6a4ed8fe2fe4e8ed51e8cd0        refs/heads/todo
    d5aef6e4d58cfe1549adef5b436f3ace984e8c86        refs/tags/gitgui-0.10.0
    3d654be48f65545c4d3e35f5d3bbed5489820930        refs/tags/gitgui-0.10.0^{}
    ...
    dcba104ffdcf2f27bc5058d8321e7a6c2fe8f27e        refs/tags/v2.9.5
    4d4165b80d6b91a255e2847583bd4df98b5d54e1        refs/tags/v2.9.5^{}
    

    你可以看到他们的Git向我的Git提供了一长串参考名称和哈希ID。

    我的Git可以选择这些并选择它喜欢的名称和/或ID,然后转到git fetch的下一阶段:问他们可以给我哪些哈希ID例如,提交e144d126d74f5d2702870ca9423743102eec6fcd(他们的master的哈希ID)。如果我告诉它将masterrefs/heads/master作为refspec的左侧,我的Git就会这样做,因为这些名称字符串与refs/heads/master匹配。

    (没有refspecs,我的Git会询问所有分支。标签比较棘手:--tags让我的Git全部拿走,--no-tags让我的Git不接受任何分支,但是介于两者之间,#39} ; git fetch内部的一些可怕的代码。)

    无论如何,它们提供了一些哈希值,我的Git说它是否想要或者有其他哈希值,并且他们的Git使用他们的git rev-list为提交,树,blob和/构造一组哈希ID或带注释的标记对象放入所谓的瘦包。在git fetch的这个阶段,您会看到有关远程计数和压缩对象的消息。

    git fetch origin

    让我现在运行一个实际的git fetch

    $ git fetch origin
    remote: Counting objects: 2146, done.
    remote: Compressing objects: 100% (774/774), done.
    remote: Total 2146 (delta 1850), reused 1649 (delta 1372)
    

    最终,他们的Git完成了他们将发送的所有对象的包装,然后发送这些对象。我的Git收到了他们:

    Receiving objects: 100% (2146/2146), 691.50 KiB | 3.88 MiB/s, done.
    

    我的Git修复了瘦包(git index-pack --fix-thin),使其成为可以存放在.git/objects/pack目录中的可行正常包:

    Resolving deltas: 100% (1850/1850), completed with 339 local objects.
    

    最后,获取最有趣的部分:

    From [url]
       ccdcbd54c..e144d126d  master     -> origin/master
       1526ddbba..093e983b0  next       -> origin/next
     + 8b97ca562...005c16f6a pu         -> origin/pu  (forced update)
       7ae8ee0ce..7a516be37  todo       -> origin/todo
    

    ->箭头左侧的名称是他们的名称;右边的名字是我的 Git的名字。由于我只运行git fetch origin(没有refspecs),我的Git使用了我的默认 refspecs:

    $ git config --get remote.origin.fetch
    +refs/heads/*:refs/remotes/origin/*
    

    所以我好像写了:

    $ git fetch origin '+refs/heads/*:refs/remotes/origin/*'
    

    使用完全限定的refspec,而不是像branchB:branchC这样的部分名称。此特定语法也使用类似glob-pattern的*字符。从技术上讲,这些都不是全部,因为它们只是字符串而不是文件名,右边有一个*,但原理类似:我要求我的Git匹配以{开头的每个名字{1}},并以refs/heads/开头的名称将这些文件复制到我自己的存储库。

    refs/remotes/origin/名称空间是我所有Git的分支名称所在的位置。 refs/heads/名称空间是我所有Git的远程跟踪名称所在的位置,refs/remotes/是我的Git和我放置了与分支名称对应的远程跟踪名称的地方在refs/remotes/origin/的Git中找到。前面的前导加号origin设置强制标志,就好像我已经运行+

    参考名称更新

    下一步要求我们查看提交图 - 我的Git存储库中找到的所有提交的Directed Acyclic Graph或DAG。在这种情况下,由于新的包文件已经集成,这包括我刚刚通过git fetch --force添加的所有新对象,因此我有新的提交(以及任何需要的树和blob) )从他们的Git获得。

    每个对象都有一个唯一的哈希ID,但这些哈希值太难以直接使用。我喜欢在StackOverflow上的文本中从左到右绘制我的图形,并使用round git fetch s或单个大写字母(或两者)来表示特定的提交。早期的提交向左移动,稍后向右提交,分支名称指向该分支的 tip 提交:

    o

    请注意,在Git对象数据库的这个视图中,我们根本不关注 index / staging-area ,而且根本没有注意到< EM>工作树的。我们只关注提交及其标签。

    由于我实际上是在...--o--o--A <-- master \ o--B <-- develop 从Git获得了我的提交,我的Git也有origin个名字,所以让我们把它们画进来:

    origin/*

    现在,假设我运行...--o--o--A <-- master, origin/master \ o--B <-- develop, origin/develop ,它会引入两个新的提交,我将标记为git fetchCD的父级是C,而AD之前的节点:

    B

    对于我的Git 保留这些提交,我的Git必须有一些名称或名称,通过它可以到达这些提交。到达 C / ...--o--o--A <-- master \ o--B <-- develop \ D 的名称将为C,到达origin/master的名称将为D。那些用于的名称分别指向origin/developA,但B告诉我的Git替换它们,给出:

    git fetch origin +refs/heads/*:refs/remotes/origin/*

    C <-- origin/master / ...--o--o--A <-- master \ o--B <-- develop \ D <-- origin/develop 的输出会将其列为:

    git fetch

    请注意 aaaaaaa..ccccccc master -> origin/master + bbbbbbb...ddddddd develop -> origin/develop (forced update) 和输出中的三个点。这是因为在将+从提交origin/master(哈希标识A)移动到提交aaaaaaa时,是快进操作,将C从提交origin/develop移动到提交B 。这需要强制标志

    即使您使用本地分支名称

    ,此过程仍然有效

    如果您运行D,则指示您的Git:

    • git fetch origin br1:br2(真origin
    • 打电话给Git
    • 获取分支名称列表
    • 使用他们的remote.origin.url(可能是br1)更新您的 refs/heads/br1 - 很有可能是您的br2,带来了必要的任何对象让这件事发生。

    此更新阶段,根据refs/heads/br2更新br2会在其上设置强制标记。这意味着当且仅当操作是快进时,您的Git才会允许更改

    (与此同时,您的Git 也会更新您的br1,因为Git会根据origin/br1进行此类机会更新。请注意 this remote.origin.fetch配置,> update 设置了强制标志。)

    快进实际上是标签移动的属性

    我们(和Git)谈论做快进合并,但这是用词不当,原因有两个。第一个也是最重要的是快进是标签运动的属性。给定一些指向某个提交remote.origin.fetch的现有引用标签(分支,标记或其他) R ,我们告诉Git:将R移动到指向提交C1代替。假设两个哈希ID都有效并指向提交,当我们检查提交DAG时,我们会发现:

    • C2C1的祖先。对 R 的此更改是快进的。
    • 或者,C2 C1的祖先。对 R 的此更改是非快进的。

    快进操作的特殊属性是,现在 R 指向C2,如果我们从C2开始并按Git一直向后工作,我们最终会遇到C2。因此C1仍受名称保护,如果 R 是分支名称,则提交C1仍在分支 R 上。如果操作是快进,C1 <{1}},C1可能不再受保护并且可能 - 取决于是否有其他任何东西保护它,以及它的相对年龄 - 在将来的某个时刻收集垃圾。

    由于上述原因,更新分支样式引用 - C2中的分支名称或C1中的远程跟踪名称 - 通常需要使用强制标记,如果更新不是快进。 Git的不同部分以不同的方式实现:refs/heads/refs/remotes/都有git fetch 前导加号,而其他Git命令(不是{0}} t refspecs)只有git push或者像--force一样,只是假设你 - 用户 - 知道你在做什么。

    (非常旧的Git版本,1.8.2及更早版本,意外地将这些快进规则应用于标记名称和分支名称。)

    --force命令知道索引和工作树

    使git reset快进合并操作与git merge知道的这种标签快速转发不同,至少略微不同的原因是什么,并使用您的索引/暂存区和您的工作树。当你跑:

    git merge

    Git计算当前HEAD提交和给定其他提交的合并基础。如果此合并库是当前提交,则操作可以作为快进标签移动完成,只要Git还带有索引和工作树。

    如果合并基础是当前提交的祖先,或者如果使用git merge标志,则git merge <commit-specifier> 必须执行真正的合并,并进行新的合并提交。 (当然还有抑制提交的标志,并将新提交作为普通的非合并提交,因此--no-ff的这个视图也会跳过一些重要的细节。 )

答案 2 :(得分:1)

那么,在阅读@torek-ans-1@torek-ans-2之后[这必须阅读才能理解git fetch / pull的工作情况],我觉得要为那些想要获得的人发布我的问题的完整答案很快。

首先,问题中的步骤是错误的。这是正确的步骤:

Step 1 : I am on branchA.

Step 2 : I do `git pull origin branchB:branchC` .

Step 3: I notice : 

a) commits from branchB on remote comes and update `refs/heads/branchC`

b) Then based on `remote.origin.fetch` was used to try to update `remotes/origin/branchB` on our local.

[ Notice that no attempts will be made to update `remotes/origin/branchC`]

c) The `branchC` was merged into `branchA`.

[订单可能因git版本不同而不同]

在步骤a)+步骤b)中,没有合并。这称为快进更新。还有一种称为快进合并的东西,其行为与此类似,但我们说当git merge表现得像快进更新时快进合并。

这里在步骤a)+步骤b)中没有调用git merge。因此,我们称之为快进更新,而不是快进合并。

步骤c)是调用git merge的地方。

简而言之:git pull origin branchB:branchC= git fetch origin branchB:branchC ((a) + (b))+ git merge branchC (c)

现在我的问题是为什么要合并2个?

没有2合并。步骤c)中只有1个合并。是的,有2个快进更新,git fetch可以做到。