首页 > 解决方案 > “承诺”和“未修改”是一样的吗?

问题描述

我从https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F读到了Git 中的三个状态 它在这里说 Git 具有三个主要状态,您的文件可以驻留在:commitedmodifiedstaged

然后,我还阅读了两种状态:从https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository跟踪或未跟踪 这里它说每个文件在您的工作目录可以处于以下两种状态之一:已跟踪或未跟踪。跟踪文件是上次快照中的文件;它们可以是未修改的、已修改的或暂存的。

三个状态中提到的状态是否与跟踪文件的子状态相似?已提交未修改是否相同?

这些图像表明它们是相同的吗?

文件状态的生命周期

Git 的三种文件状态:已修改、暂存和已提交

标签: git

解决方案


TL;博士

Tracked-ness 不是列出的三个状态的子集,列出的三个状态不足以描述(或真正理解)Git 是如何工作的。

这个“三态”的东西有点善意的谎言,这可能就是页面上说的原因:

Git有三个主要状态

(强调我的)。我认为 Pro Git 书在这里造成了一些损害,因为我认为他们正试图——出于某些充分的理由——在你最初对所有事物的看法中隐藏 Git索引的存在。但是在同一个段落中,他们介绍了暂存区的概念,这实际上只是索引的另一个名称。

事实上,这里真正发生的是每个文件通常有三个副本。一个副本在当前提交中,中间副本在索引/暂存区域中,第三个副本在您的工作树中。

从版本控制系统设计的角度来看,中间副本(索引中的副本)不是必需的。Mercurial 是另一个非常类似于 Git 的版本控制系统,每个文件只有两个副本:已提交的副本和工作树副本。这个系统更容易思考和解释。但是由于各种原因,1 Linus Torvalds 决定应该有第三个副本,夹在提交和工作树之间。

知道提交的文件副本是一种特殊的冻结、只读、压缩、仅 Git 文件格式(Git 将其称为blob 对象,尽管大多数时候您不需要知道),这很有用。因为这些文件是冻结/只读的,Git 可以在使用相同文件副本的每个提交中共享它们。这可以节省大量的磁盘空间:一个 10 兆字节文件的一次提交最多占用 10 兆字节(取决于压缩),但是使用相同的文件进行第二次提交并且新副本占用零额外字节:它只是重复使用现有副本。无论您进行多少提交,只要您继续重复使用旧文件,就不会占用更多空间来存储文件。Git 只是不断重复使用原始版本。

事实上,关于提交的一切都被永远冻结了。任何提交的任何部分——没有文件、没有作者信息、没有日志消息中的拼写错误——都不能被更改。你能做的最好的事情就是做出一个新的、改进的、不同的提交,以修复拼写错误或其他什么。然后您可以使用新的和改进的提交来代替旧的和糟糕的提交,但是新的提交是不同的提交,具有不同的哈希 ID。哈希 ID 是提交的真实名称(以及就此而言,与提交快照一起使用的 blob 对象的真实名称)。

所以提交是永久的2并且是只读的。提交中的文件被压缩为只读、​​仅 Git 的冻干格式。由于提交历史,这将永远保留历史,以防您想回顾它以了解某人做了什么、何时以及为什么。但这对于完成任何实际工作一点都不好。你需要文件是可延展的、柔韧的、塑料的、易处理的、灵活的、手上的油灰。您需要处理您的文件。简而言之,您需要一个工作树,您可以在其中进行实际工作。

当您git checkout提交时,Git 会将冻干副本提取到此工作树中。现在您的文件都在那里,您可以使用和更改它们。你会认为这git commit会从工作树中获取更新的文件并提交它们——这就是 Mercurialhg commit所做的,例如——但不,这不是 Git 所做的。

相反,Git 将每个文件的第三个副本插入到已提交副本和工作树副本之间。这第三个副本,在 Git 有时称为index,有时称为staging area,有时称为缓存的实体中——三个名字代表一件事——是冻干的 Git格式,但重要的是,因为它不在一个提交,你可以随时覆盖它。就是这样git add做的:它需要您在工作树中拥有的一个普通文件,将其冷冻干燥,然后将其填充到索引中,以代替之前该名称下的索引中的任何内容。

如果您之前的文件不在索引中git add,那么现在它是。如果它索引中……好吧,无论哪种情况,Git 都会将工作树文件压缩为适当的冻干格式并将其填充到索引中,因此现在索引副本与工作树副本匹配。如果工作树副本与提交的副本匹配(以任何适当的冷冻干燥或再水合为模),则所有三个副本都匹配。如果没有,您可能有两个匹配的副本。 但这些并不是唯一的可能性——它们只是主要的三种,我们稍后会看到。


