git copy file,而不是`git mv`

Ale*_*lls 14 git cp git-mv

我意识到git的工作原理是区分文件的内容.我有一些我想复制的文件.为了绝对防止git变得困惑,是否有一些git命令可用于将文件复制到不同的目录(不是mv,但是cp),并且还可以暂存文件?

tor*_*rek 20

简短的回答只是"不".但还有更多要知道; 它只需要一些背景知识.(正如JDB在评论中所说,我会提到为什么git mv存在这样做是为了方便.)

稍微长一点:Git会对文件进行差异化你是对的,但是 Git执行这些文件差异你可能会错.

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

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

因此,Git将差异文件的事实根本不会进入提交.没有提交依赖于先前的提交,除了存储先前提交的哈希ID(并且可能重新使用完全匹配的blob,但这是它们完全匹配的副作用,而不是在您运行时的奇特计算git commit) .

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

因此,当混帐比较文件?答案是:只有当你问它时. "询问时间"是指您git diff直接运行的时间:

git diff commit1 commit2
Run Code Online (Sandbox Code Playgroud)

或间接地:

git show commit  # roughly, `git diff commit^@ commmit`
git log -p       # runs `git show commit`, more or less, on each commit
Run Code Online (Sandbox Code Playgroud)

关于这一点有一些细微之处 - 特别是,当在合并提交上运行时git show会产生Git调用组合差异的东西,而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使用一个临时的指数,这是一个有点复杂,但它仍然承诺从一个指数,它只是使用特殊的临时的,而不是正常的.要创建临时索引,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
Run Code Online (Sandbox Code Playgroud)

第一步是显而易见的:我们需要重命名文件的工作树版本.第二步是类似的:我们需要将文件的索引版本放在适当的位置.第三个是奇怪的:我们为什么要"添加"我们刚删除的文件?好了,git add并不总是能够增加一个文件:相反,在这种情况下,检测到该文件在指数没有了.

我们还可以将第三步拼写为:

git rm --cached old
Run Code Online (Sandbox Code Playgroud)

我们所做的只是将旧名称从索引中删除.

但这里有一个问题,这就是为什么我说" 非常粗略".索引具有将在下次运行时提交的每个文件的副本git commit. 该副本可能与工作树中的副本不匹配. 事实上,它可能甚至不匹配HEAD,如果有一个HEAD.

例如,之后:

echo I am a foo > foo
git add foo
Run Code Online (Sandbox Code Playgroud)

该文件foo存在于工作树和索引中.工作树内容和索引内容匹配.但现在让我们改变工作树版本:

echo I am a bar > foo
Run Code Online (Sandbox Code Playgroud)

现在索引和工作树不同了.假设我们想要将底层文件移动foobar,但是 - 由于一些奇怪的原因3 -我们希望保持索引内容不变.如果我们运行:

mv foo bar
git add bar
Run Code Online (Sandbox Code Playgroud)

我们将进入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选项,一个Mercurial's hg mv --after.两者都应该存在,但目前不存在.(但是git mv,在我看来,对这两种方式的要求都比直接的要少.)


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

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

  • 我想你可能想知道为什么不需要“git cp”,因为“git mv”只是“mv”、“git add”和“git rm”的简写。 (2认同)