git pull
基本上是一个git fetch
+ git merge
在一个命令中
是的——但是,正如你所怀疑的,事情远不止于此。
贝内特·麦克埃尔威的评论 https://stackoverflow.com/questions/3216360/merge-update-and-pull-git-branches-without-using-checkouts/4157106#comment62355072_4157106,在您链接到的答案中,实际上有一个关键项目。他提到你可以:
Use fetch origin branchB:branchB
,如果合并不快进,它将安全失败。
另一个没有很好的记录:它是-u
aka --update-head-ok
选项中git fetch
, which git pull
sets. 文档 https://www.kernel.org/pub/software/scm/git/docs/git-fetch.html确实定义了它的作用,但有点神秘和可怕:
默认情况下git 获取拒绝更新对应的头部
到当前分支。该标志禁用检查。这纯粹是
供内部使用git pull与 沟通git 获取,
除非你正在实现你自己的 Porcelain,否则你就不会
应该使用它。
这让我们得出你的观察结果:
所以,我尝试了 gitpull upstream master:master
它起作用了。有趣的是做git pull upstream master:master
无论我是否在 master 上,都会用上游更新我的 fork。然而git fetch upstream master:master
仅当我不在主分支上时才有效。
这是由于-u
旗帜。如果你跑了git fetch upstream master:master
,对于某种意义上的意义来说,这会起作用work,但给你留下了一个不同的问题。警告的存在是有原因的。让我们看看这个原因是什么,看看这个警告是否过于严厉。警告:这里有很多东西!下面的大部分复杂性是为了弥补历史错误,同时保留向后兼容性。
分支名称、参考文献和快进
首先,我们来谈谈参考 and 快进操作.
在 Git 中,一个参考只是谈论分支名称的一种奇特方式,例如master
,或者像这样的标签名称v1.2
,或远程跟踪名称,例如origin/master
,或者,好吧,任意数量的其他名称,全部以一种常见且合理的方式:我们将每个特定的名称分组kind的名字变成名称空间,或作为单个词,命名空间 https://en.wikipedia.org/wiki/Namespace。分支名称位于refs/heads/
,标签名称位于refs/tags/
,依此类推,这样master
真的只是refs/heads/master
.
Every one of these names, all of which start with refs/
, is a reference. There are a few extra references that don't start with refs
as well, although Git is a little bit erratic internally in deciding whether names like HEAD
and ORIG_HEAD
and MERGE_HEAD
are actually references.1 In the end, though, a reference mainly serves as a way to have a useful name for a Git object hash ID. Branch names in particular have a funny property: they move from one commit to another, typically in a way that Git refers to as a fast forward.
也就是说,给定一个具有一些提交的分支(此处用大写字母表示),以及具有更多提交的第二个分支,其中包括第一个分支上的所有提交:
...--E--F--G <-- branch1
\
H--I <-- branch2
Git is allowed to slide the name branch1
forward so that it points to either of the commits that were, before, reachable only through the name branch2
.2 Compare this, to, say:
...--E--F--G------J <-- branch1
\
H--I <-- branch2
If we were to move the name branch1
to point to commit I
instead of commit J
, what would happen to commit J
itself?3 This kind of motion, which leaves a commit behind, is a non-fast-forward operation on the branch name.
These names can be shortened by leaving off the refs/
part, or often, even the refs/heads/
part or the refs/tags/
part or whatever. Git will look in its reference-name database4 for the first one that matches, using the six-step rules described in the gitrevisions documentation https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html. If you have a refs/tags/master
and a refs/heads/master
, for instance, and say master
, Git will match refs/tags/master
first and use the tag.5
1If a reference is a name that has, or can have, a reflog, then HEAD
is a reference while ORIG_HEAD
and the other *_HEAD
names are not. It's all a little fuzzy at the edges here, though.
2These commits might be reachable through more names. The important thing is that they weren't reachable through branch1
before the fast-forward, and are afterward.
3The immediate answer is actually that nothing happens, but eventually, if commit I
is not reachable through some name, Git will garbage collect the commit.
4This "database" is really just the combination of the directory .git/refs
plus the file .git/packed-refs
, at least for the moment. If Git finds both a file entry and a pathname, the pathname's hash overrides the one in the packed-refs
file.
5Exception: git checkout
tries the argument as a branch name first, and if that works, treats master
as a branch name. Everything else in Git treats it as a tag name, since prefixing with refs/tags
is step 3, vs step 4 for a branch name.
Refspecs
现在我们知道引用只是指向提交的名称,而分支名称是一种特定类型的引用,快进是日常事务,让我们看一下refspec。让我们从最常见和最容易解释的形式开始,它只有两个参考名称用冒号分隔,例如master:master
or HEAD:branch
.
每当您将两个 Git 相互连接时(例如在连接期间),Git 都会使用 refspecsgit fetch
以及期间git push
。左边的名字是source右边的名字是目的地。如果你正在做git fetch
,源是otherGit 存储库,目的地是您自己的。如果你正在做git push
,源是您的存储库,目的地是他们的。 (在特殊情况下使用.
, 意思是这个存储库,源和目标都是您自己,但一切仍然正常,就好像您的 Git 正在与另一个单独的 Git 通信一样。)
如果您使用完全限定名称(以refs/
),你确定你会得到哪一个:分支、标签或其他什么。如果您使用部分限定或不限定的名称,Git 通常会弄清楚您的意思。你偶尔会遇到 Git 的情况can't弄清楚你的意思;在这种情况下,请使用完全限定名称。
您可以通过省略两个名称之一来进一步简化 refspec。 Git 知道你省略了哪一个名字,冒号的哪一侧消失了::dst
没有来源名称,而src:
没有目的地名称。如果你写name
,Git 将其视为name:
:没有目的地的来源。
What these mean varies. An empty source for git push
means delete: git push origin :branch
has your Git ask their Git to delete the name entirely. An empty destination for git push
means use the default which is normally the same branch name: git push origin branch
pushes your branch
by asking their Git to set their branch named branch
.6 Note that it's normal to git push
to their branch directly: you send them your commits, then ask them to set their refs/heads/branch
. This is quite different from the normal fetch
!
For git fetch
,空目的地意味着不要更新我的任何参考资料。非空目的地意味着更新我提供的参考。不像git push
不过,您在这里可能使用的通常目的地是远程跟踪名称:你会拿他们的refs/heads/master
变成你自己的refs/remotes/origin/master
。这样,您的分行名称master
—your refs/heads/master
——原封不动。
但由于历史原因,通常的形式git fetch
只是写成git fetch remote branch
,省略目的地。在这种情况下,Git 做了一些看似自相矛盾的事情:
- 它写入分支名称更新nowhere。缺少目的地意味着没有(本地)分支得到更新。
- 它将哈希 ID 写入
.git/FETCH_HEAD
。一切git fetch
获取总是到这里。这就是地点和方式git pull
发现什么git fetch
did.
- 它更新远程跟踪名称,例如
refs/remotes/origin/master
,甚至认为没有被告知这样做。 Git 称其为机会主义更新.
(其中大部分实际上是通过默认参考规范你会发现在你的.git/config
file.)
您还可以通过添加前导加号来使引用规范复杂化+
。这会设置“force”标志,该标志会覆盖分支名称移动的默认“快进”检查。这是远程跟踪名称的正常情况:您希望 Git 更新您的refs/remotes/origin/master
匹配他们的 Gitrefs/heads/master
even if这是一个非快进的更改,以便您的 Git 始终记住在哪里their master
是,你的 Git 与他们的 Git 的最后一次对话。
请注意,仅当有要更新的目标时,前导加号才有意义。这里有三种可能性:
- You're creating a new name. This is generally OK.7
- 您没有更改名称:它用于映射以提交哈希H并且请求说将其设置为映射以提交哈希H。这显然是可以的。
- You are changing the name. This one breaks down into three more sub-possibilities:
- It's not a branch-like name at all, e.g., it's a tag and should not move. You will need a force flag to override the default rejection.8
- 这是一个类似分支的名字,分支动作是快进。您不需要强制标志。
- 这是一个类似分支的名字,但动作是not快进。您将需要强制标志。
这涵盖了更新参考文献的所有规则,except对于最后一条规则,我们需要更多背景知识。
6You can complicate this by setting push.default
to upstream
. In this case, if your branch fred
has its upstream set to origin/barney
, git push origin fred
asks their Git to set their branch named barney
.
7For various cases of updates, you can write hooks that do whatever you like to verify names and/or updates.
8In Git versions before 1.8.3, Git accidentally used branch rules for tag updates. So this only applies to 1.8.3 and later.
HEAD很特别
请记住,分支名称如master
只是识别一些特定的提交哈希:
$ git rev-parse master
468165c1d8a442994a825f3684528361727cd8c0
你也看到了git checkout branchname
以一种方式表现,并且git checkout --detach branchname
or git checkout hash
表现另一种方式,给出关于“分离的头”的可怕警告。尽管HEAD
在大多数方面都可以作为参考,但在某些方面,它非常特别。尤其,HEAD
通常是一个象征性参考,其中它包含分支名称的全名。那是:
$ git checkout master
Switched to branch 'master'
$ cat .git/HEAD
ref: refs/heads/master
告诉我们当前分支名称 is master
: that HEAD
附于master
. But:
$ git checkout --detach master
HEAD is now at 468165c1d... Git 2.17
$ cat .git/HEAD
468165c1d8a442994a825f3684528361727cd8c0
之后git checkout master
让我们重新开始master
照常。
这意味着当我们有一个分离头, Git 知道我们签出了哪个提交,因为正确的哈希 ID 就在那里,在名称中HEAD
。如果我们要做出一些任意的change到存储在的值refs/heads/master
,Git 仍然会知道我们已经签出了哪个提交。
But if HEAD
只包含name master
,Git 知道的唯一方法current承诺是,比如说,468165c1d8a442994a825f3684528361727cd8c0
, 就是它refs/heads/master
映射到468165c1d8a442994a825f3684528361727cd8c0
。如果我们做了某事changed refs/heads/master
对于其他一些哈希 ID,Git 会认为我们已签出其他提交。
这有关系吗?是的,它确实!让我们看看为什么:
$ git status
... nothing to commit, working tree clean
$ git rev-parse master^
1614dd0fbc8a14f488016b7855de9f0566706244
$ echo 1614dd0fbc8a14f488016b7855de9f0566706244 > .git/refs/heads/master
$ git status
...
Changes to be committed:
...
modified: GIT-VERSION-GEN
$ echo 468165c1d8a442994a825f3684528361727cd8c0 > .git/refs/heads/master
$ git status
...
nothing to commit, working tree clean
更改存储在的哈希 IDmaster
改变了 Git 对状态的看法!
状态涉及 HEAD vs 索引加上索引 vs 工作树
The git status
命令运行两个git diff
胀,git diff --name-status
es,内部):
请记住,index,又名暂存区 or the cache,保存的内容当前提交直到我们开始修改它以保存我们将做出的下一次承诺。工作树只是整个过程的一个小帮手更新索引,然后提交过程。我们需要它只是因为索引中的文件采用特殊的 Git-only 格式,我们系统上的大多数程序都无法使用它。
If HEAD
保存当前提交的原始哈希 ID,然后进行比较HEAD
无论我们如何处理我们的数据,vs 索引都保持不变分支机构名称。但如果HEAD
holds 一个具体的分行名称,以及我们change一个特定分支名称的值,然后进行比较,我们将比较不同的致力于我们的索引。索引和工作树将保持不变,但 Git 的想法相对差异(不同的)当前提交和索引之间会发生变化。
This is why git fetch
默认拒绝更新当前分支名称。这也是为什么您无法推送到非裸存储库的当前分支的原因:该非裸存储库具有一个索引和工作树,其内容是可能是为了匹配当前提交。如果您通过更改存储在分支名称中的哈希来更改 Git 对当前提交的看法,则索引和工作树可能会停止匹配提交。
这并不致命——事实上,一点也不致命。正是如此git reset --soft
作用:它改变了分店名称到哪个HEAD
被附加,而不触及索引和工作树中的内容。同时git reset --mixed
更改分支名称and索引,但保持工作树不变,并且git reset --hard
一次性更改分支名称、索引和工作树。
快进“合并”基本上是git reset --hard
When you use git pull
to run git fetch
and then git merge
, the git merge
step is very often able to do what Git calls a fast-forward merge. This is not a merge at all, though: it's a fast-forward operation on the current branch name, followed immediately by updating the index and work-tree contents to the new commit, the same way git reset --hard
would. The key difference is that git pull
checks—well, is supposed to check9—that no in-progress work will be destroyed by this git reset --hard
, while git reset --hard
itself deliberately does not check, to let you throw away in-progress work that you no longer want.
9Historically, git pull
keeps getting this wrong, and it gets fixed after someone loses a bunch of work. Avoid git pull
!
把所有这些放在一起
当你跑步时git pull upstream master:master
,Git 首先运行:
git fetch --update-head-ok upstream master:master
这让你的 Git 在列出的 URL 处调用另一个 Gitupstream
并从他们那里收集提交,通过他们的名字找到master
— 的左侧master:master
参考规格。然后你的 Git 更新你自己的master
,大概refs/heads/master
,使用参考规范的右侧。这fetch
步骤通常会失败,如果master
是您当前的分支 - 如果您的.git/HEAD
包含ref: refs/heads/master
——但是-u
or --update-head-ok
标志可以防止失败。
(如果一切顺利的话,你的git pull
将运行第二个,git merge
, step:
git merge -m <message> <hash ID extracted from .git/FETCH_HEAD>
但让我们先完成第一步。)
快进规则确保您的master
更新是一个快进操作。如果没有,则获取失败并且您的master
不变,并且pull
停在这里。所以到目前为止我们还可以:你的master
当且仅当有可能给定从以下位置获得的新提交(如果有)时,才快进upstream
.
此时,如果您的master
已经变了and这是您当前的分支,您的存储库现在不同步:您的索引和工作树不再与您的相匹配master
。然而,git fetch
已将正确的哈希 ID 留在.git/FETCH_HEAD
还有你的git pull
现在继续进行类似重置的更新。这实际上相当于使用git read-tree
而不是git reset
,但只要它成功——考虑到预先pull
检查,它should成功 - 最终效果是相同的:您的索引和工作树将与新提交匹配。
或者,也许master
is not您当前的分支机构。也许你的.git/HEAD
包含代替ref: refs/heads/branch
。在这种情况下,您的refs/heads/master
安全快进git fetch
即使没有也会做--update-head-ok
. Your .git/FETCH_HEAD
包含与更新的哈希 ID 相同的哈希 IDmaster
, 和你的git pull
runs git merge
尝试合并 - 这可能是也可能不是快进操作,具体取决于您的分支名称的提交branch
现在点。如果合并成功,Git 要么进行提交(真正的合并),要么像以前一样调整索引和工作树(快进“合并”)并将适当的哈希 ID 写入.git/refs/heads/branch
。如果合并失败,Git 会因合并冲突而停止,并让您像往常一样清理混乱。
最后一种可能的情况是你的HEAD
是分离的,但这与ref: refs/heads/branch
案件。唯一的区别是,新的哈希 ID,当一切都说完并完成后,直接进入.git/HEAD
而不是进入.git/refs/heads/branch
.