【问题标题】:git: rewriting, moving and tracking history safelygit:安全地重写、移动和跟踪历史
【发布时间】:2017-03-08 20:24:43
【问题描述】:

我创建了一个我想在更高级别添加内容的存储库。所以我用谷歌搜索了如何将回购的内容下移一个级别。 标准建议似乎是使用 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 变体(例如递归)冒泡到表面了吗? (第一季度)

我理解使用 filter-branch 例如删除所有版本存储库的密码等敏感数据。但是,建议隐藏对存储库的重大(故意而不是意外)更改似乎是一种非常糟糕的做法。

是否有推荐的等效过滤器分支(或其他最佳实践)允许跟踪历史记录以进行故意更改? (第二季度)

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

【问题讨论】:

  • 不确定我是否正确理解了您的要求...但我觉得它与git subtreegit submodule 差不多。你看过吗?

标签: git


【解决方案1】:

您所要求的在 Git 中在技术上是不可能的。原因很简单,虽然有点自相矛盾:

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

    唯一 ID 完全取决于对象的内容,实际上是通过对对象进行哈希处理形成的(以一个小标题为前缀,给出对象的类型和大小)。这意味着,例如,一个文件的哈希值包含一行只有单词helloce013625030ba8dba906f756967f9e9ca394464aUniverse 中包含这一行的每个文件都具有相同的哈希值。2

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

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

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

存储库中的历史是存储库中的一组提交:不多也不少。你查看历史的方式是从一个引用开始,例如一个分支名称或一个标签,这只是Git提供给你的一种方式来转换一个人类友好的字符串像 masterv2.2.1 到哈希 ID 中。这会让你获得最后一次(或 tip)提交。提示提交有一个或多个存储的父 ID,它可以让您获得下一位历史记录,并且这些提交有更多父级,这让您可以向后移动历史记录。

由于parenttree 行是提交对象的一部分,如果您想对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”。每个新提交只是添加一个新映射:如果提交实际上是逐位相同的,则 XY 是相等的,否则它们是不同的。而且,当然,您可以跳过一些提交(使用--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...中。提交注释是树形结构的,树 depthvariable,而不是简单的前两个/所有其余的扇出,所以它有点棘手。不过,前端git notes 界面很好地隐藏了这一切。

【讨论】:

  • 这是一个非常详细的答案,可以深入了解 git 的内部结构。但是,我不清楚它如何回答我的问题。你认为我要求的究竟是什么在技术上是不可能的?您的意思是移动的文件是不同的对象,因为提交的哈希包含路径吗?因此,为了保留提交历史,您复制每个提交,而不是保留原件并仅添加一个新提交,说“文件已移动”(例如,更改一条跟踪的元数据,即文件的路径)
  • 是:如果文件具有不同的名称,或者位于不同的路径中,则结果是不同的树对象(具有不同的哈希)。所以重命名一个文件并提交给你一个不同的树,它给出了一个不同的提交,让我们回到“复制每个后续提交”的问题。 (更改每次提交并没有错,您最终会得到一个与原始存储库无关的存储库。)重命名文件而不重写历史记录的唯一方法是将 add 作为最新操作,离开现有历史的旧名称。
  • 顺便说一下,另一种看待这个问题的方式是:提交(实际上是所有 Git 对象)都是只读的。如果你有一些现有的历史作为你现有的历史,你就会被困住。您必须放弃它们才能做出影响过去和未来的改变。
  • 我与 Perforce 对比,其中移动文件的正确方法是单个文件的历史分支(集成)。这跟踪显式重命名,而不是通过差异阈值。所以在我看来,在 git 中做的正确事情是使用 git mv 重命名文件,并且在执行该提交时,确保 git status 将更改标识为移动而不是删除+新我总是将移动文件视为与更改其内容的单独更改,因此应该可以正常工作。
  • 那么为什么这么多人快速推荐filter-branch呢?我认为这可能是他们只是不关心在项目级别保存历史。他们也可能是“懒惰的”,更喜欢通过 git log 而不是 git log 获取完整的文件历史记录——follow 我仍然有一个关注领域,这与这个问题相同,它没有一个令人满意的答案(然而)stackoverflow.com/questions/29569299/…
猜你喜欢
  • 2020-09-10
  • 2011-04-01
  • 1970-01-01
  • 2022-06-14
  • 1970-01-01
  • 2021-12-09
  • 1970-01-01
  • 1970-01-01
  • 2016-10-20
相关资源
最近更新 更多