Git:「git pull」具体是做什么的?

11

我知道git pull实际上是git fetchgit merge的结合,它基本上将远程仓库中的内容带入到了本地仓库中。

  1. 但是,这是否意味着在使用git pull之后,我的工作树将与远程仓库相同?
  2. 我发现有些情况下,使用git pull并没有改变本地仓库或创建任何新的提交。这是什么原因?
  3. git pull只对索引进行更改,这有意义吗?
  4. 如果有意义,那么我如何使索引中的更改向前移动到工作树?

如果您有尚未推送的本地更改(非空提交),则您的本地树与远程树不相同。此外,“git pull”可以是“git fetch”加上“git rebase”。但是,“git rebase”可以被视为特殊的“git merge”。 - ElpieKay
你在这里提出了5个问题 :-) - Chrillewoodz
我希望它们至少是好的。 - CrazySynthax
4个回答

17

确切地这一部分真的很难。通常说,git pull 运行 git fetch 然后是 git merge 或者 git rebase,并且事实上,git pull 曾经是一个 shell 脚本,现在是一个 C 程序,它确实直接运行了 git fetch,但现在它直接调用实现 git fetch 的 C 代码。

然而,下一步非常棘手。此外,在评论中,你补充道:

[fetch] 从远程仓库获取更改。它把它们放在哪里?

要正确理解这一点,您必须了解 Git 的对象系统。

Git 对象模型和 git fetch

每个提交记录都是一种独立的实体。每个提交记录都有一个唯一的哈希 ID:b06d364... 或其他。该哈希 ID 是该提交记录内容的加密校验和。例如:

$ git cat-file -p HEAD | sed 's/@/ /g'
tree a15b54eb544033f8c1ad04dd0a5278a59cc36cc9
parent 951ea7656ebb3f30e6c5e941e625a1318ac58298
author Junio C Hamano <gitster pobox.com> 1494339962 +0900
committer Junio C Hamano <gitster pobox.com> 1494339962 +0900

Git 2.13

Signed-off-by: Junio C Hamano <gitster pobox.com>

如果您将这些内容(减去Git添加到每个对象的标题部分,但保留HTML标记)输入SHA-1校验和计算器中,您将获得哈希ID。这意味着每个拥有此提交的人都具有相同的哈希ID。
您可以获取Git的Git存储库并运行git cat-file -p v2.13.0^{commit}以查看相同的数据。注意:标签v2.13.0转换为074ffb61b4b507b3bde7dcf6006e5660a0430860,这是一个标签对象;标签对象本身引用提交b06d364...
$ git cat-file -p v2.13.0
object b06d3643105c8758ed019125a4399cb7efdcce2c
type commit
tag v2.13.0
[snip]

为了处理提交,Git必须将提交对象(具有哈希ID“b06d364...”的项)本身以及其树对象和树需要的任何其他对象存储在某个位置。这些是您在git fetchgit push期间看到Git计数和压缩的objectsparent行告诉哪个提交(对于合并,是多个提交)是该特定提交的前任。为了拥有完整的提交集,Git还必须拥有父提交(--shallow克隆可以故意省略各种父级,其ID记录在“浅移植”的特殊文件中,但正常克隆始终会拥有所有内容)。
总共有四种对象类型:提交(commits)、(带注释的)标签、树(trees)和Git称之为“blob”对象。Blob主要存储实际文件。所有这些对象都驻留在Git的“对象数据库”中。Git可以通过哈希ID轻松地检索它们:git cat-file -p <hash>,例如,以模糊的人类可读格式显示它们。(大多数情况下,除了解压缩外,几乎不需要做任何事情,尽管树形对象具有必须首先格式化的二进制数据。)
当您运行git fetch——或者让git pull代替您运行时——您的Git会从另一个Git获取一些初始对象的哈希ID,然后使用Git传输协议来确定需要哪些其他对象才能完成您的Git仓库。如果您已经拥有某个对象,则无需再次获取它,并且如果该对象是提交对象,则也不需要其任何父对象。1因此,您只获得您还没有的提交(和树和blob)。然后,您的Git将这些内容填充到您的存储库的对象数据库中。
一旦对象被安全地保存,您的Git会将哈希ID记录在特殊的FETCH_HEAD文件中。如果您的Git版本至少为1.8.4,则此时它还会更新任何相应的远程跟踪分支名称,例如它可能会更新您的origin/master
(如果您手动运行git fetch,则您的Git将遵守所有常规的refspec更新规则,如 git fetch文档所述。这是由git pull传递的附加参数阻止了某些操作,具体取决于您的Git版本。)
那么,这就是我认为您真正的第一个问题的答案:git fetch将这些对象存储在Git的对象数据库中,可以通过它们的哈希ID检索。它将哈希ID添加到.git/FETCH_HEAD(始终),并经常更新一些引用 - refs/tags/中的标签名称以及refs/remotes/中的远程跟踪分支名称。

1除了将一个浅克隆(unshallow)变成非浅克隆之外。


git pull 的其余部分

运行git fetch会获取对象,但不会将这些对象合并到任何你的工作中。如果你想要使用获取的提交或其他数据,你需要进行第二步操作。

