git:安全地重写,移动和跟踪历史记录

时间:2016-10-26 12:51:14

标签: git

我分叉了一个存储库,我希望在更高级别添加内容。所以我用谷歌搜索了如何将一个回购的内容移动到一个级别。 标准建议似乎是使用 filter-branch 例如,其中一个答案是:

How can I rewrite history so that all files, except the ones I already moved, are in a subdirectory?

这里可能有更好的建议:

Move file and directory into a sub-directory along with commit history

在我看来,将 git mv 应用到存储库中的每个文件更有意义。没有内置的方法可以递归执行此操作,因此您必须执行以下操作:

find . -type f > files
find . -type d | xargs -idir mkdir -p subdir/dir
cat files | xargs -ifile git mv file subdir/file

我对改写历史而不是修复历史的改变持谨慎态度。如果您尝试从原始上游项目中获取(同步),我希望使用filter-branch重写历史记录会导致问题。希望git能够更好地理解(和合并)基于移动的提交。

如果是这样的话,为什么 filter-branch 建议更频繁(我的看法 - 这可能是错误的)以及为什么没有更强大的 git变体mv (例如递归)冒泡到表面了吗? (Q1)

我理解使用 filter-branch 来删除所有版本存储库的密码等敏感数据。但是,建议将主要(有意而非偶然)更改隐藏到存储库似乎是一种非常糟糕的做法。

是否建议使用过滤器分支(或其他最佳实践)的等效项,以便跟踪历史记录以进行深思熟虑的更改? (Q2)

澄清:历史记录不必附加到同一实体(文件),但必须是可跟踪的,例如使用git log --follow。

1 个答案:

答案 0 :(得分:1)

你在Git中要求的技术上是不可能的。原因很简单,虽然相当自我纠缠:

  • 存储库中有四种对象:提交,树,blob(文件)和带注释的标记。每个对象都有一个唯一的标识符,表示为40个字符的SHA-1哈希,例如7c56b20857837de401f79db236651a1bd886fbbb 1 存储库基本上是一个键/值存储,其哈希ID为key和对象的内容是值。

    唯一ID完全取决于对象的内容,实际上,它是通过对对象进行散列形成的(前缀是一个微小的标题,给出了对象的类型和大小)。这意味着,例如,包含仅包含单词hello的一行的文件的哈希值为ce013625030ba8dba906f756967f9e9ca394464a Universe中的每个文件由该一行组成,具有相同的哈希值。 2

    换句话说,哈希的唯一性取决于对象的唯一性。再次使用同一个对象,您将获得相同的哈希值。使用不同的对象,您将获得不同的哈希值。在底层,Git就是这个相同的键/值存储:给它一个键(你必须以某种方式,神奇地,知道),并且它会返回一个哈希 该键的值。

  • commit 对象记录五个项目作为其值:

    1. 单个tree ID:该提交的源树。 (树本身也存储为一个对象,但多个提交可以重用一个树。例如,如果你进行提交,然后立即为该提交进行恢复提交,则恢复的提交将重用原始树。是的,我们从tree T1开始;我们使用tree T2进行新的提交;然后我们进行还原提交,并再次tree T1。它是一个不同的提交< / em>但它存储相同的源树。)
    2. 父级列表(该提交的每个父级的哈希ID)。列表可以为空,表示根提交;有一个ID,是一个常规提交;或者有两个或多个ID,使其成为合并提交。
    3. 作者:全名,电子邮件地址以及撰写提交的时间戳,以及何时。
    4. 提交者。这与作者的想法相同,但允许一个人使用另一个人的代码并给予两个信用。提交者是将作者可怕的代码放入存储库的混蛋。 :-)(这里的想法是允许通过电子邮件发送的补丁,以及跨拉存储请求的跨存储库。)
    5. 提交消息。 Git本身并没有强制执行任何格式,尽管较短的主题后跟较长的主体是建议的标准,git log有工具来提取这些部分。
  • 最后,关键词:历史提交。

存储库中的历史记录是存储库中的提交集:仅此而已。 查看历史记录的方式是从引用开始,例如分支名称或标记,这只是Git为您提供的一种人性化字符串转换方式将masterv2.2.1添加到哈希ID中。这会让你最后一次(或提示)提交。提示提交有一个或多个存储的父ID,它可以获取历史记录的下一位,并且这些提交包含更多父项,这使您可以向后移动历史记录。

由于parenttree行是提交对象的一部分,如果您想对任何提交进行任何更改,任何地方在存储库中存储的历史记录中,您必须进行新的不同的提交。即使您完全保留作者和提交者名称+电子邮件+时间戳,即使您保留确切的消息,如果您以任何方式更改了tree,您将获得一个新的,不同的提交,新的,不同的哈希ID。

然后,由于您已经提交了属于某个提交链的某个新提交,因此您必须重新复制每个后续提交。您必须重新复制提交的子 3 才能输入新的parent行。这会为孩子产生一个新的,不同的哈希,所以现在你必须重新复制它的孩子,这是你的提交者的孙子。这迫使你重新复制曾祖母,等等,一直到尖端。

