在 git 中,将子文件夹转换为子模块时,还保留移动到目标文件夹中的文件的历史记录

In git, while converting a subfolder into a submodule, also keep the history from files that were moved into the target folder

类似于在 https://gist.github.com/korya/9047870 中使用命令

定义的过程
git filter-branch --subdirectory-filter sub/module/path HEAD -- --all

在我的历史记录中,有些文件曾在其他文件夹中并被移动到该文件夹​​中,例如。

Created testfile.txt
Modified testfile.txt
Moved testfile.txt to /sub/module/path/testfile.txt
Modified /sub/module/path/testfile.txt

我希望该文件(以及 sub/module/path 中存在的任何其他文件)的历史记录存在于新的结果存储库中。

TL;DR

你得不到你想要的东西——至少,如果你不自己写一些至少 semi-fancy 工具的话。

您也许可以轻松获得所需的东西。你必须考虑你需要什么,并决定是否尝试编写一个 semi-fancy 工具。

Git 没有 文件历史记录 。 Git 有 次提交 ,并且提交 历史记录。 (例如,与 ClearCase 进行比较,它确实具有真实的文件历史记录,以及所有这一切。)

在Git中,每个提交都有一个前导列表或个提交,每个提交都持有所有[=270]的完整快照=] 文件。因此,在您的示例中,有四个提交——或至少四个 interesting。我在这里假设总共有五个提交,我们可以像这样绘制这些提交:

A <-B <-C <-D <-E   <--master

名称 master 包含最后一次提交的实际哈希 ID E。该提交包含文件。它还包含其父提交的原始哈希 ID D

为简单起见,假设所有提交都包含一个名为 README.md 的文件。提交 A 仅包含此 README.md,即,如果我们 git checkout 提交 A,我们将得到只有一个文件的 work-tree,README.md.

在提交 B 中,您 添加了 一个名为 testfile.txt 的文件。你这样做了:

... create the file ...
git add testfile.txt
git commit -m "second commit"

这使提交 B 指向现有提交 A。提交 B 现在包含两个文件,README.md——与提交 A 相同(实际上 re-used 在 Git 的内部存储格式中),以及 testfile.txt.

然后您修改了 testfile.txt 的 work-tree 副本,再次使用 git add,并 运行 git commit 创建提交 C。提交 C 现在指向提交 B; commit C 包含 README.md (仍未更改)和 testfile.txt.

的新版本

此时,你运行:

mkdir sub/module/path
git mv testfile.txt
git commit -m "fourth commit"

(或任何等效项)提交 D,它指向 C。提交 D 包含两个文件:README.md(仍未更改)和 sub/module/path/testfile.txt:一个名称很长且包含斜杠的文件。第二个文件的内容与提交C中的shorter-named文件的内容相同,但名称不同。

最后,您修改了名为 testfile 的 work-tree 文件和名为 sub/module/path 的 work-tree directory/folder,并在其上使用了 git add,并且运行 git commit 提交 EE 指向 D 并包含两个文件。

鉴于这段历史——这一系列的提交——你现在告诉 Git:

  • 使用名称 master 查找 last 提交。
  • 对于每个提交,查看 parent-child 对并查看它是否以某种方式修改了名为 sub/module/path/testfile.txt 的文件:

    • 如果是,打印子提交的名称(哈希 ID)、它的日志消息,也许还有对文件的更改类型。
    • 如果一种变化重命名,现在开始寻找旧的名字.
  • 无论如何,继续上一个提交,如果有的话。当您 运行 没有提交时停止。

(这是您的 git log --follow -- sub/module/path/testfile.txt 命令。)

您现在正在转换为子模块。子模块 一个 Git 存储库。

每个未来的子模块 git checkout 文件集将驻留在超级项目 work-tree 的 sub/module/path sub-directory 中,因此如果子模块包含一个包含名为 testfile.txt 的文件,该文件将出现在 sub/module/path/testfile.txt 中。如果子模块包含一个包含名为 sub/module/path/testfile.txt 的文件的提交,该文件将出现在 sub/module/path/sub/module/path/testfile.txt 中,这不是您想要的。

因此,您的工作是进行一系列 新存储库的提交。在这一系列提交中,该文件将被命名为 testfile.txt。这个新存储库可能会有 all-new 次提交:在这种情况下,这个新存储库中的 none 个哈希 ID 将匹配原始存储库中的任何哈希 ID。

您可以选择是否保留原始提交 B 中的部分或全部文件,如果是,如何处理在提交 B 中的文件您关心的是 testfile.txt 而不是 sub/module/path/testfile.txt。同样,您可以保留部分或全部原始文件不提交 C.

在任何情况下,您都将以一种更简单的方式保留部分提交 DE:只需丢弃所有不是 sub/module/path/ 的内容并删除 sub/module/path/ 文件名的一部分。

如果您保留部分或全部(来自的文件)提交Band/orCtestfile.txt 在两个保留的提交中必须命名为 testfile.txt 以便它落在正确的位置。 strip-leading-sub/module/path/ 技巧会自动为剩余的提交提供正确的名称。

t运行sformation 命令用于将原始提交系列复制到新的 s一系列提交 可以 git filter-branch 及其 --subdirectory-filter。但是子目录过滤器 cannot 为您保留提交 BC 的这些部分。本质上,git filter-branch 及其子目录过滤器并没有那么聪明。 filter-branch 为您做的是:

  • first 提交开始,然后朝 forwards 方向前进(这在 Git 中很少见,因为 Git 在这方面很糟糕:Git 强烈喜欢向后工作)。
  • 每次提交:

    • 应用一些过滤器;
    • 使用结果进行新的提交,或完全跳过提交;
    • 自动 link new 提交到 new 提交链,即替换为正确的 backwards-pointing link年龄.
  • 对导致所选 b运行ch 名称或所有 b运行ch 名称的所有提交重复,作为结束提交。

  • 最后,在每个 b运行ch 名称中存储最终的 filtered 提交哈希 ID。

