这是我认为 git 文档非常糟糕的领域之一。一切都指向git pull,但 git pull 只是一种方便的方法,它建立在几个底层项目之上,而底层项目对于理解这一点至关重要。 Git 让关键的理解部分“泄露”,同时试图假装它们无关紧要。
顺便说一下,这里是实际的基本元素:
换句话说,push 的反义词实际上不是pull,而是fetch。
参考规范
为了使这两个操作(push 和 fetch)工作,git 使用它所谓的“refspecs”。请记住,在推送和获取时,涉及两个存储库。
Refspecs 通常看起来像一个单独的分支名称。然而,最简单的“真实”版本的 refspec 实际上是用冒号分隔的两个分支名称:
master:master
Editor:Editor
author:author
左右两边命名两个repos中的分支。对于push,左边的名字是你的仓库中的分支,右边的名字是他们仓库中的分支。对于fetch,左边的名字是他们仓库中的分支,右边的名字是你仓库中的分支。
这里的模型又变得有点奇怪了,而且是不对称的。 Git 相信(尽管可以说相信任何事情)当你获取时,你和他们可能都在工作;但是当你 push 时,只有 你 应该一直在做任何工作。 (这是有充分理由的,我不会详细说明,因为这已经很长了。:-))
为了使这一切发挥作用,fetch 提供了分支重命名。而不是直接将“他们的”分支(master、author 等)提取到“你的”分支——这将使访问 你 自上次提取以来所做的工作变得非常困难——你将“他们的”东西提取到 git 所谓的“远程分支”。
获取和“远程分支”
“远程分支”实际上是本地事物,尽管名称为“远程分支”。就此而言,“远程”也是如此。 “远程”是您在本地配置的名称,例如 origin 或 github。与此“远程”相关联的是一个 URL,例如https://github.com/mathpunk/punk-mathematics-text.git。还有一个fetch 行。现在不要担心fetch 行的机制(一旦创建它通常就可以工作);只知道这就是 git 在获取时知道使用什么“远程分支名称”的方式。
您确实必须在某种程度上担心遥控器的实际名称。通常的默认名称是origin,但您在执行git remote add 命令时选择该名称。远程的名称成为“远程分支”名称的一部分。具体来说,远程名称是在分支名称前面的前缀。
因此,假设您将 git fetch origin 从 github 带过来,“远程分支名称”将是 origin/master、origin/Editor 和 origin/author。
如果你 git fetch github 将东西从 github 带过来,“远程分支名称”将改为 github/master、github/Editor 和 github/author。
在所有情况下,您只需命名远程,fetch 会带来 所有 分支,但会重命名它们。通过省略 refspec,您可以使用 fetch 行中的默认值。
如果你添加一个分支名称(例如git fetch origin author),git 通过使用相同的fetch 行重命名传入的分支,将它变成一个“真正的”参考规范。实际上,git fetch origin author 变成了git fetch origin author:origin/author。 他们的分支名称,左侧的author变成你的“远程分支名称”,右侧的origin/author。
(这里的想法是您可以添加多个不同的遥控器。如果您、您的编辑和您的发布者都想直接相互共享,而不是与 github 之类的第三方共享,则可以有 两个 遥控器,例如命名为editor 和publisher,您将获得“远程分支名称”,例如editor/Editor 用于一个远程,而publisher/Editor 用于另一个。如果您使用像github 这样的单个共享站点,不过,所有这些都是毫无意义的复杂化。)
好的,回到fetch 和push。当您git fetch origin 时,您使用您的远程origin 名称来带来“他们的”分支,但将它们放在您的origin/*“远程”分支下。这使他们的工作与您的工作分开。 (当然在某些时候你需要将这些结合起来;我们稍后会谈到。)
但是,当您 push 时,“推送”不使用“远程分支”的概念。您只需直接推送到 他们的 分支。因此,如果您的 repo 中有一些更改,在您的分支 author 中,并且您想要推送这些更改,您只需 git push origin author:author。这里的origin 部分又是远程名称,最后一部分像往常一样是一个refspec,命名你的分支(author),然后是他们的分支(也只是author)。
如果您在 push 命令中包含分支名称,则此处缺少分支重命名通过:git push origin author“意味着”git push origin author:author。 您的分支名称,author,在左侧,被简单地复制以用作他们的分支名称,author,在右侧。
评论
是时候快速回顾一下了:
- 您设置了“远程”
- 你用来
fetch到你的“远程分支”,并且
- 您将其用于
push 您当地的分支机构到他们当地的分支机构。
想一想。注意少了一个步骤。
您如何将他们的工作(在第 2 步之后)现在列在您的远程分支中,进入您自己的本地分支?
这就是git merge 和git pull 进来的地方。
这也是问题标题中的项目出现的地方。快进或非快进是 git 中“标签移动”的属性。
要真正理解这一点,我们必须再做一次小旅行,讨论 git 的提交和分支模型。
提交图表
每个提交都有一个保证唯一标识符(SHA-1,9afc317... 编号)。您或其他任何人都不会创建具有该编号的任何不同提交,但如果您或其他任何人能够设法准确地重新创建该提交,您将获得 same 编号。 (这对于获取很重要。)
每个提交还包含(通过引用间接地)一个完整的独立实体,即“树”。树是该提交中所有文件的集合。然而,提交并不是完全独立的:它有一个或多个“父”提交。这些决定了提交历史,从而“构建”了实际的分支结构。
(在许多——甚至是大多数——其他版本控制系统中,树不是独立的:VCS 必须通过父子提交来提取树,和/或进行新的提交。但在 git 中,每棵树都是独立的;它只需要通过父/子排序来比较两棵树,或者确定提交历史。)
给定一个提交,git 找到它的父提交,以及它们的父提交,等等,并建立一个“提交图”:
C - F
/ \
A - B G <-- master
\ /
D - E
这是一个包含 7 个提交的存储库的图表,所有提交都位于一个名为 master 的分支上。 A 是初始提交(A 代表一些大而丑陋的唯一 SHA-1 编号),B 自 A 以来发生了一些变化(比较 A 和 B 的两棵树将显示改变),然后某人——或者可能是两个“某人”——做了所谓的“分支”:基于提交B创建提交C,并基于提交B创建提交D。
之后,有人基于D创建了提交E,并基于C创建了F。
最后,有人将这两个分支结合起来进行了一次合并提交,提交G。提交G 有其(两个)父级,F 和E。事实上,它有两个父母这一事实使其成为“合并提交”。
当所有这些都发生在一个存储库中时,它就足够简单了。使用存储库的“某人”在分支master 上提交了A、B 和C,然后可能从提交B 开始创建了一个命名分支:
git checkout -b sidebranch master~1
并提交D 和E。然后他们又回到master:
git checkout master
并提交F,然后运行:
git merge sidebranch
创建提交G。在此之后,他们可以删除分支sidebranch,因为提交G(现在是master 上的提示提交)指向提交E 以及提交F。
这种相同的模式,然而,当你在你自己的仓库中工作,以及在他们的仓库中工作的“他们”都进行提交时,就会发生这种情况。假设您正在处理master,并且您已经提交了A 和B:
A - B <-- master
此时您将您的工作推送到共享点(github),使其具有A 和B。他们克隆了这个存储库,给了他们第三个存储库,带有 github 共享点和两个提交 A 和 B。
现在您在您的 repo 中工作并创建提交 C。他们在自己的内部工作并创建D 和E,在您将C 推送到github 之前,他们将D 和E 推送到github:
[你:]
C <-- master
/
A - B
[他们和 github:]
A - B
\
D - E <-- master
此时,假设您使用git fetch github。请记住,fetch 重命名了“他们的”分支,所以结果是这样的:
C <-- master
/
A - B
\
D - E <-- github/master
Git 可以这样做,因为每个提交都有一个唯一的 SHA-1,因此它知道您的 A 和 B 以及它们的 A 和 B 是相同的,但是你的C 不同于他们的D 和E。
此时,您可以创建提交F,这会使您的master 指向您的最新提交:
C - F <-- master
/
A - B
\
D - E <-- github/master
现在如果你想分享你的工作,这是你 git push github 的时候......但问题是,你的 master 有提交 A - B - C - F,而提交 D 和在您看来,E 仅在 github/master 上。
如果你将 master 推送到 github 并使 github 的 master 指向提交 F,提交 D 和 E 将丢失。 (“他们”,不管他们是谁,仍然会拥有它们,你仍然会拥有它们,但命名为 github/master,因此可以解决此问题,但很痛苦。)
解决方案是您 修补此问题,以便“他们的”提交D 和E 也可以在您的master 上。一种简单的方法是让您合并您的工作和他们的工作,并提供:
C - F
/ \
A - B G <-- master
\ /
D - E <-- github/master
快进
注意您的分支标签 master 是如何在您每次进行新提交时“向前移动”的?
您提交了 F 和 master,它们曾经指向提交 C,向前移动以指向新提交 F。
然后,你做了合并提交 G 和 master,以前指向 F,向前移动指向新的提交,G。
标签在构建时沿着分支“向前移动”。
假设我们有另一个标签——另一个分支名称——指向(比如说)提交B,一直以来,我们还没有移动:
..............<-- br
.
. C - F
v / \
A - B G <-- master
\ /
D - E
我们现在可以让 git “滑动标签 br 前进”,并“快速”地完成它——一次完成,一路提交 G:
git checkout br
git merge --ff-only master
当我们要求 git 进行合并时,如果我们告诉它 --ff-only(仅限快进),它将查看是否有办法将标签从它现在指向的任何提交向前滑动到目标提交,在这种情况下G。 (名称master 指向提交G,因此合并选择提交G 作为快进目标。)在这种特殊情况下,实际上有两种 方法可以做到这一点,B-C-F-G或B-D-E-G;任何一个都足以允许这种“快进”。
(使用--ff-only,如果分支标签不能被快进,合并请求会被简单地拒绝。没有--ff-only,git会尝试创建一个新的,实际的合并提交, 以便标签 可以 向前移动。使用--no-ff,git merge 将创建一个合并,即使已经可以进行快进。默认值,根本没有任何选项, 是在可能的情况下快进,否则进行新的合并提交。)
push 需要快进属性
如果你让 git 推送我们的新 master,它是允许的,因为这符合“快进”测试。当我们进行推送时,我们会告诉 github:“请提交 C、F 和 G,然后从提交 @ 中移动标签 master(我们称之为 github/master) 987654495@ 提交G"。是否有从E 到G 的路径?有,所以允许。
拉
所有git pull 确实是运行git fetch,然后运行git merge。
不幸的是,这意味着您确实需要了解以上所有内容才能真正了解git pull。
不过这里有几处大皱纹。首先,我一直在使用上面的git fetch origin 和git fetch github。换句话说,我一直在命名一个遥控器。当你git pull时,遥控器从哪里来?
答案是它来自您的配置。存储库中的每个分支都可以命名一个远程:
$ git config branch.author.remote github
现在分支author 的“远程”是github。
其次,如果你运行git merge,你必须告诉它要合并什么。当你做git pull时,合并名称来自哪里?
再次,答案是它来自配置。每个分支都可以命名一个上游合并分支:
$ git config branch.author.merge author
Git 将merge 与remote 结合在一起,因此在这两个git config 命令之后,git pull 本质上就是git merge github/author。
我说“基本上”是因为还有另一个问题:在旧版本的 git 中,pull 运行 fetch 的方式不会更新远程分支名称。相反,它使用一个特殊的FETCH_HEAD 文件。 (在较新版本的 git 中,它仍然使用FETCH_HEAD,但它也更新了远程分支名称。)
最后,有一个非常大的问题:您可以将git pull 配置为使用git rebase 而不是git merge。但是这个答案现在已经足够完整了;这些细节我就不说了。