据我了解,Git 并不真正需要跟踪文件重命名/移动/复制操作,那么真正的目的是git mv
什么?手册页不是特别描述...
它已经过时了吗?它是一个内部命令,不打算由普通用户使用吗?
git mv oldname newname
只是以下的简写:
mv oldname newname
git add newname
git rm oldname
即它会自动更新旧路径和新路径的索引。
来自官方 GitFaq:
Git 有一个重命名命令
git mv
,但这只是为了方便。效果与删除文件并添加另一个具有不同名称和相同内容的文件没有区别
Git is just trying to guess for you what you are trying to do. It is making every attempt to preserve unbroken history. Of course, it is not perfect. So git mv
allows you to be explicit with your intention and to avoid some errors.
Consider this example. Starting with an empty repo,
git init
echo "First" >a
echo "Second" >b
git add *
git commit -m "initial commit"
mv a c
mv b a
git status
Result:
# 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")
Autodetection failed :( Or did it?
$ 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
and then
$ 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
Now try instead (remember to delete the .git
folder when experimenting):
git init
echo "First" >a
echo "Second" >b
git add *
git commit -m "initial commit"
git mv a c
git status
So far so good:
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# renamed: a -> c
git mv b a
git status
Now, nobody is perfect:
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: a
# deleted: b
# new file: c
#
Really? But of course...
git add *
git commit -m "change"
git log c
git log --follow c
...and the result is the same as above: only --follow
shows the full history.
Now, be careful with renaming, as either option can still produce weird effects. Example:
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
Contrast it with:
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
Result:
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... Now the history is going back to initial a instead of initial b, which is wrong. So when we did two moves at a time, Git became confused and did not track the changes properly. By the way, in my experiments the same happened when I deleted/created files instead of using git mv
. Proceed with care; you've been warned...
正如@Charles 所说,git mv
是一种速记。
这里真正的问题是“其他版本控制系统(例如 Subversion 和 Perforce)特别对待文件重命名。为什么 Git 不这样做?”
Linus 在http://permalink.gmane.org/gmane.comp.version-control.git/217以特有的机智解释:
Please stop this "track files" crap. Git tracks exactly what matters, namely "collections of files". Nothing else is relevant, and even thinking that it is relevant only limits your world-view. Notice how the notion of CVS "annotate" always inevitably ends up limiting how people use it. I think it's a totally useless piece of crap, and I've described something that I think is a million times more useful, and it all fell out exactly because I'm not limiting my thinking to the wrong model of the world.
There's a niche case where git mv
remains very useful: when you want to change the casing of a file name on a case-insensitive file system. Both APFS (mac) and NTFS (windows) are, by default, case-insensitive (but case-preserving).
greg.kindel mentions this in a comment on CB Bailey's answer.
Suppose you are working on a mac and have a file Mytest.txt
managed by git. You want to change the file name to MyTest.txt
.
You could try:
$ 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
Oh dear. Git doesn't acknowledge there's been any change to the file.
You could work around this with by renaming the file completely then renaming it back:
$ 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
Hurray!
Or you could save yourself all that bother by using 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
There's another use I have for git mv
not mentioned above.
Since discovering git add -p
(git add's patch mode; see http://git-scm.com/docs/git-add), I like to use it to review changes as I add them to the index. Thus my workflow becomes (1) work on code, (2) review and add to index, (3) commit.
How does git mv
fit in? If moving a file directly then using git rm
and git add
, all changes get added to the index, and using git diff to view changes is less easy (before committing). Using git mv
, however, adds the new path to the index but not changes made to the file, thus allowing git diff
and git add -p
to work as usual.
git mv
moves the file, updating the index to record the replaced file path, as well as updating any affected git submodules. Unlike a manual move, it also detects case-only renames that would not otherwise be detected as a change by git.
It is similar (though not identical) in behavior to moving the file externally to git, removing the old path from the index using git rm
, and adding the new one to the index using git add
.
This question has a lot of great partial answers. This answer is an attempt to combine them into a single cohesive answer. Additionally, one thing not called out by any of the other answers is the fact that the man page actually does mostly answer the question, but it's perhaps less obvious than it could be.
Three different effects are called out in the man page:
The file, directory, or symlink is moved in the filesystem:
git-mv - Move or rename a file, a directory, or a symlink
The index is updated, adding the new path and removing the previous one:
The index is updated after successful completion, but the change must still be committed.
Moved submodules are updated to work at the new location:
Moving a submodule using a gitfile (which means they were cloned with a Git version 1.7.8 or newer) will update the gitfile and core.worktree setting to make the submodule work in the new location. It also will attempt to update the submodule.<name>.path setting in the gitmodules(5) file and stage that file (unless -n is used).
As mentioned in this answer, git mv
is very similar to moving the file, adding the new path to the index, and removing the previous path from the index:
mv oldname newname
git add newname
git rm oldname
However, as this answer points out, git mv
is not strictly identical to this in behavior. Moving the file via git mv
adds the new path to the index, but not any modified content in the file. Using the three individual commands, on the other hand, adds the entire file to the index, including any modified content. This could be relevant when using a workflow which patches the index, rather than adding all changes in the file.
Additionally, as mentioned in this answer and this comment, git mv
has the added benefit of handling case-only renames on file systems that are case-insensitive but case-preserving, as is often the case in current macOS and Windows file systems. For example, in such systems, git would not detect that the file name has changed after moving a file via mv Mytest.txt MyTest.txt
, whereas using git mv Mytest.txt MyTest.txt
would successfully update its name.
Maybe git mv
has changed since these answers were posted, so I will update briefly. In my view, git mv
is not accurately described as short hand for:
# not accurate: #
mv oldname newname
git add newname
git rm oldname
I use git mv frequently for two reasons that have not been described in previous answers:
Moving large directory structures, where I have mixed content of both tracked and untracked files. Both tracked and untracked files will move, and retain their tracking/untracking status
Moving files and directories that are large, I have always assumed that git mv
will reduce the size of the repository DB history size. This is because moving/renaming a file is indexation/reference delta. I have not verified this assumption, but it seems logical.