在Git中处理文件重命名

514

我曾经读到,在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第一次没有识别到这个重命名呢?


33
Git追踪内容而非文件,所以无论您如何将索引置于适当状态——使用add+rm或者mv——Git都会生成相同的结果。然后,Git利用其重命名/复制检测功能来让您知道这是一次重命名操作。您引用的源的说法也不准确。无论您是否在同一提交中进行修改和重命名,都没有关系。当您跨越修改和重命名执行diff操作时,重命名检测会将其视为重命名+修改操作,如果修改是完全重新编写,则会显示为添加和删除操作——无论您如何执行都没有关系。 - Cascabel
7
如果这是真的,为什么我用Finder重命名后它没有被检测出来? - Greg K
30
git mv old new 命令会自动更新索引。当你在 Git 之外重命名文件时,你需要执行 git add newgit rm old 命令来将更改添加到索引中。一旦完成这些步骤后,运行 git status 命令就可以按照你的预期工作了。 - Chris Johnsen
4
我刚把一堆跟踪在 Git 中的文件移动到了 public_html 目录。进行了 git add .git commit 操作后,在 git status 中仍然显示了一堆“deleted”文件。我执行了 git commit -a,删除操作被提交了,但现在我找不到那些位于 public_html 中的文件的历史记录了。这个工作流程并不如我所愿的那么顺畅。 - Greg K
简单来说:Git只在暂存区中显示重命名。在你将文件添加到暂存区之前,它会显示为已删除的文件和未跟踪的文件。使用git mv命令可以立即将重命名添加到暂存区,而手动重命名则需要你自己将重命名添加到暂存区,然后才能在git状态中看到它被标记为"重命名"。回答关于“为什么使用Finder重命名时它没有被检测到”的问题,是因为你还没有将它添加到暂存区。一旦你将删除和未跟踪的文件都添加到暂存区,它就会被检测到。(此外,文件内容变化的百分比超过阈值后,它将不再被视为重命名。) - undefined
14个回答

383

对于git mv手册页面上说:

成功完成后,索引将进行更新, [...]

因此,首先您需要自行更新索引(使用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之间的差异,但这里有一条来自Linus的提示:

在内部,git实际上并不关心“重命名检测”,你使用重命名完成的任何提交都与我们用于“显示”重命名的启发式完全独立。

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


1
你没有提到你执行 git add mobile.css 这一步骤。如果没有这个步骤,git status -a 命令只会“看到”之前被跟踪的 iphone.css 文件被删除了,但不会触及新的未被跟踪的 mobile.css 文件。另外,Git 1.7.0 及以后版本中,git status -a 命令已经无效了。在 http://www.kernel.org/pub/software/scm/git/docs/RelNotes-1.7.0.txt 中有说明:“"git status" 不再是 "git commit --dry-run"。” 如果需要这个功能,请使用 git commit --dry-run -a 命令。正如其他人所说,只需更新索引,git status 命令就能按照 OP 的预期工作。 - Chris Johnsen
3
如果您使用普通的 git commit 命令,它不会提交重命名的文件,并且工作树仍然相同。 git commit -a 基本上违背了 Git 工作流程/思维模型的方方面面——每个更改都会被提交。如果您只想重命名文件,但希望将对 index.html 的更改在另一个提交中进行,怎么办? - knittl
6
好的回答!我曾经很困惑为什么 git status 没有检测到重命名。在添加了我的“新”文件后运行 git commit -a --dry-run,展示了重命名并最终使我有信心提交! - stephen.hanson
3
在 Git 1.9.1 中,“git status”现在的行为类似于“git commit”。 - Jacques René Mesrine
对我不起作用。我的git版本是2.34.1。我也尝试了https://dev59.com/J3E85IYBdhLWcg3wvGAR#2641227。 - ZeZNiQ
显示剩余4条评论

97

在 Git 将文件视为移动之前,您必须将这两个修改过的文件添加到索引中。

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

mv old new 然后使用 git add -A 也可以实现同样的效果。

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

请参见「git add -A」和「git add .」的区别


3
谢谢提供git add -A链接,非常有用,因为我一直在寻找这样的快捷方式! - PhiLho
12
注意,在 Git 2 中,git add . 命令确实会将删除操作添加到索引中。 - Nick McCurdy

63

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

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

由于原始文件(即css/mobile.css)之前已经在提交的文件中,因此无需进行git add操作。


12
其他答案过于复杂,这个简单明了。它保留文件提交历史记录,确保在文件重命名前后进行合并时不会出现问题。 - Phlucious

19

最好的方法是亲自尝试。

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 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实现将这两个命令视为不同的命令。

故事寓意:如果你不确定文件是否被重命名,请发出“git commit --dry-run -a”命令。如果显示文件已被重命名,则可以继续进行。


3
就Git而言,“两者都是正确的”。后者更接近您作为提交者可能看到的方式。但是,重命名和删除+创建之间的真正区别仅在于操作系统/文件系统级别(例如相同的inode#与新的inode#),但Git并不是非常关心这些细节。 - Alois Mahdal

16

步骤1:将文件从旧名称oldfile改为新名称newfile

git mv #oldfile #newfile

步骤2:git commit并添加注释

git commit -m "rename oldfile to newfile"

步骤三:将此更改推送到远程服务器

git push origin #localbranch:#remotebranch

1
请添加一些注释,以便对OP有所帮助。 - Devrath
1
第二步是不必要的。在git mv之后,新文件已经在索引中了。 - Alois Mahdal
如果有的话,哪些#是字面值? - Peter Mortensen
@PeterMortensen 没有其中任何一个。不确定他为什么在那里放它们,因为它们是 bash 的注释字符。在这里查看:https://git-scm.com/docs/git-push - Buttle Butkus

11

你需要执行 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
5
「@Greg K: git rm不会删除历史记录。它只会从索引中删除一个条目,以便下一次提交不包含该条目。但是,它仍将存在于祖先提交中。 你可能感到困惑的是(例如)git log -- new将在您提交git mv old new的点停止。如果要跟踪重命名,请使用git log --follow -- new。」 - Chris Johnsen

9

让我们从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才能从内容中识别它。


1
没错。当暂存后,Git会正确显示重命名。 - Max MacLeod

3

你没有对Finder移动的结果进行分阶段处理。我认为如果您通过Finder进行移动,然后执行git add css/mobile.css ; git rm css/iphone.css,Git将计算新文件的哈希值,然后才会意识到文件的哈希值匹配(因此这是一个重命名)。


2

如果你必须手动重命名文件,例如使用脚本批量重命名一堆文件,那么使用 git add -A . 对我来说是有效的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接