在不重写历史记录的情况下,是否有可能精简 .git 仓库?

24
我们有许多git仓库,由于历史上包含二进制测试文件和Java .jar文件而变得难以管理。
我们即将进行git filter-branch操作,重新克隆它们到每个使用它们的地方(根据仓库的不同,从几十个到数百个部署),但由于重写历史的问题,我想知道是否还有其他解决方案。
理想情况下,我希望在不重写每个仓库的历史记录的情况下外部化问题文件。从理论上讲,这应该是可能的,因为您正在检出相同的文件,具有相同的大小和哈希值,只是从不同的位置获取它们(一个远程位置而不是本地对象存储)。然而,到目前为止,我发现的潜在解决方案似乎都无法做到这一点。

git-annex开始,我能找到的最接近解决我的问题的方法是如何在已经存在于git仓库中的文件上进行后期附加, 但和删除大文件一样,这需要重写历史记录以将原始的git add转换为git annex add

接着,我开始寻找git-annex项目列表中的其他项目,因此我检查了git-bigfiles, git-mediagit-fat。不幸的是,由于我们是Eclipse商店,并且使用混合的gitEGit,我们无法使用git-bigfiles分支。看起来git-mediagit-fat也不能满足我的要求,因为虽然你可以用外部文件替换现有的大文件,但你仍需要重写历史记录以删除已经提交的大文件。

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


作为旁注,认为这应该是可能的,但可能与git当前的shallow clone实现有相同的限制。
Git已经支持同一blob的多个可能位置,因为任何给定的blob都可以在loose object store(.git / objects)或pack file(.git / objects)中,因此从理论上讲,只需要像 git-annex 这样的东西挂钩在那个层面,而不是更高层次(即如果您喜欢,具有按需下载的概念远程blob)。不幸的是,我找不到任何人已经实现或甚至建议过类似这样的东西。

据我所知,您正在询问如何在不重写历史的情况下重写历史。 - alternative
@alternative 不完全是这样,我想问的是是否有一种方法可以在不重写历史记录的情况下精简存储库。目前看来,使用浅克隆可能是唯一的方法,但限制可能与我们的工作流程不太匹配,即使它能够正常工作,那么它们也只会精简本地(克隆)存储库,而不是远程裸存储库。 - Mark Booth
唯一“瘦身”存储库的方法是删除您要瘦身的内容,因此需要重写(这就是为什么每个答案都说这是不可能的原因)。只要正确操作,重写历史实际上并没有任何问题。是的,浅克隆只会影响本地存储库。 - alternative
如果你在一个小团队中工作,并且只有几个外部协作者(在github上的forks),那么重写历史并不是什么大问题。但是,如果你有数十名开发人员、协作者甚至更多的克隆版本,那么强制更新所有这些参考可能很快就会失控。 - Mark Booth
4个回答

13
有点类似。您可以使用Git替换功能将庞大的历史记录分开,只有在需要时才下载。这就像是一个浅克隆,但没有浅克隆的限制。
这个想法是通过创建一个新的根提交来重新启动一个分支,然后挑选旧分支的尖端提交。通常情况下,这样会丢失所有的历史记录(这也意味着您不必克隆那些大型 .jar 文件),但如果需要历史记录,您可以获取历史提交并使用 git replace 将它们无缝地拼合在一起。
请参见Scott Chacon的博客文章以获取详细的解释和演示。
这种方法的优点:
  • 历史记录不会被修改。如果您需要回到旧提交,包括它的大型.jars文件和其他内容,您仍然可以这样做。
  • 如果您不需要查看旧的历史记录,则本地克隆的大小很小,任何新的克隆都不需要下载大量大多数无用的数据。

此方法的缺点:

  • The complete history is not available by default—users need to jump through some hoops to get at the history.
  • If you do need frequent access to the history, you'll end up downloading the bloated commits anyway.
  • This approach still has some of the same problems as rewriting history. For example, if your new repository looks like this:

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

    and someone has an old branch off of the historical branch that they merge in:

    * 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
                                 |
    

    then the big historical commits will reappear in your main repository and you're back to where you started. Note that this is no worse than rewriting history—someone might accidentally merge in the pre-rewrite commits.

    This can be mitigated by adding an update hook in your shared repository to reject any pushes that would reintroduce the historical root commit(s).


