git-mv的目的是什么?

400

据我所知,Git 实际上不需要跟踪文件的重命名/移动/复制操作,那么 git mv 的真正目的是什么?手册并没有特别详细的说明...

它是否已经过时了?它是一种内部命令,不适用于普通用户使用吗?

9个回答

536
git mv oldname newname

是简写形式:

mv oldname newname
git add newname
git rm oldname

也就是说,它会自动更新旧路径和新路径的索引。


55
它还内置了一些安全措施。 - Jakub Narębski
11
谢谢 @CharlesBailey - Git是否将newNameFile和oldNameFile视为不同的文件? 如果是,如果我们想要合并它们,会发生什么? 假设我们在分支A上分支了一个ant项目,然后创建了分支B,然后在B上执行mavenize操作。 文件名相同但位于不同的路径上,因为项目结构已更改。 假设两个分支在某段时间内并行增长。 如果我们想要合并这些项目,Git如何知道它是相同的文件,只是重命名了路径? (如果“git mv”==“git add + git rm”) - Rose
7
如果mv oldname newname; git add newname; git rm oldname的自动检测出现问题,那么git mv oldname newname也会出现问题(参见这个答案)。 - Ajedi32
12
请注意,git mvmv oldname newname; git add newname; git rm oldname略有不同。如果在git mv之前更改了文件,则这些更改直到您git add新文件后才会被暂存。 - Ajedi32
5
git mv 做了不同的事情,因为它处理文件名大小写的更改(例如从 foo.txt 改为 Foo.txt),而独立运行这些命令时则不会(在OSX上)。 - Grae Kindel
显示剩余5条评论

96

来自官方Git常见问题解答

Git有一个重命名命令git mv,但这只是一种便利。其效果与删除文件并添加另一个具有不同名称但相同内容的文件不可区分。


19
你是指文件历史记录会丢失吗?我原以为重命名后该目录的旧历史记录会保留下来... - Will Hancock
24
是的,也不完全是。请阅读上面的GitFaq官方链接中关于重命名的内容,并阅读Linus Torvalds长篇电子邮件,了解他为什么不喜欢SCM工具跟踪文件的概念:http://permalink.gmane.org/gmane.comp.version-control.git/217 - Adam Nofsinger
4
我已经更多地使用了git,并可以更明确地回答你:根据你的git客户端及其选项,如果文件的内部更改不大,被认为是重命名,则你将能够追踪到该文件的过去。但是,如果你修改文件太多并将其重命名,git将无法检测到它 - 在某种程度上,它表示“不,你可以将其视为完全不同的文件!” - Adam Nofsinger
1
@AdamNofsinger,“太多”到底是多少?如果像Torvalds所说的那样,在“搜索时间”中跟踪可能的重命名,那么必须为每个客户端定义一个明确的重命名规范,例如90%的字符或行已更改...是这样吗? - osolmaz
10
@AdamNofsinger,该链接已失效。这里有个备份链接:https://web.archive.org/web/20150209075907/http://permalink.gmane.org:80/gmane.comp.version-control.git/217 - Carl Walsh
2
一个更好的镜像,不会给Archive.org带来太大的压力:https://lore.kernel.org/git/Pine.LNX.4.58.0504150753440.7211@ppc970.osdl.org/ - Milind R

48

Git试图猜测您试图做什么,它会尽一切努力保留不间断的历史。当然,它并不完美。因此,git mv允许您明确表达您的意图并避免一些错误。

考虑这个例子。从一个空的代码库开始,

git init
echo "First" >a
echo "Second" >b
git add *
git commit -m "initial commit"
mv a c
mv b a
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)
#
#   modified:   a
#   deleted:    b
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   c
no changes added to commit (use "git add" and/or "git commit -a")

