TL;博士
Push、fetch 和 pull 让两个不同的 Git 相互交谈。在一种特殊情况下——包括作为问题基础的那个c:\localdir
——两个不同的 Git 存储库位于同一台计算机上,但通常,这两个不同的存储库可以位于任何两台不同的计算机上。
当存储库位于不同的计算机上时,传输方向往往更为重要,因为您无法轻松切换观点。
长
除了公认的答案git pull
(就目前而言足够准确)之外,和之间还有一些其他关键区别git push
。我们需要从这个开始:
push 的反义词是 fetch
Git在这里不小心使用了错误的动词。在 Mercurial 中,我们必须hg pull
从另一个存储库获取hg push
提交,并将提交发送到另一个存储库。但是 Gitgit pull
做了两件事:(1)获取提交;(2)签出或合并这些提交。然后 Git 不得不将这两个步骤分开,因为有时您不想立即执行第 2 步。
这意味着在 Git 中,实际相反的git push
不是git pull
,而是git fetch
。git pull
命令的意思是:
- 运行
git fetch
;然后
- 运行第二个 Git 命令。
第二个命令是事情变得最复杂的地方。如果我们可以省略它——如果我们只处理fetch 和 push——它会更简单。我们可以稍后添加第二个命令。
git fetch
总是安全的,但git push
不是
我们在这里遇到的下一个问题很简单,但是如果您还没有“得到它”,那么它会非常令人困惑,直到您突然“得到它”并且它是有道理的。
当我们拥有一个 Git 存储库时,我们实际上拥有三样东西:
我们有一个提交数据库(和其他对象,但提交是有趣的部分)。提交是编号的,但数字看起来是随机的。它们不是简单的计数:commit #1 后面没有 commit #2,实际上一开始就没有“commit #1”。这些数字是哈希 ID,它们看起来像随机涂鸦:84d06cdc06389ae7c462434cb7b1db0980f63860
例如。
提交中的内容是完全只读的。每个提交就像每个文件的完整快照。这对于存档非常有用,对于完成任何新工作毫无用处。因此,在普通(非裸)存储库中,我们还有:
一个普通的日常存储库有一个我们完成工作的地方。我们不会在这里详细介绍这一点,但这对于 fetch-vs-push 很重要并且很重要。 一些存储库故意省略了这个工作区。这些被称为裸存储库,我们通常在服务器上找到它们。
最后,每个存储库都有一个名称数据库,包括分支名称。这些名称允许您的 Git 找到您的提交。他们的意思是你不必记住84d06cdblahblahwhatever
。
当你运行时git fetch
,你的 Git 会调用其他 Git,通常是通过网络电话在一个https://
或ssh://
地址。你可以c:\localdir
用 a or或其他什么来调用其他 Git /mnt/some/path
。在这种特殊情况下,您的计算机可能会与自己对话——但通常它会与另一台计算机对话,它拥有自己完全独立的 Git 存储库。其他 Git 存储库也可以拥有所有这三个。如果它在服务器上,它可能是一个裸存储库,并且没有工作区。然而,它总是有自己的提交数据库和自己的名称数据库。
这意味着你的Git 有你的提交(也许还有他们的)和你的分支名称。 他们的Git 有他们的提交(也许你也有)和他们的分支名称。使用git fetch
,您可以让您的 Git 调用他们的 Git 并获取他们的提交(所以现在您拥有自己的和他们的);使用git push
,你让你的 Git 调用他们的 Git,并给他们你的提交(所以现在他们有他们的和你的)。
到目前为止,fetch 和 push 之间的主要区别在于数据传输的方向。 使用 fetch可以获得提交,使用 push可以提交提交。 但差异并不止于此。
完成git fetch
后,您的 Git 知道所有提交。这很好——但我们刚刚注意到Git 用来查找提交的提交编号是看起来很丑陋的乱七八糟的乱七八糟的东西。所以要做的是获取他们所有的分支名称——他们用来查找提交的名称——并将它们复制到您自己的 Git 中,但将它们更改为远程跟踪名称。例如,它们变成了你的。如果他们有,您的 Git 会创建或更新您的,依此类推。这意味着永远不要碰你自己的任何分支,这就是为什么它总是安全的。你要么得到新的提交,要么没有。你永远不会输git fetch
main
origin/main
develop
origin/develop
git fetch
您自己的任何提交。然后,您的 Git 会在必要时更新您的远程跟踪名称。然后就完成了。这就是整个正常git fetch
操作:如果合适,引入一些提交,如果合适,更新一些非分支名称。1
的最后一部分git push
,就在它完成之前,包含一个请求。你的 Git 要求他们的 Git 取悦,如果可以的话,改变他们的一些名字。例如,如果你运行git push origin develop
,你的 Git 会发送你有的任何提交,他们没有,他们需要完成操作,然后它会发送一个礼貌的请求:如果可以,请让你的分支名称develop
找到 commit ________。您的 Git 使用您的分支名称develop
找到的提交填充此空白。
这里的主要区别是git fetch
更新您的远程跟踪名称但git push
要求他们更新他们的分支名称。 如果他们正在进行开发,他们可能会认为更新他们的分支名称是不合适的。
1您可以通过多种方式运行git fetch
并告诉它更新您自己的分支名称。这不是偶然发生的;你必须让Git 去做。你不应该让 Git 去做。如果你是 Git Master,这条规则就变成了:你可能不应该让 Git 去做。
第二条命令
现在是时候查看git pull
调用的第二个命令了。嗯,时间差不多了。首先我们应该看看 Git 如何找到提交。
我之前提到过,Git 使用分支名称查找提交。这是真的,但不是完整的画面。我还提到了远程跟踪名称。Git 可以找到具有远程跟踪名称的提交。这更完整,但仍然不是真正的完整。下面是 Git 的一整套技巧:
如果你给它原始哈希 ID,Git 总能找到一个提交。好吧,如果它实际上在您的存储库中,那么您可能需要先使用它来获取它。如果 Git无法从哈希 ID 中找到提交,那只是意味着它还没有在您的存储库中。只需使用从某些具有它的 Git 中获取它,然后就可以了。git fetch
git fetch
Git 可以从名称中找到提交。各种名称都可以在这里使用:分支名称main
和develop
,远程跟踪名称origin/main
和origin/develop
,标签名称v1.2
,甚至时髦的特殊用途名称。Git 有很多你不经常看到的东西。将名称转换为哈希 ID 的规则在gitrevisions 文档中进行了描述。
Git 可以从另一个提交中找到一个提交。 这导致了gitrevisions中的许多规则。这句话在这里用粗体表示,因为它非常重要。
最后,Git 可以通过各种搜索操作找到提交,也在gitrevisions中进行了描述。
gitrevisions 里面有很多东西,你不需要记住所有的东西。请记住,有很多方法可以找到提交。使用git log
,然后剪切和粘贴哈希 ID 是一种很好的方法,但有时您可能想尝试各种快捷方式。但是,请记住另外一件事:git log
通过使用提交来查找提交来查找提交!
每个提交存储两件事:它有所有文件的完整快照,正如我们前面提到的,但它也有元数据:关于提交本身的信息。例如,这包括提交人的姓名和电子邮件地址。它还包括另一个姓名和电子邮件地址(“提交者”与“作者”),以及两个日期和时间戳。它在这个元数据中有很多东西,Git 本身的关键是它有在这个提交之前提交的原始哈希 ID 。
这一切都意味着,在 Git 中,提交形成了一个向后看的链。合并提交存储了两个或多个先前的提交哈希 ID,因此从合并中,我们可以沿着两条链倒退,甚至可能不止两条。在任何非空存储库中,至少还有一个根提交,它不会向后指向:这是历史结束或开始的地方,具体取决于您如何看待它。但是大多数提交只存储一个哈希 ID,给我们一个简单的链:
... <-F <-G <-H
如果H
here 代表某个链中最后一次提交的哈希 ID,并且如果我们有某种方法可以找到commit H
,我们也可以找到 commit G
。那是因为 commitH
存储了之前 commit 的原始哈希 ID G
。所以,从G
中,我们可以找到 commit F
,因为G
存储了 的哈希 ID F
。 F
当然还存储了一个哈希 ID,等等——所以通过从 开始H
,然后向后工作,一次提交一个,我们可以找到所有以 . 结尾的提交H
。
Git 中的分支名称只记录最后一次提交的哈希 ID。我们说分支名称指向最后一个提交,然后最后一个提交指向倒数第二个提交,它又指向一个更早的提交,依此类推。
并行开发
假设我们从某个中央服务器(例如 GitHub)克隆一些存储库。我们得到了大量的提交。我们的git clone
操作实际上是通过创建一个新的空存储库,然后复制他们所有的提交,但不复制他们的分支名称。然后,在用提交填充我们存储库的提交数据库并为其分支名称创建远程跟踪名称之后,我们的 Git 会创建一个新的分支名称。
我们得到的分支名称是我们用git clone
'-b
选项选择的。如果我们不选择一个,我们得到的名字就是他们的Git 推荐的名字。这些天通常是这样main
。有时这是他们唯一的分支名称。如果是这样,我们将获得一系列提交,以及一个远程跟踪名称origin/main
:
...--F--G--H <-- origin/main
然后我们的 Git 将创建我们自己的以main
匹配他们的main
(然后是我们的新的):git checkout
git switch
main
...--F--G--H <-- main (HEAD), origin/main
我们现在可以工作并进行新的提交。无论我们做出什么新的提交,他们都会得到新的、普遍唯一的哈希 ID。让我们在我们的:上做两个新的提交: main
I--J <-- main (HEAD)
/
...--F--G--H <-- origin/main
现在让我们假设,无论如何,他们的Git 已经将两个新的提交添加到他们的 main
. 这些新提交将获得新的通用唯一哈希 ID。当我们运行时git fetch origin
,我们会选择新的提交:
I--J <-- main (HEAD)
/
...--F--G--H
\
K--L <-- origin/main
注意我们的工作和他们的工作是如何不同的。 当有并行开发时会发生这种情况。当没有并行开发时不会发生这种情况:如果他们没有获得两个新的提交,我们仍然会有我们的——我们对他们的记忆——指向 commit 。我们的新提交添加到.origin/main
main
H
I-J
H
如果我们没有并行开发,我们git push
现在可能可以
假设我们没有任何并行开发。我们现在运行:
git push origin main
将我们的新I-J
提交发送给他们,并要求他们将他们 main
的指向设置为 commit J
。如果他们服从,他们会得到:
...--F--G--H--I--J <-- main
(请注意,他们没有origin/main
,我们不在乎他们HEAD
是什么,而不是我已经告诉过你我们HEAD
的内容)。
如果我们确实有并行开发,这是一个问题
如果他们有:
...--F--G--H--K--L <-- main
当我们运行时,在他们的存储库中git push
,我们将向他们发送我们的 I-J
. 但是我们的 commitI
连接回 commit H
。然后,我们的 Git 会要求他们设置他们 main
的指向 commit J
:
I--J <-- (polite-request: set main to point here)
/
...--F--G--H--K--L <-- main
如果他们服从这个要求,他们将失去他们的K-L
。所以他们会拒绝这个请求。我们将看到的具体错误是声称这不是快进。
有可能,根据权限,2强迫他们无论如何都要服从。但是,正如脚注 1 中一样,这不是您应该做的事情,至少在您真正理解“丢失”提交的概念之前不要这样做。
2 Git作为分布式没有这种权限检查,但是大多数托管服务,例如 GitHub,都添加了它。如果您设置自己的托管服务,您也应该考虑添加它的方法。
面对并行开发,我们需要一种结合工作的方式
让我们假设,无论以何种方式,我们发现自己处于这种情况:
I--J <-- main (HEAD)
/
...--F--G--H
\
K--L <-- origin/main
我们现在需要的是一种将我们的工作(我们为提交提交所做的工作)I
与他们的工作(无论他们是谁)结合起来的方法:他们为提交提交所做的工作。J
K-L
Git 有很多组合工作的方法,但我们不会在这里详细介绍。执行此操作的两种主要方法是 withgit merge
和 with git rebase
。因此,在git fetch
导致这种分叉之后——我们和他们都有新的提交——我们将需要第二个 Git 命令,可能是git merge
或git rebase
。
第二个命令的正确选择部分是见仁见智的问题。这里没有一个普遍正确的选择。但这是什么git pull
:
您git pull
现在运行git fetch
. 这将获得他们拥有的任何您没有的新提交,并更新您的远程跟踪名称。3 然后查看是否需要进行特殊的第二次联合作业。如果是这样,它将使用它来组合工作。如果没有,它只会对最新的提交执行git checkout
or ,同时还会将您当前的分支名称向前移动。4git switch
3在非常过时的 Git 版本(早于 1.8.4)中,git pull
不会更新远程跟踪名称。如果您遇到这些古老的 Git 版本之一,请注意这一点。
4这里有两点需要注意:
Git 称之为快进合并。这实际上不是一个合并,所以这是一个糟糕的名字。(Mercurial 只是将其称为更新。)从 Git 2.0 开始,您可以告诉只git pull
执行快进操作:如果需要工作组合,将执行提取,但随后会因错误而停止。这可能是从一开始就应该做的事情,也可能是最终会做的事情,但出于兼容性的原因,它今天不会这样做。git pull
git pull
如果您确实可以选择,并且如果您喜欢git pull
,我建议您使用git pull --ff-only
或配置pull.ff
to only
, with git config pull.ff only
。(我个人倾向于只是运行git fetch
,然后git log
或者一些类似的操作来检查,然后git merge --ff-only
手动运行,但是我的习惯在 Git 2.0 之前就已经定型了。)
该git switch
命令是 Git 2.23 中的新命令。对于这种特殊情况,git switch
两者之间没有真正的区别。git checkout
添加新命令是因为 Git 人员发现它git checkout
太复杂了——它有很多模式——而且它的一些模式具有破坏性。这种破坏有时甚至会影响经验丰富的 Git 用户。(这已得到修复:自 2.23 起,git checkout
这些情况下的错误现在已解决。)为了使 Git 更加用户友好,git checkout
将其拆分为两个单独的命令。使用新命令是个好主意,但旧命令仍然有效,因为 Git 必须长期兼容。
概括
Push 发送提交并要求他们更新他们的分支。这要求事情最终是正确的。这不能结合并行开发。
拉取提交并让您的 Git 更新您的远程跟踪名称,然后运行第二个 Git 命令来更新您的分支。第二条命令可以结合并行开发。
您可以通过使用代替来避免立即运行第二个命令。如果您想在决定如何使用它之前查看您正在处理的内容,这将非常有用。git fetch
git pull