您所要求的在 Git 中在技术上是不可能的。原因很简单,虽然有点自相矛盾:
-
存储库中有四种对象:提交、树、blob(文件)和带注释的标签。每个对象都有一个唯一标识符,表示为 40 个字符的 SHA-1 哈希,例如 7c56b20857837de401f79db236651a1bd886fbbb。1 存储库基本上是一个键/值存储,哈希 ID 是键和对象的内容就是值。
唯一 ID 完全取决于对象的内容,实际上是通过对对象进行哈希处理形成的(以一个小标题为前缀,给出对象的类型和大小)。这意味着,例如,一个文件的哈希值包含一行只有单词hello 是ce013625030ba8dba906f756967f9e9ca394464a。 Universe 中包含这一行的每个文件都具有相同的哈希值。2
换句话说,哈希的唯一性取决于对象的唯一性。再次使用相同的对象,您将获得相同的哈希值。使用一个不同的对象,你会得到一个不同的哈希值。在底层,Git 只是相同的键/值存储:给它一个键(你必须以某种方式神奇地知道),它会返回一个值,其哈希 是 那个键。
-
commit 对象记录五个项目作为其值:
- 单个
tree ID:该提交的源树。 (树本身也存储为一个对象,但多次提交可以重用一棵树。例如,如果您进行提交,然后立即为该提交进行恢复提交,恢复的提交会重用原始树。那是,我们从tree <em>T1</em> 开始;我们用tree <em>T2</em> 进行新的提交;然后我们进行还原提交,它再次具有tree <em>T1</em>。这是一个不同的commit,但它存储相同的源代码树。)
-
父级列表(该提交的每个父级的哈希 ID)。该列表可以为空,代表一个根提交;有一个 ID,作为常规提交;或者有两个或多个 ID,这就是合并提交。
-
作者:全名、电子邮件地址和时间戳,给出了提交的作者和时间。
-
提交者。这与作者的想法相同,但允许一个人使用另一个人的代码并给予两者信誉。提交者是将作者糟糕的代码放入存储库的混蛋。 :-)(这里的想法是允许通过电子邮件发送补丁,并根据拉取请求跨存储库拉取。)
- 提交消息。 Git 本身并没有为此强制执行任何格式,尽管推荐的标准是短主题后跟长主体,
git log 有工具可以提取这些部分。
最后,关键:历史是提交。
存储库中的历史是存储库中的一组提交:不多也不少。你查看历史的方式是从一个引用开始,例如一个分支名称或一个标签,这只是Git提供给你的一种方式来转换一个人类友好的字符串像 master 或 v2.2.1 到哈希 ID 中。这会让你获得最后一次(或 tip)提交。提示提交有一个或多个存储的父 ID,它可以让您获得下一位历史记录,并且这些提交有更多父级,这让您可以向后移动历史记录。
由于parent 和tree 行是提交对象的一部分,如果您想对any 提交进行任何更改,请在历史记录中anywhere存储在存储库中,您必须进行新的不同提交。即使您准确地保留了作者和提交者姓名+电子邮件+时间戳,即使您保留了确切的消息,如果您以任何方式更改了tree,您也会得到一个新的、不同的提交,以及一个新的、不同的哈希身份证。
然后,由于您已经做出了属于某个提交链中某个位置的新提交,因此您必须重新复制每个后续提交。您必须重新复制提交的 child3 才能放入新的 parent 行。这会为孩子生成一个新的、不同的散列,所以现在您必须重新复制 its 孩子,这是您提交的孙子。这会迫使您重新复制曾孙,等等,一直到小费。
1这实际上是 Git 本身的 Git 存储库中的标签 v2.2.1。理论上,这个相同的 ID 可能会被分配给另一个不同的 Git 对象,在宇宙的某个地方,只要那个不同的 Git 对象从不使用 Git 存储库的克隆。但是,一般来说,除非内容是逐位相同的,否则没有任何 ID 会在任何地方重复。并且非常关键的是,在单个存储库中没有任何 ID 会像这样重复 - 事实上,Git 确实不能使用相同的 ID 在一个存储库中存储两个不同的对象。
2如果 Git 改变了哈希算法,这将造成相当大的痛苦。 Mercurial 也使用 SHA-1,但 Mercurial 故意留出了切换到 SHA-256 的空间,并且在隐藏内部哈希方面要好得多。 Git 太容易暴露哈希值,tree 对象没有空间容纳更大的哈希值,因此过渡会更具破坏性。
3或孩子,如果提交有多个孩子。请注意,找到 children 很难,因为提交只记录他们的 parents。 Git 必须遍历整个提交图才能找到给定提交的所有子项。通常,它甚至不会打扰:大多数情况下不需要,而某些似乎需要的情况下,只需找到一部分孩子就可以逃脱。不幸的是,Git 倾向于把它留给你,用户,去弄清楚你什么时候找到所有个孩子真的很重要,并迫使你强迫 Git 这样做。
那么git filter-branch 会做什么呢?
答案很简单:git filter-branch 副本 提交。
filter-branch 脚本尽最大努力将原始提交保存为逐位相同的副本。 如果它可以准确地复制提交,那么新副本与原始副本具有相同的 ID,因此 是原始副本。但是,如果有任何东西发生了变化——在树中,或者就父 ID 而言——那么新副本就会有一个新的、不同的 ID。
Filter-branch 通过首先列出要复制到文件中的每个 ID 来执行此复制。然后它会按照“父母在孩子之前”的顺序遍历这个文件。它提取要复制的提交,应用所有过滤器,并从结果中进行新的提交。如果新提交是逐位相同的,“新”提交只是共享旧提交;否则它有一个新的、不同的 ID。
filter-branch 命令还组成一个映射文件:“旧 ID 为 X,新 ID 为 Y”。每个新提交只是添加一个新映射:如果提交实际上是逐位相同的,则 X 和 Y 是相等的,否则它们是不同的。而且,当然,您可以跳过一些提交(使用--commit-filter 参数),这使得跳过的提交映射到最近未跳过的提交:这是“重新映射到祖先”的概念显示在the documentation。
当 filter-branch 完成时,它使用累积的映射重写部分或全部引用(分支名称和可选的标签名称 - 它可能应该默认包含标签,真的)。
请注意,过滤后,您的存储库中有两组历史:原始提交,例如保存在 refs/original/refs/heads/master 中,以及重写后的 refs/heads/master 所指向的新副本对于master 分支。
ID 的唯一性和历史链接提供了 Git 的安全性
虽然 Git 本身并不意味着加密安全,但请注意,您可以对带注释的标签进行 GPG 签名。这些 GPG 签名仅验证一个特定的签名对象,即仅验证标签本身。但是,该标签实际上包含目标提交的 ID,因此您实际上已证明相应的提交是好的和有效的,不包含特洛伊木马、后门、病毒或其他 Bad Things™。而且,由于该提交包含其父提交 ID,因此您还签署了父提交及其父提交,等等,一直追溯到历史记录。
当您使用 filter-branch 并让它复制标签时,它会切断签名,因为它们不再有效:它们指向更改的、复制的提交。如果您希望对副本进行签名,则必须手动进行。 (这可能是 why filter-branch 默认不复制标签的原因。问题是完成后它会丢弃提交 ID 映射文件,所以现在为时已晚:最好复制标签,删除过程中的签名,然后让你用签名的副本替换副本。)
(您也可以对单个提交进行 GPG 签名。这与 filter-branch 的效果很差,而且总的来说很麻烦。)
Git 的“笔记”
虽然这与更改提交历史无关,但在这里提及 Git 的“注释”是个好主意。注释是两部分问题的替代解决方案,即 (a) 提交是不可变的,但是 (b) 我们希望能够进行提交,然后以某种方式标记该提交,例如,说它已经通过了一些自动化测试,或者被 42 号检查过,或者其他什么。
“注释”只是附加到提交 ID 的文件4。该文件与提交历史分开存储在“注释历史”中:提交链的提示存储在refs/notes/commits(好吧,refs/notes/ 无论如何,commits 部分是可配置的默认值,您可以拥有多组注释)。 Git 有一小部分命令可让您在提交中附加注释,默认情况下,git log 将检查每个提交,通过其哈希 ID,查看是否有注释。
由于注释是仅引用提交哈希的单独文件,因此您可以更新这些文件,从而更新附加到任何给定提交的注释。
当然,过滤会更改提交 ID,从而失去注释和提交之间的链接。 filter-branch 有可能(尽管很重要)更新笔记,但它现在不这样做。
4提交注释的“文件名”实际上是提交本身的 ID,稍作修改以加快查找速度。修改类似于对象存储在.git/objects中的方式:哈希ID为12345...的对象存储在.git/objects/12/345...中。提交注释是树形结构的,树 depth 是 variable,而不是简单的前两个/所有其余的扇出,所以它有点棘手。不过,前端git notes 界面很好地隐藏了这一切。