在Git中,HEAD、工作树和索引之间有什么区别?

643

有人能告诉我Git中HEAD、工作区和索引之间的区别吗?

据我所知,它们都是不同分支的名称。我的假设正确吗?

我找到了这个:

单个git存储库可以跟踪任意数量的分支,但您的工作树仅与其中一个相关联(“当前”或“签出”的分支),而HEAD指向该分支。

这是否意味着HEAD和工作区始终相同?


39
关于你的编辑:绝对不行。HEAD 是当前分支末端的提交记录。如果你刚刚检出了该分支,即没有修改过任何文件,则 HEAD 的内容与工作树一致。一旦你修改了任何内容,它就不再匹配。 - Cascabel
11
我认为你需要阅读这个网站:http://think-like-a-git.net/ - Andrzej Duś
9
我会尽力完成翻译,以下是所需翻译的内容:我还想在列表中添加一个“暂存区”。什么是“HEAD”、“工作树”、“索引”和“暂存区” - Green
7
如果你修改了任何东西,工作目录就不再与 HEAD 提交相匹配。 - starscream_disco_party
4
如果您以后想要真正理解这些答案中的一些内容,最好的方法是看到、感受和可视化概念化正在发生什么:这是学习Git最好的工具:http://onlywei.github.io/explain-git-with-d3/#fetchrebase - BenKoshy
显示剩余4条评论
5个回答

745

以下是关于这些主题的其他好参考资料:

workflow

我将索引用作“检查点”。
当我要进行可能会出错的更改时,例如概念上要求高的重构或更改表示类型等,我想探索一些方向,但不确定是否能够跟进或者这是否是一个好主意,我会将我的工作检查点存入索引中。
如果这是自从上次提交以来我所做的第一个更改,那么我可以使用本地仓库作为检查点,但通常我正在实现一组小步骤作为一个概念性的更改。我希望在每个步骤之后进行检查点,但在回到工作和测试代码之前保存提交。
注意:
1. 工作区 是您看到和编辑的(源)文件的目录树。
2. 索引 是一个单独的、大型的二进制文件,位于<baseOfRepo>/.git/index中,列出了当前分支中的所有文件、它们的sha1校验和、时间戳和文件名——它不是另一个带有文件副本的目录。
3. 本地仓库 是一个隐藏的目录(.git),包括一个objects目录,其中包含存储为压缩的“blob”文件的仓库中每个文件的所有版本(本地分支和远程分支的副本)。
4. 不要把上面图中表示的四个“磁盘”看成是仓库文件的独立副本。
链接:Git为什么比X更好

3 states

它们基本上是Git提交的命名引用。有两种主要类型的引用:标记和头。

  • 标记是固定的引用,标记历史上的特定点,例如v2.6.29。
  • 相反,头总是移动以反映项目开发的当前位置。

commits

(注意:正如评论者 Timo Huovinen所指出的,这些箭头不是提交指向的内容,而是工作流顺序,基本上显示为箭头1 -> 2 -> 3 -> 4,其中1是第一个提交,4是最后一个。)

现在我们知道项目中正在发生什么。
但是为了知道此时此刻正在发生什么,有一个特殊的引用叫做HEAD。它有两个主要目的:
  • 告诉Git从哪个提交中获取文件以供您检出
  • 告诉Git在哪里放置新提交
当您运行git checkout ref时,它会将HEAD指向您指定的引用并从中提取文件。当您运行git commit时,它会创建一个新的提交对象,该对象成为当前HEAD的子级。通常,HEAD指向其中一个分支,所以一切都很顺利。

checkout


