警告:此 git clean 行为将随着 Git 2.14(2017 年第三季度)略有改变
"git clean -d" 用于清理包含被忽略文件的目录,即使该命令不应丢失没有"-x" 的被忽略文件。
“git status --ignored”没有列出没有“-uall”的被忽略和未跟踪的文件。
请参阅commit 6b1db43(2017 年 5 月 23 日)和commit bbf504a、commit fb89888、commit df5bcdf、commit 0a81d4a、commit b3487cc(2017 年 5 月 18 日)Samuel Lijin (sxlijin)。
(由Junio C Hamano -- gitster -- 在commit f4fd99b 中合并,2017 年 6 月 2 日)
clean:教导 clean -d 保留被忽略的路径
有一个隐含的假设,即仅包含未跟踪和忽略的路径的目录本身应被视为未跟踪。这在我们询问是否应将目录添加到 git 数据库的用例中是有意义的,但在我们询问是否可以从工作树中安全地删除目录时则不然;因此,clean -d 会假设可以删除包含被忽略路径的“未跟踪”目录,即使这样做也会删除被忽略的路径。
为了解决这个问题,我们教 clean -d 收集被忽略的路径并在未跟踪的目录包含被忽略的路径时跳过它,而不是只删除其中未跟踪的内容。
为了实现这一点,cmd_clean() 必须收集未跟踪目录的所有未跟踪内容,以及所有忽略的路径,以确定必须跳过哪些未跟踪的目录(因为它们包含忽略的路径)以及哪些应该不 被跳过。
但是...自 2017 年以来,这种变化意味着 git status --ignored hangs indefinitely!
由Martin Melka在this thread报告,并由SZEDER Gábor分析:
深度为 120 个目录,需要 6*10^23 年才能完成
这种减速是由commit df5bcdf 引起的,它是
补丁系列修复'git clean -d'甚至删除未跟踪的目录
如果它们包含被忽略的文件。
所以...正在进行修复,将于 2020 年晚些时候发布。
Git 2.24(2019 年第四季度)说明了这种 git clean 行为变化引入了回归。
参见SZEDER Gábor (szeder)@commit 502c386(2019 年 8 月 25 日)。
(由 Junio C Hamano -- gitster -- 合并于 commit 026428c,2019 年 9 月 30 日)
t7300-clean:演示删除嵌套 repo 并忽略文件损坏
'git clean -fd' 不得删除未跟踪的目录,如果它属于
到不同的 Git 存储库或工作树。
不幸的是,如果外部存储库中的“.gitignore”规则恰好与嵌套存储库或工作树中的文件匹配,那么就会出现问题,并且“git clean -fd”确实会删除嵌套存储库的工作树的内容
除了那个被忽略的文件,可能会导致数据丢失。
添加a test to 't7300-clean.sh' 以演示此损坏。
这个问题是6b1db43中引入的回归(clean:teach
clean -d 保留忽略的路径,2017-05-23,Git v2.13.2)。
Git 2.24 进一步澄清git clean -d:
见commit 69f272b(2019 年 10 月 1 日)和commit 902b90c、commit ca8b539、commit 09487f2、commit e86bbcf、commit 3aca580、commit 29b577b、commit 89a1f4a、commit a3d89d8、@987@ commit a5e916c、commit bbbb6b0、commit 7541cc5(2019 年 9 月 17 日)Elijah Newren (newren)。
(由 Junio C Hamano -- gitster -- 合并于 commit aafb754,2019 年 10 月 11 日)
t7300:添加测试用例显示无法清理指定的路径规范
有人给我带来了一个测试用例,其中有多个 git-clean 调用
需要清除不需要的文件:
mkdir d{1,2}
touch d{1,2}/ut
touch d1/t && git add d1/t
使用此设置,用户需要运行
git clean -ffd */ut
两次删除ut 文件。
小测试显示了一些有趣的变体:
- 如果这两个 ut 文件中只有一个存在(任何一个),那么只需要一个 clean 命令。
- 如果两个目录都有跟踪文件,那么只需要一个 git clean 来清理这两个文件。
- 如果两个目录都没有跟踪的文件,那么上面的 clean 命令将永远不会清除任何一个未跟踪的文件,尽管 pathspec 明确地调用了它们。
一等分显示无法清除以commit cf424f5 开头的文件(“clean:尊重路径规范与“-d”,2014-03-10,Git v1.9.1)。
但是,这指出了一个单独的问题:虽然“-d”标志是由向我展示此问题的原始用户使用的,但该标志应该与此问题无关。
在没有“-d”标志的情况下再次测试表明,在不使用该标志的情况下存在相同的错误行为,实际上早在cf424f5 之前就存在。
所以:
clean:使用“-d”尊重路径规范
git-clean 使用 read_directory 填充 struct dir 潜在命中。但是,read_directory 实际上并没有检查我们的路径规范。它使用可能会出现误报的简化版本。因此,我们需要
检查任何命中是否符合我们的路径规范。
我们为非目录可靠地执行此操作。
对于目录,如果没有给出“-d”,我们会检查路径规范是否完全匹配(即,我们更加严格,需要显式的“git clean foo”来清理“foo/”)。但是如果给出了“-d”,而不是放宽精确匹配以允许递归匹配,我们根本不检查路径规范。
此回归是在 113f10f 中引入的(将 git-clean 设为内置,2007-11-11,Git v1.5.4-rc0)。
dir: 如果我们的 pathspec 可能匹配目录下的文件,递归到它
对于git clean,如果一个目录完全未被跟踪并且用户没有指定-d(对应于DIR_SHOW_IGNORED_TOO),那么我们通常不想删除该目录,因此不会递归到它。
但是,如果用户在该目录下的某处手动指定要删除的特定(甚至是全局)路径,那么我们需要递归到该目录,以确保我们按照用户请求删除该目录下的相关路径。
请注意,这并不意味着recursed-into目录将被添加到dir->entries以供以后删除;在本系列前面的一些提交中,在从递归进入目录返回之后运行另一个更严格的匹配检查,然后再决定将其添加到条目列表中。
因此,这只会导致给定目录下与路径规范之一匹配的文件添加到条目列表中。
还有:
dir: 还要检查目录是否匹配路径规范
即使目录与路径规范不匹配,根据精确的路径规范,也有可能在它下面的某些文件。
因此,我们针对这种情况进行特殊情况并递归到目录中。
但是,我们之前总是将递归到的任何未跟踪目录添加到未跟踪路径列表中,无论该目录本身是否与路径规范匹配。
对于git-clean 和一组“dir/file”和“more”的路径规范,
这导致了一个问题,因为我们最终会得到以下两个的 dir 条目:
"dir"
"dir/file"
然后correct_untracked_entries() 将尝试通过删除“dir/file”来帮助我们修剪重复项,因为它位于“dir”下,让我们留下
"dir"
由于原始路径规范只有“dir/file”,因此剩下的唯一条目不匹配并且没有任何内容可以删除。
(请注意,如果只指定了一个路径规范,例如只指定了“dir/file”,那么fill_directory 中的common_prefix_len 优化将导致我们绕过这个问题,使其出现在我们可以正确删除手动指定的路径规范的简单测试中.)
通过实际检查我们要添加到目录条目列表中的目录是否实际匹配路径规范来解决此问题;只有在我们已经从递归到目录中返回后才进行匹配检查。
结果:
clean:消除-d的定义歧义
-d 标志早于 git-clean 指定路径的能力。
因此,git-clean 的默认设置是仅删除
当前目录,并且 -d 存在以允许它递归到
子目录。
路径和-d 选项的交互似乎没有被仔细考虑,许多错误和缺乏测试套件中涵盖此类配对的测试证明了这一点。
事实证明,这个定义很重要,所以让我们看一下可以解释-d 选项的一些不同方式:
A) 如果没有-d,则只查看其下包含跟踪文件的子目录;使用-d,还可以在未跟踪的子目录中查找要清理的文件。
B) 如果没有用户指定的路径供我们删除,我们需要有某种默认值,所以...没有-d,只查看其下包含跟踪文件的子目录;使用-d,还可以在未跟踪的子目录中查找要清理的文件。
这里的重要区别是选项 B 表示如果指定了路径,则“-d”的存在与否是无关紧要的。
选项 B 背后的逻辑是,如果用户明确要求我们清理
指定的路径规范,那么我们应该清理任何匹配的东西
路径规范。
一些例子可能会澄清。
应该:
git clean -f untracked_dir/file
是否删除 untracked_dir/file?
不这样做似乎很疯狂,但对选项 A 的严格阅读表明它不应该被删除。
怎么样:
git clean -f untracked_dir/file1 tracked_dir/file2
或
git clean -f untracked_dir_1/file1 untracked_dir_2/file2
?
它应该删除这些文件中的一个还是两个?
是否需要多次运行才能删除列出的两个文件? (如果这听起来像是一个疯狂的问题,请参阅“t7300: Add some
显示未能清理指定路径规范的测试用例”在前面添加
这个补丁系列。)
如果使用-ffd 而不是-f 会怎样——应该允许删除这些吗?是否应该使用-ffd 多次调用?
如果使用 glob(例如 'tracked')而不是拼写目录名称会怎样?
如果文件名涉及 glob,例如
git clean -f '*.o'
或
git clean -f '*/*.o'
?
当前的文档实际上提出了一个定义,即
与选择 A 略有不同,以及在此之前的实现
系列提供了与选择 A 或 B 完全不同的东西。
(不过,实施显然只是错误的)。
可能还有其他选择。
但是,对于我能想到的几乎任何给定的-d 定义选择,上面的一些示例对用户来说都是错误的。
唯一没有负面意外的情况是选项 B:将用户指定的路径视为清除所有符合该路径规范的未跟踪文件的请求,包括递归到任何未跟踪的目录。
更改文档和基本实现以使用此定义。
有两个回归测试间接依赖于当前
实现,但都不是关于子目录处理的。
这两个测试是在提交 5b7570c ("git-clean: add tests for relative path", 2008-03-07, Git v1.5.5-rc0) 中引入的,它专门用于添加对提交 fb328947c8e ( “git-clean:正确的打印相对路径”,2008-03-07)。
两个测试都指定了一个目录,该目录恰好有一个未跟踪的子目录,但两者都只检查已删除文件的打印结果是否显示为相对路径。
适当更新这些测试。
最后,见“Git clean exclude nested sub directory”。
警告:目录遍历代码具有冗余递归调用,这使其性能特征相对于树的深度呈指数级增长,已在 Git 2.27(2020 年第二季度)中得到纠正。
这也会影响git clean。
见commit c0af173,commit 95c11ec,commit 7f45ab2,commit 1684644,commit 8d92fb2,commit 2df179d,commit 0126d14,commit cd129ee,commit cd129ee,commit cd129ee,commit 446f46d,commit 7260c7b,@097654373@,@04 2020)Elijah Newren (newren).
请参阅 Derrick Stolee (derrickstolee) 的 commit 0bbd0e8(2020 年 4 月 1 日)。
(由 Junio C Hamano -- gitster -- 合并于 commit 6eacc39,2020 年 4 月 29 日)
dir:用线性算法代替指数算法
签字人:Elijah Newren
dir 的read_directory_recursive() 自然会递归操作以遍历目录树。
处理目录有时很奇怪,因为关于如何处理目录有很多不同的排列。
一些例子:
- '
git ls-files -o --directory'只需要知道一个目录本身是未跟踪的;它不需要递归到它来查看下面的内容。
- '
git status'需要递归到一个未跟踪的目录,但只是判断它是否为空。
如果下面没有文件,则输出中将省略目录本身。
如果不为空,则仅列出目录。
- '
git status --ignored'需要递归到未跟踪的目录并报告所有忽略的条目,然后将目录报告为未跟踪-除非目录下的所有条目都被忽略,在这种情况下我们不打印任何目录下的条目,只是将目录本身报告为已忽略。
(请注意,虽然这会强制我们遍历目录下的所有未跟踪文件,但我们会从输出中删除它们,除了像 'git clean' 这样也设置了DIR_KEEP_TRACKED_CONTENTS 的用户。)
- 对于“
git clean”,我们可能需要递归到与任何指定路径规范都不匹配的目录,前提是该目录下有一个条目可以匹配其中一个路径规范。
在这种情况下,我们需要小心地从路径列表中省略目录本身(参见commit 404ebceda01c ("dir: Also check directory for matching pathspecs", 2019-09-17, Git v2.24.0- rc0))
上面提到的部分张力是目录的处理可以根据其中的文件以及dir->flags 中的各种设置而改变。
尝试在阅读代码时牢记这一点,很容易想到“treat_directory() 告诉我们如何处理目录,read_directory_recursive() 是递归的东西”。
由于我们需要查看目录以了解如何处理它,因此很容易决定(也)通过添加read_directory_recursive() 调用从treat_directory() 递归到目录。
添加这样的调用其实没问题,如果我们确保read_directory_recursive() 不会也递归到同一个目录中。
不幸的是,commit df5bcdf83aeb(“dir:递归到未跟踪的目录中忽略文件”,2017-05-18,Git v2.14.0-rc0 -- merge 在batch #5 中列出),添加的正是这样代码的一个案例,这意味着我们将两次调用read_directory_recursive() 以获得未跟踪的目录。
所以,如果我们有一个名为
的文件
one/two/three/four/five/somefile.txt
并且one/ 中没有任何内容被跟踪,然后'git status --ignored' 将在目录'one/' 上调用read_directory_recursive() 两次,并且每个人都会在目录'one/two/' 上调用read_directory_recursive() 两次',依此类推,直到 read_directory_recursive() 被称为 2^5 次为 'one/two/three/four/five/'。
通过将大量特殊逻辑移至treat_directory(),避免每个级别调用read_directory_recursive() 两次。
由于dir.c 有点复杂,随着时间的推移,围绕它建立了额外的麻烦。
在尝试解开它时,我注意到有几个实例,其中第一次调用 read_directory_recursive() 会返回,例如
path_untracked 用于某些目录,稍后会返回,例如
path_none, 尽管该目录显然应该被认为是未跟踪的。
由于第一次调用将未跟踪的条目添加到 dir->entries; 的副作用,该代码恰好可以工作,这使得它能够获得正确的输出,尽管稍后调用的返回值应该被覆盖。
我有点担心仍然存在错误,甚至可能存在错误期望的测试用例。
我已尝试仔细记录 treat_directory(),因为在此更改之后它变得更加复杂(尽管这种复杂性大部分来自其他地方,可能值得更好的 cmets 开始)。
然而,我的大部分工作感觉更像是一场游戏,试图使代码与现有的回归测试相匹配,而不是试图创建一个与某些清晰设计相匹配的实现。
这对我来说似乎是错误的,但是现有行为的规则有很多特殊情况,以至于我很难想出一些关于所有情况下正确行为的总体规则,这迫使我希望回归测试是正确和充分。
鉴于我在过去几个月中与dir.c 相关的测试用例的经验,这种希望似乎没有根据:
文档难以解析甚至错误的示例:
-
3aca58045f4f(
git-clean.txt:不要声称我们将删除带有-n/--dry-run的文件,2019-09-17,Git v2.24.0-rc0)
-
09487f2cbad3 (
clean: 避免删除嵌套 git 中未跟踪的文件
存储库,2019-09-17,v2.24.0-rc0)
-
e86bbcf987fa(
clean:消除-d的定义,2019-09-17)
测试用例被声明错误和更改的示例:
-
09487f2cbad3(
clean:避免删除嵌套 git 存储库中未跟踪的文件,2019-09-17,Git v2.24.0-rc0)
-
e86bbcf987fa(
clean:消除-d的定义,2019-09-17,Git v2.24.0-rc0)
-
a2b13367fe55 (Revert "
dir.c: make 'git-status --ignored' work 在主要目录中", 2019-12-10, Git v2.25.0-rc0)
测试用例明显不足的示例:
-
502c386ff944(
t7300-clean:演示删除嵌套 repo 并忽略文件损坏,2019-08-25,Git v2.24.0-rc0)
-
7541cc530239(
t7300:添加测试用例显示无法清理指定的路径规范,2019-09-17,Git v2.24.0-rc0)
-
a5e916c7453b(
dir:修复match_pathspec_item中的一个错误,2019-09-17,Git v2.24.0-rc0)
-
404ebceda01c(
dir:还检查目录是否匹配路径规范,2019-09-17,Git v2.24.0-rc0)
-
09487f2cbad3(
clean:避免删除嵌套 git 存储库中未跟踪的文件,2019-09-17,Git v2.24.0-rc0)
-
e86bbcf987fa(
clean:消除-d的定义,2019-09-17,Git v2.24.0-rc0)
-
452efd11fbf6(
t3011:演示目录遍历失败,2019-12-10,Git v2.25.0-rc0)
-
b9670c1f5e6b(
dir:修复对公共前缀目录的检查,2019-12-19,Git v2.25.0-rc0)
每个人都不清楚“正确行为”的示例:
其他注意事项:
但是,从积极的方面来说,它确实使代码更快。
对于空存储库中的以下简单 shell 循环:
for depth in $(seq 10 25)
do
dirs=$(for i in $(seq 1 $depth) ; do printf 'dir/' ; done)
rm -rf dir
mkdir -p $dirs
$dirs/未跟踪文件
/usr/bin/time --format="$depth: %e" git status --ignored >/dev/null
完成
我看到了以下时间,以秒为单位(请注意,每次运行的数字都有点嘈杂,但每次运行的趋势都很明显):
10: 0.03
11: 0.05
12: 0.08
13: 0.19
14: 0.29
15: 0.50
16: 1.05
17: 2.11
18: 4.11
19: 8.60
20: 17.55
21: 33.87
22: 68.71
23: 140.05
24: 274.45
25: 551.15
对于上述运行,使用 strace 我可以查找打开的未跟踪目录的数量,并可以验证它是否与预期的 2^($depth+1)-2 匹配(2^1 + 2^2 + 2^3 + ... + 2^$depth 的总和)。
在此修复后,使用strace 我可以验证打开的未跟踪目录的数量是否下降到仅 $depth,并且时间都下降到 0.00。
事实上,直到 190 个嵌套目录的深度,它有时才开始报告 0.01 秒的时间,并且在有 240 个嵌套目录之前不会始终报告 0.01 秒。以前的代码会采用
17.55 * 2^220 / (60*60*24*365) = 9.4 * 10^59 YEARS
完成了 240 个嵌套目录的案例。
您通常不会将某件事情的速度提高 3*10^69 倍。