VonC 的答案和Romain Valeri 对该答案的补充都是正确的,但可能很难以各种方式形象化或理解。这是一种理解它的方法,它可能至少有点直观。
当您在 Git 中处理提交时,每个文件最多有三个副本。一旦您知道提交包含每个文件的完整快照,以只读(以及仅 Git、压缩和重复数据删除)格式,您就会明白为什么需要有两个副本:
Git's read-only copy in HEAD your working tree copy
---------------------------- ----------------------
file1.ext file1.ext
file2.ext file2.ext
readme.md readme.md
即使内容file1.ext
在提交的内容相匹配file1.ext
你的工作树,你的工作树是在那里你能看到和工作在/与文件的Git提取从提交- Git的副本是在一些特殊的,怪异的,压缩和只读格式。只有 Git 本身甚至可以读取这个文件,并且没有任何东西——甚至 Git 本身——都不能覆盖它。1 你的工作树包含普通的日常文件,每个人都可以用普通的方式读写,所以 Git 确实必须在每次检查提交时复制它。
同样的原则也适用于其他版本控制系统:在 Mercurial、SVN、CVS 或 ClearCase 或其他任何系统中,您经常会发现文件的多个副本(精确的细节很大程度上取决于 VCS)。然而,Git 特别奇怪的是,每个文件不是只有两个副本,Git 提供了三个:
HEAD (r/o) staging area working tree
---------- ------------ ------------
file1.ext file1.ext file1.ext
file2.ext file2.ext file2.ext
readme.md readme.md readme.md
该HEAD
副本是在提交和不能改变的,不过就是这样。您可以随意使用工作树副本。奇怪的是这个额外的副本位于HEAD 和工作树版本之间。
你看不到这个额外的版本,至少不容易。2 但它就在那里。为什么?嗯,一个答案是:“无缘无故”——毕竟,其他版本控制系统不会这样做。3 但Git 确实做到了,而且Git 除了“与众不同”之外还有其他原因。特别是,这个“集结地”副本的存在可以让你使用git add -p
到部分添加一个文件。有一整套这些部分操作(git reset -p
、git checkout -p
等),我个人不是这些操作的忠实粉丝,但它们确实存在,并且经常被用作中转区存在的理由。
存储的数据在临时区域是偷偷4在同一个只读,压缩和去重复的表单里面的提交Git使用。这对 Git 本身的作用是 make git commit
go非常快(无论如何与所有其他 VCS 相比)。运行时git commit
,暂存区中的文件副本已准备好提交。几乎不需要额外的工作。5 当您运行git checkout
,Git会预先填写指数/舞台区域,这是两个方面的同样的事情,在Git中,所有的文件从提交你签出。当你运行时git add
,Git:
- 压缩和散列工作树文件的内容;
- 检查是否重复;和
- 如果是重复的,则重复使用旧的,否则保存新的
这样文件就可以使用了,并且 Git 可以使用新的或重用的内部哈希 ID 更新其索引条目/暂存副本。6 这意味着索引/暂存区现在已准备好提交。
如果我们把它们放在一起,我们会看到以下内容:
HEAD staging working tree
---- ---------- -------------
file1.ext -> file1.ext -> file1.ext
file2.ext -> file2.ext -> file2.ext
readme.md -> readme.md -> readme.md
HEAD staging working tree
---- ---------- -------------
file1.ext file1.ext file1.ext
file2.ext file2.ext <- file2.ext
readme.md readme.md readme.md
现在各种其他操作也开始变得有意义:
HEAD staging working tree
---- ---------- -------------
file1.ext file1.ext file1.ext
file2.ext file2.ext
readme.md readme.md readme.md
HEAD staging working tree
---- ---------- -------------
file1.ext file1.ext file1.ext
file2.ext -> file2.ext file2.ext
readme.md readme.md readme.md
HEAD staging working tree
---- ---------- -------------
file1.ext file1.ext file1.ext
file2.ext
readme.md readme.md readme.md
HEAD staging working tree
---- ---------- -------------
file1.ext file1.ext file1.ext
file2.ext -> file2.ext -> file2.ext
readme.md readme.md readme.md
该git checkout
命令有模仿两个的事情模式git restore
可以这样做:它可以从分段复制到工作树,或HEAD
到两者分期和工作树。7 这有点危险,因为即使您从未将其保存在任何地方,这些操作也会覆盖工作树副本。这使得使用git switch
而不是git checkout
“更安全”,因为您不会意外获得这种破坏性的操作模式。8
因此,简短的回答(为时已晚)是您的第二个git add
覆盖了您第一次 git add
编写的暂存副本,丢弃了较早的暂存副本。现在很难回来了。
1从技术上讲,只要文件存储为 Git 所谓的松散对象,阅读起来并不难:用任何 zlib 解压程序打开底层对象并解压,然后丢弃 Git 添加的标头。但是,仅仅发现单独的对象是在凯斯特痛,然后它可能是“宽松”相反,这是不是“从紧”,而是包装,然后你真的遇到了麻烦。
覆盖文件在物理上是可能的,但由于对象的名称是对象数据的加密校验和,覆盖文件只会损坏数据到 Git 会说“此对象已损坏”并拒绝提取它的程度。您会知道存储库已损坏,并且您应该找到一些其他未损坏的克隆。
2要查看它不容易,请运行git ls-files --stage
; 请注意,这会在大型存储库中转储大量输出。
3例如,Mercurial 从字面上看没有,但确实有一个隐藏的东西叫做“dirstate”,它做了一些Git 索引所做的事情。但是,Mercurial 的目录状态和 Git 的索引之间面向用户的区别在于,您甚至不必知道目录状态存在。Git 时不时地把它的索引/暂存区推到你的脸上:看!我有这个额外的副本!是不是很酷?看,看!你真的必须意识到这一点。
4使用git ls-files --stage
自曝这个“秘密”,所以它不是真的是秘密。但是,你不需要知道,除非你开始使用这个git ls-files --stage
你自己,和/或夫妇与使用git update-index
。
5 需要做的一点额外工作是 Git 必须运行内部等效的git write-tree
. 这将保存文件的名称和模式。该数据-the文件的内容-are已经“预存”,如罗曼·瓦列里指出。
6读者练习:如果您的git add
某些内容然后从未提交,例如,通过用新内容覆盖它,该怎么办?有一个内部 Git 对象似乎从未在这里使用过。看的git gc
文档,看看最终会发生。
7如果您愿意,该git restore
命令可以从HEAD
工作树复制到工作树,跳过临时副本;git checkout
不能这样做。你是否想这样做,我不知道:这取决于你。但是,如果您决定不希望这样做,请记住,git restore
是更比这个其他方式能git checkout
。
8在 Git 2.23 及更高版本中不再发生“意外破坏模式”的事情,它现在指出您的git checkout zorg
请求是模棱两可的。这是旧版本的 Git中发生的情况:
- 假设你有一个分支命名
origin/zorg
。
- 假设您还有一个名为的文件
zorg
,并且您已经运行git checkout develop
并获得了develop
该文件的-branch-tip 副本。
- 假设现在你花了最后一个小时来制定你解雇出租车司机的邪恶计划。
- 现在你休息一下(小心不要被樱桃呛到)。当你回来时,你会想:等等,我想在
zorg
树枝上。 所以你跑git checkout zorg
。
你不会有一个zorg
分支,但你有一个origin/zorg
和你期待的Git创建一个新的分支zorg
从origin/zorg
并切换到它,如果是安全的,或者给你一个错误提醒您藏匿或提交您的文件。但是相反,Git 说:哦,您希望我删除您最后一小时对文件的工作zorg
,从而将文件的暂存副本提取zorg
到您的工作树中。
如果您使用git switch zorg
,Git 就会知道您打算创建一个新分支,并且会安全地尝试。但是相反,Git 毁了你的工作。无赖!只是不要去杀死一群人(甚至是芒格洛尔人)来发泄你的沮丧,好吗?