当您执行`git fetch上游master:master`和`git pull上游master:master`时,有什么区别? [英] What is the exact difference when you execute `git fetch upstream master:master` vs `git pull upstream master:master`
问题描述
我知道git fetch
和git pull
之间的区别. git pull
本质上是一个命令中的git fetch
+ git merge
.
但是,我正在研究如何使用上游而不检查master分支来更新fork(master分支).我遇到了这样的答案-合并,更新并在没有签出的情况下拉动Git分支
但是当我已经在master上签出后使用git fetch upstream master:master
时,我遇到了这个错误-
fatal: Refusing to fetch into current branch refs/heads/master of non-bare repository
所以,我尝试了git pull upstream master:master
,它奏效了.有趣的是,执行git pull upstream master:master
会使用上游来更新我的fork,而不管我是否在主服务器上.而git fetch upstream master:master
仅在我不在master 分支上时起作用.
从知识渊博的人那里读到对此的解释将非常有趣.
git pull
基本上是一个命令中的git fetch
+git merge
是的,但是,正如您怀疑的那样,它的意义还不止于此.
Bennett McElwee的评论在您链接到的答案中,实际上是其中一项关键内容.他提到您可以:
使用
fetch origin branchB:branchB
,如果合并不快进,则会安全失败.
另一个文档并没有得到很好的记录:这是git fetch
中的-u
aka --update-head-ok
选项,由git pull
设置. 文档确实定义了它的作用,但是有点神秘和恐惧:
默认情况下, git fetch 拒绝更新对应的标头 到当前分支.该标志禁用检查.这纯粹是 供 git pull 与 git fetch 通信的内部使用, 除非您实施自己的瓷器,否则您不会 应该使用它.
这使我们得到您的观察:
因此,我尝试了git
pull upstream master:master
并成功了.有趣的是,无论我是否在master上,执行git pull upstream master:master
都会用上游更新我的fork.而git fetch upstream master:master
仅在我不在master分支上时有效.
这是由于该-u
标志.如果您运行git fetch upstream master:master
,从某种意义上说 work 可以运行,但是会遇到另一个问题.出现警告是有原因的.让我们看看原因是什么,看看警告是否过于严厉.警告:这里有很多东西!下面的许多复杂之处在于弥补了历史上的错误,同时又保持了向后兼容性.
分支名称,引用和快速转发
首先,让我们谈谈引用和快进操作.
在Git中,引用只是谈论分支名称(如master
)或标签名称(如v1.2
)或远程跟踪名称(如origin/master
)的一种好方法,或者,还有许多其他名称,都以一种常见且明智的方式出现:我们将每种特定的种类名称分组为 gitrevisions文档.例如,如果您有refs/tags/master
和refs/heads/master
并说master
,则Git将首先匹配refs/tags/master
并使用标签. 5
1 如果引用是具有或可以具有 reflog 的名称,则HEAD
是的引用,而ORIG_HEAD
和其他*_HEAD
名称不是.不过,这里的边缘有点模糊.
2 可以通过更多名称来访问这些提交.重要的是,在快进之前和之后都无法通过branch1
到达它们.
3 直接的回答实际上是什么都没有发生,但是最终,如果通过 some 名称无法到达提交I
将垃圾收集提交.
4 这个数据库"实际上至少是目录.git/refs
加上文件.git/packed-refs
的组合.如果Git同时找到文件条目和路径名,则路径名的哈希值将覆盖packed-refs
文件中的路径名.
5 异常:git checkout
尝试将参数作为分支名称 first ,如果可行,将master
视为分支名称. Git中的其他所有内容都将其视为标记名称,因为在分支名称之前加上refs/tags
是步骤3,而在步骤4之前加上前缀.
参考规格
现在我们知道引用只是一个指向提交的名称,分支名称是一种特定类型的引用,快进是正常的日常事务,让我们来看一下 refspec .让我们从最常见且可解释的形式开始,它是两个引用名称,中间用冒号分隔,例如master:master
或HEAD:branch
.
只要您将两个Git相互连接(例如在git fetch
期间和git push
期间),Git就会使用refspecs.左边的名称是 source ,右边的名称是 destination .如果您正在执行git fetch
,则源是 other Git存储库,而目标是您自己的.如果您正在执行git push
,则源是您的存储库,目的地是他们的存储库. (在使用.
的特殊情况下,这意味着此存储库,源和目的地都是您自己,但是一切仍然有效,就好像您的Git与另一个单独的Git交谈一样.)>
如果使用完全限定的名称(以refs/
开头),则可以确定要获得哪个名称:分支,标记或其他名称.如果您使用部分合格或不合格的名称,Git通常会弄清楚您的意思.您有时会遇到Git 无法弄清楚您的意思的情况;在这种情况下,请使用完全限定的名称.
您可以通过省略两个名称之一来进一步简化refspec. Git知道您在冒号的哪一侧省略了哪个名称::dst
没有源名称,而src:
没有目标名称.如果您写name
,则Git会将其视为name:
:无目的地的源.
这些平均值的含义有所不同. git push
的空来源表示删除: git push origin :branch
让您的Git要求其Git完全删除该名称. git push
的空目的地表示使用默认的,该名称通常与分支名称相同:git push origin branch
通过要求其Git设置其名为branch
的分支来推送branch
. 6 请注意,直接git push
直接将其分支 是正常的:您将提交内容发送给他们,然后要求他们设置其refs/heads/branch
.这与普通的fetch
完全不同!
对于git fetch
,目标地址为空表示不更新我的任何引用.非空目的地意味着更新我提供的参考.但是,与git push
不同,您可能在此处使用的通常目的地是远程跟踪名称:将它们的refs/heads/master
转换为您自己的refs/remotes/origin/master
.这样,您的分支机构名称master
(您的refs/heads/master
)将保持不变.
但是,由于历史原因,git fetch
的通常形式只是写为git fetch remote branch
,而省略了目的地.在这种情况下,Git做一些看似矛盾的事情:
- 它写分支名称更新 nowhere .缺少目的地意味着没有(本地)分支得到更新.
- 它将哈希ID写入
.git/FETCH_HEAD
.git fetch
提取的所有内容始终都在此处.这是git pull
在哪里以及如何找出git fetch
所做的事情. - 它更新了远程跟踪名称,例如
refs/remotes/origin/master
,甚至以为没有告知这样做. Git将此称为机会更新".
(其中许多实际上是由.git/config
文件中找到它.)
您还可以通过添加前导加号+
使refspec复杂化.这将设置力"标志,该标志将覆盖分支名称运动的默认快进"检查.这是您的远程跟踪名称的正常情况:您希望您的Git更新您的refs/remotes/origin/master
以匹配其Git的refs/heads/master
,即使这是非快进的更改,因此您的Git始终记得他们的 master
的位置,这是您的Git上一次与他们的Git交谈的时候.
请注意,前导加号仅在有要更新的目的地时才有意义.这里有三种可能性:
- 您正在创建一个新名称.通常没关系. 7
- 您没有更改名称:它曾经用于映射以提交哈希 H ,而请求则说将其设置为映射以提交哈希 H .显然没关系.
- 您正在更改名称.这分为三个子可能性:
- 这根本不是一个类似于分支的名称,例如,它是一个标记,不应移动.您将需要一个强制标志来覆盖默认拒绝. 8
- 这是一个类似分支的名称,分支运动是一个快进的动作.您不需要强制标志.
- 这是一个类似分支的名称,但是动作不是 快进.您将需要强制标志.
这涵盖了更新引用的所有规则,除了最后一条规则的 ,我们还需要更多的背景知识.
6 您可以通过将push.default
设置为upstream
来使 this 复杂化.在这种情况下,如果分支fred
的上游设置为origin/barney
,则git push origin fred
要求其Git设置其名为barney
的分支.
7 对于各种更新情况,您可以编写钩子来执行您想验证名称和/或更新的任何操作.
8 在1.8.3之前的Git版本中,Git意外地使用了分支规则来更新标签.因此,这仅适用于1.8.3和更高版本.
HEAD很特别
请记住,像master
这样的分支名称仅标识某些特定的提交哈希:
$ git rev-parse master
468165c1d8a442994a825f3684528361727cd8c0
您还已经看到,git checkout branchname
表现为一种方式,而git checkout --detach branchname
或git checkout hash
表现为另一种方式,给出了有关分离的HEAD"的可怕警告.虽然HEAD
在大多数情况下都像参考,但在某些方面却非常特殊.特别是HEAD
通常是符号引用,其中包含分支名称的全名.那就是:
$ git checkout master
Switched to branch 'master'
$ cat .git/HEAD
ref: refs/heads/master
告诉我们当前分支名称是master
:HEAD
附加到master
.但是:
$ git checkout --detach master
HEAD is now at 468165c1d... Git 2.17
$ cat .git/HEAD
468165c1d8a442994a825f3684528361727cd8c0
之后,git checkout master
像往常一样使我们回到master
.
这意味着当我们有一个分离的HEAD 时,Git知道我们已经签出了哪个提交,因为正确的哈希ID就在其中,名称为HEAD
.如果我们要对存储在refs/heads/master
中的值进行任意的更改,Git仍然会知道我们已经签出了哪个提交.
但是,如果HEAD
仅包含名称 master
,则Git知道 current 提交的唯一方法是,例如468165c1d8a442994a825f3684528361727cd8c0
refs/heads/master
映射到468165c1d8a442994a825f3684528361727cd8c0
.如果我们做了将更改 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
更改存储在master
中的哈希ID改变了Git的状态观念!
状态包括HEAD vs索引加上index vs工作树
git status
命令运行两个git diff
(内部是git diff --name-status
es):
- 比较HEAD与索引
- 比较索引与工作树
请记住,索引(又名<临时>暂存区或缓存)保存了当前提交的内容. >直到我们开始对其进行修改以保存我们将进行的下次提交的内容.工作树只是整个更新索引,然后提交过程的次要助手.我们只需要它,因为索引中的文件采用特殊的仅Git格式,因此我们系统上的大多数程序都无法使用.
如果HEAD
保留了当前提交的原始哈希ID,则无论我们对分支名称进行什么操作,比较HEAD
vs索引均保持不变.但是,如果HEAD
拥有一个特定的分支名称,并且我们更改一个特定的分支名称的值,然后进行比较,我们将比较一个不同的提交到我们的索引. 索引和工作树将保持不变,但是Git关于(不同的)当前提交和索引之间的相对差异的想法将会改变.
这是为什么 git fetch
默认情况下拒绝更新当前分支名称的原因.这也是为什么您不能推送到非裸存储库的当前分支的原因:该非裸存储库具有索引和工作树,其内容可能旨在与当前提交匹配.如果您通过更改存储在分支名称中的哈希来更改Git关于当前提交的概念,则索引和工作树可能会停止与提交匹配.
这不是致命的-实际上根本没有.这正是git reset --soft
所做的:在不触及索引和工作树中内容的情况下,它更改了附加了HEAD
的分支名称.同时,git reset --mixed
更改分支名称和,但保持工作树不变,而git reset --hard
一次性更改分支名称,索引和工作树. /p>
快进合并"基本上是git reset --hard
使用git pull
依次运行git fetch
和git merge
时,git merge
步骤通常能够执行Git所谓的快进合并.不过,这根本不是合并:对当前分支名称进行快速操作,然后立即将索引和工作树内容更新为新提交,就像git pull
检查-应当应该检查 9 -此git reset --hard
不会破坏正在进行的工作,而
9 从历史上看,git pull
一直会出错,并且在有人丢掉一堆工作后得到修复.避免git pull
!
将所有这些放在一起
运行git pull upstream master:master
时,Git首先运行:
git fetch --update-head-ok upstream master:master
让您的Git在upstream
列出的URL上调用另一个Git,并从它们中收集提交,如通过它们的名称master
(master:master
refspec的左侧)所找到的.然后,您的Git使用refspec的右侧更新您自己的master
(可能是refs/heads/master
).如果master
是当前分支(如果.git/HEAD
包含ref: refs/heads/master
),则fetch
步骤通常会失败,但是-u
或--update-head-ok
标志可以防止失败.
(如果一切顺利,您的git pull
将运行第二个步骤git merge
:
git merge -m <message> <hash ID extracted from .git/FETCH_HEAD>
但是让我们先完成第一步.
快进规则确保您的master
更新是快进操作.如果不是,则提取失败并且您的master
保持不变,并且pull
在此处停止.因此,到目前为止,我们还可以:您的master
只要且仅在从upstream
获得新提交(如果有)的情况下,才可以快速转发.
这时,如果您的master
已更改 ,并且它是您当前的分支,则您的存储库现在不同步:您的索引和工作树不再与您的master
匹配.但是,git fetch
也将正确的哈希ID保留在.git/FETCH_HEAD
中,并且您的git pull
现在继续进行类似重置的更新.这实际上使用的是git read-tree
而不是git reset
的等效项,但是只要它成功-在pull
之前的检查中,它应该成功-最终效果是相同的:您的索引和工作树将与新提交匹配.
或者,也许master
不是您当前的分支.也许您的.git/HEAD
包含了ref: refs/heads/branch
.在这种情况下,即使没有--update-head-ok
,您的refs/heads/master
也可以安全地快速前进git fetch
.您的.git/FETCH_HEAD
包含与更新后的master
相同的哈希ID,并且您的git pull
运行git merge
尝试合并-这可能是快进操作,也可能不是快进操作,具体取决于分支的提交名称branch
指向现在.如果合并成功,则Git会进行提交(实际合并),或者像以前一样调整索引和工作树(快进合并"),并将适当的哈希ID写入.git/refs/heads/branch
中.如果合并失败,则Git会因合并冲突而停止,并让您像往常一样清理混乱.
最后一种可能的情况是您的HEAD
是分离的,但这的工作方式与ref: refs/heads/branch
情况相同.唯一的区别是说完了所有新的哈希ID后,它们直接进入.git/HEAD
而不是.git/refs/heads/branch
.
I know the difference between git fetch
and git pull
. git pull
is basically a git fetch
+ git merge
in one command.
However, I was researching on how to update my fork (master branch) with the upstream without checking out the master branch. I came across this SO answer - Merge, update and pull Git branches without checkouts
But when I used git fetch upstream master:master
after I was already checked out on master, I ran into this error -
fatal: Refusing to fetch into current branch refs/heads/master of non-bare repository
So, I tried git pull upstream master:master
and it worked. What is interesting is that doing git pull upstream master:master
updates my fork with upstream regardless of whether I am on master or not. Whereas git fetch upstream master:master
only works when I am NOT on master branch.
It will be very interesting to read explanation on this, from the knowledgeable folks out here.
git pull
is basically agit fetch
+git merge
in one command
Yes—but, as you suspected, there is more to it than that.
Bennett McElwee's comment, in the answer you linked-to, actually has one of the key items. He mentions that you can:
Use
fetch origin branchB:branchB
, which will fail safely if the merge isn't fast-forward.
Another is not very well documented: it's the -u
aka --update-head-ok
option in git fetch
, which git pull
sets. The documentation does define what it does, but is a bit mysterious and scary:
By default git fetch refuses to update the head which corresponds to the current branch. This flag disables the check. This is purely for the internal use for git pull to communicate with git fetch, and unless you are implementing your own Porcelain you are not supposed to use it.
This gets us to your observation:
So, I tried git
pull upstream master:master
and it worked. What is interesting is that doinggit pull upstream master:master
updates my fork with upstream regardless of whether I am on master or not. Whereasgit fetch upstream master:master
only works when I am NOT on master branch.
This is due to that -u
flag. If you ran git fetch upstream master:master
, that would work, for some sense of the meaning work, but leave you with a different problem. The warning is there for a reason. Let's look at what that reason is, and see whether the warning is overly harsh. Warning: there is a lot here! Much of the complication below is to make up for historical mistakes, while retaining backwards compatibility.
Branch names, references, and fast-forwarding
First, let's talk about references and fast-forward operations.
In Git, a reference is just a fancy way of talking about a branch name like master
, or a tag name like v1.2
, or a remote-tracking name like origin/master
, or, well, any number of other names, all in one common and sensible fashion: we group each specific kind of name into a name space, or as a single word, namespace. Branch names live under refs/heads/
, tag names live under refs/tags/
, and so on, so that master
is really just 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.
That is, given a branch with some commits, represented by uppercase letters here, and a second branch with more commits that include all the commits on the first branch:
...--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. 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
Now that we know that a reference is just a name pointing to a commit, and a branch name is a specific kind of reference for which fast forwards are normal everyday things, let's look at the refspec. Let's start with the most common and explainable form, which is just two reference names separated by a colon, such as master:master
or HEAD:branch
.
Git uses refspecs whenever you connect two Gits to each other, such as during git fetch
and during git push
. The name on the left is the source and the name on the right is the destination. If you are doing git fetch
, the source is the other Git repository, and the destination is your own. If you are doing git push
, the source is your repository, and the destination is theirs. (In the special case of using .
, which means this repository, both source and destination are yourself, but everything still works just as if your Git is talking to another, separate Git.)
If you use fully-qualified names (starting with refs/
), you know for sure which one you will get: branch, tag, or whatever. If you use partially-qualified or unqualified names, Git will usually figure out what you mean anyway. You will occasionally run into a case where Git can't figure out what you mean; in that case, use a fully qualified name.
You can simplify a refspec even further by omitting one of the two names. Git knows which name you omit by which side of the colon is gone: :dst
has no source name, while src:
has no destination name. If you write name
, Git treats that as name:
: a source with no destination.
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
, an empty destination means don't update any of my references. A non-empty destination means update the reference I supply. Unlike git push
, though, the usual destination you might use here is a remote-tracking name: you would fetch their refs/heads/master
into your own refs/remotes/origin/master
. That way, your branch name master
—your refs/heads/master
—is left untouched.
For historical reasons, though, the usual form of git fetch
is just written as git fetch remote branch
, omitting the destination. In this case, Git does something seemingly self-contradictory:
- It writes the branch name update nowhere. The lack of a destination means that no (local) branch gets updated.
- It writes the hash ID into
.git/FETCH_HEAD
. Everythinggit fetch
fetches always goes here. This is where and howgit pull
finds out whatgit fetch
did. - It updates the remote-tracking name, such as
refs/remotes/origin/master
, even thought it was not told to do so. Git calls this an opportunistic update.
(Much of this is actually controlled through a default refspec that you will find in your .git/config
file.)
You can also complicate a refspec by adding a leading plus sign +
. This sets the "force" flag, which overrides the default "fast forward" check for branch name motion. This is the normal case for your remote-tracking names: you want your Git to update your refs/remotes/origin/master
to match their Git's refs/heads/master
even if that's a non-fast-forward change, so that your Git always remembers where their master
was, the last time your Git talked with their Git.
Note that the leading plus only makes sense if there is a destination to update. There are three possibilities here:
- You're creating a new name. This is generally OK.7
- You're making no change to the name: it used to map to commit hash H and the request says to set it to map to commit hash H. This is obviously OK.
- 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
- It's a branch-like name, and the branch motion is a fast-forward. You won't need the force flag.
- It's a branch-like name, but the motion is not a fast-forward. You will need the force flag.
This covers all the rules for updating references, except for one last rule, for which we need yet more background.
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 is very special
Remember that a branch name like master
just identifies some particular commit hash:
$ git rev-parse master
468165c1d8a442994a825f3684528361727cd8c0
You have also seen that git checkout branchname
behaves one way, and git checkout --detach branchname
or git checkout hash
behaves another way, giving a scary warning about a "detached HEAD". While HEAD
acts like a reference in most ways, in a few, it's very special. In particular, HEAD
is normally a symbolic reference, in which it contains the full name of a branch name. That is:
$ git checkout master
Switched to branch 'master'
$ cat .git/HEAD
ref: refs/heads/master
tells us that the current branch name is master
: that HEAD
is attached to master
. But:
$ git checkout --detach master
HEAD is now at 468165c1d... Git 2.17
$ cat .git/HEAD
468165c1d8a442994a825f3684528361727cd8c0
after which git checkout master
puts us back on master
as usual.
What this means is that when we have a detached HEAD, Git knows which commit we have checked out, because the correct hash ID is right there, in the name HEAD
. If we were to make some arbitrary change to the value stored in refs/heads/master
, Git would still know which commit we have checked out.
But if HEAD
just contains the name master
, the only way that Git knows that the current commit is, say, 468165c1d8a442994a825f3684528361727cd8c0
, is that refs/heads/master
maps to 468165c1d8a442994a825f3684528361727cd8c0
. If we did something that changed refs/heads/master
to some other hash ID, Git would think that we have that other commit checked out.
Does this matter? Yes, it does! Let's see why:
$ 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
Changing the hash ID stored in master
changed Git's idea of the status!
The status involves HEAD vs index plus index vs work-tree
The git status
command runs two git diff
s (well, git diff --name-status
es, internally):
- compare HEAD vs index
- compare index vs work-tree
Remember, the index, aka the staging area or the cache, holds the contents of the current commit until we start modifying it to hold the contents of the next commit we will make. The work-tree is merely a minor helper for this whole update the index, then commit process. We only need it because the files in the index are in the special Git-only format, that most of the programs on our systems cannot use.
If HEAD
holds the raw hash ID for the current commit, then comparing HEAD
vs index stays the same regardless of what we do with our branch names. But if HEAD
holds one specific branch name, and we change that one specific branch name's value, and then do the comparison, we'll compare a different commit to our index. The index and work-tree will be unchanged, but Git's idea of the relative difference between the (different) current commit and the index will change.
This is why git fetch
refuses to update the current branch name by default. It's also why you cannot push to the current branch of a non-bare repository: that non-bare repository has an index and work-tree whose contents are probably intended to match the current commit. If you change that Git's idea of what the current commit is, by changing the hash stored in the branch name, the index and work-tree are likely to stop matching the commit.
That's not fatal—not at all, in fact. That's precisely what git reset --soft
does: it changes the branch name to which HEAD
is attached, without touching the contents in the index and the work-tree. Meanwhile git reset --mixed
changes the branch name and the index, but leaves the work-tree untouched, and git reset --hard
changes the branch name, the index, and the work-tree all in one go.
A fast-forward "merge" is basically a 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
!
Putting all this together
When you run git pull upstream master:master
, Git first runs:
git fetch --update-head-ok upstream master:master
which has your Git call up another Git at the URL listed for upstream
and collect commits from them, as found via their name master
—the left side of the master:master
refspec. Your Git then updates your own master
, presumably refs/heads/master
, using the right side of the refspec. The fetch
step would normally fail if master
is your current branch—if your .git/HEAD
contains ref: refs/heads/master
—but the -u
or --update-head-ok
flag prevents the failure.
(If all goes well, your git pull
will run its second, git merge
, step:
git merge -m <message> <hash ID extracted from .git/FETCH_HEAD>
but let's finish with the first step first.)
The fast-forward rules make sure that your master
update is a fast-forward operation. If not, the fetch fails and your master
is unchanged, and the pull
stops here. So we're OK so far: your master
is fast-forwarded if and only if that's possible given the new commit(s), if any, obtained from upstream
.
At this point, if your master
has been changed and it's your current branch, your repository is now out of sync: your index and work-tree no longer match your master
. However, git fetch
has left the correct hash ID in .git/FETCH_HEAD
as well, and your git pull
now goes on to the reset-like update. This actually uses the equivalent of git read-tree
rather than git reset
, but as long as it succeeds—given the pre-pull
checks, it should succeed—the end effect is the same: your index and work-tree will match the new commit.
Alternatively, perhaps master
is not your current branch. Perhaps your .git/HEAD
contains instead ref: refs/heads/branch
. In this case, your refs/heads/master
is safely fast-forwarded the way git fetch
would have done even without --update-head-ok
. Your .git/FETCH_HEAD
contains the same hash ID as your updated master
, and your git pull
runs git merge
to attempt a merge—which may or may not be a fast-forward operation, depending on the commit to which your branch name branch
points right now. If the merge succeeds, Git either makes a commit (real merge) or adjusts index and work-tree as before (fast-forward "merge") and writes the appropriate hash ID into .git/refs/heads/branch
. If the merge fails, Git stops with a merge conflict and makes you clean up the mess as usual.
The last possible case is that your HEAD
is detached, but this works in the same way as for the ref: refs/heads/branch
case. The only difference is that the new hash ID, when all is said and done, goes straight into .git/HEAD
rather than into .git/refs/heads/branch
.
这篇关于当您执行`git fetch上游master:master`和`git pull上游master:master`时,有什么区别?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!