我正在编写一个 git filter-branch --tree-filter
命令,该命令使用 git log --follow
来检查是否应在执行期间保留或删除某些文件过滤。
基本上,我想保留包含文件名的提交,即使该文件被重命名和/或移动。
这是我正在运行的过滤器:
git filter-branch --prune-empty --tree-filter '~/preserve.sh' -- --all
这是我在 preserve.sh
中使用的命令:
git log --pretty=format:'%H' --name-only --follow --all -- "$f"
结果是,当我在新路径中搜索文件时,创建后来移动到另一个路径的文件的提交将从历史记录中删除,这是不应该发生的。例如:
commit 1: creates
foo/hello.txt
;commit 2: moves
foo/hello.txt
tobar/hello.txt
;using
git filter-branch
passingbar/hello.txt
yields a history with only commit 2.
一开始,我以为问题发生是因为我没有在git log
中使用--all
,即在分析commit 1<时/em> 它找不到 foo/hello.txt
因为它只在过去的历史记录中查找 bar/hello.txt
在任何地方都没有提到。但后来我添加了 --all
,它会查看所有提交(包括“ future ”的提交),但是没有任何变化。
我检查了正在创建文件的提交,运行了该日志命令并且它起作用了(列出了 foo/hello.txt
和 bar/hello.txt
),所以没有什么问题。我还记录了由 filter-branch 运行时 log 命令的结果,在这种情况下,我可以看到在 commit 1 中找不到文件(仅 bar/hello.txt
已列出)。
我认为发生这个问题是因为 git 在内部将每个提交复制到“新的存储库”结构,因此当它分析提交 1 时,较新的提交尚不存在。
有没有办法解决这个问题,或者有其他方法来解决重写历史记录同时保留重命名/移动的问题?
我正在运行 this answer 中找到的脚本的修改版本.
最佳答案
本质上,您想要在这里做的是:
- 构建存储库中所有提交的映射,并按哈希 ID 进行索引。
- 对于每次提交,确定您希望在运行过滤器时保留/使用的路径名。
- 运行
git filter-branch
- 或者,此时,只需运行您自己的代码,因为您在步骤 1 中构建的 map 以及您在步骤 2 中计算的内容是重要部分filter-branch 的作用——将旧提交复制到新提交。 - 如果您使用自己的代码,请为最后复制的提交创建或更新分支名称。
你可以git read-tree
将每次提交复制到一个索引中——可以使用主索引,也可以使用临时索引——然后使用Git工具修改索引,以便安排其中包含您希望保留的名称和哈希 ID。然后使用 git write-tree 和 git commit-tree 构建新的提交,就像 filter-branch 一样。
更简单的情况
如果您没有太多的文件替代名称,您也许可以稍微简化一下。例如,假设存储库中的历史记录(提交链)如下所示,有两个巨大的历史瓶颈 B1
和 B2
:
_______________________ ________________ _________
/ \ / \ / \--bra
< large cloud of commits >--B1--< cloud of commits >--B2--< ... >--nch
\_______________________/ \________________/ \_________/--es
您想要保留的文件名在三个大气泡中的任何一个内都相同,但在提交B2
时进行了大规模重命名,因此中间气泡中的名称不同,同样在 B1
处进行了大规模重命名,因此第一个气泡中的名称不同。
在这种情况下,您可以在任何过滤器(树过滤器、索引过滤器等任何您喜欢的过滤器(但索引过滤器比树过滤器快得多))中执行清晰的历史测试,以确定要保留的文件名。请记住,过滤器分支正在按拓扑顺序逐一复制提交,以便在创建任何新复制的子级之前创建新复制的父级。也就是说,它首先处理第一组的提交,然后复制瓶颈提交 B1
,然后处理第二组的提交,依此类推。
正在复制的提交的哈希 ID 可用于您的过滤器(无论您使用哪个过滤器):它是 $GIT_COMMIT
。所以你只需要测试:
$GIT_COMMIT
是B1
的祖先吗?如果是这样,您就属于第一组。$GIT_COMMIT
是B2
的祖先吗?如果是这样,您就属于第一组或第二组。
因此,由“保留名称集中的名称”组成的索引过滤器可以写为:
if git merge-base --is-ancestor $GIT_COMMIT <hash of B1>; then
set_of_names=/tmp/list1
elif git merge-base --is-ancestor $GIT_COMMIT <hash of B2>; then
set_of_names=/tmp/list2
else
set_of_names=/tmp/list3
fi
...
其中文件 /tmp/list1
、/tmp/list2
和 /tmp/list3
包含要保留的文件的名称。您现在只需编写 ...
代码来实现“在索引过滤操作期间保留固定的文件名集”。这实际上已经完成了,无论如何,在 this answer 中。至 extract multiple directories using git-filter-branch (正如您今天早些时候发现的那样)。
关于git log --all 在过滤器分支内不起作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59385247/