我读了这个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)都合并。
答案 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
命令分解为两个组件:
git fetch origin branchB:branchC
。在同一设置上运行此命令,即设置branchC
以指向它在git pull
命令之前指向的提交。
git merge <hash-id>
。实际的哈希ID取自.git/FETCH_HEAD
,git fetch
离开它。在同一设置上运行此命令,branchA
设置为指向它在git pull
命令之前指向的提交。
请注意,第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)。如果我告诉它将master
或refs/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 fetch
和C
。 D
的父级是C
,而A
是D
之前的节点:
B
对于我的Git 保留这些提交,我的Git必须有一些名称或名称,通过它可以到达这些提交。到达 C
/
...--o--o--A <-- master
\
o--B <-- develop
\
D
的名称将为C
,到达origin/master
的名称将为D
。那些用于的名称分别指向origin/develop
和A
,但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
)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时,我们会发现:
C2
是C1
的祖先。对 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
可以做到。