是否可以在不重写历史记录的情况下精简 .git 存储库?

Posted

技术标签:

【中文标题】是否可以在不重写历史记录的情况下精简 .git 存储库?【英文标题】:Is it possible to slim a .git repository without rewriting history? 【发布时间】:2013-07-02 11:43:06 【问题描述】:

由于历史上包含二进制测试文件和 java .jar 文件,我们有许多 git 存储库已增长到无法管理的大小。

我们即将完成git filter-branching 这些存储库的练习,在使用它们的任何地方重新克隆它们(每个部署从几十个到数百个,具体取决于存储库)并给出problems with rewriting history 我是想知道是否还有其他解决方案。

理想情况下,我希望在不重写每个存储库的历史记录的情况下将问题文件外部化。从理论上讲,这应该是可能的,因为您正在检查相同的文件,具有相同的大小和相同的哈希,只是从不同的地方(远程而不是本地对象存储)采购它们。唉,到目前为止,我发现的任何潜在解决方案似乎都不允许我这样做。

从git-annex 开始,我能找到的最接近我的问题的解决方案是How to retroactively annex a file already in a git repo,但与删除大文件一样,这需要重写历史记录以转换原始git add变成git annex add

从那里开始,我开始查看what git-annex is not 上列出的其他项目,因此我检查了git-bigfiles、git-media 和git-fat。不幸的是,我们不能使用 gitgit-bigfiles 分支,因为我们是 Eclipse shop 并且混合使用了 git 和 EGit。它看起来不像 git-mediagit-fat 可以做我想要的,因为虽然你可以用外部等价物替换现有的大文件,但你仍然会需要重写历史以删除已经提交的大文件。

那么,是否可以在不重写历史记录的情况下精简 .git 存储库,或者我们是否应该回到使用 git filter-branch 的计划并进行大量重新部署?


顺便说一句,相信这应该是可能的,但可能与gits 当前shallow clone 实现的限制相同。

Git 已经支持同一个 blob 的多个可能位置,因为任何给定的 blob 都可能在 loose object store (.git/objects) 或 pack file (.git/objects) 中,所以理论上你只需要类似的东西git-annex 被挂在那个级别而不是更高级别(即,如果您愿意,可以考虑按需下载远程 blob)。不幸的是,我找不到任何人实施甚至提出过类似的建议。

【问题讨论】:

据我所知,你问的是如何在不重写历史的情况下重写历史。 @alternative 不完全是,我在问是否有办法在重写历史的情况下精简存储库。目前看起来使用 shallow clones 可能是唯一的方法,但是这些限制可能不适用于我们的工作流程,即使这样做了,它们也只会缩小本地(克隆)存储库,而不是远程裸仓库。 “精简”存储库的唯一方法是删除您正在精简的内容 - 因此,重写(这就是为什么每个答案都说这是不可能的)。只要你做得正确,重写历史真的没有任何问题。是的,浅层克隆只会影响本地存储库。 @alternative - 如果您在一个小团队中工作并且外部合作者很少(github 上的分支),那么重写历史并不是什么大问题。如果您有数十名开发人员、合作者甚至更多克隆人,那么强制所有这些 ref 更新的成本可能会迅速失控。 【参考方案1】:

有点。您可以使用Git's replace feature 将臃肿的历史记录放在一边,以便仅在需要时下载。它就像一个浅克隆,但没有浅克隆的限制。

这个想法是你通过创建一个新的根提交来重启一个分支,然后挑选旧分支的提示提交。通常,您会以这种方式丢失所有历史记录(这也意味着您不必克隆那些大的.jar 文件),但如果需要历史记录,您可以获取历史提交并使用git replace 无缝拼接它们回来了。

请参阅Scott Chacon's excellent blog post 了解详细说明和演练。

这种方法的优点:

历史未修改。如果您需要返回到旧提交完成它的大 .jars 和所有内容,您仍然可以。 如果您不需要查看旧的历史记录,那么您的本地克隆的大小又好又小,而且您制作的任何新克隆都不需要下载大量无用的数据。

这种方法的缺点:

默认情况下不提供完整的历史记录 - 用户需要跳过一些障碍才能了解历史记录。 如果您确实需要频繁访问历史记录,无论如何您最终都会下载臃肿的提交。

这种方法仍然存在一些与重写历史相同的问题。例如,如果您的新存储库如下所示:

* modify bar (master)
|
* modify foo  <--replace-->  * modify foo (historical/master)
|                            |
* instructions               * remove all of the big .jar files
                             |
                             * add another jar
                             |
                             * modify a jar
                             |

并且有人从他们合并的历史分支中有一个旧分支:

* merge feature xyz into master (master)
|\__________________________
|                           \
* modify bar                 * add feature xyz
|                            |
* modify foo  <--replace-->  * modify foo (historical/master)
|                            |
* instructions               * remove all of the big .jar files
                             |
                             * add another jar
                             |
                             * modify a jar
                             |

