我正在编写一个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"
结果是,当我在新路径中搜索文件时,创建了一个后来被移至另一个路径的文件的提交已从历史记录中删除,这是不应该发生的。例如:
提交1:创建
foo/hello.txt
;提交2:将
foo/hello.txt
移至bar/hello.txt
;使用
git filter-branch
传递bar/hello.txt
会产生仅提交2的历史记录。
起初,我以为是因为我没有在--all
中使用git log
,也就是说,在分析 commit 1 时找不到{ {1}},因为它只是看过去的历史,而在任何地方都没有提到foo/hello.txt
。但是后来我添加了bar/hello.txt
,它可以查看所有提交(包括“未来”提交),但是没有任何变化。
我检出了要在其中创建文件的提交,运行了该日志命令并且它起作用了(列出了--all
和foo/hello.txt
),所以它没有任何问题。当它由filter-branch运行时,我还记录了log命令的结果,在这种情况下,我可以看到在 commit 1 中找不到该文件(仅列出bar/hello.txt
)。
我认为发生此问题是因为git在内部将每个提交复制到“新仓库”结构中,因此在分析 commit 1 时,尚不存在较新的提交。
有没有一种方法可以解决此问题,或者有另一种方法可以解决在保留重命名/移动的同时重写历史记录的问题?
我正在运行this answer中的脚本的修改版本。
答案 0 :(得分:1)
还是在保留重命名/移动的同时解决重写历史问题的另一种方法?
考虑自git filter-branch
is soon deprecated起使用新的newren/git-filter-repo
。
但是即使是新工具(基于git fast-export
/ git fast-import
)也不会跟随重命名的文件。
请参见newren/git-filter-repo
issue 25,它间接地说明了在考虑重命名文件的情况下(使用旧的git filter-branch
或新的filter-repo
命令过滤存储库的挑战)。
[...]这与
rev-list
,log
和fast-export
git子命令的工作方式一致。例如。git log -- src/ledger/bin/app/app.cc
不会显示该文件被重命名或复制的其他路径的任何历史记录(或文件的部分来自何处)。
您专门使用了--follow
标志,这是一个巨大的漏洞,甚至在git log
文档中也提到了(它提到仅在指定单个文件时有效)。 br /> 如果rev-list
/log
/fast-export
等在重命名之后有一个--follow
选项,那么我可以简单地从filter-repo
公开它,但是尽管有这种选择多年来没有人实施。
那里也有一些很好的挑战,例如我们可能需要遍历拓扑顺序,并且可能需要两次通过-一次创建拓扑顺序,第二次通过重命名建立其他路径。 (在这种情况下可能是必要的:某些分支建立在'master
'的顶部,并且在指定的pathspec中具有一些路径,这些路径来自在存在'master'时对pathspec以外的内容进行重命名。如果'主人”在另一个分支之前被遍历,那么我们本来已经选择了更为有限的pathspec并错过了多余的所需路径。)但是,即使
--follow
在多个文件或目录或多个目录的重命名之后实现了,这也不一定足够,因为用户可能需要复制检测(即,不是从某处重命名的文件)否则,它会被复制)。
但是,通过复制检测并不清楚是否要获取原件的完整历史记录。我可以想象在某些情况下您会,但在其他情况下不会。如果我们开始进行重命名或复制检测,那么我们将从定义明确的正确行为转向启发式。
对于差异,日志或什至合并,这是很好的,因为结果将由用户解释(即使在合并中,如果检测错误,用户也可以修正冲突并进行其他编辑)。
在这里,我们用石头记录了启发式方法的结果。这让我有些担心……这还意味着我们必须打开一堆旋钮(至少要有一个相似性百分比,以及是否需要重命名以外的其他副本)才能进行配置。所有这些,我在使用时也想要类似的东西。
我想到的最好的折衷方案是让人们事先运行“git filter-repo --analyze
”,查看“重命名”子报表,并根据此手动选择其他路径以馈入他们的filter-repo运行。
--analyze
选项仍然具有few caveats with the rename detection,但这基本上是问题的根本。提供它并让用户决定要包含的内容(尽管我什至不担心复制检测),这似乎是我所能得到的最好选择。
答案 1 :(得分:1)
基本上,您想在这里做的是:
git filter-branch
-或此时,只需运行您自己的代码,因为在步骤1中构建的地图以及在步骤2中计算的内容是filter-branch的重要组成部分—将旧的提交复制到新的提交。您可以git read-tree
将每个提交复制到索引中(可以使用主索引或临时索引),然后使用Git工具修改索引,以便在其中排列名称和哈希您希望保留的ID。然后像过滤器分支一样使用git write-tree
和git commit-tree
构建新的提交。
如果文件的替代名称太多,您 可能可以对此进行一些简化。例如,假设存储库中的历史记录(提交链)看起来像这样,有两个巨大的历史记录瓶颈B1
和B2
:
_______________________ ________________ _________
/ \ / \ / \--bra
< large cloud of commits >--B1--< cloud of commits >--B2--< ... >--nch
\_______________________/ \________________/ \_________/--es
您要保留的文件名在三个大气泡中的任何一个内都相同,但是在提交B2
时会进行大量重命名,因此名称不同在中间的气泡中,同样在B1
处有一个重命名,因此第一个气泡中的名称不同。
在这种情况下,您可以在任何过滤器(树过滤器,索引过滤器)中进行任意操作(但索引过滤器比树过滤器快得多),从而可以确定要保留哪些文件名,这是一个清晰的历史测试。请记住,filter-branch是按拓扑顺序逐一复制提交,以便在必须创建任何新复制的子代之前创建新复制的父代。也就是说,它首先处理来自第一组的提交,然后复制瓶颈提交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中完成了(如您今天早些时候所发现的那样。)