【问题标题】:Handling file renames in Git在 Git 中处理文件重命名
【发布时间】:2026-02-04 07:35:01
【问题描述】:

我读到renaming files in Git 时,您应该提交任何更改,执行重命名,然后暂存重命名的文件。 Git 会从内容中识别文件,而不是将其视为新的未跟踪文件,并保留更改历史记录。

但是,今晚就这样做,我最终恢复到git mv

> $ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#    modified:   index.html
#

我将Finder 中的样式表从iphone.css 重命名为mobile.css

> $ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#    modified:   index.html
#
# Changed but not updated:
#   (use "git add/rm <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#    deleted:    css/iphone.css
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#    css/mobile.css

所以 Git 现在认为我删除了一个 CSS 文件,并添加了一个新文件。这不是我想要的。让我们撤消重命名,让 Git 完成工作。

> $ git reset HEAD .
Unstaged changes after reset:
M    css/iphone.css
M    index.html

我又回到了我开始的地方:

> $ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#    modified:   index.html
#

让我们改用git mv

> $ git mv css/iphone.css css/mobile.css
> $ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#    renamed:    css/iphone.css -> css/mobile.css
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#    modified:   index.html
#

看起来我们很好。那么为什么我第一次使用 Finder 时 Git 没有识别出重命名呢?

【问题讨论】:

  • Git 跟踪内容,而不是文件,因此无论您如何使索引进入正确的状态(add+rmmv),它都会产生相同的结果。然后 Git 使用它的重命名/复制检测来让你知道它是一个重命名。您引用的消息来源也不准确。您是否在同一个提交中修改+重命名并不重要。当您对修改和重命名进行比较时,重命名检测会将其视为重命名+修改,或者如果修改是完全重写,它将显示为添加和删除 - 仍然无关紧要您如何执行它。
  • 如果这是真的,为什么我使用 Finder 重命名时它没有检测到它?
  • git mv old new 自动更新索引。当您在 Git 之外重命名时,您必须执行 git add newgit rm old 来暂存对索引的更改。完成此操作后,git status 将按预期工作。
  • 我刚刚将一堆文件移到了public_html 目录中,这些文件在 git 中进行了跟踪。在执行了git add .git commit 之后,它仍然在git status 中显示了一堆“已删除”的文件。我执行了git commit -a 并提交了删除,但现在我没有关于public_html 中的文件的历史记录。这个工作流程没有我想的那么顺利。

标签: git git-mv


【解决方案1】:

对于git mv the manual page

成功完成后更新索引, […]

所以,首先,您必须自己更新索引 (通过使用git add mobile.css)。然而git status 仍然会显示两个不同的文件:

$ git status
# On branch master
warning: LF will be replaced by CRLF in index.html
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   index.html
#       new file:   mobile.css
#
# Changed but not updated:
#   (use "git add/rm <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       deleted:    iphone.css
#

您可以通过运行git commit --dry-run -a 获得不同的输出,这会产生您想要的结果 期望:

Tanascius@H181 /d/temp/blo (master)
$ git commit --dry-run -a
# On branch master
warning: LF will be replaced by CRLF in index.html
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   index.html
#       renamed:    iphone.css -> mobile.css
#

我无法确切告诉您为什么我们会看到这些差异 在git statusgit commit --dry-run -a 之间,但是 这里是a hint from Linus

git 真的不关心整个 内部“重命名检测”,以及您拥有的任何提交 完成重命名完全独立于 然后我们使用启发式方法显示重命名。

dry-run 使用真正的重命名机制,而 git status 可能不会。

【讨论】:

  • 您没有提及您执行git add mobile.css 的步骤。如果没有它,git status -a 只会“看到”先前跟踪的 iphone.css 文件的删除,但不会触及新的、未跟踪的 mobile.css 文件。此外,git status -a 在 Git 1.7.0 及更高版本中无效。 ““git status”不再是“git commit --dry-run”。”在kernel.org/pub/software/scm/git/docs/RelNotes-1.7.0.txt。如果您需要此功能,请使用 git commit --dry-run -a。正如其他人所说,只需更新索引,git status 就会按照 OP 的预期工作。
  • 如果你做一个普​​通的git commit 它不会提交重命名的文件并且工作树仍然是一样的。 git commit -a 几乎击败了 git 工作流/思维模型的每个方面——每一个更改都被提交。如果您只想重命名文件,但在另一个提交中将更改提交到 index.html 怎么办?
  • @Chris:是的,我当然添加了mobile.css,我应该提到这一点。但这就是我回答的重点:当您使用 git-mv 时,手册页会说 the index is updated。感谢status -a 的澄清,我使用了 git 1.6.4
  • 很好的答案!我把头撞在墙上,试图弄清楚为什么git status 没有检测到重命名。在添加我的“新”文件后运行git commit -a --dry-run 显示了重命名,最终让我有信心提交!
  • 在 git 1.9.1 git status 现在的行为类似于 git commit
【解决方案2】:

您必须将两个修改后的文件添加到索引中,Git 才会将其识别为移动。

mv old newgit mv old new 之间的唯一区别是 git mv 还将文件添加到索引中。

