33

我意识到 git 通过区分文件的内容来工作。我有一些文件要复制。为了绝对防止 git 感到困惑,是否有一些 git 命令可用于将文件复制到不同的目录(不是 mv,而是 cp),并暂存文件?

4

1 回答 1

43

简短的回答是“不”。但是还有更多要知道的;它只需要一些背景。(正如JDB 在评论中建议的那样,我会提到为什么git mv存在是为了方便。)

稍长一点:Git 会比较文件是对的,但对于Git何时执行这些文件比较可能是错误的。

Git 的内部存储模型建议每次提交都是该提交中所有文件的独立快照。进入新提交的每个文件的版本,即该路径的快照中的数据,是您运行时该路径下的索引中的任何内容git commit1

第一级的实际实现是将每个快照文件以压缩形式捕获为 Git 数据库中的blob 对象。blob 对象完全独立于该文件的每个先前和后续版本,除了一种特殊情况:如果您进行新的提交,其中没有数据更改,您将重新使用旧的 blob。因此,当您连续进行两次提交时,每次提交都包含 100 个文件,并且仅更改了一个文件,第二次提交会重复使用 99 个之前的 blob,并且只需将一个实际文件快照到一个新的 blob 中。2

因此,Git 将区分文件这一事实根本不会影响提交。没有提交依赖于先前的提交,除了存储先前提交的哈希 ID(并且可能重新使用完全匹配的 blob,但这是它们完全匹配的副作用,而不是运行时的花哨计算git commit) .

现在,所有这些独立的 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 所谓的组合差异,而git log -p通常只是跳过合并提交的差异——但是这些以及其他一些重要的情况是当 Git运行git diff

Git 运行时git diff,您可以(有时)要求它查找或不查找副本。该-C标志也拼写为--find-copies=<number>,要求 Git 查找副本。该标志(Git 文档称之为“计算成本高”)看起来比普通标志--find-copies-harder更难复制。-C-B中断不适当的配对)选项会影响-C. -Maka--find-renames=<number>选项也会-C影响. 可以告诉该git merge命令调整其重命名检测级别,但至少目前不能告诉该命令查找副本,也不能破坏不适当的配对。

(一个命令,git blame,做一些不同的复制查找,上面并不完全适用于它。)


1如果您运行git commit --include <paths>orgit commit --only <paths>git commit <paths>or git commit -a,请将这些视为在运行前修改索引git commit。在特殊情况下--only,Git 使用临时索引,这有点复杂,但它仍然从索引提交——它只是使用特殊的临时索引而不是普通索引。为了制作临时索引,Git 会复制提交中的所有文件,然后用您列出HEAD的文件覆盖这些文件。--only对于其他情况,Git 只是将工作树文件复制到常规索引中,然后照常从索引中进行提交。

2事实上,将 blob 存储到存储库中的实际快照发生在git add. 这秘密git commit地加快了速度,因为您通常不会注意到在启动git add之前运行所需的额外时间git commit


为什么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

现在索引和工作树不同。假设我们想要将底层文件从 移动foobar,但是——出于某种奇怪的原因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's hg mv --after。两者都应该存在,但目前不存在。git mv(不过,在我看来,对其中任何一个的要求都比对 straight 的要求要少。)


3对于这个例子,它有点愚蠢和毫无意义。但是,如果您使用git add -p为中间提交仔细准备补丁,然后决定与补丁一起重命名文件,那么能够做到这一点绝对很方便,而不会弄乱您精心修补的中间版本。

4这不是不可能的:git ls-index --stage会像现在一样从索引中获取您需要的信息,并git update-index允许您对索引进行任意更改。您可以将这两者结合起来,以及一些复杂的 shell 脚本或用更好的语言进行编程,以构建实现git mv --aftergit cp.

于 2017-11-20T22:47:16.937 回答