你可以执行的两个主要操作是git mergegit rebase。最好的理解它们的方法是在其他地方阅读有关它们的内容(其他SO文章、其他文档等)。这两个命令都很复杂,而且git pull有一个特殊情况没有被这两个命令覆盖:特别地,你可以将git pull拉取到一个不存在的分支中。你有一个不存在的分支(Git也称之为孤立分支或未出生分支),有两种情况:

  • 在新的空存储库中(没有提交),或者
  • 在运行git checkout --orphan newbranch之后
在这两种情况下,都没有“当前提交”,因此没有可重排或合并的内容。但是,索引和/或工作树不一定为空!在一个新的、空的存储库中,它们最初是空的,但在运行git pull之前,您可能已经创建了文件并将它们复制到索引中。
这种类型的git pull传统上存在错误,因此要小心:Git 1.8之前的版本有时会破坏未提交的工作。我认为最好完全避免在这里使用git pull:只需自己运行git fetch,然后确定您想要做什么。据我所知,在现代的Git中,这些版本不会破坏您的索引和工作树,但是我习惯于避免使用git pull。
无论如何,即使您不在孤儿/未出生/不存在的分支上,尝试使用脏索引和/或工作树(“未提交的工作”)运行git merge也不是一个好主意。现在git rebase命令有了一个自动存储选项(rebase.autoStash),因此您可以让Git自动运行git stash save将这些未提交的工作创建为一些离线分支提交。然后再运行rebase,之后Git可以自动应用并删除存储。 git merge命令没有这个自动选项,但是您当然可以手动执行。
请注意,如果您处于冲突合并的中间状态,则所有这些都不起作用。在此状态下,索引具有额外的条目:您无法提交这些条目,直到解决冲突,并且您甚至无法将它们存储(这与git stash实际上创建提交的事实自然相符)。您可以在任何时候运行git fetch,因为它只会向对象数据库添加新对象;但是当索引处于此状态时,您无法合并或重新设置基础。

4
  1. 但是,“git pull”后,我的工作区是否与远程 repo 相同?

不一定。你在拉取的分支上有任何本地提交将会与上游的更改合并。使用 git pull --rebase,将本地更改放置在上游提交之上。如果没有使用 --rebase,你可能会得到一些非常奇怪的合并路径。

  1. 我发现有些情况下执行“git pull”不会改变本地 repo 或者创建新的提交?

如果上游没有新的提交,本地副本也不会发生任何变化。

  1. “git pull”仅对索引进行更改,这有意义吗?

据我所知,不是这样的。也许如果它无法与你的本地提交合并,但那么你至少应该在过程中得到一些错误提示。

  1. 如果是这样的话,如何使索引中的更改向前移动到工作树中?

git pull :) 或者 git rebase <upstream> <branchname>。这将基于分支 <branchname> 上游提交的基础上,将本地提交变基。


如果我想把远程更改放在我的本地提交之上怎么办? - CrazySynthax
2 - 我可以得出这样的结论吗:如果“git pull”更改了本地存储库,那么一定有一个新的提交? - CrazySynthax
@CrazySynthax,你需要先使用fetch命令,然后将远程跟踪分支checkout到本地分支上,并对其进行变基操作。但这样会重写历史记录。 - Quentin

3
  1. No :
    if you have local commits, which you haven't pushed yet, or some indexed changes (git added), you will still have those local changes on top of the last public commit (or merged with the last public commit) ;

  2. Yes :
    if nothing was pushed to the remote repo since your last git pull, you are already up to date, so nothing will change ;

  3. No :
    if you see changes in the index after a git pull, the files were already indexed before you ran git pull ;

  4. git already will, with the following caveat : if one of your indexed files should be updated by the merge, git will not perform the merge, and print a message :

    error: Your local changes to the following files would be overwritten 
    by merge:
        bb
    Please commit your changes or stash them before you merge.
    Aborting
    

    In that case : you should probably create a commit from your index, and run git merge origin/current/branch (or git rebase origin/current/branch) to incorporate the remote modifications with your local modifications.


git fetch [origin] 的默认行为是读取存储在远程仓库中的所有分支,并更新存储在 refs/remotes/[origin]/* 下的所有本地引用。

然后,您可以在所有标准的 git 命令中使用 origin/branch/name 作为有效的树形名称:

# difference with remote "master" branch :
$ git diff HEAD origin/master

# history of remote branch "feature" alongside your local branch "feature" :
$ git log --oneline --graphe feature origin/feature

# merge changes from remote "master" :
$ git merge origin/master

# rebase your local commits on top of remote "develop" branch :
$ git rebase origin/develop

# etc ...

你还可以使用一种快捷方式表示“与我当前所在分支关联的远程分支”:@{u}

$ git diff @{u}
$ git log --oneline --graph HEAD @{u}
$ git merge @{u}
$ git rebase @{u}
# etc ...

“git pull” 究竟是做什么的?

在执行 git fetch 后,git 会更新一个特殊的引用,名为 FETCH_HEAD,它通常与当前分支的 @{u} 匹配;
git pull 执行的是 git fetch && git merge FETCH_HEAD

我尝试在上面的段落中用自己的话解释了 git fetch


谢谢你的回答,但我仍在寻找关于我的原问题:“git pull到底是做什么?”的答案。它会从远程仓库带来变更。它把它们放在哪里? - CrazySynthax

-1

获取您当前使用的分支相关的远程仓库更改的最新内容


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接