mv old new 然后git add -A 也可以。

请注意,您不能只使用git add .,因为这不会将删除添加到索引中。

Difference between "git add -A" and "git add ."

【讨论】:

  • 感谢git add -A 链接,非常有用,因为我正在寻找这样的快捷方式!
  • 请注意,使用 git 2,git add . 确实将删除添加到索引中。
【解决方案3】:

对于 Git 1.7.x,以下命令对我有用:

git mv css/iphone.css css/mobile.css
git commit -m 'Rename folder.'

不需要git add,因为原始文件(即css/mobile.css)之前已经在提交的文件中。

【讨论】:

  • 这个。所有其他答案都是荒谬且不必要的复杂。这会维护提交之间的文件历史记录,以便文件重命名之前/之后的合并不会被破坏。
【解决方案4】:

最好的办法就是自己尝试一下。

mkdir test
cd test
git init
touch aaa.txt
git add .
git commit -a -m "New file"
mv aaa.txt bbb.txt
git add .
git status
git commit --dry-run -a

现在git statusgit commit --dry-run -a 显示两个不同的结果,其中git statusbbb.txt 显示为一个新文件/ aaa.txt 被删除,并且--dry-run 命令显示实际重命名。

~/test$ git status

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   new file:   bbb.txt
#
# 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:    aaa.txt
#


/test$ git commit --dry-run -a

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   renamed:    aaa.txt -> bbb.txt
#

现在继续办理登机手续。

git commit -a -m "Rename"

现在您可以看到文件实际上已重命名,而git status 中显示的内容是错误的,至少在这种情况下是这样。下划线 GIT 实现可能会分别处理这两个命令。

故事的寓意:如果您不确定您的文件是否被重命名,请发出“git commit --dry-run -a”。如果显示文件已重命名,那么您就可以开始了。

