试图了解 git 跟踪、分期等

Trying to understand git tracking, staging, etc

我已经看到很多关于这个的话题,但答案往往不一致和矛盾,所以如果可能的话,我会尽可能简短地解释:

据我目前的理解,我们有一个 工作目录 用于制作项目的文件,一个 暂存区 ,以及一个 存储库 。暂存区在技术上也是一个 index 并使用 cache,只是工作目录中所有文件的一个大列表a commit,它将增量更改复制到存储库。

所以例如我有一个文件,test.txt,我在里面写了“1234”,然后将它添加到暂存区,然后将它提交到 repo,所以我假设整个 test.txt 保存在某处的某个 repo 文件中。然后我编辑文件并将文本更改为“1235”并提交该更改。我假设回购协议现在不保存“1235”的另一个副本,而只是注释类似 "the fourth character changed from '4' to '5'" 之类的东西。

无论如何,在我开始之前,test.txt 未被追踪。然后当我做 git add test.txt 它现在变成了一个 tracked 文件,一个 new 文件,和一个 staged 索引上的文件。

然后如果我git commit -m "some message"文件变成...什么?坚定的?跟踪但不上演?

然后,如果我再次编辑文件,它会变成 trackedmodified,但不一定要暂存,除非我再次添加它,等等?

我正在尝试了解什么定义为什么地方。到目前为止我的理解正确吗?哪里需要更正?

在我深入讨论之前,tracked file 的定义很简单:当且仅当文件当前在索引中时,它才会被跟踪。

这就是您需要测试的全部内容:"Is path P in the index?" 如果是,则跟踪 P。如果不是,则 P 未被跟踪(因此 just 未被跟踪,或者可能未被跟踪并被忽略)。

棘手的部分是找出索引中的内容!

直接问答

To my current understanding, we have a working directory of files that we are making our project with, a staging area, and a repository. The staging area is technically also an index ...

是的。存储库本身是一种数据库;在这个数据库中,我们有四种类型的对象。此时最有趣的是 commit 对象,1 充当快照,通过保存制作——作为永久副本,带有一些附加信息——您 运行 git commit 时索引中的任何内容。第二个最有趣的是 blob 对象。 Blob 有多种用途,但最主要的用途是存储文件内容,我们稍后会看到。

重要的是,总是有一个当前提交。您可以通过多种方式命名,但始终有效的一种方式是单词 HEAD。符号 @ 通常也有效(它只在真正古老的 Git 版本中失败)。但是,哪个提交是 "current",随着时间的推移会发生变化。

... and uses a cache,

缓存只是索引/暂存区的第三项。

just a big list somewhere of all the files from the working directory that are slated for a commit, which copies the incremental changes over to the repository.

这不太对。

索引/暂存区/缓存中内容的格式通常并不有趣(并且记录不足并且也可能会更改),但正如已经指出的那样,每个提交都充当完整的快照,不是变更集。

So for instance I have a file, test.txt, I write "1234" inside it and add it to the staging area then commit it to the repo, so I assume the entirety of test.txt is saved in some repo file somewhere.

它——也就是1234\n数据——保存在一个对象中,特别是上面提到的blob对象之一。这并不一定意味着 file。存储库对象有一个内部格式,关于它的承诺相对较少。如果您想深入研究这些细节,您应该知道它们可能存储 松散(每个文件一个在单独的文件中),或 打包(许多在一个包文件中,包索引与 index/staging-area/cache 索引无关)。

承诺你可以使用 git cat-file --batch 通过 ID 将任何对象提取回其原始形式(这会产生原始数据并且有点使用起来很棘手)或 git cat-file -p (这会产生一个漂亮的印刷变体并且易于使用)。对于标记和提交对象,原始数据通常已经可以打印,因此 git cat-file -p <object-id> 按原样打印。树对象是混合的,因此 git cat-file -p 将已知的二进制内容转换为文本。 Blob 对象完全按原样保存。2

Then I edit the file and change the text to "1235" and commit that change. I assume the repo now doesn't save another copy of "1235" but just notes something like "the fourth character changed from '4' to '5'" or something.

不,这是完全错误的。新提交具有新内容的新的完整副本。此外,每个 blob 都是该特定文件内容的特定版本的完整快照,与文件名无关。 (如果这两个 blob 都是松散的对象,并且都是非常大的文件——比如说,每个 4.5 GB 的 DVD 数据,而且压缩性不是很好——那么你只需要 9 GB 的磁盘 space 来处理这两个松散的对象。请参阅下面的对象压缩和打包部分,了解这是否是一个问题。)

