在开始之前,这里有一些有用的背景知识:
- Git 只存储文件,不存储文件夹。
- 每个 Git 提交都会存储您的所有文件的完整快照,或者更确切地说,存储所有提交的文件。这就是 Git 所谓的未跟踪文件的来源。
- 快照是由 Git 调用的文件创建的,不同的是,您的 index 或 暂存区。 (它还有一个旧名称,现在应该用于其他用途,但有时有些东西会引用 cache。所有三个名称都差不多。)
Git 将这些文件存储在 commits 中。 Git 真的是关于提交的。每个提交都有编号——但不是以“commit #1, commit #2, ...”的简单顺序。相反,每个提交都会获得一个唯一的哈希 ID,哈希 ID 看起来完全随机且与之前的提交无关。这些哈希 ID 是由 83232e38648b51abbcbdb56c94632b6906cc85a6 之类的字母和数字组成的大而难看的字符串,git log 会吐出。
由于每个文件都在每个提交中,Git 以一种不会立即用完整个磁盘驱动器的方式保存它们很重要。因此,保存的文件会被压缩,并在不同的提交之间进一步共享。 Git 可以这样做,因为它使用一种特殊的、仅限 Git 的冻干格式来存储文件。这种形式的文件无法更改,但可以共享。这意味着现有的提交不能更改。 您的存储库中的每个提交都会被存档,或多或少是永久的。1将提交视为永久的(它们大部分是)并且不可更改。它们是历史,存储在存储库中。
1可以删除提交,但这有点困难,而且 Git 通常也不会立即执行此操作 - 所以即使您认为提交已消失,并且无法立即找到它,它可能还在里面。
完成工作
现在,这一切都很好,很适合存档,但是这些只读的冻干文件对于真正完成任何实际的工作 完全没有用处。为此,Git 提供了 Git 所称的工作树。这就是您工作的地方。
在工作树中,Git 从某个提交中提取冻干文件,对它们进行再水化,使它们具有正常的日常形式。您现在可以查看和使用这些文件。你只需选择一个提交——通常是某个分支上的last提交——然后说:把那个提交给我,Git就会这样做。它找到冻结的提交并枚举其中的所有文件:
-
main-folder/sub1/file1:啊哈,Git 说,这个工作树没有 main-folder,让我们做一个。我刚刚制作的main-folder 中没有sub1,让我们也制作它。现在我可以创建新文件main-folder/sub1/file1。
-
main-folder/sub1/file2:嘿,Git 说,已经有一个主文件夹/sub1,我可以在其中创建新文件 file2。
此过程根据需要重复:Git 有 文件,如提交中所列,它必须重新构建。完成后,如果您开始时工作树 是空的,那么现在它具有来自该提交的每个文件的再水化版本。没有存储文件夹,但没有必要存储它们。
如果您现在从 那个 提交切换到另一个提交,Git 将删除它仅为该提交创建的所有文件,并将它们替换为另一个文件,不同的提交。如果它从main-folder/sub1 中删除所有 文件,它也会删除目录main-folder/sub1。如果它最终删除了main-folder 中的所有内容,它也会删除它。然后它会从您现在想要的提交中提取所有文件,根据需要创建任何目录/文件夹。
实际上,Git 交错了所有这些工作,创建、删除和优化:如果你从提交 a123456... 切换到提交 b789abc...,并且两次提交中 99% 的文件是相同的 ,好吧,毕竟没有必要在工作树中与他们胡闹,是吗?而且,通过git checkout 的这种特殊形式,Git 在切换提交之前添加了安全检查:对于我必须删除或替换的每个文件,工作树中的文件是否“干净”?该文件是“干净的”,删除或替换它是安全的。如果它是“脏的”——如果你在 Git 提取它之后更改了它,并且你可能希望保留你的更改以防切换会破坏——Git 会警告你,并且默认情况下拒绝切换提交。
索引/暂存区
在这个过程中有一个巨大的喇叭皱纹。阅读以上内容,您会想:好吧,我们有冻干文件的提交,以及普通文件的工作树。但是 Git 将第三个实体 置于之间 这两个。这是索引/暂存区。
与提交本身一样,索引大多是不可见的。它实际上只是一个普通文件,在大多数情况下是.git/index——这最终会变得更加复杂,但它一开始就是这个普通文件。本质上,文件中的内容是您提取的提交的副本——所有冻干文件,仅使用哈希 ID(如提交哈希 ID)来识别它们。但是,与提交中实际冻结的文件不同,索引中的副本 可以更改。2
这就是git add 所做的:它冻干文件并将那个 版本保存在索引中。如果文件之前没有在索引中,那么现在是。如果它之前在索引中,则会退出之前的版本。 无论哪种情况,新的冻干文件都已准备好提交。 当您运行 git commit 时,Git 只是将索引中所有准备就绪的文件打包到新提交中.这就是 为什么git commit 如此之快:它真的几乎没有什么工作要做。
索引中的文件根本不存储在文件夹中。只有一个巨大的列表:文件path/to/file1 有这些 冻干内容,文件path/to/file2 有这些其他 冻干内容,等等。但无论如何,文件在索引中的存在——连同冻干的准备提交内容——是使工作树中的再水化文件被跟踪的原因。 已跟踪文件是索引中的文件,因此未跟踪文件只是工作树中的任何文件,但不在索引中。 由于git commit 归档索引 中的内容,而不是工作树中的内容,因此只有跟踪 文件被提交。
2这里棘手的部分是,将一个新文件放入索引实际上会冻结文件并将其存储在 repository 中,如果出现以下情况,则会创建一个新的哈希 ID新内容是真正的新内容,或者如果冻干内容与任何现有文件匹配,则共享一些现有的哈希 ID。现在,所谓的新文件已被缩减为只是一个哈希 ID,它适合旧文件占用的索引中的同一插槽!
有了这些,答案现在很容易
要进行一个新的提交,在该提交中, 只存储某些文件,只需进行设置,以便您的 index 中只有这些文件。为此,删除您的索引中所有完全不需要的文件,这也将删除工作树副本:
git rm ...
由于索引通过它们的相对于工作树顶部的路径来存储它们,因此您需要将所有要保存的文件保存在某个地方。最简单的方法是在工作树和索引中重命名它们:
git mv main-folder/sub1 sub1
如果需要,它将在您的工作树中创建(在这种情况下通过重命名)sub1 文件夹,然后重命名索引中的所有 跟踪 文件——记住 git mv 有使用索引和工作树——从它们的 main-folder/sub1/file1 等路径到 sub1/file1 等路径。 git mv 命令与git rm 命令一样,然后将工作树文件一起拖动。
(很方便,或者有时不会,当git mv 就地重命名文件夹时,也会重命名其中的任何 untracked 文件。因为 Git 的其余部分对 untracked 并不真正感兴趣文件,以后的git checkout 不会将它们移回!)
由于在所有内容的基础上,Git 按内容存储文件——使用冻干文件的哈希 ID——所有这些重命名大部分都是免费的。 Git 需要一点空间来存储更新后的 names——提交必须存储全名和哈希 ID,而 names 不能在这里轻松共享因为它们不同3——但实际内容与其他提交中具有不同名称的文件共享。
请注意,当您在具有这些 sub1/file1 类型名称的此提交之间来回切换到具有 main-folder/sub1/file1 类型名称的任何提交时,Git 可能不得不在您的工作树上努力搅拌,删除所有sub1/file1 首先命名,然后创建新的、空的 main-folder 和 main-folder/sub1 目录来保存(最终相同!)曾经在 sub1/file1 中的文件,依此类推。当 Git 足够聪明地意识到它可以重命名工作树中的那些文件时,Git 可能会这样做,但是 Git 通常开始使用的简单愚蠢的方法是只是删除并重新创建它们。这将显示在操作系统级别的文件时间戳中:如果 Git 删除文件并重新创建它,它将“现在”作为其在磁盘上的工作树文件时间戳。
3在提交内部——但不在索引中——Git 直接回到树形结构的命名方案。因此,如果这个新提交的顶级sub1 与其他提交的main-folder/sub1 中的sub1 100% 相同,Git 实际上将共享底层的 tree 对象sub1 新提交的根树的子树。根树当然会有所不同,因为它将sub1 命名为其子树之一,而不是将main-folder 命名为其子树之一。但所有这些都只是实现细节:它们都没有出现在索引和工作树中。