0

我们已经从我们的程序中删除了私人信息,并将这些文件添加到我们的 git 忽略文件中。我们现在想公开我们的回购协议,但我担心访问者可以从 git 历史记录中恢复机密信息。解决方案是什么?

4

1 回答 1

2

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

历史改写是关于什么

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

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

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

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

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

四种对象类型

那么,现在让我们看看四种对象类型。这些都是:

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

  • 提交对象。提交包含元数据,这是有关提交的信息,例如提交人、提交时间及其日志消息,以及对象的哈希 ID。树对象代表提交的数据:快照。换句话说,提交不是直接保存快照,而是只保存快照的哈希 ID。这意味着如果两个提交拥有相同的源树,它们可以共享它——只有一个快照。

    每个提交还可以列出一个或多个前置(“父”)提交的哈希 ID。这才是历史真正存在的地方;我们稍后再讨论这个问题。

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

    • 一个模式,它是来自一小组允许值的数值;
    • a name,它是一个组件名称,例如file.cor subdir;和
    • 一个哈希 ID


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

  • 最后一种对象是blob 对象。它保存文件的数据,或者——对于符号链接 (mode 120000) 树条目,作为链接目标的文件名。

因此,Git 存储库的对象部分是存储所有文件的地方。 每个文件的每个提交版本都以 blob 的形式出现在此数据库中,这些 blob 列在树中,这些树列在其他提交中列出的提交中。偶尔(很少或从不)blob 或树直接由标记对象或标记名称列出,而不那么偶尔,提交哈希 ID 直接由标记对象或分支名称列出。

结合这两个数据库会产生一个有用的存储库

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

标签名称通常列出标签对象或提交。通过找到它的底层提交来“剥离”标签会导致你提交。该提交具有它所拥有的任何父级,并且按照与使用分支名称相同的方式,通过对象数据库的提交部分生成跟踪。

对每个名称都执行此过程“到达”某些提交集。根据定义,对象数据库中的任何剩余提交都是不可访问的。可达的提交是那些git clone将被复制的提交;无法到达的将被丢弃。1

你可能想知道为什么我在这里一直提到克隆;我们将在下一节中讨论。


1这里有一些关于 reflog 的麻烦。 每个名字都有或可以有一个 reflog。Reflogs 有时间和日期戳条目;每个条目存储为哈希 ID。Runninggit clone不会复制或使用 reflog,而是git gc使用它们来避免过快丢弃东西。reflog 条目让原本死掉的对象(通常是提交)保持存在,因此默认情况下您可以使它们恢复生机至少 30 天。我们已经知道 ref 名称(例如分支名称)包含对象的哈希 ID。例如,当我们进行新的提交时,分支名称会定期更新以存储新的哈希 ID。此时,Git 将 name 的值写入分支的 reflog。

(一个标签,无论是否带注释,直接指向树或 blob 对象,也使该对象保持活动状态。不过,通常您没有树或 blob 对象的标签。此外,索引中的条目将使 blob 保持活动状态,因为那是您已git add-ed 但尚未git commit-ed 的文件的存储位置。不过,这些文件都不会被克隆。)


重写历史是关于复制提交

任何提交——事实上,任何类型的 Git 对象——都不能被改变,一点也不能改变。原因是对象的哈希 ID 是对象内容的(加密)校验和。改变一点,你所拥有的是一个新的、不同的对象,具有不同的校验和。2

要“重写历史”,这正是我们想要的:我们遍历存储库中所有可访问的提交。对于每个这样的提交,我们决定:是否复制这个提交? 对于每一个我们决定答案是:是的,复制它,我们还决定:在我们做的时候做一些改变,还是不做?

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

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

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

作为对象数据库中的整个提交集,使用一个名称master查找最后一个提交,P. 我们将进行复制,在复制期间,我们将保留提交B,但将其更改为删除文件、保持C原样提交、保留提交JK和、完全M删除(除了和)、保留、删除和保留。生成的副本如下所示:DLJKNOP

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'作为其父项。我们必须复制JtoJ'才能C'用作父级;我们必须同样复制KK';我们必须复制合并提交MM'使其具有C'K'作为它的两个父级,依此类推。

复制了选定的提交并在此过程中进行了一些更改,我们让我们的 Git 存储库更改名称 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',但它会指向现有的 commit A,并且从 开始master,我们只会访问新副本,直到到达B',然后返回 commit A

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

因此,使用git filter-branch“重写”历史实际上意味着:通过复制大多数提交,同时更改有关它们的某些内容,大约使我的存储库数据库的大小增加一倍。 新的和改进的副本与原件相邻。根据您选择复制的内容和选择更改的内容,他们甚至可能会共享一些提交到历史的最早部分。

如果这两个历史不共享任何东西,那么您的新历史就是独立的。如果他们确实共享了某些东西,那么您的新历史记录就像您选择的那样干净:它只共享第一个(历史上最早的)提交,当复制时,您说不要管这些,它们就像它们一样好

您现在可以用来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/whatevergit clone


2这其中的“加密”部分只是意味着很难设计哈希冲突。哈希冲突导致 Git 噘嘴并拒绝生成新对象,或者至少在理论上,这是应该发生的。实际上,哈希冲突从未真正发生过。另请参阅git 中的哈希冲突

于 2020-02-05T00:36:32.300 回答