在 Git 中处理文件重命名

Posted

技术标签:

【中文标题】在 Git 中处理文件重命名【英文标题】:Handling file renames in Git 【发布时间】:2011-02-08 03:04:12 【问题描述】:

我读到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 中的文件的历史记录。这个工作流程没有我想的那么顺利。 【参考方案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 commit --dry-run -a”。如果显示文件已重命名,那么您就可以开始了。

【讨论】:

对于 Git 来说重要的是,两者都是正确的。不过,后者更接近你的方式,正如提交者可能看到的那样。重命名和删除 + 创建之间的真正区别仅在操作系统/文件系统级别(例如,相同的 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 提交之前完成:

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

    renamed:    Project/OldName.h -> Project/NewName.h
    renamed:    Project/OldName.m -> Project/NewName.m
    

    commit -m 'name change'

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

【讨论】:

【参考方案12】:

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

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

【讨论】:

【参考方案13】:

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

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

    在项目根目录中创建脚本文件。
// 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);
    使用参数运行脚本
node rename.js ./frontend/apollo/queries

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

【讨论】:

【参考方案14】:

在 Window 10 上的 git 2.33 上测试

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

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

【讨论】:

以上是关于在 Git 中处理文件重命名的主要内容,如果未能解决你的问题,请参考以下文章

git重命名许多文件和文件夹

处理重命名:svn vs. git vs. mercurial

git提交重命名文件

在 git 文件重命名后使 Xcode 5 跟随历史

git重命名文件和文件夹

如何重命名git根文件夹?