当您执行`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`

查看:148
本文介绍了当您执行`git fetch上游master:master`和`git pull上游master:master`时,有什么区别?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我知道git fetchgit 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)的一种好方法,或者,还有许多其他名称,都以一种常见且明智的方式出现:我们将每种特定的种类名称分组为.git/refs加上文件.git/packed-refs的组合.如果Git同时找到文件条目路径名,则路径名的哈希值将覆盖packed-refs文件中的路径名.

5 异常:git checkout尝试将参数作为分支名称​​ first ,如果可行,将master视为分支名称. Git中的其他所有内容都将其视为标记名称,因为在分支名称之前加上refs/tags是步骤3,而在步骤4之前加上前缀.


参考规格

现在我们知道引用只是一个指向提交的名称,分支名称是一种特定类型的引用,快进是正常的日常事务,让我们来看一下 refspec .让我们从最常见且可解释的形式开始,它是两个引用名称,中间用冒号分隔,例如master:masterHEAD: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将此称为机会更新".

(其中许多实际上是由默认refspec 控制的,您可以在.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 branchnamegit 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 fetchgit 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 a git 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 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.

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. Everything git fetch fetches always goes here. This is where and how git pull finds out what git 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 diffs (well, git diff --name-statuses, 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屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