当 git 历史记录包含私人信息时,如何开放源代码?

How to make source open when git history includes private information?

我们已经从我们的程序中删除了私人信息,并将这些文件添加到我们的 git 忽略文件中。我们现在想制作我们的 repo public,但我担心访问者可以从 git 历史记录中恢复机密信息。解决办法是什么?

正如大家在评论中所言,你们想要改写历史。通常的工具是 git filter-branch,使用起来有点复杂,因为它有太多选项。查看任意数量的现有 Whosebug 帖子,了解使用它的多种方法(以及一些替代方法)。

什么是历史重写

请记住,一个 Git 存储库主要是两个数据库:

  • 大数据库由Git对象组成。有四种对象,我们将在下面详细说明。每个对象都有自己唯一的哈希 ID,特定于该对象。

  • 较小的数据库由名称组成:分支名称、标记名称和其他类似名称。每个名字都有一个对象哈希ID。

克隆 Git 存储库包括从大数据库中复制其部分或全部对象,如通过在较小数据库中查找哈希 ID 找到的那样;并从较小的数据库中复制一些名称。

历史,在 Git 存储库中,只是该存储库中的 提交对象 。根据您希望对定义的慷慨程度,您还可以向其中添加 带注释的标记对象 。名称,如分支和标签名称,可让您找到提交。带注释的标记对象可让您找到提交。 Commits 让您可以找到提交……仅此而已:您可以从名称开始开始——找到提交对象哈希 ID。您也需要一个名称来查找带注释的标记对象,因此即使我们使用扩展定义,您也可以从一个名称开始。

四种对象类型

那么,现在让我们来看看四种对象类型。它们是:

  • 带注释的标签。我们已经提到了带注释的标签对象。这些包含您的标签消息,可能还有 GPG 签名密钥或类似密钥,以及标签目标对象哈希 ID。通常这将是提交的 ID,尽管这里允许四种对象类型中的任何一种。

  • 提交对象。提交包含 元数据,这是关于关于 提交的信息,例如提交人和时间以及他们的日志消息,加上 about =152=]树对象。树对象表示与提交一起使用的数据:快照。换句话说,不是直接持有快照,提交只持有快照的哈希ID。这意味着如果两个提交拥有相同的源代码树,他们可以共享它——只有一个快照。

    每个提交还可以列出一个或多个前任 ("parent") 提交的哈希 ID。这就是 history 真正存在的地方;我们稍后会回到这个话题。

  • 树对象。我们在上面提到了这些。它们包含小结构,每个结构都恰好包含三个值:

    • a mode,这是一小组允许值中的数值;
    • a name,这是一个组件名称,如file.csubdir;和
    • 一个哈希ID.


    哈希 ID 在某些情况下是另一棵树的哈希 ID,或者在大多数其他情况下是 blob 对象的哈希 ID。 (剩下的情况是他们可以在某些 other 存储库中保存某些提交的哈希 ID,这是一种称为 gitlink,仅当模式设置为 160000 时才允许。这就是子模块的工作方式:超级项目提交在某个树对象中保存子模块存储库的提交哈希 ID。)

  • 最后一种对象是 blob 对象。它保存文件的数据,或者对于符号 link(模式 120000)树条目,作为 link.

    [=222 的目标的文件名=]

因此,Git 存储库的对象部分是存储所有文件的地方。 Every every 文件的提交版本出现在这个数据库中,以 blob 的形式出现在树中,在提交中列出,在其他提交。偶尔 - 很少或从不 - 一个 blob 或树直接由标签对象或标签名称列出,并且不太偶尔,提交哈希 ID 直接由标签对象或分支名称列出。

将两个数据库结合起来形成一个有用的存储库

根据定义,分支名称包含分支中last 提交的哈希ID。从那里,Git 找到每个较早的(父)提交。这会通过对象数据库的提交部分生成跟踪。

标签名称通常列出标签对象或提交。 "Peeling off" 标签通过查找其底层提交引导您进行提交。该提交具有它所拥有的任何父级,并且按照与分支名称相同的方式跟随这些父级,通过对象数据库的提交部分生成跟踪。

每个名称"reaches"完成一些提交集的过程。根据定义,对象数据库中的任何剩余提交都是 unreachable。可到达的提交是 git clone 将复制的提交;无法访问的将被丢弃。1

你可能想知道为什么我在这里一直提到 clone;我们将在下一节中介绍。


1这里对 reflogs 有点挑剔。 每个名字都有,或者可以有,reflog。 Reflogs 有时间和日期标记的条目;每个条目存储为哈希 ID。 运行 git clone 不复制或使用引用日志,但 git gc 使用它们来避免过快地丢弃东西。 reflog 条目让原本死掉的对象(通常是提交)持续存在,这样您就可以在默认情况下至少 30 天让它们恢复生机。我们已经知道引用名称(例如分支名称)包含对象的哈希 ID。例如,当我们进行新提交时,分支名称会定期 更新 以存储 new 哈希 ID。此时Git将name的old值写入分支的reflog。

