好的,根据您对评论的回答,我们现在可以说发生了什么。
git filter-branch
的作用是将(部分或全部)提交复制到新的提交中,然后更新引用。这意味着您的存储库最初会变得更大(而不是更小)。
被复制的提交是通过给定引用可达的那些提交。在这种情况下,您给出的引用是 HEAD
(Git 将其转换为“当前分支”,可能是 master
,但无论在 filter-branch
命令时您的当前分支是什么)。仅当新副本与原始版本完全相同(按位)时,它实际上就是原始版本,没有进行实际复制(而是重用原始版本)。但是,一旦您进行任何更改,例如从那个时间点开始删除 folder/subfolder
,从那时起,这些内容就真正成为了副本。
在这种情况下,复制的内容会更小,因为您已经删除了一些项目。(通常情况下不会太小,因为git可以很好地压缩项目。)但是您仍然向存储库添加更多内容:新提交,引用新树,这些树幸运地引用与以前相同的旧blob(文件对象),只是这次稍微少了一些(
folder/subfolder
文件的对象仍然存在于存储库中,但是复制的提交和树对象不再引用它们)。
在这个
filter-branch
过程中,在图示上,我们现在有旧的提交和:
R--o--o---o--o <-- master
\ /
o--o <-- feature
新的内容(我会假设folder/subfolder
出现在原始根提交R
中,以便我们在这里有一个副本R'
):
R'-o'-o'--o'-o'
\ /
o'-o'
在复制过程结束时,
filter-branch
会重新指向一些引用(分支和标签名称,主要是这些)。它重新指向的引用是你告诉它的那些,“正向引用”是文档所称的。在这种情况下,如果你在
master
上(即,
HEAD
是
master
的另一个名称),你给出的唯一正向引用就是
master
...这就是
filter-branch
重新指向的全部内容。它还创建了一个名字以
refs/original/
开头的备份引用。这意味着现在你有以下提交记录:
R--o--o---o--o <-- refs/original/refs/heads/master
\ /
o--o <-- feature
R'-o'-o'--o'-o' <-- master
\ /
o'-o'
请注意,
feature
仍然指向所有
旧的(未复制)提交,因此即使/在您摆脱任何
refs/original/
引用之后,git仍将保留所有仍被引用的提交,跨越任何垃圾回收活动,从而得到:
R
\
o
R'-o'-o'--o'-o' <-- master
\ /
o'-o'
为了让
filter-branch
更新
所有引用,您需要命名它们所有。一种简单的方法是使用
--all
,它确实命名了所有引用。在这种情况下,初始的“after”图片看起来像这样:
R--o--o---o--o <-- refs/original/refs/heads/master
\ /
o--o <-- refs/original/refs/heads/feature
R'-o'-o'--o'-o' <-- master
\ /
o'-o' <-- feature
现在,如果你删除所有的
refs / original /
引用,所有旧的提交将变为未引用,可以被垃圾回收。但是,如果有指向它们的标签,它们就不会被回收。
对于标签引用,
filter-branch
仅在您提供
--tag-name-filter
时以任何方式更新它们。通常你想要
--tag-name-filter cat
,这将保持标签名称不变,但使
filter-branch
指向新复制的提交。这样你就不会卡住旧的提交:整个演习的重点是让所有东西使用新副本,并丢弃旧副本,以便可以回收大文件对象。
将所有东西放在一起,不是:
:
git filter-branch -f --tree-filter 'rm -rf folder/subfolder'
你可以使用:
git filter-branch -f --tree-filter 'rm -rf folder/subfolder' \
--tag-name-filter cat -- --all
您不需要反斜杠换行符序列;我只是为了使该行更好地适合stackoverflow而这样做。请注意,--tree-filter
非常慢:对于这种特殊情况,使用--index-filter
要快得多。此处的索引过滤器命令将是git rm --cached --ignore-unmatch -r folder/subfolder
。
还要注意,您需要在(原始的)存储库的副本上执行所有这些操作(您备份了吗?)。 (如果您没有备份,则refs/originals/
可能会拯救您。)
编辑:好的,所以你进行了一些filter-branch
操作,并且删除了任何refs/originals/
。 (在我的临时存储库实验中,对HEAD
运行git filter-branch
会使用我所在的任何分支作为重新定位的分支,并创建先前值的“originals”副本。)没有存储库的备份。现在怎么办?
好吧,作为第一步,现在备份。这样,如果情况变得更糟,您至少可以回到“仅略有损失”的状态。要备份repo,您可以简单地克隆它(或:克隆它,然后称原始副本为“备份”,然后开始使用克隆)。供将来参考,由于git filter-branch
可能非常具有破坏性,通常最好从执行此备份过程开始。 (另外,我将注意到,在未push
到时,bitbucket上的克隆将起作用。不幸的是,您进行了push
。也许bitbucket可以从其自己的某些备份或快照中检索存储库的早期版本。)
接下来,让我们注意一下提交以及它们的SHA-1“真名”的特殊性,这是我之前提到过的。提交的SHA-1名称是其内容的加密校验和。让我们来看一下Git自己源代码树中的一个示例提交(仅为缩短长度而削减了一些内容,并且电子邮件地址被删除以防止收集者)。
$ git cat-file -p 5de7f500c13c8158696a68d86da1030313ddaf69
tree 73eee5d136d2b00c623c3fceceffab85c9e9b47e
parent c4ad00f8ccb59a0ae0735e8e32b203d4bd835616
author Jeff King <peff peff.net> 1405233728 -0400
committer Junio C Hamano <gitster pobox.com> 1406567673 -0700
alloc: factor out commit index
We keep a static counter to set the commit index on newly
allocated objects. However, since we also need to set the
[snip]
在这里,我们可以看到此提交的内容(其“真实名称”为
5de7f50...
)以
tree
和另一个SHA-1、
parent
和另一个SHA-1、
author
和
committer
开头,然后是空行,接着是提交消息文本。
如果您查看
tree
,您会看到它包含子树(子目录)和文件对象(Git术语中的“blob”)的“真实名称”(SHA-1值),以及它们的模式 - 实际上只是blob是否应该设置执行权限以及它们在目录中的名称。例如,上述
tree
的第一行是:
100644 blob 5e98806c6cc246acef5f539ae191710a0c06ad3f .gitattributes
这意味着需要提取存储库对象
5e98806...
,将其放入名为
.gitattributes
的文件中,并设置为非可执行文件。
如果我要求git创建一个新提交,并设置其内容:
- 相同的树(
73eee5d...
)
- 相同的父级(
c4ad00f...
)
- 相同的作者和提交者
- 以及相同的空行和消息
那么当我让git将该提交写入存储库时,它将生成相同的“真实名称”
5de7f50...
。换句话说,它实际上是相同的提交:它已经在存储库中,
git commit-tree
只会给我返回现有的ID。虽然设置所有这些有点棘手,但这正是
git filter-branch
最终要做的:它提取原始提交,应用您的过滤器,设置所有内容,然后执行
git commit-tree
。
对您的影响是什么?
在您的原始存储库上,您运行了一个
git filter-branch
命令,将提交复制到新的修改提交中(具有不同的
tree
,因此在某些时候,拥有不同的真实名称,这导致在后续提交中具有不同的父ID等)。但是,如果您通过应用这次“什么也不做”的过滤器来复制那些已复制的提交,则新的
tree
对象将与旧的相同。如果新的
parent相同,并且作者、提交者和消息也都保持不变,则副本的新提交ID将与旧ID
相同。
也就是说,这些
新的副本实际上并不是副本,它们只是原始提交!
任何其他提交 - 那些在第一遍没有被复制的提交 - 确实会被复制,因此具有不同的ID。
这里就变得棘手了。
如果您当前的存储库看起来像这样(从图形上讲):
R--o--o---o--o <-- xxx [needs a name so that filter-branch will process it]
\ /
o--o <-- feature
R'-o'-o'--o'-o' <-- master
\ /
o'-o'
我们对所有引用(甚至“除了
master
”之外的所有引用)应用一个新的
filter-branch
,以便以这种方式生成相同的树,这次它将再次复制
R
,新树将与
R'
的树匹配,因此复制实际上将是
R'
。然后它将复制第一个
R
节点之后的节点,进行相同的更改,复制将实际上是第一个
R'
,
o'
节点。这将为所有节点重复进行,可能甚至包括
R'
和所有的
o'
s。如果
filter-branch
复制
R'
,则生成的副本将仍然是
R'
,因为“删除不存在的目录”不会有任何更改:我们的过滤器对这些特定提交没有任何影响。
最后,filter-branch将移动标签,保留
refs/originals/
版本:
R--o--o---o--o <-- refs/originals/refs/xxx
\ /
o--o <-- refs/originals/refs/feature
R'-o'-o'--o'-o' <-- master, xxx
\ /
o'-o' <-- feature
这实际上是期望的结果。
如果存储库看起来更像这样怎么办? 也就是说,如果没有指向原始(经过过滤前的)
master
的
xxx
或类似的标签,那么你会得到这个:
R
\
o
R'-o'-o'--o'-o' <-- master
\ /
o'-o'
filter-branch
脚本仍将复制
R
,结果仍将是
R'
。然后它将复制第一个
o
节点,结果仍将是第一个
o'
节点,依此类推。它不会复制已删除的节点,但也不必复制:我们已经通过分支名称
master
可达到这些节点。与以前一样,
filter-branch
可能会复制
R'
和各种
o'
节点,但这没关系,因为过滤器什么都不做,所以复制品实际上就是原件。
最后,filter-branch
将像往常一样更新引用:
R
\
o
R'-o'-o'--o'-o' <-- master
\ /
o'-o' <-- feature
这一切都能够实现的关键是过滤器不会对已经修改过的提交进行更改,因此它们的第二个“副本”只是第一个副本。
1
完成后,您可以执行与
git filter-branch
文档 中描述的相同缩小操作,以摆脱
refs/originals/
名称并垃圾回收现在未被引用的对象。
1如果您使用的过滤器不能轻易地重复(例如,一个会使用“当前时间”作为其时间戳进行新提交的过滤器),则您确实需要一个未更改的原始存储库,或者这些refs/originals/
引用(其中任何一个都足以保留“原始副本”)。
master
以外的其他分支和/或标签吗?(这里可能是HEAD
指向master
...) - torekgit gc --prune=now
命令,Git 会保留对之前更改的引用。如果您的本地仓库变小了,它最终也会在 Bitbucket 上变小。 - Willem D'Haeseleer