移动 git 历史记录中的文件

Moving a file in git history

在我的功能分支中,我有大约 50 个提交。在第一次提交中,我创建了一个文件,该文件在后续提交中进行了大量修改。我现在意识到将该文件存储在不同的目录中会更好,所以我想回到第一次提交并在正确的位置开始创建它,以保持历史记录干净。我可以通过编辑第一个提交并移动文件来使用交互式 rebase 来做到这一点,但是随后所有涉及该文件的后续提交都会产生冲突,我必须手动解决这些冲突。有没有办法告诉每次提交文件已被移动,以便他们自动将更改应用到正确的位置?

TL;DR

使用git filter-branch。您可以使用 --index-filter 来提高速度,但这更难使用;只有 50 次提交,使用 --tree-filter 速度慢得多但更容易使用:

git filter-branch --tree-filter <fill this in> --tag-name-filter cat -- --all

您通常应该在原始存储库的 copy(克隆)上执行此操作,因为它很容易弄乱过滤器分支并且是 的简单方法从那里恢复就是删除副本并重新开始。

一旦成功,请按照 the git filter-branch documentation 中所述删除所有 refs/original/ 名称。存储库最终会消肿(过滤器分支的大小会暂时增加一倍)。

Git 中的历史是(是?)提交。要更改历史记录,您需要将旧提交(提供旧历史记录)复制到新的不同提交(提供新历史记录)。因此,您的目标是将所有 50 次左右的提交替换为相同的新提交 除了 文件已重新定位到其他路径。

正如您所提到的,您可以使用交互式变基来做到这一点,但这很痛苦:变基通过将每个提交到副本转换为变更集来工作(通过比较该提交到它的父级,看看发生了什么变化),然后将相同的更改应用于一些现有的提交。

有一个更重的命令,git filter-branch,其目的是在应用某种提交修饰符的同时复制提交。它有很多选择,因为它天生就很慢;但从根本上说,它包括:

  • 列出要操作的每个提交(通过哈希 ID)。在您的情况下,这只是 "every commit"。此外,创建一个旧哈希 ID → 新哈希 ID 的空映射。
  • 然后,从最根(oldest/most-ancestral)开始提交:

    1. 将提交提取到临时工作区。
    2. 应用各种过滤器。
    3. 根据结果构建新的提交。使用散列映射来映射父 ID,以便新提交指向更早复制的新提交。这为命令提供了新提交的哈希 ID。
    4. 从旧提交哈希 → 新提交哈希向映射添加条目。
  • 最后,在对每个要过滤的提交执行上述操作后,循环遍历您告诉它更改的所有 references(主要是分支名称,但如果您使用 --tag-name-filter):

    ,标签名称也是如此
    1. 将原始引用从 refs/whatever 重命名为 refs/original/refs/whatever
    2. 使用映射中找到的新哈希创建一个新的 refs/whatever

在此过程结束时,您拥有所有原始提交(使用 refs/original 来引用它们)加上所有新提交(使用分支名称)。

如果你只有一个分支名称(没有标签),你唯一需要提供的名称就是这个分支名称,可能 master,但是 --all 会告诉 Git 查看所有引用,--tag-name-filter cat 会告诉 Git 它应该在更新标签名称时对标签名称所做的更改毕竟是不做任何更改。

--tree-filter 指示 git filter-branch,对于第 1 步(提取提交),它应该进行完整和完整的提取,到 git filter-branch 将在其上构建的临时目录自己的。 (其他过滤器选项试图通过更快的仅提取到临时索引的技巧来逃避。)您提供给 tree-filter 的命令或命令在这个临时目录中是 运行,所以如果所有你需要做的是重命名一个文件,命令:

mv old-relative-path new-relative-path

足够(假设 Unix/Linux-ish 系统)。