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>
选项也会影响 -C
。 git 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 --after
和 git cp
.
的东西
我意识到 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>
选项也会影响 -C
。 git 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 --after
和 git cp
.