git - “承诺”和“未修改”是一样的吗?
问题描述
我从https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F读到了Git 中的三个状态 它在这里说 Git 具有三个主要状态,您的文件可以驻留在:commited、modified和staged。
然后,我还阅读了两种状态:从https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository跟踪或未跟踪 这里它说每个文件在您的工作目录可以处于以下两种状态之一:已跟踪或未跟踪。跟踪文件是上次快照中的文件;它们可以是未修改的、已修改的或暂存的。
三个状态中提到的状态是否与跟踪文件的子状态相似?已提交和未修改是否相同?
这些图像表明它们是相同的吗?
解决方案
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.md
和main.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
. 这真正的意思是,如果我们进行两次比较——从HEAD
vs 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.py
README.md
HEAD
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 commit
,README.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
,图片会变得更加复杂,它会生成两个临时索引(索引?)。但是我们不要去那里。:-)
推荐阅读
- python - 我在 pandas 中导入一个 CSV 文件,但该列作为多索引导入
- webi - 如何重新定义维度?
- android - 无法在导航抽屉按钮单击时显示吐司
- r - 如何将 df 从列表中分离出来
- java - Java 中使用 ArrayList 的年龄过滤器
- javascript - 如何使用 v-text-field 自动填充
- linux - 如何让 Awk 在 shell 脚本中使用 Bash 数组变量?
- excel - 如何根据行之间的分隔符合并 Excel 中的行?
- python - Anaconda Prompt 创建的 Python New Env 未显示在 Jupyter Notebook 内核列表中
- r - Preview v. Adobe 浏览的 PDF 中带有绿色的奇怪颜色外观