设置
假设这是您要删除的提交的历史记录
... o - o - o - o ... ... o
^ ^ ^ ^
| | +- next |
| +- bad +-- master (HEAD)
start
地点:
-
bad 是您要删除的提交;
-
start 是您要删除的提交的父级;
-
next 是bad 之后的下一个提交;很好,你想保留它和它之后的所有时间线;它将在 rebase 后替换 bad。
先决条件
为了能够安全地删除bad,重要的是在bad 创建时没有其他分支被合并到bad 之后的主时间线中。 IE。通过从历史图表中删除 bad 及其与其父提交和子提交的连接,您将获得两个断开连接的时间线片段。
即使在bad 之后合并了另一个现有分支,也可能删除bad。我没有检查这种情况,但由于合并提交,我预计会遇到一些障碍。
想法
每个git 提交都由使用提交属性计算的哈希标识:内容、消息、作者和提交者日期和电子邮件。
变基总是改变提交者的日期。它还可以更改提交者电子邮件、提交消息和内容。
为了在变基后恢复原始提交者日期,我们需要将它们与一些可以识别变基后的每次提交的信息一起保存。
因为要修改一个提交,所以在 rebase 期间提交内容会发生变化。添加或删除文件或提交会更改所有未来提交的内容。
这使我们没有唯一标识提交并且在所需的变基期间不会更改的属性。我们可以尝试使用两个或多个在 rebase 期间不会更改的属性。
电子邮件(作者和提交者)几乎没有用。如果有一个人在该项目上工作,则它们对于所有提交都是相同的,并且不能使用。剩下的属性(在大多数提交上不同,不受变基的影响)是 author date 和 commit message(第一行)。
如果这对(作者日期,提交消息)为受 rebase 影响的所有提交提供唯一值,那么我们可以在之后恢复提交日期而不会出错。
验证是否可以安全完成
有一种简单的方法可以验证(作者日期、提交消息)对对于受影响的提交是否是唯一的。
运行以下两条命令:
$ git log --format="%aI %s" start...master | uniq | wc -l
$ git log --oneline start...master | wc -l
如果它们显示相同的数字,那么你很幸运:这对(作者日期,提交消息)可用于唯一标识提交。继续阅读。
如果数字不同(第一个命令生成的数字总是小于或等于第二个命令生成的数字),那么你就不走运了。
提取在变基后修复提交日期所需的信息
这个命令
$ git log --format="%H %cI %aI %s" start...master > /tmp/hashlist
提取所有以start 开头的提交的提交哈希、提交者日期(有效负载)、作者日期和提交消息(密钥),并将它们存储在一个文件中。
备份当前master
虽然git“重写历史”是一个常见的误解,但实际上它只是生成了一条替代历史线并确定它是正确的历史。它不会更改或删除“重写”的提交;它们在其数据库中仍然存在一段时间,并且可以在操作失败时恢复。
我们可以主动备份当前的历史记录行,以便在需要时轻松恢复。我们所要做的就是创建一个指向master 的新分支。这样,当git rebase 将master 移动到新时间线时,仍然可以使用新分支访问旧时间线。
$ git branch old_master
上面的命令创建了一个名为 old_master 的分支,它保持当前时间线为焦点,直到我们完成所有更改并对新的世界秩序感到满意。
做rebase
从历史记录中删除提交 bad 很简单:
$ git rebase --preserve-merges --onto start bad
修正提交日期
以下命令“重写”历史记录并使用我们之前保存的值更改提交者日期:
$ git filter-branch --env-filter 'export GIT_COMMITTER_DATE=$(fgrep -m 1 "$(git log -1 --format="%aI %s" $GIT_COMMIT)" /tmp/hashlist | cut -d" " -f2)' -f start...master
工作原理:
git 遍历标记为start 和master 的提交之间的历史记录,并且对于每个提交,它在重写提交之前运行作为--env-filter 的参数提供的命令。它使用被重写的提交的哈希设置环境变量GIT_COMMIT。
由于我们已经做了一个rebase 修改了所有提交的哈希值,我们不能直接使用$GIT_COMMIT 来识别提交的原始提交日期(因为$GIT_COMMIT 是由git rebase 生成的提交,我们对他们的提交者日期不感兴趣)。
我们提供给--env-filter的命令
export GIT_COMMITTER_DATE=$(fgrep -m 1 "$(git log -1 --format="%aI %s" $GIT_COMMIT)" /tmp/hashlist | cut -d" " -f2)
运行git log -1 --format="%aI %s" $GIT_COMMIT 以生成上面讨论的密钥对(作者日期、提交消息)。它的输出作为参数传递给命令fgrep -m 1 "..." /tmp/hashlist | cut -d" " -f2,该命令在先前保存的哈希列表(fgrep)中找到该对,并从保存的行(cut)中提取原始提交日期。最后,提交日期的值存储在环境变量GIT_COMMITTER_DATE 中,git 使用该变量来重写提交。
验证
再次使用git log 命令
$ git log --format="%cI %aI %s" start...master
您可以验证重写的历史记录是否与原始历史记录匹配。如果您使用图形git 客户端,您可以通过目视检查更轻松地检查结果。分支old_master 保持旧的历史记录行在客户端可见,您可以轻松地将old_master 分支的每个提交日期与master 分支的对应日期进行比较。
如果某些事情进展不顺利,或者您需要修改程序,您可以通过以下方式轻松重新开始:
$ git reset --hard old_master
清理
当您对结果感到满意时,您可以删除备份分支和用于存储原始提交日期的文件:
$ git branch -D old_master
$ rm /tmp/hashlist
就是这样!