当 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.c
或subdir
;和
- 一个哈希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
不变,保留提交 J
和 K
和 M
,完全删除 D
到 L
(J
和 K
除外),保留 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。
我们已经从我们的程序中删除了私人信息,并将这些文件添加到我们的 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.c
或subdir
;和 - 一个哈希ID.
哈希 ID 在某些情况下是另一棵树的哈希 ID,或者在大多数其他情况下是 blob 对象的哈希 ID。 (剩下的情况是他们可以在某些 other 存储库中保存某些提交的哈希 ID,这是一种称为 gitlink,仅当模式设置为160000
时才允许。这就是子模块的工作方式:超级项目提交在某个树对象中保存子模块存储库的提交哈希 ID。)最后一种对象是 blob 对象。它保存文件的数据,或者对于符号 link(模式
[=222 的目标的文件名=]120000
)树条目,作为 link.
因此,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
不变,保留提交 J
和 K
和 M
,完全删除 D
到 L
(J
和 K
除外),保留 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。