假设您已经了解git的“对象”模型(您的提交和文件等都只是git数据库中的“对象”,其中“松散”对象 - 未打包以节省空间的对象保存在.git/objects/12/34567...
等位置)...
你是正确的:git fetch
检索"they"(在这种情况下是origin
)拥有但你没有的对象,并对它们进行标记:origin/master
等。更具体地说,您的git通过互联网电话(或任何其他合适的传输方式)呼叫他们并询问:您有哪些分支,这些是哪些提交ID?他们有master
,ID是1234567...
,因此您的git请求1234567...
和所需的任何其他对象,使您的origin/master
指向提交对象1234567...
。
git push
中对称的部分是:您的git像往常一样在同一个互联网电话上呼叫他们的git,但这次不仅要询问他们的分支情况,还要告诉他们您的分支和您的 git存储库对象,然后说:“我能让您将您的master
设置为56789ab...
吗?”
他们的git查看您发送的对象(新提交56789ab...
和他们没有的任何其他对象,他们需要将其包含在内),然后考虑将他们的master
设置为56789ab...
。
正如Chris K已经回答的那样,在这里没有合并发生:您的git仅建议他们的git使用此新的提交ID覆盖他们的master
。是否允许由他们的git决定。
如果“they”(无论是谁)没有设置任何特殊规则,则git在这里使用的默认规则非常简单:如果更改是“快进”的,则允许覆盖。它还有一个额外的功能:如果使用“强制”标志进行更改,则也允许覆盖。通常不建议在此处设置force标志,因为默认规则“仅快速转发”通常是正确的规则。
显而易见的问题是:什么是快进?我们稍后会详细介绍;首先我需要更详细地介绍标签或“引用”。
Git的引用
在git中,分支(branch)、标签(tag)甚至像stash和HEAD这样的东西都是参考(reference)。它们大多数都可以在git存储库的子目录.git/refs/下找到(包括HEAD等一些顶级引用直接在.git文件夹本身)。引用只是一个包含类似于7452b4b5786778d5d87f5c90a94fab8936502e20的SHA-1 ID的文件。SHA-1 ID对人来说很笨重且难以记住,因此我们使用名称来保存它们,例如v2.1.0 (在这种情况下是git本身的版本2.1.0的一个标签)。
有些引用是完全静态的,或者至少打算如此。标签v2.1.0不应该指向除上述SHA-1 ID之外的任何内容。但是有些引用更加动态。具体来说,您自己的本地分支(例如master)是移动目标。一个特殊情况是HEAD,它甚至不是自己的目标:它通常包含移动目标分支的名称。所以有一个“间接”的引用例外:HEAD通常包含字符串ref:refs/heads/master或ref:refs/heads/branch,或类似的内容;git不能实施“永远不要改变”规则,特别是对于分支经常会有变化。
如何知道引用是否应该更改?很多情况下这只是约定俗成:分支移动,标签不会移动。但是您应该问:如何知道引用是分支、标签还是其他内容?
引用的名称空间:refs/heads/,refs/tags/等
除了特殊的顶级引用之外,git的所有引用都在refs/中。然而,在refs/目录(如果您使用Windows或Mac,则为“文件夹”)中,我们可以拥有整个子目录集合。到目前为止,git有四个明确定义的子目录:refs/heads/包含所有分支,refs/tags/包含所有标签,refs/remotes/包含所有“远程跟踪分支”,以及refs/notes/包含git的“笔记”(在这里我将忽略它们,因为它们变得有点复杂)。
由于所有分支都在refs/heads/
中,Git 可以确定这些分支应该被允许更改;而由于所有标签都在refs/tags/
中,Git 可以确定这些标签不应该被更改。
分支的自动移动
当您创建一个新的提交时,并且位于像master
这样的分支上时,Git会自动地移动引用。新的提交是使用前一个分支末端作为其“父提交”创建的,一旦新的提交被安全保存好,Git就更改master
以包含新的提交的ID。换句话说,它确保分支名称,即在heads
子目录下的引用,始终指向最顶端的提交。
(事实上,在存储库中作为提交图的一部分的提交集合中制作出的分支是一种数据结构。它唯一与分支名称的连接是分支本身的尖端提交存储在具有该名称的引用标签中。如果随着存储库增长了许多提交,分支名称被更改或擦除,那么这点很重要。现在只是要记住一点:在“分支”的单一名称下存在不同概念之间的差异,即“分支顶端”,它是“分支名称”指向的位置,以及作为提交DAG子集的分支。 Git倾向于将这些不同的概念归为一个单一的名称“分支”,这有点不幸。)
什么是快进合并?
通常您会在合并的上下文中看到“快进合并”,通常在git pull
的第二个步骤中执行合并。但实际上,“快进”实际上是标签移动的属性。
让我们画一些提交图。小的o
节点表示提交,每个提交都有一个指向其父节点(或父节点们)的箭头,指向左侧、左上侧或左下侧(或在一个案例中,两个箭头)。为了能够按名称引用三个提交,我将给它们大写字母名称而不是o
。此外,这种基于字符的艺术品没有箭头,因此您必须想象它们;只需记住它们都指向左侧或左侧,就像这三个名称一样。
o - A <-- name1
/
o - o - o - o - B <-- name2
\ /
o - C <-- name3
当您要求git更改引用时,您只需要求它将新的提交ID插入标签即可。在此情况下,这些标签位于refs/heads/
中,因此它们是分支名称,因此可以接受新值。
如果我们告诉git将B
放入name1
中,我们得到:
o - A
/
o - o - o - o - B <-- name1, name2
\ /
o - C <-- name3
注意,提交
A
现在没有名称,左侧的
o
只能通过找到
A
来找到...这很困难,因为
A
没有名称。提交
A
已被放弃,这两个提交已经可以进行“垃圾回收”。(在git中,“reflog”中留下了一个“幽灵名称”,通常会保留带有
A
的分支30天。但那是完全不同的话题。)
让git将
B
放入
name3
怎么样?如果我们接下来这样做,我们得到这个:
o - A
/
o - o - o - o - B <-- name1, name2, name3
\ /
o - C
在这里,提交C
仍然有一种方法可以找到它:从B
开始,向下和向左工作,到达其另一个(第二个)父提交,就会发现提交C
。因此,提交C
并没有被放弃。
像这样更新name1
不是快进,但更新name3
是。
更具体地说,如果引用原来指向的对象(通常是提交)仍然可以通过从新位置开始向后沿着所有可能的反向路径工作而到达,则引用更改为“快速前进”。 从图形术语来说,如果旧节点是新节点的祖先,则它是快进的。
通过合并使push
成为快速前进
当您所做的唯一事情是添加新提交时,分支名称就会进行快速前进;但是,当您添加了新提交时,如果您还合并了其他人添加的任何新提交,那么也会发生快进。也就是说,假设您的存储库在您进行了一次新提交后包含以下内容:
o <-- master
/
...- o - o <-- origin/master
现在,如果对 origin/master
执行 "向上且向右" 的移动操作,则会发生快进。 但是,如果其他人更新了另一个 (origin
) 存储库,那么您需要执行 git fetch
命令,并从他们那里获取一个新的提交。此时,您的 git 将在您的存储库中进行快进操作,移动您的 origin/master
标签:
o <-- master
/
...- o - o - o <-- origin/master
此时,将 origin/master
移动到 master
,不会是一次快进(fast-forward),因为它会放弃一个新提交(commit)。
但是,你可以执行 git merge origin/master
操作,在你的 master
上创建一个新的提交,其中包含两个父提交 ID。这样我们就称其为 M
(代表合并操作):
o - M <-- master
/ /
...- o - o - o <-- origin/master
现在你可以执行git push
操作,将代码推送回origin
,并要求他们将他们的master
(你称其为origin/master
)设置为新的你的M
,因为对于他们而言,这是一个快进操作!
请注意,你也可以使用git rebase
命令,但我们将把它留给另一个stackoverflow帖子。 :-)
1实际上,git引用始终作为各种子目录中的单个文件开始,但如果一个引用很长时间没有更新,它就会“打包”(连同所有其他大多数静态引用一起)到一个包含打包引用的单个文件中。 这只是一个节省时间的优化,关键在于不依赖于确切的实现方式,而是使用git的rev-parse
和update-ref
命令从引用中提取当前SHA-1,或更新引用以包含一个新的SHA-1。
git push --force
,仍然存在竞争条件,但如果你有某些原因需要强制推送,可以通过git push --force-with-lease
来关闭它。 - torekgit push --force
,如果在 他们的 存储库中有新的提交,如果他们接受了你的git push
就会丢失,他们将拒绝你的git push
并提示 "non-fast-forward"。这是你必须运行git fetch
并将他们的提交合并到最终推送的内容中的信号:你可以使用git merge
(在这种情况下,fetch+merge =git pull
)或者使用git rebase
或其他你认为合适的方式。 - torek