Git不真正进行重命名,所有的重命名都是“事后”计算的:git比较一个提交和另一个提交,并且在比较时决定是否存在重命名。这意味着,无论Git是否认为某些内容是“重命名”,都会动态变化。当你询问 git(通过 git show
、 git log -p
或 git diff HEAD^ HEAD
)“最后一次提交发生了什么”,它会运行前一次提交( HEAD ^
或 HEAD~1
或前一次提交的实际原始 SHA-1—任何一个都可以用于标识它)和当前提交( HEAD
)之间的差异。在制作差异时,它可能会发现以前有个 old.txt
,现在没有了;而且没有 new.txt
,但现在有了。这些文件名—曾经存在但现在不存在的文件名,以及现在存在但以前不存在的文件名—被放进标记为“重命名候选项”的堆中。然后,对于堆中的每个名称,git会比较“旧内容”和“新内容”。由于git将内容缩减为SHA-1,因此对于“精确匹配”的比较非常容易;如果精确匹配失败,则git会切换到可选的“内容至少相似”的diff来检查重命名。使用 git diff
,此可选步骤由 -M
标志控制。对于其他命令,它要么由您的 git config
值设置,要么被硬编码到命令中。
现在回到暂存区和 git status
:git在索引/暂存区中存储的基本上是“下一次提交的原型”。当您 git add
某个东西时,git会在该点存储文件内容,同时计算SHA-1,然后将SHA-1存储在索引中。当您 git rm
某个东西时,git会在索引中存储一个注释,说“这个路径名在下一次提交中被故意删除”。因此, git status
命令只是进行了差异比较,确切地说,是两个差异比较: HEAD
与索引的比较,用于将被提交的内容;以及索引与工作树的比较,用于可能(但尚未)被提交的内容。
在第一个差异中,Git使用与以往相同的机制来检测重命名。如果HEAD提交中存在路径不存在于索引中,并且索引中存在一个新的路径,不在HEAD提交中,则它是重命名检测的候选项。 git status命令将重命名检测硬编码为“on”(文件计数限制为200;对于只有一个重命名检测候选项,此限制已经足够)。
这对你的情况意味着什么呢?你重命名了一个文件(没有使用git mv
,但这并不重要,因为git status
会在git status
时间找到或未找到重命名),现在有一个更新的、不同版本的新文件。
如果你用git add
添加新版本,那么新版本就会进入仓库,它的SHA-1值将在索引中,并且当git status
做差异比较时,它会比较新旧版本。如果它们至少“50%相似”(git status
的硬编码值),Git会告诉你文件已被重命名。
当然,用git add
添加修改后的内容并不完全符合你的要求:你想做一个中间提交,其中文件仅被重命名,即一个具有新名称但旧内容的树的提交。
你不必这样做,因为以上所有动态重命名检测都可以处理。如果你想这么做(无论出于任何原因)...好吧,Git并不是特别容易实现。
最简单的方法就是按照你建议的方式:将修改后的内容移到一边,使用git checkout -- old-name.txt
,然后git mv old-name.txt new-name.txt
,然后提交。git mv将重命名索引/暂存区中的文件,并重命名工作树版本。
如果git mv有一个类似于git rm的--cached选项,你可以只用git mv --cached old-name.txt new-name.txt
,然后git commit
。第一步将在索引中重命名文件,而不影响工作树。但事实并非如此:它坚持要覆盖工作树版本,并坚持要求旧名称必须从工作树中开始存在。
在不触及工作树的情况下执行此操作的单个步骤方法是使用git update-index --index-info
,但这也有点混乱(我稍后会展示它)。幸运的是,我们还有最后一件事情可以做。我已经设置了与你相同的情况,通过将旧名称重命名为新名称并修改文件:
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: old-name.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
new-name.txt
现在我们要做的是
首先将文件手动放回旧名称下,然后使用git mv
再次切换到新名称。
$ mv new-name.txt old-name.txt
$ git mv old-name.txt new-name.txt
这次git mv
会更新索引中的名称,但将原始内容保留为索引SHA-1值,并将工作树版本(新内容)移动到工作树中:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: old-name.txt -> new-name.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: new-name.txt
现在只需使用git commit
命令进行提交,以便将重命名的文件提交到原地,但不包括新的内容。
(请注意,这取决于没有用旧名称创建的新文件!)
那么使用git update-index
呢?首先,让我们将事情恢复到“工作树中已更改,索引与HEAD提交匹配”的状态:
$ git reset --mixed HEAD # set index=HEAD, leave work-tree alone
现在让我们看看old-name.txt
索引中的内容:
$ git ls-files --stage -- old-name.txt
100644 2b27f2df63a3419da26984b5f7bafa29bdf5b3e3 0 old-name.txt
因此,我们需要使用git update-index --index-info
来清除old-name.txt
的条目,但要创建一个与之前完全相同的new-name.txt
条目:
$ (git ls-files --stage -- old-name.txt;
git ls-files --stage -- old-name.txt) |
sed -e \
'1s/^[0-9]* [0-9a-f]*/000000 0000000000000000000000000000000000000000/' \
-e '2s/old-name.txt$/new-name.txt/' |
git update-index --index-info
(注意:我将上面的内容分开发布,但当我输入时它是一行;在sh/bash中,只需添加反斜杠以继续“sed”命令,就可以按照这种方式工作。)
还有其他方法可以做到这一点,但简单地提取索引条目两次并将第一个修改为删除,第二个修改为新名称似乎是最容易的,在这里使用了sed
命令。第一个替换更改文件模式(100644,但任何模式都将变成全零)和SHA-1(匹配任何SHA-1,替换为git的特殊全零SHA-1),第二个保持模式和SHA-1不变,同时替换名称。
当update-index完成后,索引记录了旧路径的删除和新路径的添加(具有与旧路径相同的模式和SHA-1)。
请注意,如果索引中存在未合并条目的old-name.txt
,则可能会严重失败,因为该文件可能具有其他阶段(1至3)。
git mv
无效。您需要执行保存/恢复过程,就像您所概述的那样... 在此过程中,您可以使用git mv
而不是mv + git add
。由于在更改时 Git 没有跟踪new-name.txt
,因此它无法帮助分离这些更改。 - hineroptera