What's the purpose of git-mv?
Asked Answered
F

9

406

From what I understand, Git doesn't really need to track file rename/move/copy operations, so what's the real purpose of git mv? The man page isn't particularly descriptive...

Is it obsolete? Is it an internal command, not meant to be used by regular users?

Furring answered 7/7, 2009 at 19:22 Comment(0)
F
542
git mv oldname newname

is just shorthand for:

mv oldname newname
git add newname
git rm oldname

i.e. it updates the index for both old and new paths automatically.

Fumed answered 7/7, 2009 at 19:42 Comment(10)
Also it has a few safeties built in.Liederman
Thanks @CharlesBailey - Does git then consider the files newNameFile and oldNameFile as different? If yes, what happens if we want to merge them? Say we branch an ant project on branch A and create Branch B then mavenize projects on B. The file names are the same but put on different paths as the project structure changed. Say both branches grew for some time in parallel. At some point if we want to merge the projects how will git know that it's the same file just renamed it's path? (if "git mv" == "git add + git rm")Alexis
I guess, it is the same thing just with 99.9999% probability. Obviously, the auto-detection could go wrong if you have e.g. multiple files with the same name and/or the same content.Cartilaginous
@SergeyOrshanskiy If auto detection goes wrong for mv oldname newname; git add newname; git rm oldname, it will also go wrong for git mv oldname newname (see this answer).Samaniego
Note that git mv is slightly different from the mv oldname newname; git add newname; git rm oldname, in that if you made changes to the file before git mving it, those changes won't be staged until you git add the new file.Samaniego
git mv is doing something different, as it handles changes in filename case (foo.txt to Foo.txt) while those commands run individually do not (on OSX)Perr
@JakubNarębski can you be specific about the built-in safeties of git mv?Dow
@Dow for one, it handles modern submodules right (updating path in other places). Second, it won't touch files that are not under version control.Liederman
The submodule update is mentioned in the man page: "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)."Lowry
This is not true. If you have a folder with a mix of tracked and untracked files, then git mv will preserve this for you in the newly created folder. Although the OP emphasized file operation, the operation is valid for folders.Stalingrad
S
98

From the official GitFaq:

Git has a rename command git mv, but that is just a convenience. The effect is indistinguishable from removing the file and adding another with different name and the same content

Sissel answered 9/6, 2010 at 21:30 Comment(7)
So do you loose the file history? I was presume rename would keep the old history for that directory...Otherwise
Well, yes and no. Read the official GitFaq link above about renames, and then read Linus Torvalds lengthy e-mail about why he doesn't like the notion of an SCM tool tracking files: permalink.gmane.org/gmane.comp.version-control.git/217Sissel
@WillHancock I have used git a bit more now, and can answer you more definitively: depending on your git client and its options, you will be able to trace the file past the rename if the file changed internally little enough that it considers it a rename. If you change the file too much AND rename it though, git won't detect it - in a sense it is saying "no, you might as well consider that a completely different file!"Sissel
@AdamNofsinger how much exactly is "too much"? If a possible rename is tracked in "search time" as Torvalds has stated, then there must be a clear definition of a rename specific for each client, e.g. 90% of the characters or lines have changed... Is that so?Unsphere
@AdamNofsinger that link is dead. Here's a mirror: web.archive.org/web/20150209075907/http://…Anthurium
A better mirror that doesn't stress the Archive.org as much : lore.kernel.org/git/…Crusade
"In other words, I'm right. I'm always right, but sometimes I'm more right than other times. And dammit, when I say "files don't matter", I'm really really Right(tm)." Peak LInus.Indonesia
C
48

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...

Cartilaginous answered 12/10, 2013 at 4:39 Comment(2)
+1 for the detailed explanation. I've been looking for problems that might happen in log history if files are moved in git, your answer was really interesting. Thank you! Btw, do you know any other pitfalls that we should avoid while moving files in git? (or any reference you could point to.... not very lucky googling for it)Millman
Well, my examples are pessimistic. When the files are empty, it is much more difficult to properly interpret the changes. I imagine that if you just commit after every set of renames, you should be fine.Cartilaginous
R
40

As @Charles says, git mv is a shorthand.

The real question here is "Other version control systems (eg. Subversion and Perforce) treat file renames specially. Why doesn't Git?"

Linus explains at http://permalink.gmane.org/gmane.comp.version-control.git/217 with characteristic tact:

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.

Rikkiriksdag answered 23/5, 2013 at 10:5 Comment(0)
S
19

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
Saleh answered 18/4, 2019 at 14:45 Comment(0)
L
17

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.

Answer Motivation

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.

Detailed Explanation