28
读了很多次有关Git的内容,我从未完全理解它,这让我感到非常沮丧,我想说脏话;但我在社区里! 你提到了头部,但是在上面的图片中总是只有一个HEAD,其余的头去哪儿了?“通常HEAD指向其中一个分支,所以一切都能正常运作。” 我请求您解释一下您的陈述。 - Necktwi
16
@neckTwi 所说的 HEAD 是你当前正在使用的提交(https://dev59.com/h3NA5IYBdhLWcg3wbNQ4#964927)。它通常是“分支头”之一(由分支引用的提交,代表这些分支的末端)。但是你可以切换到(并且工作于)任何提交。如果你切换到一个不是(分支)头的提交,那么你就处于“分离的 HEAD”模式下:https://dev59.com/CG865IYBdhLWcg3wIrFg#3965714 - VonC
16
关于索引,我认为最有用的说法就是“索引只是暂存区的另一个名称”,就像@ashraf-alam所说的那样。我觉得在大多数讨论中,它通常被称为暂存区,这就是为什么我没有自动将其与索引联系起来的原因。 - Pete
1
@Pete 我同意。关于缓存和索引的区别,可以参考我的另一个回答https://dev59.com/0mw15IYBdhLWcg3wSJo3#6718135 - VonC
1
因为我认为rebase应该在git pull --rebase的上下文中理解:即从远程仓库进行fetch操作,然后在远程跟踪分支的基础上进行rebase操作。 - VonC
显示剩余20条评论

171
HEAD(当前分支或当前分支上最近一次提交的状态)、index(也称为暂存区)和working tree(检出文件的状态)之间的区别在Scott Chacon的《Pro Git》书中“1.3 Git Basics”章节的“The Three States”部分中有描述(根据知识共享许可协议)。以下是该章节中说明它的图片:

Local Operations - working directory vs. staging area (index) vs git repository (HEAD)

在上述图像中,“工作目录”与“工作树”相同,“暂存区”是git“索引”的另一个名称,而HEAD指向当前检出的分支,该分支的末端指向“git目录(存储库)”中的最后一次提交。
请注意,git commit -a将在一步中暂存更改并提交。

2
一张图片胜过千言万语。谢谢Jakub..还有感谢提供的链接。 - Joyce Babu
6
注意:现在似乎更倾向于使用“工作树(working tree)”而非“工作目录(working directory)”。请参见 https://github.com/git/git/commit/89aef71d0eb5b5e06216c2efbba76cffe17679f7。 - VonC
5
这张图片并不完全准确,因为暂存区包含在名为“index”的单个文件中-而该索引文件恰好位于.git目录的根目录中。因此,如果您将仓库定义为.git目录,则暂存区在技术上是在仓库内部的。第三列最好标记为“HEAD的根树对象”,以指示检出的文件来自提交对象,并且提交会将新树写入提交对象-这两个提交对象都由HEAD指向。 - Jazimov
1
@Jazimov 你可能是对的,但正如他所写的,他已经从著名的Pro Git书中获取了那张图片,并提供了链接。因此,如果这张图片可以改进或者甚至是错误的,有人应该告诉那本书的作者...总的来说,我愿意这样做,但老实说,我还是一个Git初学者,还没有理解你所说的内容,所以在这种情况下我肯定不是正确的人选。 - Binarus
1
@Binarus 我认为这实际上是一个语义问题,而不是一个“错误”。该图似乎表明“.git目录”和“repo”是同义词,并且暂存区是独立的。我希望看到一个跨越暂存区和仓库的“.git目录”标签,但我也希望将“Repo”标签更改为“DAG”。这些变化可能会使初学者感到不知所措,但它们呈现了更准确的情况。让我们希望怀疑的读者能够在这里找到我们的讨论! :) 感谢您的评论和想法 - 您正在正确地思考问题。 - Jazimov
显示剩余3条评论

86

工作树是您当前工作的实际文件内容。

HEAD是指向您上次检出的分支或提交的指针,如果您进行新提交,它将成为新提交的父提交。例如,如果您在master分支上,则HEAD将指向master,并且当您提交时,新提交将是指向的修订版的后代,并且master将更新为指向新提交。

索引是准备新提交的暂存区域。本质上,索引中的内容是将进入新提交的内容(尽管如果您执行git commit -a,这会自动将Git知道的所有更改添加到索引中,以在提交之前提交当前工作树的内容)。git add将从工作树中添加或更新文件到您的索引中。


