git 复制文件,而不是 `git mv`

git copy file, as opposed to `git mv`

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

简短的回答就是"no"。但是还有更多要知道的;它只需要一些背景。 (作为 ,为了方便起见,我会提到为什么存在 git mv。)

稍微长一点:Git 会比较文件,你是对的,但你可能错了什么时候 Git 会比较这些文件。

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

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

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

现在,所有这些独立的 blob 对象最终确实占用了过多的 space。 此时、Git可以将"pack"个对象放入.pack个文件中。它将每个对象与选定的一组其他对象进行比较——它们可能在历史上更早或更晚,并且具有相同的文件名或不同的文件名,理论上 Git 甚至可以针对 blob 压缩提交对象对象,反之亦然(尽管在实践中它不会)——并尝试找到一些方法来使用更少的磁盘 space 来表示许多 blob。但结果仍然是,至少在逻辑上,一系列独立的对象,使用它们的哈希 ID 以它们的原始形式完整地检索。因此,即使此时使用的磁盘数量 space 下降(我们希望!),所有对象都与以前完全相同。

那么什么时候 Git比较文件?答案是:只有当你要求它时。而"ask time"是当你运行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 log -p 通常只是跳过合并提交的差异——但是这些,以及其他一些重要的情况,是 Git 运行s git diff.

当 Git 运行s git diff 时,您可以(有时)要求它查找或不查找副本. -C 标志,也拼写为 --find-copies=<number>,要求 Git 查找副本。 --find-copies-harder 标志(Git 文档称为 "computationally expensive")看起来比普通的 -C 标志更难复制。 -B(打破不适当的配对)选项影响 -C-M 又名 --find-renames=<number> 选项也会影响 -Cgit merge 命令可以被告知调整其重命名检测级别,但至少目前不能被告知查找副本,也不能破坏不适当的配对。

(一个命令,git blame,进行一些不同的复制查找,以上并不完全适用。)


1如果你运行git commit --include <paths>git commit --only <paths>git commit <paths>git commit -a,想想这些就像在 运行ning 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

第一步很明显:我们需要重命名文件的工作树版本。第二步类似:我们需要将文件的索引版本放到位。但是,第三个 很奇怪: 我们为什么要 "add" 一个刚刚删除的文件?好吧,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 为中间提交仔细准备补丁,然后决定连同补丁一起重命名文件,那么能够做到这一点而不会弄乱你的小心翼翼绝对方便-修补在一起的中间版本。

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

的东西