git-mv 的目的是啥?

Posted

技术标签:

【中文标题】git-mv 的目的是啥?【英文标题】:What's the purpose of git-mv?git-mv 的目的是什么? 【发布时间】:2010-11-08 19:39:40 【问题描述】:

据我了解,Git 并不真正需要跟踪 file 重命名/移动/复制操作,那么真正的目的是什么 git mv 的?手册页没有特别的描述性......

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

【问题讨论】:

【参考方案1】:

也许git mv 自从这些答案发布以来已经改变,所以我将简要更新一下。在我看来,git mv准确地描述为以下简称:

 # not accurate: #
 mv oldname newname
 git add newname
 git rm oldname

我经常使用 git mv 有两个原因,之前的答案中没有描述:

    移动大型目录结构,其中包含已跟踪和未跟踪文件的混合内容。跟踪和未跟踪的文件都将移动,并保持其跟踪/取消跟踪状态

    移动大的文件和目录,我一直认为git mv 会减少存储库数据库历史大小的大小。这是因为移动/重命名文件是索引/参考增量。我没有验证这个假设,但它似乎合乎逻辑。

【讨论】:

【参考方案2】:

git mv 移动文件,更新索引以记录替换的文件路径,以及更新任何受影响的 git 子模块。与手动移动不同,它还检测仅大小写的重命名,否则 git 不会将其检测为更改。

在行为上与将文件从外部移动到 git、使用 git rm 从索引中删除旧路径并使用 git add 将新路径添加到索引中的行为相似(尽管不相同)。

回答动机

这个问题有很多很好的部分答案。这个答案试图将它们组合成一个有凝聚力的答案。此外,任何其他答案都没有提到的一件事是 man page 实际上确实主要回答了这个问题,但它可能没有那么明显。

详细说明

手册页中列出了三种不同的效果:

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

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

    索引已更新,添加新路径并删除之前的路径:

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

    移动的子模块已更新以在新位置工作:

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

this answer 中提到,git mv 与移动文件、将新路径添加到索引以及从索引中删除之前的路径非常相似:

mv oldname newname
git add newname
git rm oldname

然而,正如this answer 指出的那样,git mv 在行为上并不严格相同。通过git mv 移动文件会将新路径添加到索引中,但不会添加文件中的任何修改内容。另一方面,使用三个单独的命令将整个文件添加到索引中,包括任何修改的内容。这在使用修补索引而不是在文件中添加所有更改的工作流时可能是相关的。

此外,如 this answer 和 this comment 中所述,git mv 具有处理 case-insensitive 但 case-preserving 文件系统上的仅大小写重命名的额外好处,这在当前的 macOS 中经常出现和 Windows 文件系统。例如,在此类系统中,通过mv Mytest.txt MyTest.txt 移动文件后,git 不会检测到文件名已更改,而使用git mv Mytest.txt MyTest.txt 会成功更新其名称。

【讨论】:

较新版本的 git 中的 git mv 如何简化子模块的重定位是巨大的工作流程改进。【参考方案3】:

正如@Charles 所说,git mv 是一种速记。

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

Linus 在http://permalink.gmane.org/gmane.comp.version-control.git/217 以特有的机智解释:

请停止这种“跟踪文件”的废话。 Git 跟踪准确什么是重要的, 即“文件集合”。没有其他东西是相关的,甚至 认为它是相关的,只会限制你的世界观。注意如何 CVS“注释”的概念总是不可避免地最终限制人们如何使用 它。我认为这是一个完全没用的废话,我已经描述过 我认为有用一百万倍的东西,结果都失败了 完全正确,因为我并没有将我的想法限制在错误的模型上 世界。

【讨论】:

【参考方案4】:

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

【讨论】:

【参考方案5】:

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

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

【讨论】:

+1 获取详细说明。如果文件在 git 中移动,我一直在寻找日志历史记录中可能发生的问题,您的回答真的很有趣。谢谢!顺便说一句,你知道在 git 中移动文件时我们应该避免的任何其他陷阱吗? (或任何你可以指出的参考......不是很幸运的谷歌搜索) 好吧,我的例子是悲观的。当文件为空时,正确解释更改会更加困难。我想如果你只是在每组重命名后提交,你应该没问题。【参考方案6】:

git mv 还有一个用途,上面没有提到。

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

git mv 如何适应?如果直接移动文件然后使用git rmgit add,所有更改都会添加到索引中,并且使用 git diff 查看更改不太容易(在提交之前)。但是,使用 git mv 会将新路径添加到索引中,但不会对文件进行更改,从而允许 git diffgit add -p 照常工作。

【讨论】:

【参考方案7】:

来自official GitFaq:

Git 有一个重命名命令git mv,但这只是为了方便。效果 与删除文件并添加另一个文件没有区别 名称和内容相同

【讨论】:

那么您是否丢失了文件历史记录?我认为重命名会保留该目录的旧历史...... 嗯,是的,也不是。阅读上面关于重命名的官方 GitFaq 链接,然后阅读 Linus Torvalds 冗长的电子邮件,了解他为什么不喜欢 SCM 工具跟踪文件的概念:permalink.gmane.org/gmane.comp.version-control.git/217 @WillHancock 我现在使用 git 的次数更多,并且可以更明确地回答您:根据您的 git 客户端及其选项,如果文件更改,您将能够通过重命名跟踪文件内部足够小以至于它认为它是一个重命名。如果你更改文件太多并重命名它,git 不会检测到它 - 从某种意义上说,它是在说“不,你不妨考虑一个完全不同的文件!” @AdamNofsinger “太多”到底是多少?如果像 Torvalds 所说的那样在“搜索时间”中跟踪可能的重命名,那么必须有一个明确的定义,明确定义每个客户端特定的重命名,例如90% 的字符或行都变了……是这样吗? @AdamNofsinger 该链接已失效。这是一面镜子:web.archive.org/web/20150209075907/http://…【参考方案8】:
git mv oldname newname

只是以下的简写:

mv oldname newname
git add newname
git rm oldname

即它会自动更新旧路径和新路径的索引。

【讨论】:

它还内置了一些安全措施。 谢谢@CharlesBailey - git 是否认为文件 newNameFile 和 oldNameFile 不同?如果是,如果我们想合并它们会发生什么?假设我们在分支 A 上分支一个 ant 项目并创建分支 B,然后在 B 上 mavenize 项目。文件名相同,但随着项目结构的变化放在不同的路径上。假设两个分支并行增长了一段时间。在某些时候,如果我们想合并项目,git如何知道它是同一个文件,只是重命名了它的路径? (如果“git mv”==“git add + git rm”) @SergeyOrshanskiy 如果mv oldname newname; git add newname; git rm oldname 的自动检测出错,git mv oldname newname 也会出错(参见this answer)。 请注意git mvmv oldname newname; git add newname; git rm oldname 略有不同,因为如果您在git mv 之前对文件进行了更改,这些更改将不会暂存,直到您git add新文件。 git mv 正在做一些不同的事情,因为它处理文件名大小写的变化(foo.txt 到 Foo.txt),而这些命令单独运行则不(在 OSX 上)

以上是关于git-mv 的目的是啥?的主要内容,如果未能解决你的问题,请参考以下文章

Docker,它是啥,目的是啥

AsQueryable() 的目的是啥?

@ConditionalOnProperty 注释的目的是啥?

toBeFalsy() 的目的是啥?

移位的目的是啥? [关闭]

docker build 输出的目的是啥?