非常感谢您的解释,Brian。因此,工作树包含所有未提交的更改。如果我使用git commit -a提交我的更改,那么在特定时间我的工作树和索引将是相同的。当我推送到我的中央存储库时,所有三个都将是相同的。我是正确的吗? - Joyce Babu
3
基本上是这样的。你可以在工作树中有 Git 不知道的文件,这些文件不会随 git commit -a 提交(需要用 git add 添加),因此你的工作树可能有额外的文件,而你的索引、本地仓库或远程仓库没有。 - Brian Campbell
3
@Vinod:在不提交更改的情况下,工作树和索引可以变得相同(git add从工作树更新索引,git checkout <path>从索引更新工作树)。HEAD指的是最近的提交,因此当您提交时,您将HEAD更新为新的提交,它与索引匹配。推送与此无关 - 它使远程的分支与本地存储库中的分支匹配。 - Cascabel

68

工作树

你正在处理的文件就是你的工作树。

Git索引

  • Git的"索引"是你要提交到git仓库中的文件存放的地方。

  • 这个索引也被称为缓存目录缓存当前目录缓存暂存区已暂存文件

  • 在你将文件"提交"(checkin)到git仓库之前,首先需要将文件放在git的"索引"中。

  • 索引不是工作目录: 你可以输入命令,例如git status,git会告诉你哪些文件被添加到了git索引中(例如通过使用git add filename命令)。

  • 索引不是git仓库: git索引中的文件是git如果使用git commit命令提交到git仓库的文件。