哇,谢谢Richard,这看起来可能正是我一直在寻找的东西。我下周会试着让它工作,如果成功了,你也会得到一个赞... - Mark Booth
啊,我明白了,所以这个例子重写了最近提交的历史记录,以删除大量的历史提交,而不需要重写那些历史提交的历史记录,但使用git replace可以让你在需要时重新带回历史提交。所以,这并不完全是我想要的,但我会再考虑一下如何利用它来解决我的问题。 - Mark Booth
我真希望在我们从旧的svn仓库创建git仓库时知道这个。我们不必在选择从svn开始一个新纪元或者从积累了多年的svn垃圾开始我们的git仓库之间做出选择,而是可以将整个svn仓库保留在一组历史git仓库中,然后使用git replace在需要时将它们带回来。事实上,我想知道我们是否仍然能够回去添加追溯的git replace目标。有趣,非常有趣... - Mark Booth
@MarkBooth:是的,你可以使用git replace附加旧历史记录。现在还不算太晚 ;)。 - Chronial
谢谢Richard,我今天向我的团队介绍了这个解决方案,我们决定在一个特别混乱的代码库中试用这种方法。现在我们只需要jgit/egit支持git replace,目前它还不支持。 - Mark Booth
1
@MarkBooth 你可以看一下 grafts - 它们非常相似,而且可能会得到支持,因为它们比较古老。但请注意,这种方法继承了历史重写方法的所有问题,所以只要你知道有大文件不应该在仓库中,最好将它们从历史记录中删除。 - Chronial

8
不,这是不可能的 - 您将不得不重写历史。但以下是一些提示:
请注意以下几点:
  • As VonC mentioned: If it fits your scenario, use BFG- repo cleaner – it’s a lot easier to use than git filter-branch.
  • You do not need to clone again! Just run these commands instead of git pull and you will be fine (replace origin and master with your remote and branch):

    git fetch origin
    git reset --hard origin/master
    

    But note that unlike git pull, you will loose all the local changes that are not pushed to the server yet.

  • It helps a lot if you (or somebody else in you team) fully understand how git sees history, and what git pull, git merge and git rebase (also as git rebase --onto) do. Then give everybody involved a quick training on how to handle this rewrite situation (5-10 mins should be enough, the basic dos and don’ts).
  • Be aware that git filter-branch does not cause any harm in itself, but causes a lot of standard workflows to cause harm. If people don’t act accordingly and merge old history, you might just have to rewrite history again if you don’t notice soon enough.
  • You can prevent people from merging (more precisely pushing) the old history by writing (5 lines) an appropriate update hook on the server. Just check whether the history of the pushed head contains a specific old commit.

感谢Chronial。重新克隆的唯一真正问题是必须在本地使用reset重置每个分支(以摆脱所有对过时分支的本地引用),并运行git gc --prune=now --aggressive来实际缩小存储库。如果您执行此操作,而存储库没有缩小,则说明您错过了某个引用。重新克隆消除了所有这些步骤的需要(我们使用buckminster部署我们的20多个git存储库,因此重新克隆所有内容对我们来说很容易)。不幸的是,我们还使用gitolite来托管我们的git存储库,它为自己的使用保留了update钩子。 - Mark Booth
我不熟悉 gitolite,但是 hooks and gitolite 上说 你可以安装除了这些之外的任何钩子:(所有仓库) gitolite 保留 update 钩子,所以我必须等待我们的 gitolite 专家回来告诉我是否有绕过此问题的方法。 - Mark Booth
2
@MarkBooth 在 gitolite V3 中,自定义更新钩子称为 VREF(例如在此答案中:https://dev59.com/mWbWa4cB1Zd3GeqPUC00#11517112),您可以定义尽可能多的“gitolite-update hook”(或 VREF):https://dev59.com/emgv5IYBdhLWcg3wCsc8#10888358。Gitolite V2 将使用挂钩链接(http://stackoverflow.com/a/15941289/6309)。 - VonC

5

我不知道有什么解决方案可以避免重写历史记录。

在这种情况下,使用像 BFG- repo cleaner 这样的工具清理存储库是最简单的解决方案(比 git filter-branch 更容易)。


2

我真的想不出有什么方法可以做到这一点。如果你考虑Git向用户"保证"数据完整性方面的内容,我无法想象你如何从仓库中删除一个文件并保持相同的哈希值。换句话说,如果你所询问的是可能的话,那么Git将变得不太可靠...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接