【问题标题】:git copy file, as opposed to `git mv`git 复制文件,而不是 `git mv`
【发布时间】:2017-11-20 22:01:35
【问题描述】:

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

【问题讨论】:

  • git mv 是一个东西,但git cp 不是一个东西
  • 你认为 git 会如何混淆?
  • 可能不会,我猜我可以用cp
  • 是的……这里没有危险。 git mv 只是一个shortcut(对于mvgit removegit rm)......这不是为了避免混淆。
  • git cp 非常适合这个用例:stackoverflow.com/questions/16937359/…

标签: git cp git-mv


【解决方案1】:

简短的回答是“不”。但是还有更多要知道的;它只需要一些背景。 (作为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 --aftergit cp 的东西。

【讨论】:

  • 我想您可能想了解为什么不需要git cp,因为git mv 只是mvgit addgit rm 的简写。
  • @JDB:完成,虽然我实际上认为“git cp”会有意义(请参阅新部分)。
猜你喜欢
  • 2014-05-02
  • 1970-01-01
  • 2017-12-25
  • 1970-01-01
  • 2010-11-08
  • 1970-01-01
  • 1970-01-01
  • 2011-04-08
相关资源
最近更新 更多