如何在Git历史记录中删除给定日期之前的所有提交?

39
给定一个代码库,我想删除在某个特定提交或历史日期之前的所有提交。
我的代码库中有大约10000个提交,我只想保留最后1000个提交,并删除其余部分。基本上,我想要做的是说“将第一个提交向前移动到X”。
起初,我以为我可以重装和合并所有这些提交成为一个提交,但这会在重装过程中导致很多合并冲突。如果有一种方法可以压缩提交,使得压缩后的版本成为最后一个提交,那也可以。

2
你能提供更多信息吗?因为如果你这样做,你将会删除你的仓库中所有最初的提交记录!这是不可能的。你真正想做什么?也许是在你的仓库中创建一个新的根,并将你想要的提交记录重新基于这个提交记录。 - Philippe
更新了更多信息。基本上,我只想删除大部分历史记录,同时保留最新的提交。 - Jakub Arnold
1
换句话说,我想将数千个提交压缩成一个,忽略合并冲突,并仅保留范围内最后一次提交时的存储库状态。 - Jakub Arnold
@JakubArnold 这就像是砍断你坐在上面的树枝,不好。你可以使用git filter-branch来完成,就像Brian的回答中所示,但要小心谨慎。 - jub0bs
@Jubobs 是的,这是一件需要小心谨慎对待的事情,不应该轻率行事。然而,有时候这是必要的;例如一个专有产品变成开源,你需要删除其他人拥有版权的代码,或者你没有意识到不应该将大型二进制文件提交到 git 中,并且在早期做了很多这样的操作,现在需要清理它,以便你不会永远携带这些历史记录,或者类似的情况。虽然有一些合理的理由去这么做,但通常最好尽量避免陷入这些情况。 - Brian Campbell
4个回答

27

警告:以下操作非常危险,会重写历史记录。在进行此类重大历史重写之前,请务必确保备份您的存储库。

请将下面的哈希替换为您想要作为新的第一次提交的父提交的哈希。

git filter-branch --parent-filter '
    read parent
    if [ "$parent" = "-p 5bdd44e5919cb0a95a9924817529cd7c980f88b5" ]
    then
        echo
    else
        echo "$parent"
    fi'

这将重写每个提交的父级;对于大多数提交,它们保持不变,但是与给定哈希值匹配的父级,将替换为空父级,这意味着它现在将成为一个没有父级的提交。这将使您的旧历史记录全部脱离。

请注意,如果您想使某个合并提交成为您的第一个提交,则需要针对合并提交的每个父级以正确的顺序匹配类似于-p parent1 -p parent2 -p parent3的内容。

如果您想将此应用于所有分支和标签而不仅仅是当前分支,请在命令(脚本)的结尾处传递--all

完成此操作并检查其是否正常工作后,您可以删除原始分支并运行gc以清除未引用的提交:

git update-ref -d refs/original/refs/heads/master

请注意,由于git倾向于尽可能保留数据,因此为了实际释放空间,您还需要从reflog中删除提交,并运行gc进行清理。

git reflog expire --expire-unreachable=all --all
git gc --prune=all

如果你不是为了节省空间或清除旧的提交历史,可以在一个分支中保留旧的历史记录,例如 git branch old-master refs/original/refs/heads/master; 你甚至可以使用 git replace "虚拟重新连接"它,此时你将有两个不相关的历史记录(因此当你推送到远程仓库时,只会推送截断的历史记录),但当你在本地仓库中查看历史记录时,你将看到完整的历史记录。


我的提交次数减少了,但是在应用你的解决方案后我仍然可以在 Github 上看到我的提交历史。有什么想法吗? - Ferit
我尝试了这个,首先使用 cp -a 复制了仓库,因为我想要所有的分支,然后当我运行这个命令时,根据 git log 的记录,我想要作为第一个提交的那个现在消失了,但是之前的所有提交都还在,这正是我想要删除的。在你能得到更好的答案之前,我不会使用它。 - blamb
嗨,Brian。这似乎是一个相当优雅的解决方案。谢谢你。我愚蠢地克隆了一个我不需要所有提交的存储库,并忘记了深度参数。这个过程虽然有点耗时,但似乎已经奏效了。 - stubsthewizard
嗨,布莱恩。虽然这个方法确实解决了我具体提出的问题,但我认为这并不是正确的问题。我的代码库现在已经达到了1.6GB,而在执行这个过程之前它只有1.1GB。我认为更好的问题是如何减小代码库的大小,因为我只需要关于4次提交的数据。 - stubsthewizard

10

对我来说最简单的方法是使用git replace(编辑:已成功测试!)。