【讨论】:

  • 对于 Git 来说重要的是,两者都是正确的。不过,后者更接近你的方式,正如提交者可能看到的那样。 rename 和 delete + create 之间的真正区别仅在操作系统/文件系统级别(例如,相同的 inode# 与新的 inode#),Git 并不真正关心这一点。
【解决方案5】:

第 1 步:将文件从 oldfile 重命名为 newfile

git mv #oldfile #newfile

第 2 步:git commit 并添加 cmets

git commit -m "rename oldfile to newfile"

第 3 步:将此更改推送到远程服务器

git push origin #localbranch:#remotebranch

【讨论】:

  • 请添加一些评论,以便对OP有帮助
  • 第 2 步是不必要的。在git mv之后,新文件已经在索引中了。
  • #s 中有哪些是字面的,如果有的话?
  • @PeterMortensen 没有一个。不知道他为什么把它们放在那里,因为它们是 bash 的注释字符。见这里:git-scm.com/docs/git-push
【解决方案6】:

你必须git add css/mobile.css 新文件和git rm css/iphone.css,所以 Git 知道它。然后它将在git status 中显示相同的输出。

在状态输出中可以看得很清楚(文件的新名称):

# Untracked files:
#   (use "git add <file>..." to include in what will be committed)

和(旧名):

# Changed but not updated:
#   (use "git add/rm <file>..." to update what will be committed)

我认为在幕后git mv 只不过是一个包装脚本,它确实做到了这一点:从索引中删除文件并以不同的名称添加它

【讨论】:

  • 我认为我不必git rm css/iphone.css,因为我认为这会删除现有的历史记录。也许我误解了 git 中的工作流程。
  • @Greg K:git rm 不会删除历史记录。它只从索引中删除一个条目,以便下一次提交将没有该条目。但是,它仍将存在于祖先提交中。您可能会感到困惑的是(例如)git log -- new 将停止在您提交git mv old new 的位置。如果您想关注重命名,请使用git log --follow -- new
【解决方案7】:

让我们从 Git 的角度考虑您的文件。

请记住,Git 不会跟踪有关您文件的任何元数据

您的存储库有(以及其他)

$ cd repo
$ ls
...
iphone.css
...

而且它在 Git 控制之下:

$ git ls-files --error-unmatch iphone.css &>/dev/null && echo file is tracked
file is tracked

对此进行测试:

$ touch newfile
$ git ls-files --error-unmatch newfile &>/dev/null && echo file is tracked
(no output, it is not tracked)
$ rm newfile

当你这样做时

$ mv iphone.css mobile.css

从 Git 的角度来看,

  • 没有任何 iphone.css(它已被删除 -git 对此发出警告 -)。
  • 有一个新文件 mobile.css
  • 这些文件完全不相关。

因此,Git 会建议它已经知道的文件 (iphone.css) 和它检测到的新文件 (mobile.css),但仅当文件在索引或HEAD,Git 开始检查它们的内容。

此时,“iphone.css删除”和mobile.css都不在索引中。

将iphone.css删除添加到索引中:

$ git rm iphone.css

Git 会告诉你到底发生了什么(iphone.css 已被删除。没有更多的事情发生):

然后添加新文件mobile.css

$ git add mobile.css

这次删除和新文件都在索引中。现在 Git 检测到上下文相同并将其公开为重命名。事实上,如果文件有 50% 相似,它会检测为重命名,让您可以稍微更改 mobile.css,同时将操作保持为重命名。

看到这可以在git diff 上重现。现在您的文件在索引中,您必须使用--cached。稍微编辑 mobile.css,将其添加到索引中,看看两者之间的区别:

$ git diff --cached

$ git diff --cached -M

-Mgit diff 的“检测重命名”选项。 -M 代表 -M50%(50% 或更多的相似性将使 Git 将其表示为重命名),但如果您编辑文件 mobile.css,您可以将其减少为 -M20% (20%) > 很多。

【讨论】:

    【解决方案8】:

    Git 会从内容中识别文件,而不是将其视为新的未跟踪文件

    那是你出错的地方。

    只有在您添加文件之后,Git 才会从内容中识别它。

    【讨论】:

    • 没错。暂存时 git 将正确显示重命名。
    【解决方案9】:

    您没有展示 Finder 移动的结果。我相信如果你通过 Finder 进行移动,然后进行 git add css/mobile.css ; git rm css/iphone.css,Git 会计算新文件的哈希值,然后才意识到文件的哈希值匹配(因此它是重命名)。

    【讨论】:

      【解决方案10】:

      如果您确实必须手动重命名文件,例如,使用脚本批量重命名一堆文件,然后使用 git add -A . 对我有用。

      【讨论】:

        【解决方案11】:

        对于 Xcode 用户:如果您在 Xcode 中重命名文件,您会看到徽章图标更改为附加。如果您使用 Xcode 进行提交,您实际上会创建一个新文件并丢失历史记录。

        解决方法很简单,但您必须在使用 Xcode 提交之前完成:

        1. 对您的文件夹执行 Git status。您应该看到分阶段的更改是正确的:

          renamed:    Project/OldName.h -> Project/NewName.h
          renamed:    Project/OldName.m -> Project/NewName.m
          
        2. commit -m 'name change'

          然后回到 Xcode,你会看到徽章从 A 变成了 M,并且它被保存以提交将来使用 Xcode 的更改。

        【讨论】:

          【解决方案12】:

          刚刚遇到这个问题 - 如果您更新了一堆文件并且不想执行 git mv 所有这些文件,这也可以:

          1. 将父目录从/dir/RenamedFile.js重命名为/whatever/RenamedFile.js
          2. git add -A 进行更改
          3. 将父目录重命名回/dir/RenamedFile.js
          4. git add -A 再次,将重新执行该更改,强制 git 获取文件名更改。

          【讨论】:

            【解决方案13】:

            在 Window 10 上的 git 2.33 上测试

            1. 在资源管理器中重命名您需要的文件夹和文件。
            2. git add .
            3. git commit -m "commit message"
            4. git push

            Git 将检测重命名。您可以通过运行git status(提交后)进行检查

            【讨论】:

              【解决方案14】:

              当我将文件夹和文件大写时,我使用Node.js' file-system 来解决此问题。

              在开始之前不要忘记提交。我不确定它有多稳定:)

              1. 在项目根目录中创建脚本文件。
              // File rename.js
              
              const fs = require('fs').promises;
              const util = require('util');
              const exec = util.promisify(require('child_process').exec);
              
              const args = process.argv.slice(2);
              const path = args[0];
              
              const isCapitalized = (s) => s.charAt(0) === s.charAt(0).toUpperCase();
              
              const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
              
              async function rename(p) {
                const files = await fs.readdir(p, { withFileTypes: true });
                files.forEach(async (file) => {
                  const { name } = file;
                  const newName = capitalize(name);
                  const oldPath = `${p}/${name}`;
                  const dumbPath = `${p}/dumb`;
                  const newPath = `${p}/${newName}`;
                  if (!isCapitalized(name) && name !== 'i18n' && name !== 'README.md') {
                    // 'git mv' won't work if we only changed size of letters.
                    // That's why we need to rename to dumb name first, then back to current.
                    await exec(`git mv ${oldPath} ${dumbPath}`);
                    await exec(`git mv ${dumbPath} ${newPath}`);
                  }
                  if (file.isDirectory()) {
                    rename(newPath);
                  }
                });
              }
              
              rename(path);
              

              重命名目录(或单个文件)中的所有文件脚本更简单:

              // rename.js
              
              const fs = require('fs').promises;
              const util = require('util');
              const exec = util.promisify(require('child_process').exec);
              const args = process.argv.slice(2);
              const path = args[0];
              
              async function rename(p) {
                const files = await fs.readdir(p, { withFileTypes: true });
                files.forEach(async (file) => {
                  const { name } = file;
                  const currentPath = `${p}/${name}`;
                  const dumbPapth = `${p}/dumb`;
                  await exec(`git mv ${currentPath} ${dumbPath}`);
                  await exec(`git mv ${dumbPath} ${currentPath}`);
                  if (file.isDirectory()) {
                    rename(newPath);
                  }
                });
              }
              
              rename(path);
              
              1. 使用参数运行脚本
              node rename.js ./frontend/apollo/queries
              

              How to run shell script file or command using Node.js

              【讨论】: