在 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+rm
或mv
),它都会产生相同的结果。然后 Git 使用它的重命名/复制检测来让你知道它是一个重命名。您引用的消息来源也不准确。您是否在同一个提交中修改+重命名并不重要。当您对修改和重命名进行比较时,重命名检测会将其视为重命名+修改,或者如果修改是完全重写,它将显示为添加和删除 - 仍然无关紧要您如何执行它。
如果这是真的,为什么我使用 Finder 重命名时它没有检测到它?
git mv old new
自动更新索引。当您在 Git 之外重命名时,您必须执行 git add new
和 git 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 status
和git 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 new
和 git 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 status
和git commit --dry-run -a
显示两个不同的结果,其中git status
将bbb.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
-M
是git 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.js
。
git add -A
进行更改
将父目录重命名回/dir/RenamedFile.js
。
git 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 中处理文件重命名的主要内容,如果未能解决你的问题,请参考以下文章