在 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
提交 E
。 E
指向 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
.
在任何情况下,您都将以一种更简单的方式保留部分提交 D
和 E
:只需丢弃所有不是 sub/module/path/
的内容并删除 sub/module/path/
文件名的一部分。
如果您做保留部分或全部(来自的文件)提交B
and/orC
,testfile.txt
在两个保留的提交中必须命名为 testfile.txt
以便它落在正确的位置。 strip-leading-sub/module/path/
技巧会自动为剩余的提交提供正确的名称。
t运行sformation 命令用于将原始提交系列复制到新的 s一系列提交 可以 是 git filter-branch
及其 --subdirectory-filter
。但是子目录过滤器 cannot 为您保留提交 B
和 C
的这些部分。本质上,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
的原因。)
类似于在 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
提交 E
。 E
指向 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
.
在任何情况下,您都将以一种更简单的方式保留部分提交 D
和 E
:只需丢弃所有不是 sub/module/path/
的内容并删除 sub/module/path/
文件名的一部分。
如果您做保留部分或全部(来自的文件)提交B
and/orC
,testfile.txt
在两个保留的提交中必须命名为 testfile.txt
以便它落在正确的位置。 strip-leading-sub/module/path/
技巧会自动为剩余的提交提供正确的名称。
t运行sformation 命令用于将原始提交系列复制到新的 s一系列提交 可以 是 git filter-branch
及其 --subdirectory-filter
。但是子目录过滤器 cannot 为您保留提交 B
和 C
的这些部分。本质上,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
的原因。)