1
请注意,Git 2.5将引入多个工作树 (https://dev59.com/SW025IYBdhLWcg3wAg_p#30185564)。+1 - VonC
3
我不确定“索引不是工作目录”这个说法是否完全正确。它应该是“索引不是工作目录,但它包括整个工作目录和你想提交的更改”。证明呢? 进入一个git仓库,用reset --hard HEAD确保你的索引与工作树相同。然后: mkdir history && git checkout-index --prefix history/ -a 结果是在你的“history/”目录中复制了你整个的工作树。 因此,git索引 >= git工作目录。 - Adam Kurkiewicz
3
索引不是工作目录,也不必包含工作目录。索引只是存储您想提交的信息的git仓库中的文件。 - Boon
3
“索引”保存的是工作树内容的快照,正是这个快照被作为下一个提交的内容。因此,在对工作目录进行任何更改之后,在运行提交命令之前,您必须使用“add”命令将任何新文件或修改过的文件添加到索引中。” - anth
3
如果你在git reset --HARDgit checkout-index步骤之前或之后先运行echo untracked-data > untracked-file,那么证明将会失败。你会发现未跟踪的文件不在history目录中。你也可以独立修改索引和工作树,但是要想在不改变工作树的情况下修改索引是困难的(需要使用git update-index --index-info)。 - torek
显示剩余3条评论

57

这是来自ProGit书籍的一份不可避免又易于理解的长篇解释:

注意: 您可以阅读该书的第7.7章重置解密进行参考

Git作为一个系统,在正常操作中管理和操作三棵树:

  • HEAD: 最后提交的快照,下一级父节点
  • Index: 提出下次提交的快照
  • Working Directory: 沙盒

HEAD

HEAD是指向当前分支引用指针,而该分支引用则是指向在该分支上最后做出的提交指针。这意味着HEAD将是创建的下一个提交的父对象。通常,最简单的方法是将HEAD视为在该分支上最后一个提交的快照

它包含什么?
要查看该快照的样子,请在您的存储库的根目录中运行以下命令:

                                 git ls-tree -r HEAD

这将导致类似于这样的结果:

                       $ git ls-tree -r HEAD  
                       100644 blob a906cb2a4a904a152... README  
                       100644 blob 8f94139338f9404f2... Rakefile  
                       040000 tree 99f1a6d12cb4b6f19... lib  

索引

Git使用这个索引记录你最后一次检出到工作目录的所有文件内容以及它们在最初检出时的样子。然后,你会用新版本的一些文件替换它们,并将其提交到新的提交树中。

它包含什么?
使用 git ls-files -s 命令查看它的内容。你应该看到类似于以下内容:

                 100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README   
                 100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile  
                 100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb  

工作目录

这是您的文件所在的位置,在将其提交到暂存区(索引)然后进入历史记录之前,您可以在此处尝试更改。

可视化示例

让我们看看这三个树(正如ProGit书籍所称)如何共同工作?
Git的典型工作流程是通过操作这三个树来记录项目的连续快照,以获得不断改进的状态。请看这张图片:

enter image description here

为了更好地理解,请考虑以下场景。假设您进入一个只有一个文件的新目录。将其称为文件的v1版本,用蓝色表示。运行git init将创建一个Git存储库,并创建一个指向未出生的主分支的HEAD引用。{{请注意保留原文中的代码格式和标签}}

enter image description here

此时,只有工作目录树才有任何内容。 现在我们想要提交这个文件,因此我们使用git add将工作目录中的内容复制到索引中。

enter image description here

然后我们运行git commit,它会将索引的内容保存为永久快照,并创建一个指向该快照的提交对象,然后更新主分支以指向该提交。

enter image description here

如果我们运行git status,我们会看到没有任何更改,因为所有三个树是相同的精华 git status以以下方式显示这些树之间的差异:
- 如果工作树与索引不同,则git status将显示有一些未提交的更改 - 如果工作树与索引相同,但它们与HEAD不同,则git status将在其结果中显示一些文件在要提交的更改部分下 - 如果工作树与索引不同,并且索引与HEAD不同,则git status将在其结果中显示一些文件在未提交的更改部分下,并在要提交的更改部分下显示其他一些文件。
对于更好奇的人 git reset命令的注意事项
希望了解reset命令的工作原理将进一步解释存在这三个树背后的原因。 reset命令是你在git中的时间机器,可以轻松地带您回到过去并为您带来一些旧的快照供您使用。以这种方式,HEAD是你可以通过它穿越时间的虫洞。让我们看一个来自书籍的例子来了解它的工作原理:
考虑以下存储库,其中有一个文件和3个提交,它们以不同的颜色和不同的版本号显示:

enter image description here

树的状态如下图所示:

enter image description here

步骤1:移动HEAD(--soft):

reset命令的第一件事是移动HEAD指向的位置。这并不等同于改变HEAD本身(这是checkout所做的)。reset移动HEAD指向的分支。这意味着如果HEAD设置为主分支,运行git reset 9e5e6a4将使主分支指向9e5e6a4。如果使用--soft选项调用reset,则会在此处停止,而不更改索引工作目录。现在我们的repo看起来像这样:
注意:HEAD~是HEAD的父级

enter image description here

第二步:更新索引(--mixed):
重新审视一下这张图片,我们可以看到该命令本质上是撤销了最后一次提交。由于工作树和索引相同但与HEAD不同,git状态现在将显示以绿色准备提交的更改。
使用--mixed选项运行reset会使用HEAD指向当前快照的内容更新索引,保持工作目录不变。这样做,您的存储库看起来就像您已经完成了一些未暂存的工作,而git状态将显示为红色的未暂存更改。此选项还将撤销最后一次提交并取消所有更改的暂存。就像你做了一些更改,但还没有调用git add命令一样。现在我们的repo看起来像这样:

enter image description here

步骤三:更新工作目录(--hard)

如果您使用 reset 命令并带上 --hard 选项,它将把 HEAD 所指向的快照内容复制到 HEAD、索引和工作目录中。执行 reset --hard 命令后,就好像您回到了过去的某个时间点,并且在那之后没有做过任何事情。请参见下图:

enter image description here

结论

我希望现在你对这些“树”有了更好的理解,并且对于它们通过允许您更改存储库中的文件以撤消或重做您错误执行的操作所带来的强大功能有了一个很好的想法。


仅供参考,这是书中的第7.7章 Git工具 - 重置解密 - Cnly
非常好的解释。 - puerile

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