Three different effects are called out in the man page:

  1. The file, directory, or symlink is moved in the filesystem:

    git-mv - Move or rename a file, a directory, or a symlink

  2. 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.

  3. 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.

Lowry answered 16/1, 2021 at 9:17 Comment(1)
How git mv in newer versions of git allows simplified relocation of submodules is massive workflow improvement.Spoon
B
12

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.

Bringhurst answered 12/2, 2014 at 9:41 Comment(0)
P
2

There is a 2023 discussion advocating for Git to record renames explicitly.
(Note: still a discussion, certainly not yet a feature).

It includes a comment regarding 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.

And:

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

Which yields:

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
Polyphone answered 4/5, 2023 at 6:53 Comment(0)
S
1

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:

  1. 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

  2. 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.

Stalingrad answered 27/2, 2021 at 12:50 Comment(13)
Logic I too share, but as Spock would say, "It is not logical" (with the one eyebrow going up).Miner
#2 is definitely not correct. git addresses objects, including files, based on a hash of their contents, not their location. If two files have the same content, regardless of their name and location, they will be considered the same object, and only stored once. Not only does it not matter how you rename a file, you can have multiple copies at the same time, or reintroduce a file you deleted 20 commits ago, and git will simply calculate the hash, find that the object is already in the database, and not store anything new.Varga
@IMSoP, I think your use of the term "addresses" is ambigous. of course Git handles locations of files and folders, and uses that to distinguish different files. If two files have the same content and even the same name filename (just different path), they canno t be connected by GIT in the way you say, because it would break tracking of history of the files. You can test this easily by checking git status on any repo. See the difference after doing a git mv, vs doing a regular mv, followed by a git addStalingrad
spoiler... if you do git mv, your file will be listed by git status as "renamed" to a new location, while if you do mv + git add... you will have your old file listed as deleted, and the new file listed as a new file in stagingStalingrad
In git (and in most file systems), the location and name of a file aren't actually properties of the file, they're part of the containing directory. If you add a file called "test", git will: 1) calculate the SHA-1 hash of its content, let's say "abc123"; 2) store the content in its database as entry "abc123"; 3) add a line to the current "tree" saying "blob abc123 test". If you add a file "test2" with the same content, it will 1) calculate the same SHA-1 hash; 2) find that the object already exists, so doesn't need to be stored; 3) add a line to the tree saying "blob abc123 test2".Varga
As for when files show as "renamed", this information is not stored anywhere in the repository, it is detected on demand when traversing the history. In your example, you are missing the git rm step, which stages the deletion of the file under the old name; once you do that, you will see that git status shows the file as renamed, just as it did with git mv. Other version control systems do treat files or directories as fundamental units, and track renames; git simply stores snapshots of the repository, and calculates everything else as needed.Varga
To see this in action, try creating an empty directory and running this sequence of commands: git init; echo 'hello' > test; git add test; git commit -m initial; git cat-file -p HEAD^{tree}; cp test test2; git add test2; git commit -m duplicated; git cat-file -p HEAD^{tree}; git rm test; git commit -m deleted; git cat-file -p HEAD^{tree}; git diff HEAD^^ HEAD You can even see the objects created with ls .git/objects/* - there will be 7 objects: 3 commits, 3 trees, and 1 blob, no matter how many duplicates you commit.Varga
I agree with the results that you get from your commands, and it makes perfect sense to me. What I have always assumed is that GIT leverages the history of files through diffs. Perhaps that is not the case.... but if so, when you move a file using git mv, you are conneccting it with its previous version... this enables for better encoding. But, as I said, this I have not verified. It would take a slightly more intricate testing procedure than what you outlined... but not very complicated. Maybe I will get around to do that test, since you put the effort in above (for the single commit test).Stalingrad
such a test would include putting in a lot of changes in files that are versioned, and doing git mv vs mv + git add inbetween the commits.Stalingrad
@StefanKarlsson It may sound strange, but git does not store changes, of any sort. Every time you create a commit, what git stores is the state of the entire repository, nothing more. Everything else is calculated by looking back at those snapshots. When you ask git "what changed in this commit?" what it actually tells you is "what is the difference between this commit and its parent(s)?" When you ask "when did this file last change?" it actually tells you "which is the most recent ancestor of the current commit where this file had a different hash, and therefore different content?"Varga
@StefanKarlsson You might be interested in reading up on the internals of git, e.g. in the official Book. You'll see that there simply isn't anywhere for git to store the information "test was renamed to test2"Varga
Thanks for the info. I am as deep into the nitty gritty of GIT as I would like to be though. I will consider your points as very likely true for now.Stalingrad
Point 1 in my answer still holds though, and is very easy to verify.Stalingrad

© 2022 - 2024 — McMap. All rights reserved.