如何在 git 中进行重命名而不进行后续编辑?

How to stage a rename without subsequent edits in git?

我有一个文件,我已经重命名并编辑了它。我想告诉 Git 进行重命名,而不是内容修改。也就是说,我想分阶段删除旧文件名,并用新文件名添加旧文件内容。

所以我有这个:

Changes not staged for commit:

        deleted:    old-name.txt

Untracked files:

        new-name.txt

但想要这个:

Changes to be committed:

        new file:   new-name.txt
        deleted:    old-name.txt

Changes not staged for commit:

        modified:   new-name.txt

或者这个:

Changes to be committed:

        renamed:    old-name.txt -> new-name.txt

Changes not staged for commit:

        modified:   new-name.txt

(相似性度量应为 100%)。

我想不出一个简单的方法来做到这一点。

是否有获取特定文件的特定修订版内容并将其添加到指定路径下的 git 暂存区的语法?

删除部分,git rm,可以:

$ git rm old-name.txt

这是我正在努力解决的重命名的添加部分。 (我可以保存新内容,签出新副本(旧内容),shell、git add 中的 mv,然后恢复新内容,但这似乎很长的路要走!)

谢谢!

Git 并没有真正重命名。它们全部以"after the fact"方式计算:git将一个提交与另一个提交进行比较,在比较时间,决定是否有重命名。这意味着 git 是否考虑某事 "a rename" 动态变化。我知道你问的是你还没有做出的承诺,但请耐心等待,这真的很相关(但答案会很长)。


当您询问 git(通过 git showgit log -pgit diff HEAD^ HEAD)"what happened in the last commit" 时,它会运行上一次提交的差异(HEAD^HEAD~1 或前一次提交的实际原始 SHA-1——其中任何一个都可以识别它)和当前提交 (HEAD)。在进行比较时,它可能会发现曾经有一个 old.txt 而现在已经不存在了;以前没有 new.txt 但现在有了

这些文件名——过去存在但不存在的文件,以及现在存在但不存在的文件——被放入标记为 "candidates for rename" 的堆中。然后,对于堆中的每个名字,git 比较 "old contents" 和 "new contents"。 完全匹配 的比较非常简单,因为 git 将内容缩减为 SHA-1;如果完全匹配失败,git 切换到可选的 "are the contents at least similar" diff 来检查重命名。对于 git diff,此可选步骤由 -M 标志控制。对于其他命令,它要么由您的 git config 值设置,要么硬编码到命令中。

现在,回到暂存区和git status:git 存储在索引/暂存区的基本上是"a prototype for the next commit"。当您 git add 某些内容时,git 会在该点存储文件内容,在此过程中计算 SHA-1,然后将 SHA-1 存储在索引中。当你 git rm 某些东西时,git 会在索引中存储一个注释 "this path name is being deliberately removed on the next commit"。

git status 命令,然后,简单地做一个差异——或者实际上,两个差异:HEAD 与索引,用于将要提交的内容;和索引与工作树,因为 可能 将(但尚未)提交。

在第一个差异中,git 使用与往常相同的机制来检测重命名。如果 HEAD 提交中有一条路径在索引中消失了,并且索引中有一条路径是新的而不是 HEAD 提交中的路径,则它是重命名检测的候选者。 git status 命令将重命名检测硬连接到 "on"(并且文件计数限制为 200;只有一个候选重命名检测这个限制就足够了)。


这对您的情况意味着什么?好吧,你重命名了一个文件(没有使用 git mv,但这并不重要,因为 git statusgit status 时找到了重命名,或者找不到它),现在有了新文件的更新版本。

如果你 git add 新版本,那个较新的版本进入回购协议,它的 SHA-1 在索引中,当 git status 做一个差异时,它会比较新的和老的。如果它们至少“50% 相似”(git status 的固定值),git 会告诉您文件已重命名。

当然,git add-ing 修改后的 内容并不完全符合您的要求:您想在文件为 [=109 的地方进行中间提交=]仅重命名,即使用新名称但旧内容的树的提交。

不必执行此操作,因为所有上述动态重命名检测。如果您想要 这样做(无论出于何种原因)......好吧,git 并没有那么容易。

最直接的方法就是按照你的建议:将修改的内容移到别处,使用git checkout -- old-name.txt,然后git mv old-name.txt new-name.txt,然后提交。 git mv 将重命名 index/staging-area 中的文件,并重命名工作树版本。

如果 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 怎么样?好吧,首先让我们把事情恢复到 "changed in work-tree, index matches HEAD commit" 状态:

$ 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。

当更新索引完成时,索引记录了旧路径的删除和新路径的添加(与旧路径中的模式和 SHA-1 相同)。

请注意,如果索引具有 old-name.txt 的未合并条目,这可能会严重失败,因为文件可能还有其他阶段(1 到 3)。

@torek 给出了非常清晰和完整的答案。那里有很多非常有用的细节;非常值得一读。

但是,为了那些匆忙的人,给出的非常简单的解决方案的关键是:

What we do now is, first, manually put the file back under its old name, then use git mv to switch again to the new name:

$ mv new-name.txt old-name.txt
$ git mv old-name.txt new-name.txt

(我只是缺少 mv 后背,使 git mv 成为可能。)

如果您觉得有帮助,请为 @torek 的 回答点赞。

我使用 Git GUI Sourcetree。它具有暂存和取消暂存文件“块”或突出显示各个行并暂存或取消暂存它们的功能。

我能够暂存已删除的文件和新文件,让 GUI 将其检测为“重命名”。然后我检查并突出显示所有已更改的行并取消暂存它们。所以现在我已经重命名了,但是文件的 none 发生了变化。

我知道不是每个人都使用这个 UI。但据我所知,它是基于此处描述的“补丁”的交互式分期: https://git-scm.com/book/en/v2/Git-Tools-Interactive-Staging 。所以我会调查一下。然后 un-stage 补丁。