如果您输入的一系列提交是:

A--B--C--G   <-- branch1
       \
        D--E--F   <-- branch2

并且您的过滤器保留 B(进行了一些更改)和所有后续提交(可能还进行了其他更改),最终结果是:

A--B--C--G   [abandoned]
       \
        D--E--F   [abandoned]

B'-C'-G'  <-- branch1
    \
     D'-E'-F'  <-- branch2

现在,按照 Git 通常的方式工作,从名称 branch1 开始,然后向后工作,我们看到 copied-and-filtered 提交 B'-C'-G'(以其他顺序),然后从 branch2 开始,我们看到 B'-C'-D'-E'-F'(以其他顺序)。所以 git filter-branch 现在已经完成了它的工作。如果我们将新的 commit-chains 和两个名称推送到一个新的存储库,我们将拥有一个根本不再有提交 A 的存储库。

(请注意,所有原始提交仍然存在。我们只是无法看到它们。如果我们再次克隆这个过滤后的克隆,它们就会全部退出并真正消失。或者,我们可以删除 filter-branch 留下的痕迹,以防您想撤消效果,并且 Git 最终会清除原始提交。)

filter-branch中的--subdirectory-filter的工作方式是丢弃所有不在所选子目录前缀中的文件,并重命名 剩余的文件去掉所选的前缀。如果丢弃这些文件的结果是 "no files at all" 或 "the same as the previously stripped-down commit",提交本身也会被丢弃。但这会丢弃不在子目录中的 testfile.txt 的副本。

通常,这就是 想要的 ,因为原始存储库仍然存在并且在 "pre-submodule" 提交中仍然有该文件。您没有更改这些提交;事实上,你不能改变任何提交,永远。这就是为什么 Git 正在做所有这些 copying: 它确实必须这样做的原因。我们能得到的最好结果是 new 提交形成一个 new 历史,我们(和 Git)通过从更新的名称开始并工作找到向后,就像 Git 那样。

但这不是你想要的。它可能就足够了——它可能是您真正需要的——在这种情况下,现有的子目录过滤器将为您工作。

filter-branch 确实有一个通用的 "arbitrary script" 选项

git filter-branch 支持以下两个内置过滤器:

  • --index-filter
  • --tree-filter

这两个都需要 command-line-style 命令到 运行。该命令可以使用您用任何语言编写的任何程序,或者只是一系列 shell 命令。这两者之间的主要区别在于如何它们运行你的命令——命令运行的环境。

(您可以改用新的 git filter-repo 命令,它写在 Python 中,与 filter-branch 做的事情相同,但让您执行 Python 函数。虽然我没有任何如何使用它的例子,而且它还没有内置到 Git 中:你必须单独安装它。)

索引过滤器快很多,但也更难编写。要了解如何使用它,首先了解树过滤器会有所帮助。

树形过滤器使用简单。 filter-branch 在 运行 执行树过滤器命令之前所做的是将整个快照提取到临时 directory-tree.

(这棵临时树不是您的 work-tree!不要期望它是您的 work-tree。它位于临时 sub-directory 中,隐藏在您意想不到的地方。什么都不假设,除了它有你所有的文件,而且只有你的文件,从那个提交中提取到文件夹中,但是你的 OS 需要。)

你的命令现在的工作是:对这些文件做任何你想做的事。您可以就地编辑它们、重命名它们、更改它们的权限以添加或删除 "executable" 标志(chmod 它们),等等。 您留在此树中的任何文件都将进入 filter-branch 将进行的替换提交。 因此您可以重命名文件和删除文件。例如,你可以检查顶层是否存在 testfile.txt,如果存在,将其保留在原位。您可以删除不在 sub/module/path 中的所有其他文件,然后将所有 sub/module/path 文件移动到顶层。这很可能是您在此处的新替换提交中想要的。

然后,完成所有这些后,您的命令应该以成功状态结束。如果您编写程序来完成这项工作,请使用 OS-level exit(0) 函数。如果这是一个 shell 脚本,例如 /tmp/shuffle-the-files.sh,让它以状态零退出。

tree-filter现在会自言自语:啊,命令成功;我现在从保留在隐藏临时目录中的一组文件中进行新提交。

filter-branch 代码将为要复制的链中的每个提交 重复此过程。这可能需要 long 时间:数小时或数天。但最终,你有了新的提交,通过复制原始提交,git filter-branch 更新 b运行ch 名称,如所述。

索引过滤器与树过滤器相同,但不是:

  • 将整个快照提取到临时区域
  • 运行任意命令
  • 把临时区变成新的快照

索引过滤器使用 Git 的索引。 Git 读取提交以复制到其索引中——就像它对常规 git checkout 一样,真的——速度非常快。然后它 运行 是你的命令。您的命令的工作是 就地更新索引 。您可以删除或重命名索引内的文件,然后退出零。 Git 然后根据索引中的任何内容进行新的替换提交,这非常快。所以索引过滤器一般比树过滤器快几百倍。

不幸的是,标准 Git 中索引 工具中唯一的 重命名文件是 git mv,它要求文件存在于 work-tree 同样,它不会。因此,要使用索引过滤器,您必须做一些花哨的 git update-index 工作,这可能意味着编写程序。如果你只有几百次提交,甚至几千次,你可能最好使用树过滤器,它更容易使用。

git filter-branch 的普遍缓慢和 difficulty-of-use 是它现在被淘汰以支持 git filter-repo 的原因。)