运行git filter-branch后如何删除旧的历史记录?

19
假设我有这样一棵树:
... -- a -- b -- c -- d -- ...
             \
              e -- a -- k

我希望你能够将其变为

... -- a -- b -- c -- d -- ...

我知道如何将分支名称附加到 "e"。我知道我要做的事情会改变历史记录,这是不好的。同时我猜我需要使用类似于rebase或filter-branch的东西。但具体来说 - 我迷失了。

好的。情况如下:现在我有一个相当大的树(像这样)

                 s -- p -- r   
                /
a -- b -- c -- d -- e --- g -- w
           \               \
            t -- p -- l     y -- k

在我最初的提交中(例如对于“b”),我添加了二进制文件,这使得整个代码库非常沉重。所以我决定将它们移除。我使用filter-branch命令来完成此操作。现在,我有两个长分支的提交记录,从第二个提交开始完全相同。

                 s -- p -- r   
                /
a -- b -- c -- d -- e --- g -- w
      \    \               \
       \    t -- p -- l     y -- k
        \
         \             s'-- p'-- r'  
          \           /
           b'-- c'-- d'-- e'--- g'-- w'
                 \               \
                  t'-- p'-- l'    y'-- k'

其中b'是没有二进制文件的提交。因此我无法进行合并。我不希望整个树在历史记录中重复出现。


你为什么要这样做呢?难道不能使用 git merge 吗? - Chris Frederick
@Christopher - 我已经在问题中添加了解释。 - Aleksandr Motsjonov
5个回答

40

在导入具有多年历史的Subversion存储库后,我遇到了与许多二进制资产相关的类似问题。 在git: shrinking Subversion import中,我描述了如何将我的git存储库从4.5 GiB缩减到大约100 MiB。

假设您想要从“Delete media files” (6fe87d)中删除的文件从所有提交中删除,并使其适应于您的repo,请参考我的博客文章中的方法:

$ git filter-branch -d /dev/shm/git --index-filter \
  "git rm --cached -f --ignore-unmatch media/Optika.1.3.?.*; \
   git rm --cached -f --ignore-unmatch media/lens.svg; \
   git rm --cached -f --ignore-unmatch media/lens_simulation.swf; \
   git rm --cached -f --ignore-unmatch media/v.html" \
  --tag-name-filter cat --prune-empty -- --all

您的GitHub repo没有任何标签,但我包括了一个标记名称过滤器,以防您有私有标签。

git filter-branch文档涵盖了--prune-empty选项。

--prune-empty
某些类型的过滤器会生成不会更改树的空提交。此选项允许git-filter-branch忽略这样的提交...

使用此选项意味着您重新编写的历史记录将不包含“Delete media files”提交,因为它不再影响树。在新历史记录中从未创建媒体文件。

此时,由于文档记录的行为,您的存储库中会出现重复。

如果原有的引用与改写后的不同,将会存储在命名空间refs/original/中。

如果您对新重写的历史记录满意,则可以删除备份副本。

$ git for-each-ref --format="%(refname)" refs/original/ | \
  xargs -n 1 git update-ref -d

Git非常注意保护您的工作,因此即使进行了所有这些有意的重写和删除,reflog仍然保留旧提交。使用以下两个命令序列清除它们:

$ git reflog expire --verbose --expire=0 --all
$ git gc --prune=0

现在您的本地存储库已准备好,但您需要将更新推送到GitHub。您可以逐个更新它们。例如,对于一个名为master的本地分支,您可以运行以下命令:

$ git push -f origin master

假设您不再拥有名为issue5的本地分支。您的克隆仍然有一个称为origin/issue5的引用,跟踪其在GitHub存储库中的位置。运行git filter-branch也会修改所有的origin引用,因此您可以更新GitHub而无需分支。

$ git push -f origin origin/issue5:issue5

如果所有本地分支与其在GitHub端的相应提交匹配(即没有未推送的提交),则可以执行批量更新。

$ git for-each-ref --format="%(refname)" refs/remotes/origin/ | \
  grep -v 'HEAD$' | perl -pe 's,^refs/remotes/origin/,,' | \
  xargs -n 1 -I '{}' git push -f origin 'refs/remotes/origin/{}:{}'

第一阶段的输出是一个引用名列表:

$ git for-each-ref --format="%(refname)" refs/remotes/origin/
refs/remotes/origin/HEAD
refs/remotes/origin/issue2
refs/remotes/origin/issue3
refs/remotes/origin/issue5
refs/remotes/origin/master

