如何进行Git的变基操作并保留提交时间戳?

10
我想要进行一次变基以从历史记录中删除特定的提交。我知道如何做到这一点。但是如果我这样做,提交的时间戳将设置为我完成变基的时刻。我希望保持提交的时间戳。
我看到了这里的最后一个答案:https://dev59.com/v3A85IYBdhLWcg3wCe5Z#19522951,但它并没有起作用。
最后一个重要的命令只显示了一个新行。
>

所以我要开一个新的问题。

2
"没有起作用", "一个答案"...请提供答案链接并可能进行更深入的解释。 - Vogel612
@Vogel612,问题在于该问题是关于作者日期的。我不想改变的是提交日期。我所指的答案是唯一符合我的需求的答案。 - TheWatcher
你所描述的问题已经在这个答案中得到解决... 你是否尝试在某个沙盒存储库中测试过? - Vogel612
@Vogel612 可能是一个重复问题,但对我来说,使用作者日期作为提交日期不是一个解决方案,我想保留旧的提交日期。 - TheWatcher
“old commit dates”指的是“作者日期”,前提是您尚未通过rebase更改提交时间戳,因为您没有使用“--committer-date-is-author-date”。 - Vogel612
显示剩余10条评论
3个回答

19

设置

假设这是与您想要删除的提交相关的历史记录

... o - o - o - o ...       ... o
        ^   ^   ^               ^
        |   |   +- next         |
        |   +- bad              +-- master (HEAD)
      start

其中:

  • bad是你想要移除的提交;
  • start是你想要移除提交的父级;
  • nextbad之后的下一个提交;它是好的,你想要保留它和它之后的所有时间线;在rebase之后,它将替换bad

先决条件

为了能够安全地移除bad,重要的是,在创建bad时不存在其他分支已合并到主时间线之后。也就是说,通过从历史图中删除bad及其与其父级和子级提交的连接,您会得到两个不相关的时间线片段。

即使在bad之后合并了另一个现有分支,可能仍然可以删除bad。我没有检查过这种情况,但我预计由于合并提交而会遇到一些障碍。

思路

每个git提交都由一个哈希标识,该标识使用提交的属性(内容、消息、作者和提交者日期和电子邮件)计算。

重新贴标签始终会更改提交者日期。它还可以更改提交者电子邮件、提交消息和内容。

为了在rebase之后恢复原始的提交日期,我们需要将其与一些可以在rebase后标识每个提交的信息一起保存。

因为你想修改一个提交,所以提交内容在rebase期间会发生变化。添加或删除文件或提交会更改所有未来提交的内容。

这使我们没有一个唯一标识提交并在所需的rebase过程中不改变的属性。我们可以尝试使用两个或多个在rebase期间不发生变化的属性。

电子邮件(作者和提交者)几乎没有用处。如果有单个人在项目上工作,则它们对所有提交都是相同的,无法使用。留下的属性(在大多数提交上不同,在rebase上不受影响)是作者日期提交消息(第一行)。

如果对于受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开始的提交的提交哈希、提交者日期(有效负载)、作者日期和提交消息(关键字),并将它们存储在一个文件中。

备份当前主分支

尽管有一个常见的误解认为 git "重写历史",但实际上它只是生成了一条替代的历史线,并决定它是正确的历史。它不会更改或删除“重写”的提交;它们仍然存在于数据库中,并且在操作失败的情况下可以恢复。

我们可以主动备份当前的历史线,以便在需要时轻松恢复。我们所要做的就是创建一个指向 master 的新分支。这样,当 git rebasemaster 移动到新时间线时,旧的时间线仍然可以使用新分支访问。

$ git branch old_master

上述命令创建了一个名为old_master的分支,使当前时间轴保持关注,直到我们完成所有更改并对新世界秩序感到满意。

执行变基操作

从历史记录中删除提交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 遍历标记为 startmaster 的提交之间的历史记录,并在重写每个提交之前运行作为参数提供给 --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,该命令在之前保存哈希列表中查找并提取保存的行中的原始提交日期(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

就这些!


非常感谢!看起来很有前途 :) 今天会尝试一下。 - TheWatcher
缺少一个等号(=)。正确的命令是 git log --format="%aI %s" ...。已更新答案。 - axiac
嘿,所有的工作都完成了,但在最后一个真正的步骤中,它显示:(1/219)致命错误:无效的日期格式:%cI 无法编写重写提交。 - TheWatcher
确切的错误信息是什么,哪个命令生成了它?你之前的评论含糊不清:%cIgit log 一起使用,但 "could not write rewritten commit" 部分看起来像是由 git filter-branch 产生的错误(而该命令不使用 %cI)。 - axiac
1
最终,我用根目录下的绝对路径/c/.../commit-list替换了相对路径,然后它就可以工作了。 :p - Zarepheth
显示剩余5条评论

6

所以,以下是一种繁琐的方法(取决于需要变基的提交数量),但我已经尝试过它,它是有效的。当您进行交互式变基时,请使用“e”标记每个提交以便您可以编辑它。这将导致git在每个提交后暂停。在每个暂停时,您可以指定要使用的日期并继续下一个提交:

GIT_COMMITTER_DATE="Wed Feb 16 14:00 2011 +0100" git commit --amend   
git rebase --continue

或者如果你希望保留提交者和作者的日期相同:

GIT_COMMITTER_DATE="Wed Feb 16 14:00 2011 +0100" git commit --amend --date "Wed Feb 16 14:00 2011 +0100"
git rebase --continue

--amend后添加--no-edit,如果你不想打开编辑器来更改提交内容。当然,这是个比较棘手的问题,你必须提前知道所有提交日期,但如果没有其他方式,至少应该能够起作用。

嗯,我有大约300个提交记录... 所以手动完成这个任务应该非常困难,但也许我们可以自动化这个命令集? - TheWatcher
哇,300绝对太多了,除非你手头有很多时间。我唯一能建议的是看一下这个问题最后一个答案。 - David Deutsch
啊,抱歉。 - David Deutsch
也许你可以帮我解决这个问题,因为那个答案对我来说失败了。 - TheWatcher

2
真正的答案来自于 Reddit,这个地方。
git -c rebase.instructionFormat='%s%nexec GIT_COMMITTER_DATE="%cD" git commit --amend --no-edit' rebase -i

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