我读到三态从Git的https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F 这里说Git有三个主要的州,你的文件可以驻留在:commited、modified和staged。
然后,我也看了一下两种状态:跟踪或未经跟踪从https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository 这表示,在每个文件您的工作目录可以处于以下两种状态之一:已跟踪或未跟踪。跟踪文件是上次快照中的文件;它们可以是未修改的、已修改的或已暂存的。
三个状态中提到的状态是否与跟踪文件的子状态相似?已提交和未修改是否相同?
这些图片显示它们是一样的吗?
Tracked-ness 不是列出的三个状态的子集,并且列出的三个状态不足以描述(或真正理解)Git 是如何工作的。
这个“三个状态”的东西有点像善意的谎言,这可能是页面上说的原因:
Git有三个主要状态
(强调我的)。我认为 Pro Git 的书在这里做了一些损害,因为我认为他们正试图——出于一些很好的理由——从你对一切的最初看法中隐藏 Git索引的存在。但是在同一个段落中,他们引入了staging area的概念,这实际上只是索引的另一个名称。
事实上,这里真正发生的是每个文件通常有三个副本。一个副本位于当前提交中,中间副本位于索引/暂存区中,第三个副本位于您的工作树中。
从版本控制系统设计的角度来看,中间副本——索引中的副本——不是必需的。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
Run Code Online (Sandbox Code Playgroud)
很难从这个表中看出哪个文件有哪个版本,所以让我们添加一个版本号:
HEAD index work-tree
--------- --------- ---------
README.md(1) README.md(1) README.md(1)
main.py(1) main.py(1) main.py(1)
Run Code Online (Sandbox Code Playgroud)
这与 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)
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)
表中的一个细微变化是,现在在比较中,HEAD-vs-index 显示已README.md更改,而 index-vs-work-tree 显示它们相同。Git 将这种情况的变化称为提交阶段。这与 Pro Git 书的“修改和暂存”状态相匹配。
如果你犯了一个新的,现在提交,Git会打包无论是在指数现在-即,在一个版本main.py和版本2 README.md-和使新使用这些文件的提交。然后它会调整一些东西,这HEAD意味着新的提交,而不是你之前签出的那个。所以现在,即使旧提交仍然具有版本 1 形式的两个文件,您现在拥有:
HEAD index work-tree
--------- --------- ---------
README.md(2) README.md(2) README.md(2)
main.py(1) main.py(1) main.py(1)
Run Code Online (Sandbox Code Playgroud)
现在所有三个副本都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)
Run Code Online (Sandbox Code Playgroud)
然后你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)
Run Code Online (Sandbox Code Playgroud)
当我们现在比较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)
Run Code Online (Sandbox Code Playgroud)
版本 3 现在刚刚消失。3 所以现在README.md工作树中的文件是一个未跟踪的文件。如果你把一个版本-任何版本的README.md运行前回到指数git commit,README.md可以追溯到被跟踪,因为它是在索引中。
由于从您签出的提交中git checkout 填充索引(和工作树),因此可以说上次提交中的文件可能被跟踪并没有错。但正如我在这里所说的,这是一种误导。跟踪性是文件在索引中的函数。 它如何到达那里与跟踪性无关。
3从技术上讲,Git 仍然在其对象数据库中将冻干副本作为 blob 对象,但如果没有其他人使用该冻干副本,则它现在有资格进行垃圾收集,并且随时可能消失。
我们已经在上面提到了其中的一些内容,但让我们再次回顾一下,因为这对于理解 Git 至关重要。
Git 中的每个提交(实际上,任何类型的每个对象)都有一个特定于该特定提交的哈希 ID。如果您记下哈希 ID,并再次输入所有内容,Git 可以使用该哈希 ID 来查找提交,只要该提交仍在 Git 的“所有对象”的主数据库中。
每个提交还存储了一些较早提交的哈希 ID。通常这只是一个先前的哈希 ID。这个先前的哈希 ID 是提交的parent。
每当您(或 Git)手头有这些哈希 ID 之一时,我们就说您(或 Git)有一个指向底层对象的指针。所以每个提交都指向它的父级。这意味着给定一个小的存储库,比如只有三个提交,我们可以绘制提交。如果我们使用单个大写字母来代表我们的提交哈希 ID,结果对人类更有用,尽管我们当然会很快用完 ID(所以我们不要只绘制几个提交):
A <-B <-C
Run Code Online (Sandbox Code Playgroud)
这C是最后一次提交。我们必须以某种方式知道它的哈希 ID。如果我们这样做,我们可以让 Git 从数据库中获取实际提交,并C保存其前一个提交的哈希 ID B。我们可以让 Git 使用它来B找出并找到A. 我们可以用它来找出A自己,但这一次,没有以前的哈希 ID。不可能有:A是第一次提交;没有更早的提交A指向回。
必要时,所有这些指针总是向后指向。提交后,任何提交的任何部分都不能更改,因此B可以持有A的 ID,但我们不能A将内容B的 ID更改为A. C可以指向,B但我们无法更改B以使其指向C。但是我们所要做的就是记住 的真实哈希 ID C,这就是分支名称的用武之地。
让我们选择名称master并C在该名称下使用 Git save的哈希 ID。由于名称包含哈希 ID,因此名称指向C:
A--B--C <-- master
Run Code Online (Sandbox Code Playgroud)
(出于懒惰和/或其他原因,我已停止将提交中的连接器绘制为箭头。没关系,因为它们无法更改,而且我们知道它们指向后。)
现在让我们检查 commit C, using git checkout master,它从使用 commit 保存的文件中填充我们的索引和工作树C:
git checkout master
Run Code Online (Sandbox Code Playgroud)
然后我们将修改一些文件,用于git add将它们复制回索引,最后,运行git commit. 该git commit命令将收集我们的姓名和电子邮件地址,我们或从一开始日志信息-m标志,添加当前时间,并通过节省无论是在指数新提交现在。这就是为什么我们必须首先git add将文件添加到索引中。
这个新提交将使用 commitC的哈希 ID 作为新提交的父级。写出提交的行为将计算新提交的哈希 ID,但我们只是将其称为D。所以我们现在有:
A--B--C <-- master
\
D
Run Code Online (Sandbox Code Playgroud)
但是现在 Git 做了一些非常聪明的事情:它将D哈希 ID 写入name master,因此master现在指向D:
A--B--C
\
D <-- master
Run Code Online (Sandbox Code Playgroud)
现在提交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运行两次比较HEAD——-vs-index,然后是 index-vs-work-tree——这一切都更有意义。
4这也是一个善意的谎言:Git 实际上为这种情况制作了一个临时索引。临时索引作为真实索引的副本开始,然后 Git 将文件添加到那里。然而,如果提交一切顺利,临时索引就变成了索引——真正的主索引,就像它一样——所以添加到临时索引具有相同的效果。这个问题表现为,当提交的唯一一次失败,或者,如果你是偷偷摸摸的是,当你去检查版本库的状态,而在git commit -a仍正在进行中。
如果您使用git commit --only,图片会变得更加复杂,这会产生两个临时索引(索引?)。但我们不要去那里。:-)