1这些原因大多归结为性能。Git 的git commit速度比 Mercurial 的快数千倍hg commit。其中一些是因为 Mercurial 主要是用 Python 编写的,但很多是因为 Git 的索引。

2更准确地说,提交一直持续到没有人可以通过哈希 ID 找到它们为止。当您从旧的和糟糕的提交切换到新的和改进的副本时,就会发生这种情况。在那之后,旧的和糟糕的提交,如果它们真的无法找到(而不是仅仅隐藏在不经意的观察中),就有资格被 Git 的垃圾收集器删除,git gc.


对于每个文件,检查其在三个副本中的状态

您已经选择了一些提交作为当前 ( HEAD) 提交,通过git checkout. Git 发现这个提交有一些文件;它已将它们全部提取到索引工作树中。假设您只有文件README.mdmain.py. 他们现在是这样的:

  HEAD           index        work-tree
---------      ---------      ---------
README.md      README.md      README.md
main.py        main.py        main.py

从这个表中很难判断哪个文件有哪个版本,所以让我们添加一个版本号:

  HEAD           index        work-tree
---------      ---------      ---------
README.md(1)   README.md(1)   README.md(1)
main.py(1)     main.py(1)     main.py(1)

这与 Pro Git 书籍的第一个状态相匹配。

现在您修改工作树中的文件之一。(这些是您可以使用普通非 Git 命令查看和处理的唯一文件。)假设您将版本 2README.md放入工作树:

  HEAD           index        work-tree
---------      ---------      ---------
README.md(1)   README.md(1)   README.md(2)
main.py(1)     main.py(1)     main.py(1)

Git 现在会说您有未暂存的更改以提交README.md. 这真正的意思是,如果我们进行两次比较——从HEADvs index 开始,然后转到 index vs work-tree——我们在第一次比较中看到相同,在第二次比较中看到不同。这与 Pro Git 书籍的“已修改但未上演”状态相匹配。

如果你现在运行git add README.md,Git 将冻干更新的工作树版本 2README.md并覆盖索引中的那个:

  HEAD           index        work-tree
---------      ---------      ---------
README.md(1)   README.md(2)   README.md(2)
main.py(1)     main.py(1)     main.py(1)

表中的一个细微变化是,现在,在比较中,HEAD-vs-index 显示已README.md更改,而 index-vs-work-tree 显示它们相同。Git 将这种情况更改称为暂存为 commit。这与 Pro Git 书籍的“修改和分阶段”状态相匹配。

如果您现在进行提交,Git 将立即打包索引中的任何内容——即版本 1和版本 2——并使用这些文件进行新的提交。然后它会调整一些东西,这意味着的提交,而不是你之前签出的那个。所以现在,即使的提交仍然有两个文件的版本 1 形式,你现在有:main.pyREADME.mdHEAD

  HEAD           index        work-tree
---------      ---------      ---------
README.md(2)   README.md(2)   README.md(2)
main.py(1)     main.py(1)     main.py(1)

现在所有三个副本都README.md匹配。

但是假设您README.md现在在工作树中进行更改以制作版本 3,那么git add

  HEAD           index        work-tree
---------      ---------      ---------
README.md(1)   README.md(3)   README.md(3)
main.py(1)     main.py(1)     main.py(1)

然后,您再README.md进行一些更改以制作与所有三个先前版本不同的版本 4:

  HEAD           index        work-tree
---------      ---------      ---------
README.md(1)   README.md(3)   README.md(4)
main.py(1)     main.py(1)     main.py(1)

当我们现在比较HEAD-vs-index 时,我们看到它README.md为 commit 暂存的,但是当我们比较 index 和 work-tree 时,我们看到它也不是为 commit 暂存的。这与三个州中的任何一个都不匹配——但这是可能的!

跟踪与未跟踪

跟踪的文件是上次快照中的文件...

不幸的是,这具有很大的误导性。事实上,被跟踪的文件非常简单,就是现在索引中的任何文件。请注意,该指数具有延展性。它现在可能包含README.md第 3 版,但您可以将其替换README.md为另一个版本,甚至完全删除README.md

如果你删除它,README.md你会得到:

  HEAD           index        work-tree
---------      ---------      ---------
README.md(1)                  README.md(4)
main.py(1)     main.py(1)     main.py(1)

版本 3 刚刚消失3 所以现在README.md工作树中的那个是一个未跟踪的文件。如果您README.md在运行之前将一个版本(任何版本)放回索引中git commitREADME.md则会返回被跟踪,因为它在索引中。

由于从您签出的提交git checkout 中填写索引(和工作树),因此可以跟踪上次提交中的文件并没有错。但正如我在这里所说,这是一种误导。跟踪性是索引中文件的函数。 它如何到达那里与跟踪无关。