如果您在两个单独的提交中存储相同的 content,但是,无论是使用相同的名称还是不同的名称,您都只存储了 blob 一次。即使您在一次提交中将一个文件存储两次,这仍然成立。例如,任何仅包含文本 Hello world(和换行符)的文件的哈希 ID 为 3b18e512dba79e4c8300dd08aeb37f8e728b8dad:

$ echo hello world | git hash-object -t blob --stdin
3b18e512dba79e4c8300dd08aeb37f8e728b8dad

如果您创建六个文件只包含一行 hello world,您的 tree 对象(请参阅脚注 1)将有六个名称与此哈希 ID 关联:

$ for i in 1 2 3 4 5 6; do
>     echo hello world > copy_$i.txt; git add copy_$i.txt
> done
$ git commit -m 'six is just one'
[master (root-commit) 5a66ef1] six is just one
 6 files changed, 6 insertions(+)
 create mode 100644 copy_1.txt
 create mode 100644 copy_2.txt
 create mode 100644 copy_3.txt
 create mode 100644 copy_4.txt
 create mode 100644 copy_5.txt
 create mode 100644 copy_6.txt
$ git cat-file -p HEAD^{tree}
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    copy_1.txt
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    copy_2.txt
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    copy_3.txt
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    copy_4.txt
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    copy_5.txt
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    copy_6.txt

现在是:六个文件,一个对象。

除此之外,假设您的 test.txt 文件有 1234 加上换行符。它的哈希 ID 是:

$ echo 1234 | git hash-object -t blob --stdin
81c545efebe5f57d4cab2ba9ec294c4b0cadf672

Universe 中的每个文件——更准确地说,是你存储库中所有文件的 Universe3——具有内容 1234\n 的每个文件都具有此哈希.名字叫test.txt也没关系。谁做的,什么时候做的并不重要。它是存储在本地驱动器上还是云端并不重要。4 重要的是 1234\n 散列到上述数字。

与此同时,1235\n 散列到不同的数字:

$ echo 1235 | git hash-object -t blob --stdin
d729899c33fcf5c75fda5369a64898c85a46bcf7

因此您的 1235\n 内容进入另一个 blob。

如果 blob 已经在数据库中,则不会发生任何有趣的事情:您只需重新使用它。如果它不在数据库中,Git 将它添加到数据库中。 blob 必须有一个唯一的哈希 ID,不同于数据库中的每个其他对象。不过,它总是如此;再次参见脚注 3。


1为了完整性,四种对象类型分别是tagannotated tag提交;和 blob。提交通常很短:尝试 git cat-file -p HEAD 查看您当前的提交。请注意,它通过哈希 ID 指向一个 treetree 依次引用您的每个文件,给出它们的名称和它们的 blob 哈希 ID。如果您有子目录,它们将存储为子树。

2您可以使用 .gitattributes 和其他技巧启用特定的转换,例如行尾转换。这些转换发生在索引与工作树操作期间;出于技术原因,存储库中的表示始终与索引中的表示完全匹配(特别是,索引仅存储对象哈希 ID,因此此时对象必须在 "repo format" 中)。

3这就是你的工作方式the breaking of SHA-1 into a problem for Git. Simply find two different files whose blob-hash is the same, and you can no longer store both of those files in the same repository. Since Git's blob-hash is not quite a straight SHA-1 hash of the two files, the example file that the researchers have supplied is not itself a problem for Git. Some other file-pair would be.

任意两个对象 ID 随机碰撞的概率(目前,使用 SHA-1)为 1 in 2160,这个概率很小,我们可以忽略它。但是,由于, it's wise to keep the number of objects in any Git repository 。如果普通对象只占用一个字节,那么在几千兆兆字节(即几艾字节)之后你就会 运行 出问题了。显然,平均对象大小更大,因此在可能出现问题之前计算存储库大小的 EB 数——当然,工程破解 Git.

除外

4Git 并没有真正将东西存储在云存储中,因为它 "likes" 本地文件系统,尽管没有理由你不能使用云支持的本地文件系统。事实上,Dropbox 和类似的公司正试图这样做——但他们也坚持使用同名文件解析在不同计算机上执行的操作,这严重干扰了 Git 的操作,因为 Git 需要维护自己的元数据 about 其内部文件。如果你使用 Git-LFS,它有自己的技巧将大文件卸载到单独的存储区域,使用 "clean" 和 "smudge" 过滤器和一些聪明的 .gitattributes 文件黑客。


