简短的回答是“不”。但是还有更多要知道的;它只需要一些背景。 (作为JDB suggests in a comment,我会提到为什么git mv 存在是为了方便。)
稍微长一点:Git 会比较文件是对的,但你可能错了什么时候Git 会做这些文件比较。
Git 的内部存储模型建议每次提交都是该提交中所有文件的独立快照。进入新提交的每个文件的版本,即该路径的快照中的数据,是您运行 git commit 时该路径下的索引中的任何内容。1
第一级的实际实现是将每个快照文件以压缩形式捕获为 Git 数据库中的 blob 对象。 blob 对象完全独立于该文件的每个先前和后续版本,除了一种特殊情况:如果您进行了 no 数据已更改的新提交,您将 重新使用旧斑点。因此,当您连续进行两次提交时,每次提交包含 100 个文件,并且仅更改了一个文件,第二次提交会重新使用 99 个以前的 blob,并且只需将一个实际文件快照到一个新的 blob 中。2
因此,Git 将区分文件这一事实根本不会影响提交。没有提交取决于先前的提交,除了存储先前提交的哈希 ID(并且可能重新使用完全匹配的 blob,但这是它们完全匹配的副作用,而不是运行时的花哨计算 @ 987654324@).
现在,所有这些独立的 blob 对象最终都会占用大量空间。 此时,Git 可以将对象“打包”到.pack 文件中。它将每个对象与一些选定的其他对象集进行比较——它们可能在历史上更早或更晚,并且具有相同的文件名或不同的文件名,理论上 Git 甚至可以将提交对象压缩到 blob 对象,反之亦然(尽管实际上它没有)——并尝试找到某种方法来使用更少的磁盘空间来表示许多 blob。但结果仍然是,至少在逻辑上,一系列独立的对象,使用它们的哈希 ID 以原始形式完整地检索。因此,即使此时使用的磁盘空间量减少了(我们希望如此!),所有对象都与以前完全相同。
那么当 Git 比较文件时呢?答案是:只有当你请求它时。“请求时间”是你运行git diff 时,或者直接:
git diff commit1 commit2
或间接:
git show commit # roughly, `git diff commit^@ commmit`
git log -p # runs `git show commit`, more or less, on each commit
这有很多微妙之处——特别是,git show 在合并提交上运行时会产生 Git 所谓的 combined diffs,而 git log -p 通常只是跳过差异合并提交——但这些以及其他一些重要情况是 Git 运行 git diff 的时候。
当 Git 运行 git diff 时,您可以(有时)要求它查找或不查找副本。 -C 标志,也拼写为 --find-copies=<number>,要求 Git 查找副本。 --find-copies-harder 标志(Git 文档称之为“计算成本高”)看起来比普通的 -C 标志更难复制。 -B(中断不适当的配对)选项会影响-C。 -M 又名--find-renames=<number> 选项也会影响-C。可以告诉git merge 命令调整其重命名检测级别,但至少目前不能告诉它查找副本,也不能破坏不适当的配对。
(一个命令,git blame,在复制查找方面有些不同,上述内容并不完全适用。)
1如果您运行git commit --include <paths> 或git commit --only <paths> 或git commit <paths> 或git commit -a,请将这些视为在运行git commit 之前修改索引。在 --only 的特殊情况下,Git 使用临时索引,这有点复杂,但它仍然从 an 索引提交——它只是使用特殊的临时索引而不是普通索引。为了建立临时索引,Git 会复制 HEAD 提交中的所有文件,然后用您列出的 --only 文件覆盖这些文件。对于其他情况,Git 只是将工作树文件复制到常规索引中,然后照常从索引中进行提交。
2实际上,将 blob 存储到存储库中的实际快照发生在 git add 期间。这秘密地使git commit 更快,因为您通常不会注意到在启动git commit 之前运行git add 所需的额外时间。
为什么存在git mv
git mv old new 所做的是,非常大致:
mv old new
git add new
git add old
第一步很明显:我们需要重命名文件的工作树版本。第二步类似:我们需要将文件的索引版本放置到位。然而,第三个是奇怪:我们为什么要“添加”一个我们刚刚删除的文件?好吧,git add 并不总是添加文件:相反,在这种情况下,它会检测到文件 曾 在索引中并且不再存在。
我们也可以将第三步拼写为:
git rm --cached old
我们真正要做的就是从索引中删除旧名称。
但是这里有一个问题,这就是为什么我说“非常粗略”。该索引具有每个文件的副本,下次运行git commit 时将提交该副本。 该副本可能与工作树中的副本不匹配。 事实上,如果 HEAD 中有一个副本,它甚至可能与 HEAD 中的副本不匹配。
例如,之后:
echo I am a foo > foo
git add foo
文件foo 存在于工作树和索引中。工作树内容和索引内容匹配。但现在让我们更改工作树版本:
echo I am a bar > foo
现在索引和工作树不同了。假设我们想将底层文件从foo 移动到bar,但是——出于某种奇怪的原因3——我们希望保持索引内容不变。如果我们运行:
mv foo bar
git add bar
我们将在新索引文件中获得I am a bar。如果我们随后从索引中删除旧版本的foo,我们将完全丢失I am a foo 版本。
所以,git mv foo bar 并没有真正移动并添加两次或移动添加和删除。相反,它重命名工作树文件和重命名索引内副本。如果原始文件的索引副本与工作树文件不同,则重命名的索引副本仍与重命名的工作树副本不同。
没有像git mv这样的前端命令很难做到这一点。4当然,如果你打算git add一切,你不需要所有这些东西在第一名。而且,值得注意的是,如果git cp 存在,则在制作索引副本时,它可能也 复制索引版本,而不是工作树版本。所以git cp 真的应该存在。还应该有一个 git mv --after 选项,一个 la Mercurial 的 hg mv --after。两者都应该存在,但目前不存在。 (不过,在我看来,对这两者的要求都比直接git mv 要少。)
3对于这个例子,它有点愚蠢和毫无意义。但是,如果您使用git add -p 为中间提交仔细准备补丁,然后决定与补丁一起重命名文件,那么在不弄乱您精心打补丁的情况下这样做绝对很方便-together 中间版本。
4这不是不可能的:git ls-index --stage 会从索引中获取您现在需要的信息,git update-index 允许您对索引进行任意更改。您可以将这两者结合起来,以及一些复杂的 shell 脚本或用更好的语言进行编程,以构建实现 git mv --after 和 git cp 的东西。