(一个标记,无论是否注释,直接进入树或 blob 对象,也使该对象保持活动状态。不过,通常您没有树或 blob 对象的标记。此外,条目index 将使 blob 保持活动状态,因为这是存储您已 git add-ed 但尚未 git commit-ed 的文件的位置。None 其中要么被克隆。)


重写历史就是复制提交

没有提交——事实上,任何类型的 Git 对象都不能更改,一点也不能。这样做的原因是对象的哈希 ID 是对象内容的(加密)校验和。更改一位,您将拥有一个新的、不同的对象,具有不同的校验和。2

至"rewrite history",这正是我们想要的:我们遍历存储库中所有可到达的提交。对于每个这样的提交,我们决定:是否复制此提交? 对于我们决定答案是的每个提交:是,复制它, 我们还决定:是否进行一些更改?

如果我们制作的副本与原件完全相同,那么副本就是原件。它保持不变,我们实际上只是重新使用原始提交。但是如果我们改变任何东西——包括快照——我们会得到一个新的、不同的提交,带有一个新的唯一哈希 ID。通过确保以正确的顺序复制提交——从有史以来的第一个提交开始并向前工作,而不是 Git 首选的向后顺序——我们确保当我们 复制一个提交,以后 提交将使用一组不同的父哈希 ID,我们将把那些后来的提交复制到具有新的和改进的提交-改进了它们背后的历史。

最好通过示例查看此过程。假设我们有这个现有的历史:

A--B--C--D--E--H--I--L--M--N--O--P   <-- master
       \               /
        F--G-------J--K

作为对象数据库中的整个提交集,一个名称 master 找到 last 提交,P。我们将做一个副本,在复制过程中,我们将保留提交 B 但将其更改为删除文件,保持提交 C 不变,保留提交 JKM,完全删除 DLJK 除外),保留 N,删除 O,并保持 P。生成的副本如下所示:

A--B--C--D--E--H--I--L--M--N--O--P   <-- refs/original/refs/heads/master
       \               /
        F--G-------J--K

B'-C'-----M'-N'-P'   <-- master
    \    /
     J'-K'

我们删除了 A,因此我们不得不通过两种方式更改 B:新副本有 没有 父级,并且它省略了我们的文件不想。这意味着我们必须复制 C 才能以一种方式更改它:副本将 B' 作为其父级。我们必须将 J 复制到 J' 才能使用 C' 作为父项;同样,我们必须将 K 复制到 K';我们必须将合并提交 M 复制到 M' 以使其具有 C'K' 作为其两个父项,依此类推。

复制了选定的提交,并在此过程中进行了一些更改,我们让我们的 Git 存储库更改 name master 以指向新的提交P'。请注意,通过从 master 开始并向后工作,我们 永远不会访问任何原始提交 。但是,如果我们保持 A 不变,我们将得到:

A--B--C--D--E--H--I--L--M--N--O--P   <-- refs/original/refs/heads/master
 \     \               /
  \     F--G-------J--K
   \
    B'-C'-----M'-N'-P'   <-- master
        \    /
         J'-K'

也就是说,我们只改变了B一种方式来删除不需要的文件。我们仍然有 B',但它会指向现有的提交 A,并且从 master 开始,我们将只访问新的副本,直到我们到达 B',然后回去提交 A.

这个 refs/original/refs/heads/master 这个时髦的名字怎么样? 那个 名字——以及脚注 1 中提到的引用日志——会让我们看到原始历史。但是 名称没有被 git clone 复制,reflog 也没有。这个时髦的名字本身是 git filter-branch 的副产品,当我们告诉它复制 master 并删除或修改一些提交时,它将原始名称保存在这个新的 refs/original/ 组名称下方式。

因此,使用 git filter-branch 到 "rewrite" 历史实际上意味着:通过复制大多数提交,同时更改它们的某些内容,使我的存储库数据库的大小大约增加一倍。 新的和改进的副本 紧挨着 原件。他们甚至可能共享一些提交,朝向历史的最早部分,这取决于您选择复制什么以及您选择更改什么。

如果这两个历史没有共享任何内容,则您的新历史是独立的。如果他们确实分享了一些东西,你的新历史就像你选择的那样干净:它只分享 first(历史上最早的)提交,当复制时,你说 不要管这些,它们就这样很好

您现在可以使用git clone复制 复制的提交。由于 git clone 忽略了 refs/original/ 名称,并忽略了 reflog,因此当您将当前版本的存储库复制到新版本时,您得到的是:

B'-C'-----M'-N'-P'   <-- master (HEAD), origin/master
    \    /
     J'-K'

(假设您没有告诉 filter-branch 保留 A;如果您这样做了,请在左侧插入 A)。名称 master 出现在这里只是因为 git clone 本身 在将存储库复制到新的数据库对后创建了 它。原始存储库中的分支名称已全部替换为 origin/<em>whatever</em>,任何 git clone.[= 的通常方式76=]


2其中的 "cryptographic" 部分只是意味着设计哈希冲突非常困难。哈希冲突导致 Git 噘嘴并拒绝创建新对象,或者至少在理论上,这就是 应该 发生的情况。实际上,哈希冲突从未真正发生过。另见 Hash collision in git