TL;DR
git pull 表示运行git fetch,然后运行第二个 Git 命令。第一步——git fetch——不会影响任何你的分支。如果您正在处理任何事情,它不会改变您正在处理的任何事情。
second 步骤,默认运行git merge,会影响您的当前分支。它不会创建新的分支,因此一般来说,在另一个 Git 中创建的任何 new 分支名称都不相关,除非您在 git pull 命令中明确命名它们。
假设您在没有额外参数的情况下运行git pull,则git pull 运行git fetch 的remote 是与当前分支关联的远程,以及用于rebase-的提交or-merge 是与当前分支的 upstream 关联的分支,由 git fetch 步骤更新。 Git 对存储库中分支名称的上游设置施加了限制:特别是,如果您的 Git 还不知道另一个 Git 中存在某个名称,那么您的 Git 不会让您将其设置为上游。所以“新”分支——我们实际上没有正确定义——是不相关的。
如果您在git pull 命令行中添加更多参数,情况会变得更加复杂。
长
无论我拉取还是取回新分支没有区别吗?
Git pull 总是意味着:运行git fetch,然后运行第二个 Git 命令。显然这些是不同的,因为git fetch 不会运行第二个 Git 命令。获取步骤是否看到您的 Git 以前没有看到的分支名称在这里无关紧要。
如果是,那么从 Git 内部的角度来看有什么区别?
在这里,您需要密切了解 Git 的真正工作原理。为了保持这个答案简短(ish),我会说看到很多我的其他答案以获得更多细节,但是:
- 每个提交都有一个唯一的哈希 ID,这是
git log 向您显示的长随机提交名称:例如 commit 1c56d6f57adebf2a0ac910ca62a940dc7820bb68。
每次提交都会存储您所有文件的快照。每次提交中的文件都是一种特殊的、只读的、仅限 Git 的压缩格式,并且永久冻结。
每个提交还存储一些关于提交的元数据: 信息不是与提交一起保存的文件,而是包含诸如谁提交、何时以及为什么提交的信息(他们的日志消息)。在此元数据中,每个提交存储其直接 父 提交的哈希 ID(对于大多数提交;有些存储两个或更多父提交,这些是 合并提交,并且至少一个将是存储库中的第一个提交,因此不会有父级)。
-
像master 这样的分支名称 只是保存链中last 提交的原始哈希ID。因此,如果你有一个名为 master 的分支和一些提交,master 持有一些哈希 ID H,并且提交 H 指向一些更早的提交 G,它指向一个更早的提交 @ 987654342@等:
... <-F <-G <-H <--master
要添加一个提交到一个分支,我们选择那个分支名称,它选择了那个提交。这会将提交的冻结的、仅 Git 的文件 out 放到我们可以处理它们的区域中。我们根据需要处理它们并最终告诉 Git:进行新的提交。 Git 使 new 提交点回到我们得到的那个,保存我们所有文件的新快照,然后,在做出新提交后,更改分支名称,使其指向新的提交:
...--F--G--H--I <-- master
分支名称并不是唯一可以记住提交哈希 ID 的名称。多个名称也可以识别任何单个提交。
git clone 命令通过调用另一个 Git 存储库来工作。你告诉你的系统:
- 创建一个新的空目录/文件夹(或使用
git clone 指向的空文件夹)。
- 在那里创建一个新的空存储库:
git init。
- 以
origin(或您告诉 Git 使用的任何其他名称)的名称存储一个 URL 供以后使用:git remote add。
- 使用
git clone 命令执行您告诉 Git 执行的任何其他配置。
- 通过
origin(在存储的 URL)调用另一个 Git,并让它列出其分支(和其他)名称及其原始哈希 ID。然后,向 Git 询问提交......在这种情况下,all 提交。将其提交的所有复制到我们原本为空的存储库中。取其分支名称并重命名它们:例如,将其master 变为我们的origin/master,并将其develop 变为我们的origin/develop,等等。
- 最后,对于其中一个名称(可能是
master),使用重命名的 origin/ 版本的名称来创建一个 branch 名称,并将该分支名称指向与我相同的提交origin/ 版本的名称。
所以在最初的git clone 之后,您有远程跟踪名称,通常是origin/* 的形式,用于每个其他Git 的分支 名称。然后,您有一个自己的分支名称,通常是master,指向与您的origin/master 相同的提交。如果他们有master 和develop,也许你现在有:
...--G--H <-- master, origin/master
\
I--J <-- origin/develop
上面的六步git clone序列中的第5步实际上是git fetch。然而,git fetch 所做的不是获取每个提交,而是与另一个 Git 交谈,看看他们有哪些提交而你没有。在初始克隆期间,您没有 任何 提交,因此它们自动全部提交。后来,他们的新。
当您稍后运行git fetch 时,如果他们仍然有他们的master 识别提交H 和他们的develop 识别提交J,您的Git 将在您的存储库中查找,使用@ 987654374@ 和 J 代表,看看你已经拥有它们。你的 Git 不需要获得任何新的提交。但是,如果他们向 他们的 develop 添加了另一个提交,他们将有新的提交 K,你会得到它:
...--G--H <-- master, origin/master
\
I--J <-- origin/develop
\
K
然后您的git fetch 将更新您的远程跟踪名称 origin/develop 指向提交K:
...--G--H <-- master, origin/master
\
I--J--K <-- origin/develop
如果他们做了一些不寻常的事情并迫使他们的develop返回一步,而你再次运行git fetch,你将保持提交K一段时间——默认情况下通常至少 30 天——但你的 Git 会调整你的 origin/develop 以匹配他们的 develop:
...--G--H <-- master, origin/master
\
I--J <-- origin/develop
\
K [no name: hard to find!]
Git 通常发现从某个名称开始提交——无论是你的分支名称,还是你的远程跟踪名称,或者任何其他名称——然后向后工作。
(每个名称都有之前存储的哈希ID的隐藏日志,您可以通过它找到K。这些日志中的条目最终会过期,这就是30天限制的来源:30天后,保留K 的条目过期。此后一段时间,如果没有人创建新的名称 保护它。)
像这样运行git fetch,根本没有名字——默认为origin,通常——或者只有远程的名称,例如origin,只要你没有设置特别是——从另一个 Git 获取 all 的分支名称,并相应地创建或更新您的远程跟踪名称的 all。然而,设置一个叫做单分支克隆的东西会以不同的方式配置你的 Git,所以git fetch 只更新一个远程跟踪名称。您可以稍后重新配置它,或者使用 refspec 覆盖要更新的名称集,但我们不会在此详细介绍。
到目前为止,这都是关于git fetch;让我们开始使用分支名称
同样,Git 的 fetch 是从另一个 Git 获取新提交的部分。获得新的提交后,如果有一些要获得,git fetch 会调整您的远程跟踪名称。它对您的任何分支名称都没有影响。您的分支名称都不受干扰。
如果你从来没有自己的任何分支名称——这很奇怪,尽管这样做是可能的——并且永远不要自己做任何工作,这对你来说就不那么奇怪和明智了某些应用程序(例如存档存储)就足够了。但您可能确实使用分支。
假设您创建您自己的 分支名称,dave 或任何您喜欢的名称。假设您将此名称指向现有提交 H:
...--G--H <-- dave, master, origin/master
\
I--J--K <-- origin/develop
既然您有多个分支名称,我们想让 Git 记住您实际使用的是哪一个。我们将为其中一个附加特殊名称HEAD:
...--G--H <-- dave (HEAD), master, origin/master
\
I--J--K <-- origin/develop
所以现在我们可以知道您正在使用 name dave 和 commit H。三个名称,dave 和 master 和 origin/master,现在都标识提交 H。
我们在上面提到,提交中保存的文件是一种特殊的、只读的、仅 Git 的、压缩和冻结的格式,只有 Git 可以使用。因此,Git 已将这些文件复制到 Git 的 index 和您的工作区中。工作区是您的工作树 或工作树。它具有以您计算机的普通格式存储的普通文件。
通过操作这些普通文件,然后使用git add 将它们复制回 Git 的索引,您通常无论如何都会做出新的提交。这会将文件重新压缩为冻结格式,准备好进行新的提交。当你运行git commit 时,Git 会将当时在其索引中的文件打包。因此我们可以说索引的主要功能是存储你打算放入下一个提交的内容。 (它还有其他的功能,这里就不赘述了。)
最终你的文件已经成型,git add-ed,你运行git commit。 Git 收集适当的元数据并写出一个新的提交,它为新的提交分配其唯一的哈希 ID。然后 Git 将新提交的哈希 ID 存储到当前分支 name 中,给我们:
L <-- dave (HEAD)
/
...--G--H <-- master, origin/master
\
I--J--K <-- origin/develop
你同样可以在 master 或 develop 上工作,它开始指向提交 K 或其他任何方式,但无论如何,你做出一个新的提交,它指向你的任何提交告诉 Git 使用开头。
现在,如果您运行 git fetch 和 他们,无论他们是谁,做出或以其他方式获得了您尚未见过的新提交,这些新提交已添加到 他们的 分支。你的 Git 在他们的存储库中看到它们,看到你还没有它们,然后获取它们。让我们画一个(并停止画I-J-K,因为它们挡住了,但是字母已经用完了,所以接下来我将使用M):
L <-- dave (HEAD)
/
...--G--H <-- master
\
M <-- origin/master
您可能希望以某种方式合并他们的新提交。
究竟如何合并他们的新提交取决于您。例如,您可以:
-
git checkout master 然后git merge origin/master
-
git merge origin/master 现在在提交时 L 在分支上 dave
或做任何其他事情。
如果你:
git checkout master; git merge origin/master
不过,您的 Git 会执行 Git 所说的快进合并。这根本不是一个合并——它的名字有点不好——但它有这样的效果:
L <-- dave
/
...--G--H--M <-- master (HEAD), origin/master
事实上,如果您运行git checkout master; git rebase origin/master,在这种特殊情况下会发生同样的事情。在其他情况下,可能会发生不同的事情。
这就是git pull 的用武之地
通常,一旦您使用 git fetch 从其他 Git 带来了新的提交,您往往想要对它们做点什么。如果您在您的master 上并且他们已经更新了他们的master,那么您可能想要做的是更新您的master。两种最常见的方法是运行git merge 或git rebase。
git pull 命令可以被告知将其中任何一个作为其第二个命令运行。默认是运行git merge。 git merge 和git rebase 都在当前分支上操作。 即他们看的是特殊名称HEAD。只要它附加到某个分支名称(通常是这样),它们就会影响您的分支名称。他们对 Git 的索引和你的工作树进行了更改;两者都可以更改当前分支名称选择的提交; git merge 可能会进行新的合并提交,或者执行快进操作,或者有时什么也不做。
我不喜欢git pull 的部分之一是你并不总是知道,当你点击 Enter 时,git fetch 的确切提交最终会获取,以及它在哪里可以移动任何远程跟踪名称。但是你死定了 使用这些新的提交和更新的名称运行git merge 或git rebase。 (这在技术上有点偏离,正如我们将看到的那样——它不直接使用更新后的 origin/* 名称——但在这里已经足够接近了。)
即使新提交不是您想用来影响当前分支的东西,您也会遇到这种情况。你无法告诉它是否会发生。您可以先使用某个查看器检查另一个 Git 存储库,但如果您查看它会发生什么,然后在您按下 Enter 之前,其他人 更改 另一个存储库?尽管如此,人们还是非常喜欢它,并且一直在使用它,所以让我们来看看您的详细问题吧。
我还再次打开了.git/FETCH_HEAD 文件并看到了该分支的行:<sha-1> not-for-merge branch side_branch_2 of <url>.
这是关于git fetch 和git pull 的历史秘密(或不那么秘密):它们太老了,以至于git pull 本身在origin/master 这样的远程跟踪名称之前就已经存在。遥控器和远程跟踪名称是在 Git 版本 1.4 和 1.5 之间的某个时候发明的,并且有些人摸索着不同的想法。 git pull 命令一直按照人们希望的方式工作,在整个过渡时期,随着新奇的遥控器和远程跟踪名称的开发。
为了避免过于频繁地更改太多代码,和/或因为远程和远程跟踪名称还不存在,git fetch 总是将所有内容写入.git/FETCH_HEAD。为了让早期的git pull 脚本找出将哪个提交哈希 ID 提供给git merge,git fetch 指出我们现在使用的我们的 分支名称之一——这就是“HEAD 附加在哪里”检查——以及从 other Git 中使用什么名称。然后它用not-for-merge 标记每个.git/FETCH_HEAD 行,或者不标记它,具体取决于您提供给git fetch 的参数。
当你运行git pull 时,你可以给git pull 命令提供一堆参数:
git pull # no arguments at all
git pull origin # just a remote
git pull origin master # a remote and a branch name *on the remote*
当git pull 真正运行git fetch 时,它将这些参数传递给git fetch。它现在内置了git fetch,但它仍然可以正常工作。如果您在此处给出一个或多个分支名称,或者那些是 git fetch 不 在 .git/FETCH_HEAD 文件中标记为 not-for-merge 的分支名称。
同样,当git pull 仍然是一个shell 脚本时——它是最近用C 重写的——这就是git pull 决定将哪个哈希ID 传递给git merge 或者,如果你选择git rebase 作为你的第二个命令,到git rebase。它现在的所作所为更加晦涩难懂。由于 fetch 部分现在内置为 C 编码的函数调用,因此它可以将原始哈希 ID 保留在内存中。
在 Git 版本 1.8.4 中,Git 人员决定 git fetch origin master 应该更新 origin/master。在此之前,git fetch origin 将更新 all 远程跟踪名称,但 git fetch origin master 将更新 none。从 Git 1.8.4 开始,git fetch origin master 更新 origin/master。它仍然不会更新其他远程跟踪 origin/* 名称,因为它不会带来与任何更新名称相对应的提交。 (在某些情况下它仍然可以更新远程跟踪名称,但它不会。)
结论
git pull 运行的git fetch:
- 主要是获取您提供的参数:例如,
git pull xyzzy one two three 运行 git fetch xyzzy one two three。 “大部分”只是在这里,因为某些选项会影响要使用的 second 命令,和/或被git pull 本身吃掉,和/或传递给第二个命令而不是传递给@987654491 @。
- 从指定的远程(或从给定的 URL,但这会改变很多东西)获取,从而更新一些远程跟踪名称;
- 在
.git/FETCH_HEAD 中记录它所做的一切,以防您仍在使用旧的git pull shell 脚本。
一般来说,git fetch 可以随时安全运行。 (如果您真的愿意,可以通过不恰当地设置 remote.<em>name</em>.fetch 或传递不安全的 refspec 参数来将其配置为不安全的。但值得注意的是,git fetch 具有内置的安全检查即使你这样做。旧的pull 脚本将它们关闭!)
随后的git merge 或git rebase 在当前分支上运行,如果您有未提交的工作,让这些发生往往不是一个好主意。 Git 通常会检测到这种情况,并在这些情况下完全阻止第二个命令运行。然而,在遥远的过去,pull 命令可能(并且确实)破坏了正在进行的工作而无法恢复,因为git pull——无论如何,旧脚本——关闭了许多安全检查。
在任何情况下,第二个命令——merge-or-rebase 步骤——获得了一堆额外的参数,使其在 Git 1.4 到 1.6 的过渡期间远程和远程跟踪名称发生变化时的工作方式相同。那是差不多 15 年前的事了,但它仍然以同样的方式工作。如果你使用:
git fetch
git merge
并且您的 Git 进行了合并提交,默认的合并消息将类似于:
merge branch origin/dave into dave
但如果你使用:
git pull
默认的合并消息会更像:
merge branch dave of <url> into dave
“类似”是因为这里每条消息的确切拼写取决于分支名称(显然),以及您是否合并到master——这省略了into <branch> 部分——并且有一些引用插入的标记,我不想在这里打扰。 :-)