我们不想要 HEAD 伪参考,并使用 grep -v 去掉它。对于其余的内容,我们使用 Perl 来剥离 refs/remotes/origin/ 前缀,并针对每一个运行以下形式的命令:

$ git push -f origin refs/remotes/origin/BRANCH:BRANCH

其中 BRANCH 是指以下分支名之一:

  • section_merge
  • side-media-icons
  • side-pane-splitter
  • side-popup
  • v2

2
+1 很棒的文章。正是我正在寻找的(关于将工作中的大型代码库拆分为多个仓库,并使用它们作为子模块):) - ralphtheninja
顺便提一下,当我将本地的git repo“克隆”到另一个位置时,就没有这个附加的提交分支了。但是!当我使用强制推送到我的主远程库时,它会与原始提交重复。所以现在我又有了两个提交分支。 - Aleksandr Motsjonov
@Aleksandr 旧的提交是如何保留的?你的其他分支是否有包含这些大量提交的历史记录?在运行 git filter-branch 时是否使用了 --tag-name-filter 参数?你是否可以访问主机上的shell? - Greg Bacon
@Greg,当我使用--tag-name-filter-- --all(如链接所示)时,我得到了一个非常混乱的树形结构,有很多重复。但是,如果我不使用--tag-name-filterHEAD在末尾,则可以生成描述中显示的漂亮分割树。 - Aleksandr Motsjonov
顺便说一下,我没有ssh,这是在github上的一个小项目https://github.com/soswow/e-textbook/network,所以你可以看看它是什么。目前还没有修改过的版本被推送了。 - Aleksandr Motsjonov
@Aleksandr 几个提交添加了名为 media/lens.svgmedia/lens_simulation.swf 的文件。您想从所有提交中删除它们还是仅从 b 中删除? - Greg Bacon

1

你可以再次使用git filter-branch,但这次要加上--parent-filter选项。通过这个选项,你可以将提交与父级引用断开链接。我认为你也可以使用--commit-filter选项来达到同样的目的。这将在你的仓库中留下许多不同的松散对象,因此你需要运行git gc --prune=now命令。

以下是如何使用--parent-filter选项删除父级的示例 http://git.661346.n2.nabble.com/purging-unwanted-history-td1507638.html


我认为这里没有必要删除任何父级 - 我们想要删除多余的(重复的)子级,而不是父级。 - Paŭlo Ebermann
我应该如何准确地做到这一点?如果我可以将b的父对象设置为null,然后进行垃圾回收,我认为这是合乎逻辑的。 我尝试了一些变体,但还没有成功。 - Aleksandr Motsjonov
@Alex:我添加了一个链接到讨论此问题的帖子。正如您所看到的,他们正在使用sed将父级替换为空字符串。 - ralphtheninja
@Paũlo:我的意思是从提交到其父级的父级引用。如果历史记录是A-B-C-D-E...,并且您想要删除C-D-E,则应将C、D和E的父引用设置为无,以使提交悬空。 - ralphtheninja
如果提交是悬空的,它是否有父引用并不重要,而是是否有标签或分支指向其某个子节点。 - Paŭlo Ebermann

0
尝试使用以下命令:

git branch -d 名称

如果不行,可以尝试使用以下命令:

git branch -D 名称


它只会删除标签,提交记录将保留。 - Aleksandr Motsjonov
4
@Aleksandr,没有标签的提交会被 git gc 收集,该命令会定期自动运行,或者您也可以手动运行它。 - svick
然后,如果有对应的远程分支,你可以删除它们。 - Robin Green

0

您可以使用git branch -D 分支名称删除分支,并使用git push 远程名称 :分支名称删除远程分支。

提交将在您的存储库中保持未引用状态一段时间(请参见git gc doc),但只有在您后来意识到自己犯了错误时才会占用磁盘空间。

由于您已删除远程分支,因此新的git clone不应检索未引用的提交。


1
不,这仅删除引用,而不是提交。因为提交仍通过其子代引用。 - ralphtheninja
我猜他会删除他的模式中的所有子分支(rwkl)。因此,如果k从任何分支都无法访问,则git将无法访问y,因为它永远不会考虑k的父级。 - Michaël Witrant
编辑了您的帖子,这样我就可以取消我的-1 :) - ralphtheninja

-2

从你的例子来看,你可以尝试使用git rebase b b'


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