3从技术上讲,Git 仍然在其对象数据库中将冻干副本作为 blob 对象,但如果没有其他人使用该冻干副本,它现在可以进行垃圾收集,并且随时可能消失。


Git 从索引中做出新的提交;新提交引用旧提交

我们已经在上面提到了一些,但让我们再回顾一遍,因为它对于理解 Git 至关重要。

Git 中的每个提交(实际上,任何类型的每个对象)都有一个特定于该特定提交的哈希 ID。如果您记下哈希 ID,然后再次输入,Git 可以使用该哈希 ID 来查找提交,只要提交仍在 Git 的“所有对象”的主数据库中。

每个提交还存储了一些早期提交的哈希 ID。通常这只是一个先前的哈希 ID。这个前一个哈希 ID 是提交的父级

每当您(或 Git)手头有这些哈希 ID 之一时,我们就说您(或 Git)有一个指向底层对象的指针。所以每个提交都指向它的父级。这意味着给定一个只有三个提交的小型存储库,我们可以绘制提交。如果我们使用单个大写字母来代替我们的提交哈希 ID,结果对人类来说更有用,当然我们很快就会用完 ID(所以我们不要只绘制几个提交):

A <-B <-C

C最后一次提交。我们必须以某种方式知道它的哈希 ID。如果这样做,我们可以让 Git 从数据库中获取实际提交,并C保存其前任提交的哈希 ID B。我们可以让 Git 使用它来B找出A. 我们可以用它来钓鱼A,但这一次,没有以前的哈希 ID。不可能:A是第一次提交;没有更早的提交A指向。

所有这些指针总是向后指向,这是必要的。任何提交的任何部分在我们提交后都无法更改,因此B可以保存A的 ID,但我们不能A将 stuffB的 ID 更改为A. C可以指向,B但我们不能改变B使它指向C。但我们所要做的就是记住 的真正哈希 ID C这就是分支名称的来源

让我们选择名称master并让 GitC在该名称下保存哈希 ID。由于名称包含哈希 ID,因此名称指向C

A--B--C   <-- master

(由于懒惰和/或其他原因,我已经停止将提交中的连接器绘制为箭头。没关系,因为它们无法更改,我们知道它们指向后。)

现在让我们检查一下 commit C, using git checkout master,它从使用 commit 保存的文件中填充我们的索引和工作树C

git checkout master

然后我们将修改一些文件,用于git add将它们复制回索引中,最后运行git commit. 该git commit命令将收集我们的姓名和电子邮件地址,从我们或从标志获取日志消息,添加当前时间,并通过立即-m保存索引中的任何内容来进行新的提交。这就是为什么我们必须首先将文件放到索引中。git add

这个新提交将具有 commitC的哈希 ID 作为新提交的父级。写出提交的行为将计算新提交的哈希 ID,但我们将称之为D. 所以我们现在有:

A--B--C   <-- master
       \
        D

但是现在 Git 做了一件非常聪明的事情:它把D's hash ID 写入name master,所以master现在指向D:

A--B--C
       \
        D   <-- master

现在提交D是最后一次提交。我们只需要记住名字master;Git 会为我们记住哈希 ID。

怎么样git commit -a

Git 确实有办法提交工作树中的任何内容,使用git commit -a. 但实际上,这实际上是在提交之前运行:对于当前实际git add -u索引中的每个文件,Git 检查工作树副本是否不同,如果是,Git 补充说文件索引。然后它从索引中进行新的提交。4

每个文件的中间第三个副本——索引中的那个——就是为什么你必须一直这样做git add的原因。作为 Git 的新用户,它通常会妨碍您。很想用 来解决它git commit -a,并假装它不存在。但是,当索引出现问题而出现故障时,这最终会让您陷入困境,并且完全无法解释已跟踪与未跟踪的文件。

此外,索引的存在允许各种巧妙的技巧,例如git add -p,对于某些工作流程实际上非常有用和实用,因此了解索引并不是一个坏主意。您可以将其中的很多内容留到以后,但请记住,有一个中间冻干副本,它git status运行两个比较——<code>HEAD-vs-index,然后是 index-vs-work-tree——这一切都使更有意义。


4这也是一个善意的谎言:Git 实际上为这种情况建立了一个临时索引。临时索引作为真实索引的副本开始,然后 Git 将文件添加到那里。但是,如果提交一切顺利,临时索引将成为索引——实际上是主索引——因此添加到临时索引具有相同的效果。唯一出现的情况是提交失败时,或者,如果您足够狡猾,当您进入并检查存储库状态git commit -a仍在进行中。

如果使用git commit --only,图片会变得更加复杂,它会生成两个临时索引(索引?)。但是我们不要去那里。:-)


推荐阅读