了解(和查看)索引

如果您想直接查看索引,Git 有它所谓的 管道命令——即,一个不适合人类使用的命令——来做这个:git ls-files。尝试一下——尤其是使用 --stage 参数——但请注意,在大型存储库中,它会产生太多输出。

通常最好间接查看索引。请记住,总是有一个当前提交(HEAD 或@)。您 也可以 查看当前的工作树。假设你有这两个文件:

   HEAD       index     work-tree
---------   ---------   ---------
README.md   README.md   README.md
test.txt    test.txt    test.txt

您可以 运行 两个 git diff,一个用于比较 HEAD 与索引,一个用于比较索引与工作树。这就是 git status 所做的。

如果两个文件的所有三个版本完全相同,而你 运行 git status,它什么也没说。

如果您更改 test.txt 和 运行 git status 的工作树版本,它发现 HEAD 与索引没有区别,但发现 index 与工作-树是不同的。所以它告诉你你有未暂存的更改test.txt

如果您将新的工作树版本复制到索引中,然后再次 运行 git status,现在索引和工作树匹配,但第一个差异(HEAD 与索引)不再匹配火柴。所以现在 git status 表示您有 阶段性更改

如果你添加一个 new 文件 z 到工作树,图片看起来像这样:

   HEAD       index     work-tree
---------   ---------   ---------
README.md   README.md   README.md
test.txt    test.txt    test.txt
                        z

现在 git status 表示您有一个 未跟踪的 文件,因为 z 不在索引中。使用git add z复制到索引中得到这张图:

   HEAD       index     work-tree
---------   ---------   ---------
README.md   README.md   README.md
test.txt    test.txt    test.txt
            z           z
再次

运行 git status 并再次执行两次 git diff。现在 z 在索引中(被跟踪)但不在 HEAD 中,所以它是一个新文件并被暂存。它在索引和工作树中是相同的,所以 git status 没有第二次提到它。5


5我真的很喜欢 git status --short 的输出,它向您显示 一次 每个文件的两个差异。未跟踪文件有两个问号,而跟踪文件有一个或两个字母,放在前两列中,用于描述 HEAD-vs-index 和 index-vs-work-tree。

对象压缩和打包

notes something like "the fourth character changed from '4' to '5'" ...

Git 做这种事情,但是——与大多数版本控制系统不同——它做这种 delta compression 比 blob 的级别低

松散的对象只是用 zlib 压缩。您可以在 .git/objects/ 中的哈希 ID 名称下找到这些对象(将前两个字符拆分成一个目录,这样目录就不会变得太大)。打开其中一个文件,读取它,然后对其进行解压缩,您将看到松散的对象数据格式。

当有足够多的零散物品值得购买时,Git 打包 零散物品。这种打包使用了一种启发式算法(因为做一个完美的工作需要太多的计算),但它本质上相当于找到 "smell enough alike" 的对象,表明基于其他对象对一个对象进行增量编码将使一个更小的打包文件。

如果对象打包器选择这两个文件,它会注意到一个是1234\n,另一个是1235\n,它可以将第二个对象表示为"replace the 4th character of previous object"。没有特别的承诺 1235\n 将基于 1234\n——它可以按其他顺序进行——但通常 Git 会尝试 保持"most recent" "least compressed" 中的对象基于较旧历史被更频繁访问的理论。

请注意,一个对象可以基于之前的对象,而该对象本身又基于之前的对象。这称为 chaindelta chain:我们必须按顺序将每个对象向下扩展到 base依次应用每个增量以得出链中的最后一个对象。 Git的deltifier会限制delta链的长度;查看调用它的各种事物的 --depth 参数。

(在这种特殊情况下,对象 ID 比对象的内容长得多,因此尝试在此处创建增量没有任何好处。不过,该原则适用于更大的文件。另请注意增量应始终在 之前 应用任何二进制压缩:增量压缩依赖于输入文件中较小的 Shannon entropy 值,而像 gzip 和 bzip2 这样的压缩器通过压缩这种熵来工作。)

打包文件的格式已经改变了好几次。另见 Are Git's pack files deltas rather than snapshots?