1 这实际上是Git本身的Git存储库中的标记v2.2.1。从理论上讲,这个相同的ID将被分配给另一个不同的Git对象,在宇宙的某个地方,只要不同的Git对象从不使用Git存储库的克隆为Git的。但一般情况下,除非内容逐位相同,否则ID不会在任何地方重复;并且非常关键的是,在单个存储库中没有ID会像那样重复 - 实际上,Git字面上可以使用相同的ID在一个存储库中存储两个不同的对象。 / p>

2 如果Git曾经改变哈希算法,这将导致一些痛苦。 Mercurial也使用SHA-1,但Mercurial故意留下空间切换到SHA-256,并且更好地隐藏内部哈希值。 Git太容易暴露哈希值,而tree个对象没有更大的哈希空间,因此转换会更具破坏性。

3 或者孩子,如果提交有多个孩子。请注意,查找 children 很难,因为提交只记录他们的父母。 Git必须遍历整个提交图以查找给定提交的所有子项。通常情况下,它并不麻烦:大多数情况并非如此,并且一些似乎需要的案例可以通过找到一部分孩子而逃脱。 Git有一种不幸的倾向,让用户知道你找到所有孩子的真正重要性,并让你强迫Git这样做。

那么git filter-branch会做什么呢?

答案很简单:git filter-branch 副本提交。

filter-branch脚本尽力将原始提交保留为逐位相同的副本。 如果它可以完全复制提交,则新副本与原始具有相同的ID,因此 原始。但是如果树中的任何内容发生了变化,或者父ID也发生了变化,那么新副本就会有一个新的不同ID。

Filter-branch通过首先列出要复制到文件中的每个ID来执行此复制。然后它通过这个文件,在父母面前的父母和#34;订购。它提取要复制的提交,应用所有过滤器,并从结果中进行新的提交。如果新提交是逐位相同的,那么&#34; new&#34;提交只是分享旧的;否则它有一个新的,不同的ID。

filter-branch命令也构成一个映射文件:&#34;旧ID是 X ,新ID是 Y &#34;。每个新提交只添加一个新映射: X Y 相等,如果提交实际上是逐位相同的,否则它们是不同的。当然,您可以跳过一些提交(使用--commit-filter参数),这会使跳过的提交映射到最近未跳过的提交:这是&#34;重新映射到祖先&#34;在the documentation中显示的概念。

当filter-branch完成时,它会使用累积的映射重写部分或全部引用(分支名称和可选的标记名称 - 它可能默认包含标记)。

请注意,在过滤后,您的存储库中有两组历史记录:原始提交,例如保存在refs/original/refs/heads/master中,以及重写{指向的新副本} {1}}分支{1}}。

ID的唯一性和历史链接提供了Git的安全性

虽然Git本身并不是加密安全的,但请注意您可以对带注释的标签进行GPG签名。这些GPG签名仅验证一个特定的签名对象,即仅验证标签本身。但是,标记实际上包含目标提交的ID,因此您实际上已经证明相应的提交是好的和有效的,不包含特洛伊木马,后门,病毒或其他Bad Things™。并且,由于该提交包含其父提交ID,因此您还在父级及其父级上签字,依此类推历史记录。

当你使用filter-branch并让它复制标签时,它会切断签名,因为它们不再有效:它们指向已更改的复制提交。如果要签署副本,则必须手动执行此操作。 (这可能是为什么 filter-branch默认情况下不会复制标记。问题是它在完成时会丢弃提交ID映射文件,所以现在它也是如此迟到:最好复制标签,删除过程中的签名,然后让你用签名副本替换副本。)

(你也可以对个人提交进行GPG签名。这对于filter-branch来说效果不佳,无论如何通常都会造成很大的麻烦。)

Git&#34;注意&#34;

虽然这与更改提交历史几乎没有关系,但提及Git&#34;注意事项&#34;是一个好主意。这里。 Notes是两部分问题的替代解决方案,(a)提交是不可变的,但(b)我们希望能够进行提交,然后以某种方式标记该提交,例如,说它已通过一些自动化测试,或已被第42号检查,或其他任何。

A&#34; note&#34;只是一个附加到commit-ID的文件 4 。此文件与提交历史记录分开存储在&#34;注释历史记录&#34;:提交链中,其提示存储在refs/heads/master(嗯,master无论如何,{{1 part是可配置的默认值,你可以有多组笔记)。 Git有一小组命令可以让你将一个注释附加到一个提交,默认情况下,refs/notes/commits检查每个提交,通过它的哈希ID来查看是否有注释它

由于注释是仅返回提交哈希值的单独文件,因此您可以更新这些文件,从而更新附加到任何给定提交的注释。

当然,过滤会更改提交ID,这会丢失注释和提交之间的链接。 refs/notes/更新笔记是可能的(尽管不是很重要),但它现在不会这样做。

4 &#34;文件名&#34;提交注释实际上是提交本身的ID,稍微修改以便更快地查找。修改类似于对象在commits中的存储方式:哈希ID为git log的对象存储在filter-branch中。提交注释采用树形结构,树深度变量,而不是简单的前两个/所有其余的扇出,所以它是#s有点棘手。但是,前端.git/objects界面非常好地隐藏了这一切。