首先将您想要压缩的所有提交合并为一个: (我们将称最后一个要压缩的提交的 sha 为 "last_commit_sha", 第一个提交的 sha 为 "root_commit_sha")

git checkout -b big_squash <LastSha>
git reset --soft <RootSha>
git commit --amend -m "My new root"

现在,您必须将分支big_squash指向一个新的根(这里称为<NewRootSha>)。我们只对sha1感兴趣,一旦您成功完成操作,该分支最终可以被删除。

然后您有两种选择:

  • 如果很容易做到,可以使用git rebase --onto命令重叠后续提交记录(这是Git书中推荐的解决方案,但在测试了其他方案后,这不是我的首选;))
  • 使用git replace隐藏旧的历史记录(历史记录仍然在存储库中!但我们将使用git filter-branch使其永久化)

要用新创建的提交替换要合并的最后一个提交:

git replace <RootSha> <NewRootSha>

现在,你可以在 git replace 后进行 git filter-branch 操作以使其变为永久性的!

在替换后,执行以下操作:

git filter-branch master, <put here the name of all your branches>
如果您满意结果,那么请删除文件夹.git/refs/original(其中包含所有git filter-branch之前保存的引用)和文件夹.git/refs/replace(其中包含您不再需要的替换引用)。此解决方案的优点是简单和可逆的(除了您删除文件夹后的最后一步操作 ;))。 完成了! 这里可以找到文档:

1
你对big_squash分支做什么?在当前的解决方案中,你的主分支仍将保持不变。 - volpato
无法工作...分支big_squash最终只有一个提交“我的新根”。即使运行git filter-branch master - Vinay W
@vinaywadwa 你肯定忘记在之前执行 git replace 命令了。 - Philippe
@volpato 这是replace的目标,它将”隐藏”所有需要合并的提交,而filter-branch将重新构建所有分支的历史记录并更新SHA1。 “master”将被修改,我可以向您保证! - Philippe
针对接下来要使用该代码的人:我已经纠正了上面的代码,现在应该可以正常工作了。@Philippe,请更新如何还原 .git/refs/original 目录下的更改的说明,然后再删除它。我认为这是最好的解决方案,感谢您的帮助! - dmvianna

4

2

您想要的东西并不完全符合实际情况,因为您无法从存储库中删除任何内容,只能向其中添加新内容。

简单来说,通过提交图形绘制,您现在拥有的是:

<jumble of commits> - K - L - M - etc ...  <-- master
                        \      / (merges)  <-- etc
                        (branches)

你想要的是什么(同样简化):

K - L - M - etc ...  <-- master
 \      / (merges)  <-- etc
 (branches)

因此K现在成为了根提交。

您无法得到那个,但是您可以获得一个几乎完全相同K的新根提交,只有两个重要的差异:不同的SHA-1和没有父提交ID。该提交将与提交K具有相同的树和所有相同的文件。

K复制到K'后,您可以将L复制到L'等等,以便您得到一个具有相同形状和相同文件等的新提交图,只是所有新的SHA-1 ID。

执行此操作的git命令是filter-branch

使用filter-branch至少有两种方法可以实现此目标。一种方法是使用提交筛选器:

  • 跳过所有提交,直到出现提交K,然后
  • 复制所有提交(包括K本身)

(然后添加通常的--tag-name-filter cat等)。这种方法稍微有些麻烦,因为提交筛选器不会被eval评估,所以您必须在外部“记住”跳过/保留状态(例如,在文件中)。

另一种方法是使用--parent-filter,如Brian Campbell所述

它们之间的区别在于--parent-filter方法更容易,但也复制了所有“pre-K”提交,因此您会在副本中获得两个独立的图形。您可能需要这样做,也可能不需要;并且如果在清除refs/original名称空间后没有对“pre-K'”提交的引用,则它们将像往常一样被垃圾收集器回收,使差异消失。


使用git filter-branch的任何方法都会通过refs/original/...备份分支保留旧提交记录。由于我编写的--parent-filter仅涉及一个提交记录,因此对于早于该点的所有提交记录,它将是无操作的,因此它们将与您在refs/original备份分支中保留的完全相同。 - Brian Campbell
@BrianCampbell:是的,没错;我主要是在考虑删除了refs/original备份之后会发生什么。如果你省略了“copies”(正如你所指出的那样,它们实际上只是重用原始对象),filter-branch也会执行“remap to ancestor”的操作。假设一些早期提交(比如E)有一个分支或标签指向它,如果你“复制”,它仍然会指向E。如果E及更早的提交都不存在了,我其实不确定remap-to-ancestor会怎么做... - torek

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