自动检测失败 :( 或者说没有?

$ git add *
$ git commit -m "change"
$ git log c

commit 0c5425be1121c20cc45df04734398dfbac689c39
Author: Sergey Orshanskiy <*****@gmail.com>
Date:   Sat Oct 12 00:24:56 2013 -0400

    change

然后

$ git log --follow c

Author: Sergey Orshanskiy <*****@gmail.com>
Date:   Sat Oct 12 00:24:56 2013 -0400

    change

commit 50c2a4604a27be2a1f4b95399d5e0f96c3dbf70a
Author: Sergey Orshanskiy <*****@gmail.com>
Date:   Sat Oct 12 00:24:45 2013 -0400

    initial commit

现在请尝试使用以下方式(请记得在实验时删除 .git 文件夹):

git init
echo "First" >a
echo "Second" >b
git add *
git commit -m "initial commit"
git mv a c
git status

目前为止都还不错:

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


git mv b a
git status

现在,没有人是完美的:

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   a
#   deleted:    b
#   new file:   c
#

真的吗?当然...

git add *
git commit -m "change"
git log c
git log --follow c

...结果与上面相同:只有--follow显示完整历史记录。


现在,重命名要小心,因为任何选项都可能导致奇怪的影响。例如:

git init
echo "First" >a
git add a
git commit -m "initial a"
echo "Second" >b
git add b
git commit -m "initial b"

git mv a c
git commit -m "first move"
git mv b a
git commit -m "second move"

git log --follow a

commit 81b80f5690deec1864ebff294f875980216a059d
Author: Sergey Orshanskiy <*****@gmail.com>
Date:   Sat Oct 12 00:35:58 2013 -0400

    second move

commit f284fba9dc8455295b1abdaae9cc6ee941b66e7f
Author: Sergey Orshanskiy <*****@gmail.com>
Date:   Sat Oct 12 00:34:54 2013 -0400

    initial b

与之对比:

git init
echo "First" >a
git add a
git commit -m "initial a"
echo "Second" >b
git add b
git commit -m "initial b"

git mv a c
git mv b a
git commit -m "both moves at the same time"

git log --follow a

结果:

commit 84bf29b01f32ea6b746857e0d8401654c4413ecd
Author: Sergey Orshanskiy <*****@gmail.com>
Date:   Sat Oct 12 00:37:13 2013 -0400

    both moves at the same time

commit ec0de3c5358758ffda462913f6e6294731400455
Author: Sergey Orshanskiy <*****@gmail.com>
Date:   Sat Oct 12 00:36:52 2013 -0400

    initial a

糟糕...现在历史记录回到了初期a而不是初期b,这是错误的。因此,当我们一次进行两个移动操作时,Git会变得混淆,并且无法正确跟踪更改。顺便说一下,在我的实验中,当我删除/创建文件而不使用git mv时也发生了同样的事情。请小心处理;你已经被警告了...


7
谢谢!+1 详细的解释。我一直在寻找如果在Git中移动文件可能会发生的历史记录问题,你的答案真的很有趣。顺便问一下,您是否知道在移动Git中的文件时应避免的任何其他陷阱?(或者您可以提供的任何参考资料...在谷歌上没有找到幸运的结果。) - pabrantes
2
好的,我的例子比较悲观。当文件为空时,正确解释更改会更加困难。我想如果你在每次重命名后都进行提交,那么应该没问题。 - Sergey Orshanskiy

39

正如@Charles所说,git mv是一个缩写。

这里真正的问题是:“其他版本控制系统(例如Subversion和Perforce)特别处理文件重命名。为什么Git不这样做?”

Linus在http://permalink.gmane.org/gmane.comp.version-control.git/217中解释了这个问题:

请停止这种“跟踪文件”的废话。Git跟踪确切重要的东西,即“文件集合”。没有其他相关的东西,甚至思考它是否相关只会限制您的世界观。注意到CVS的“annotate”概念总是不可避免地限制人们使用它。我认为那是一堆完全没用的垃圾,并且我描述了我认为有用一百万倍的东西,所有这些都落实得恰恰因为我不把我的思维限制在错误的世界模型上


17

有一个特殊情况下,git mv非常有用:当你想在大小写不敏感的文件系统上更改文件名的大小写时。默认情况下,APFS(mac)和NTFS(Windows)都是大小写不敏感的(但会保留大小写)。

greg.kindel在CB Bailey的回答评论中提到了这一点。

假设你正在使用mac并管理着一个由git管理的文件Mytest.txt。你想把文件名改成MyTest.txt

你可以尝试:

$ mv Mytest.txt MyTest.txt
overwrite MyTest.txt? (y/n [n]) y
$ git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

哦,亲爱的。Git 没有意识到文件已经被更改。

你可以通过将文件完全重命名然后再重命名回来来解决这个问题:

$ mv Mytest.txt temp.txt
$ git rm Mytest.txt
rm 'Mytest.txt'
$ mv temp.txt MyTest.txt
$ git add MyTest.txt 
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    Mytest.txt -> MyTest.txt

太棒了!

或者你可以使用 git mv 来节省所有的麻烦:

$ git mv Mytest.txt MyTest.txt
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    Mytest.txt -> MyTest.txt

14

git mv命令可以移动文件,更新索引以记录替换文件的路径,同时更新任何受影响的git子模块。与手动移动不同,它还可以检测到只更改大小写的重命名,这些更改在git中不会被检测到。

它的行为类似于将文件在git之外移动,使用git rm从索引中删除旧路径,并使用git add将新路径添加到索引中(尽管不完全相同)。

回答动机

这个问题有很多很好的部分答案。这个答案试图将它们合并成一个单一的连贯的答案。此外,其他任何答案都没有指出的一件事是,man page实际上大部分回答了这个问题,但它可能不够明显。

详细解释

手册页中指出了三个不同的效果:

  1. 文件、目录或符号链接在文件系统中移动:

    git-mv-移动或重命名文件、目录或符号链接

  2. 更新索引,添加新路径并删除先前的路径:

    成功完成后更新索引,但仍需提交更改。

  3. 移动的子模块被更新以在新位置工作:

    使用gitfile(这意味着它们是使用Git版本1.7.8或更新版本克隆的)移动子模块将更新gitfile和core.worktree设置,使子模块在新位置中工作。它还将尝试更新gitmodules(5)文件中的submodule.<name>.path设置,并将该文件暂存(除非使用了-n选项)。

此答案所述,git mv与移动文件、将新路径添加到索引并从索引中删除先前路径非常相似:

mv oldname newname
git add newname
git rm oldname

然而,正如这个答案所指出的那样,git mv 的行为并不完全相同。通过git mv移动文件会将新路径添加到索引中,但不会添加文件中的任何修改内容。另一方面,使用三个单独的命令会将整个文件添加到索引中,包括任何修改的内容。当使用修补索引的工作流程时,这可能是相关的,而不是在文件中添加所有更改。
此外,正如这个答案这个评论中提到的那样,git mv 在处理仅大小写重命名的文件系统上具有额外的好处,这种文件系统是不区分大小写保留大小写,通常在当前macOS和Windows文件系统中存在这种情况。例如,在这些系统中,git不会检测到文件名在通过mv Mytest.txt MyTest.txt移动文件后已更改,而使用git mv Mytest.txt MyTest.txt将成功更新其名称。

在较新版本的Git中,git mv允许简化子模块的重定位,这是一个巨大的工作流改进。 - openCivilisation

12

除了上面没有提到的用法,我还有另外一种使用 git mv 的方式。

自从发现了 git add -p(git add 的 patch 模式;参见 http://git-scm.com/docs/git-add),我喜欢在将更改添加到索引时使用它来审查更改。因此我的工作流程变为 (1) 编写代码,(2) 审查并添加到索引,(3) 提交。

git mv 如何适用呢?如果直接移动文件,然后使用 git rmgit add,所有更改都会被添加到索引中,而使用 git diff 查看更改变得不那么容易(在提交之前)。但是使用 git mv 将新路径添加到索引中,而不是对文件进行更改,这样就可以像往常一样使用 git diffgit add -p 了。


2

有一个2023讨论提倡Git明确记录重命名操作。(注:仍然是一场讨论,当然 不会 是一个功能)。

它包括关于git mv的评论:

git show :0:oldname > newname
git add newname
git rm --cached
oldname mv oldname newname ```

Basically, a move but also preserving staged/unstaged contents.
But yes. How git handles renames can sometimes be a bit of a PITA.

并且:

git add file1
commit -m "Commit 1"
echo 'Changed' > file1
git mv file1 file2
git status

这将产生:

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        renamed:    file1 -> file2

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   file2

1
也许自这些答案发布以来,git mv已经发生了变化,因此我将简要更新。在我看来,git mv不能准确地被描述为:
 # not accurate: #
 mv oldname newname
 git add newname
 git rm oldname

我经常使用git mv,有两个原因在之前的答案中没有描述:
1. 移动大型目录结构,其中我混合了跟踪和未跟踪文件的内容。所有跟踪和未跟踪的文件都将移动,并保留其跟踪/未跟踪状态。
2. 移动大文件和目录,我一直认为git mv会减小存储库数据库历史记录大小。这是因为移动/重命名文件是索引/引用增量。我没有验证这个假设,但它似乎是合理的。

逻辑我也分享,但正如Spock所说,“这是不合逻辑的”(一只眉毛扬起)。 - Adrian
#2 明显是错误的。Git 根据文件内容的哈希值而非位置来寻址对象,包括文件。如果两个文件具有相同的内容,无论它们的名称和位置如何,它们都将被视为相同的对象,并且只存储一次。不仅重命名文件无关紧要,而且您可以同时拥有多个副本,或重新引入 20 次提交前删除的文件,Git 只需计算哈希值,找到该对象已经存在于数据库中,并且不会存储任何新内容。 - IMSoP
@IMSoP,我认为你使用“地址”这个术语是有歧义的。当然,Git处理文件和文件夹的位置,并使用它来区分不同的文件。如果两个文件具有相同的内容甚至相同的文件名(只是路径不同),它们不能像你说的那样被GIT连接,因为这会破坏文件历史记录的跟踪。您可以通过在任何存储库上检查git状态来轻松测试此功能。在执行git mv之后查看差异,然后执行常规mv,再执行git add。 - Stefan Karlsson
如果你使用 git mv 命令,Git 会将你的文件在 git status 中标记为“重命名”到新位置,而如果你使用 mv 命令加上 git add,你的旧文件将被标记为已删除,新文件将被标记为新文件添加到暂存区。 - Stefan Karlsson
1
我的回答中的第一点仍然成立,并且非常容易验证。 - Stefan Karlsson
显示剩余8条评论

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