GIT PUSH 究竟是做什么的?

45

我似乎找不到一个好的解释。

我知道 git pull 的作用:

1)进行fetch操作,即将服务器中的所有额外提交复制到本地repo中,并将origin/master分支指针移动到提交链的结尾

2)将origin/master分支合并到master分支中,将master分支指针移动到新创建的提交处,而origin/master指针保持不变。

我认为git push做法非常相似,但我不确定。我相信它做了以下一种或几种操作:

  • 复制所有本地提交并在那里进行合并(与git pull相反);但在这种情况下,服务器没有我的本地master分支,因此我看不到它正在合并什么

或者

  • 将我的master分支合并到origin/master中,将结果提交到服务器并将其链接到现有的结尾提交旁边,同时移动服务器的master;这似乎不正确,因为然后我的本地origin/master与服务器不同步。

我目前正在使用git进行基本操作,所以我做得很好,但我想全面了解这些内部信息。

5个回答

93

假设您已经了解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-parseupdate-ref命令从引用中提取当前SHA-1,或更新引用以包含一个新的SHA-1。


3
非常好的回答! - Rahul
1
@masi:如果你的意思是:Git在进行push之前会执行fetch吗?答案是否定的。如果你的意思是:在进行push之前,你应该先执行fetch吗?答案通常是肯定的。先执行fetch,看看fetch做了什么,然后再决定是否合并、变基、立即推送或其他操作。如果你使用git push --force,仍然存在竞争条件,但如果你有某些原因需要强制推送,可以通过git push --force-with-lease来关闭它。 - torek
1
请注意,pull 不是 push 的相反操作。Fetch 是最接近相反操作的命令,但它也不是完全相反,因为 Fetch 会在你的代码库中设置 远程跟踪名称。而 push 命令则是请求其他 Git 设置其 分支 名称。 - torek
1
另一个关键是要考虑在你可能获取或推送的URL处的其他 Git 存储库中可能会发生什么。还有谁可以从/向该其他 Git 存储库获取和/或推送?自上次检查以来他们添加了多少提交? - torek
2
只要你不执行 git push --force,如果在 他们的 存储库中有新的提交,如果他们接受了你的 git push 就会丢失,他们将拒绝你的 git push 并提示 "non-fast-forward"。这是你必须运行 git fetch 并将他们的提交合并到最终推送的内容中的信号:你可以使用 git merge(在这种情况下,fetch+merge = git pull)或者使用 git rebase 或其他你认为合适的方式。 - torek
显示剩余2条评论

7
它只执行复制操作,不执行合并操作。
更具体地说,它会复制本地repo/branch中缺失的远程端部分的对象存储。这包括提交对象、引用、树和blob。
标签是一个明显的例外,需要包含--tags标志。
以下博客文章git is simpler than you think有更多细节。

1
您可能需要提及它也移动引用。 - jub0bs

6

我最简单的描述是,push 命令只需要执行以下操作:(假设你执行的是 git push origin master

  • 将本地代码库中不存在于远程代码库的提交复制到远程代码库
  • 将本地 git 中的 origin/master 和远程 git 中的 origin/master 都移动到相同的本地 master 提交处
  • Push 操作并不会合并代码

然而,它会检查你的本地 master 分支是否基于 origin/master 分支。从概念上来说,这意味着在 git 图形化界面中,你可以通过 "向下" 移动直接回到 origin/master(不是你本地 git 上的 origin/master,而是远程代码库上的 master 分支),这表示在 push 前远程代码库没有被修改。否则 push 将被拒绝。


2

下面的图表可能会更好地解释这个问题:

推送之前:

Before push

推送之后:

After push

Git push 命令会将当前分支中所有在目标分支中缺失的提交(a38de, 893cf, 756ae)复制并移动指针到本地分支中相同的提交,同时也会更新远程跟踪分支。需要注意的是,它不会执行任何合并操作。如果推送失败,则会被拒绝。


2
手册中得到的技术性、术语化的答案如下:

git push "使用本地引用更新远程引用,同时发送完成给定引用所需的对象。"

所以基本上,它是在复制信息,以确保您的远程库与本地库同步。但是什么是引用和对象?引用在手册条目中的释义是文件,它们“将[对象(例如提交)]的SHA-1值存储在一个简单的名称下,以便您可以使用该指针而不是原始的SHA-1值”[来查找与之关联的内容]。您可以通过导航到类似于.git/refs/heads/<branch name>.git/refs/remotes/origin/<branch name>的目录在您的库中查看它们。
对象(手册条目)包括提交、树、块和标签(默认情况下不推送最后一个)。举个例子,引用另一个SO答案的马克·朗格(Mark Longair)说,“提交记录了源代码在那个时间点的确切内容,包括日期、作者姓名和父提交的引用”。
因此,当您git push时,git使用本地引用(由您键入git commit创建)来更新远程上等效的文件,从而更新指向最新提交的指针,然后将您创建的任何新内容复制到git系统中作为对象,并带有一些元数据和SHA-1引用。
作为对引用的额外说明,在Github API文档中,他们展示了请求给定库中引用的API调用的JSON结果示例。这可能有助于您理解不同信息之间的关系。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接