删除 git 历史记录中的前 x 次提交,并从其余历史记录中删除所有合并分支

delete first x commits in git history and remove all merge branches from the rest of the history

我有一个 git 项目历史记录,其中我有将近 400 次提交。我想删除第一个(最早的)200 个提交。然后在剩下的 200 次提交中,我只想删除所有合并提交并保持其余的顺序。

完成后我想检查所有剩余的提交并更改一个特定的作者电子邮件。

有没有办法优雅地做到这一点?

首先请三思,如果你真的想这样做。 (更改历史记录,尤其是在 public 存储库中,通常不是一个好主意。)

您可以使用 git rebase -i 这样做。在那里你可以使用 fixup 将两个提交合并为一个,你可以使用 edit 来更改提交。 (包括更改作者。)

对于多次提交的自动更改,您可以使用 git filter-branch。但只有当你知道自己在做什么时才使用它。

正如几个人已经说过的,这很少是一个好主意,原因有几个,我不再重复。不过,我想再添加一件事,然后展示如何使用 git filter-branch.

这不是删除,而是新的副本:本质上是一个新的 repo

了解这一点的关键是您不能从一系列提交的前面或中间删除提交。原因很简单:每个提交记录,作为其身份的一部分,其父提交的身份。这个的技术术语是提交图形成 Merkle Tree.

更具体地说,提交的身份——"true name",如果你愿意的话——是它的 SHA-1。 SHA-1 是提交中数据的加密 1 哈希。其中一条数据是 parent 行。这是 git 来源本身的实际提交(减去 @ 标志以阻止垃圾邮件收集):

tree 55c0d854767f92185f0399ec0b72062374f9ff12
parent 8413a79e67177d026d2d8e1ac66451b80bb25d62
author Junio C Hamano <gitster pobox.com> 1436563740 -0700
committer Junio C Hamano <gitster pobox.com> 1436563740 -0700

The last minute bits of fixes

Signed-off-by: Junio C Hamano <gitster pobox.com>

如果您尝试删除链中任何位置的父提交,您将获得一个新的、不同的子提交哈希值。这意味着所有 它的 子级也需要更改,以合并新的 SHA-1,所有链条中的所有内容。

这对您意味着要获得任何内容,包括 git filter-branch似乎 删除一些提交,您必须 copy 将每个 commit-to-keep 复制到具有新的、不同 ID 提交的新提交(具有相同的 treemessage 和以前一样,但有不同的 parent 行)。2

本质上,执行 git filter-branch 的结果是制作一个 存储库的新副本 ,其中至少有一些,也许是全部,新的和不同的提交在里面。这反过来意味着任何其他使用旧存储库的人都必须放弃他们的旧存储库并切换到新存储库。

git 过滤分支

虽然 git filter-branch 有很多选择,但其核心工作归结为这一点。对于每个提交:3

  • 展开提交的源代码树
  • 获取作者和提交者(姓名、电子邮件和时间戳)
  • 应用所有过滤器:
    • 对树进行任何必要的更改
    • 对作者和提交者进行任何必要的更改
    • 保留或跳过这个特定的提交:如果保留这个提交,从剩下的内容开始新的提交
  • 向映射文件添加一个条目,"original SHA-1"到"new SHA-1"

此处的要点列表是 "copy" 步骤,之后是最后一项任务 "update references"。要正确理解这部分,您需要知道 git 的引用是如何工作的,但简而言之,检查分支名称(如果您添加 --tag-filter,标记名称为 wee)以查看它们是否指向到一个被重写的旧提交。如果是这样,它们将更改为指向新副本,或者在提交被跳过的情况下指向最近的新副本提交,

为了实现你想要的,你需要编写一个提交过滤器,它使用 skip_commit 函数来省略你想要删除的提交(前 200 个和合并),并使用 git commit-tree剩下的。有关详细信息,请参阅 the git filter-branch documentation

git filter-branch 有这么多选项的一个原因是扩展和重新压缩整个源代码树非常慢。脚本试图避免这种情况,如果您的所有过滤器都可以在索引和提交图——无需扩展源代码树——过滤器完成得更快。)

基于新提交根的示例实现:

下面的代码将创建一个新的存储库,其中仅包含指定的新 STARTCOMMIT 下的所有提交。保留分支和标签。

export STARTCOMMIT=.....

git filter-branch --tag-name-filter cat \
   --commit-filter '
     git merge-base --is-ancestor ${STARTCOMMIT} ${GIT_COMMIT};
     if [ $? -eq 1 ]; 
     then
        skip_commit "$@";
     else
        git commit-tree "$@";
     fi' \
   -- --all

# remove original references
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
# reduce repo size
git reflog expire --expire=now --all && git gc --aggressive --prune=all

1"cryptographic" 形容词的含义是您不能简单地对提交进行微小的更改,例如,向消息添加文本,以生成与之前相同的旧 SHA-1。在计算上可行的时间内做到这一点的唯一方法是破解加密。

2在更改不太密集的情况下,如果您制作原始提交的精确副本,您最终会得到与之前相同的 SHA-1。例如,如果您有一个过滤器分支操作删除链中倒数第二个提交,则只有最尖端的提交会获得新的 SHA-1。不过,在这种特殊情况下,我们建议删除根提交,这必然会对每个后续提交重新编号。

3要复制的提交是从您作为过滤器分支操作的一部分提供的 gitrevisions 样式参数中获得的。要重写的分支名称也取自此处,使用 "positive references".