然后大的历史提交将重新出现在您的主存储库中,您将回到您开始的地方。请注意,这并不比重写历史更糟糕——有人可能会不小心合并到重写前的提交中。

可以通过在您的共享存储库中添加 update 挂钩来拒绝任何会重新引入历史根提交的推送来缓解这种情况。

【讨论】:

哇,谢谢理查德,这看起来可能正是我一直在寻找的。我会看看下周能不能让它工作,如果是这样,你也会有一个滴答声…… 啊,我明白了,所以该示例重写了 最近提交 的历史记录以删除大型历史提交,而无需重写那些 历史提交 的历史记录 i>,但使用git replace 允许您稍后在需要时带回历史提交。所以,这不是我所追求的,但我会更多地思考如何利用它来解决我的问题。 我真希望在我们从旧的 svn 存储库创建 git 存储库时知道这一点。不必选择从svn 开始一个没有历史的新纪元,或者用多年积累的svn cruft 开始我们的git 回购,我们可以将整个svn 回购保留在一组历史@ 987654336@ repos,然后在需要时使用git replace 将它们带回来。事实上,我想知道我们是否仍然能够返回并添加回顾性git replace 目标。很有趣,很有趣…… @MarkBooth:是的,您可以使用git replace 附加旧历史记录。现在还为时不晚;)。 @MarkBooth 你可以看看移植物——它们非常相似,并且可能会受到支持,因为它们更老了。但请注意,这种方法继承了历史重写方法的所有问题,所以只要您知道有大文件不应该在 repo 中,您最好将它们从历史中删除。【参考方案2】:

不,这是不可能的——你必须重写历史。但这里有一些提示:

As VonC mentioned:如果适合您的场景,请使用BFG- repo cleaner——它比git filter-branch 更容易使用。

您无需再次克隆!只需运行这些命令而不是 git pull 就可以了(将 originmaster 替换为您的远程和分支):

git fetch origin
git reset --hard origin/master

但请注意,与git pull 不同,您将丢失所有尚未推送到服务器的本地更改。

如果您(或您团队中的其他人)完全了解 git 如何看待历史,以及 git pullgit mergegit rebase(也称为 git rebase --onto)做什么,这将大有帮助。然后让所有相关人员快速培训如何处理这种重写情况(5-10 分钟就足够了,基本的注意事项)。 请注意,git filter-branch 本身不会造成任何伤害,但会导致许多标准工作流程造成伤害。如果人们不采取相应行动并合并旧历史,如果您没有及时注意到,您可能只需要重新改写历史。 您可以通过在服务器上写入(5 行)适当的update hook 来防止人们合并(更准确地说是推送)旧的历史记录。只需检查推送头部的历史记录是否包含特定的旧提交。

【讨论】:

感谢 Chronial。 not 重新克隆的唯一真正问题是必须 reset 每个本地使用的分支(以摆脱对过时分支的所有本地引用)并运行 git gc --prune=now --aggressive 以实际缩小回购。如果你这样做并且 repo 没有缩小,那么你知道你在某个地方错过了一个 ref。重新克隆消除了对所有这些步骤的需要(我们使用 buckminster 部署了大约 20 个 git 存储库,因此重新克隆 一切 对我们来说很容易)。遗憾的是,我们还使用 gitolite 来托管我们的 git 存储库,它保留了 update 钩子供自己使用。 我不知道 gitolite,但 hooks and gitolite 说 您可以安装除这些之外的任何钩子:(所有 repos)gitolite 保留 update 钩子 所以我必须等到我们的 gitolite 专家回来告诉我是否有办法解决这个问题。 @MarkBooth gitolite V3 中的自定义更新挂钩称为 VREF(如在此答案中:***.com/a/11517112/6309),您可以定义尽可能多的“gitolite-update hooks”(或 VRefs)需要:***.com/a/10888358/6309。 Gitolite V2 将使用钩子链接 (***.com/a/15941289/6309)。【参考方案3】:

我不知道可以避免重写历史的解决方案。

在这种情况下,使用 BFG- repo cleaner 之类的工具清理 rpeo 是最简单的解决方案(比 git filter-branch 更简单)。

【讨论】:

【参考方案4】:

老实说,我想不出办法来做到这一点。如果您考虑一下 Git 对您作为用户的“承诺”,关于数据完整性,我想不出一种方法可以从存储库中删除文件并保持相同的哈希值。换句话说,如果你的要求是可能的,那么 Git 的可靠性就会大大降低......

【讨论】:

以上是关于是否可以在不重写历史记录的情况下精简 .git 存储库?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在不重写的情况下将数据添加到文件中?

我可以在不删除最近提交的情况下删除源/主合并历史记录吗?

修改GIT已提交的用户名和邮箱

在 Git 中,如何在不挑选新分支的情况下对历史中具有多个合并提交的分支进行 rebase + squash

是否可以在不手动将重写的克隆方法添加到 C++ 中的每个派生类的情况下克隆多态对象?

如何在不更改浏览器历史记录的情况下更改 url