511

I'd read that when renaming files in Git, you should commit any changes, perform your rename and then stage your renamed file. Git will recognise the file from the contents, rather than seeing it as a new untracked file, and keep the change history.

However, doing just this tonight I ended up reverting to git mv.

> $ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#    modified:   index.html
#

I renamed my stylesheet in Finder from iphone.css to 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

So Git now thinks I've deleted one CSS file, and added a new one. It is not what I want. Let’s undo the rename and let Git do the work.

> $ git reset HEAD .
Unstaged changes after reset:
M    css/iphone.css
M    index.html

I am back to where I began:

> $ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#    modified:   index.html
#

Let's use git mv instead:

> $ 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
#

It looks like we're good. So why didn't Git recognise the rename the first time around when I used Finder?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Greg K
  • 10,770
  • 10
  • 45
  • 62
  • 31
    Git tracks content, not files, so it doesn't matter how you get your index into the proper state - `add+rm` or `mv` - it produces the same result. Git then uses its rename/copy detection to let you know it was a rename. The source you quoted is inaccurate, too. It really doesn't matter whether you modify+rename in the same commit or not. When you do a diff across both the modify and rename, the rename detection will see it as a rename+modification, or if the modification is a total rewrite, it'll show as added and deleted - still doesn't matter how you performed it. – Cascabel Apr 14 '10 at 21:53
  • 6
    If this is true, why didn't it detect it with my rename using Finder? – Greg K Apr 14 '10 at 22:45
  • 30
    `git mv old new` automatically updates the index. When you rename outside of Git, you will have to do the `git add new` and `git rm old` to stage the changes to the index. Once you have done this `git status` will work as you expect. – Chris Johnsen Apr 15 '10 at 01:16
  • 4
    I just moved a bunch of files into a `public_html` dir, that are tracked in git. Having performed `git add .` and `git commit`, it still showed a bunch of 'deleted' files in `git status`. I performed a `git commit -a` and the deletions were commited but now I've no history on the files that live in `public_html` now. This work flow is not as smooth as I'd like. – Greg K May 08 '10 at 11:18

14 Answers14

379

For git mv the manual page says

The index is updated after successful completion, […]

So, at first, you have to update the index on your own (by using git add mobile.css). However git status will still show two different files:

$ 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
#

You can get a different output by running git commit --dry-run -a, which results in what you expect:

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
#

I can't tell you exactly why we see these differences between git status and git commit --dry-run -a, but here is a hint from Linus:

git really doesn't even care about the whole "rename detection" internally, and any commits you have done with renames are totally independent of the heuristics we then use to show the renames.

A dry-run uses the real renaming mechanisms, while a git status probably doesn't.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
tanascius
  • 53,078
  • 22
  • 114
  • 136
  • 1
    You failed to mention the step where you did `git add mobile.css`. Without it `git status -a` would only have ‘seen’ the removal of the previously tracked `iphone.css` file but would not have touched the new, untracked `mobile.css` file. Also, `git status -a` is invalid with Git 1.7.0 and later. “"git status" is not "git commit --dry-run" anymore.” in http://www.kernel.org/pub/software/scm/git/docs/RelNotes-1.7.0.txt . Use `git commit --dry-run -a` if you want this functionality. As others have said, just update the index and `git status` will just work as the OP expects. – Chris Johnsen Apr 15 '10 at 01:12
  • 3
    if you do a normal `git commit` it will not commit the renamed file and the working tree is still the same. `git commit -a` defeats pretty much every aspect of git's workflow/thinking model—every change is committed. what if you only want to rename the file, but commit changes to `index.html` in another commit? – knittl Apr 15 '10 at 05:52
  • @Chris: yep, of course I added `mobile.css` which I should have mentioned. But that's the point of my answer: the man page says that `the index is updated` when you use `git-mv`. Thanks for the `status -a` clarification, I used git 1.6.4 – tanascius Apr 15 '10 at 12:37
  • @knittl: I forgot to write that I added mobile.css before running `git status` (but you can tell that I did so by the output). I edited the answer and it should show me point much better, now – tanascius Apr 15 '10 at 12:58
  • I just expierienced a case where not even `git commit -a --dry-run` identified my renaming. However, after commit git seems to have identified it correctly. Now you know. – Ztyx Sep 23 '11 at 09:24
  • 5
    Great answer! I was banging my head against the wall trying to figure out why `git status` wasn't detecting the renaming. Running `git commit -a --dry-run` after adding my "new" files showed the renames and finally gave me confidence to commit! – stephen.hanson Sep 29 '13 at 17:53
  • 3
    In git 1.9.1 `git status` now behaves like `git commit`. – Jacques René Mesrine Oct 05 '15 at 16:11
  • [Here's an updated link for the Git 1.7.0 release notes](https://github.com/git/git/blob/master/Documentation/RelNotes/1.7.0.txt) – torek Apr 21 '19 at 17:17
  • Does not work for me. My git version is 2.34.1. I also tried https://stackoverflow.com/a/2641227/4726668 – ZeZNiQ Sep 06 '22 at 16:32
95

You have to add the two modified files to the index before Git will recognize it as a move.

The only difference between mv old new and git mv old new is that the git mv also adds the files to the index.

mv old new then git add -A would have worked, too.

Note that you can't just use git add ., because that doesn't add removals to the index.

See Difference between "git add -A" and "git add ."

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
nonrectangular
  • 2,080
  • 20
  • 9
59

For Git 1.7.x the following commands worked for me:

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

There was no need for git add, since the original file (i.e., css/mobile.css) was already in the committed files previously.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
GrigorisG
  • 888
  • 8
  • 13
  • 12
    This. All the other answers are ridiculously and needlessly complex. This maintains file history between commits so that merges before/after the file rename aren't broken. – Phlucious Oct 04 '18 at 00:13
19

The best thing is to try it for yourself.

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

Now git status and git commit --dry-run -a shows two different results where git status shows bbb.txt as a new file/ aaa.txt is deleted, and the --dry-run commands shows the actual rename.

~/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
#

Now go ahead and do the check-in.

git commit -a -m "Rename"

Now you can see that the file is in fact renamed, and what's shown in git status is wrong, at least in this case. It might be the case that underline GIT implementation treats the two commands separately.

Moral of the story: If you're not sure whether your file got renamed, issue a "git commit --dry-run -a". If it's showing that the file is renamed, you're good to go.

dimuthu
  • 865
  • 11
  • 15
  • 3
    For what matters for Git, *both are correct*. The latter is being closer to how you, as commiter probably see it, though. The *real* difference between rename and delete + create is only at the OS/filesystem level (e.g. same inode# vs. new inode#), which Git does not really care very much about. – Alois Mahdal Apr 04 '14 at 08:08
16

Step 1: rename the file from oldfile to newfile

git mv #oldfile #newfile

Step 2: git commit and add comments

git commit -m "rename oldfile to newfile"

Step 3: push this change to the remote sever

git push origin #localbranch:#remotebranch
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Haimei
  • 12,577
  • 3
  • 50
  • 36
11

You have to git add css/mobile.css the new file and git rm css/iphone.css, so Git knows about it. Then it will show the same output in git status.

You can see it clearly in the status output (the new name of the file):

# Untracked files:
#   (use "git add <file>..." to include in what will be committed)

And (the old name):

# Changed but not updated:
#   (use "git add/rm <file>..." to update what will be committed)

I think behind the scenes git mv is nothing more than a wrapper script which does exactly that: delete the file from the index and add it under a different name

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
knittl
  • 246,190
  • 53
  • 318
  • 364
  • I didn't think I had to `git rm css/iphone.css` because I thought this would remove the existing history. Maybe I'm misunderstanding the workflow in git. – Greg K Apr 14 '10 at 22:50
  • 5
    @Greg K: `git rm` will not remove history. It only removes an entry from the index so that the next commit will not have the entry. However, it will still exist in the ancestral commits. What you may be confused about is that (for example) `git log -- new` will stop at the point where you committed `git mv old new`. If you want to follow renames, use `git log --follow -- new`. – Chris Johnsen Apr 15 '10 at 01:56
9

Let's think about your files from a Git perspective.

Keep in mind Git doesn't track any metadata about your files

Your repository has (among others)

$ cd repo
$ ls
...
iphone.css
...

And it is under Git control:

$ git ls-files --error-unmatch iphone.css &>/dev/null && echo file is tracked
file is tracked

Test this with:

$ touch newfile
$ git ls-files --error-unmatch newfile &>/dev/null && echo file is tracked
(no output, it is not tracked)
$ rm newfile

When you do

$ mv iphone.css mobile.css

From a Git perspective,

  • there isn't any iphone.css (it is deleted -git warns about that-).
  • there is a new file mobile.css.
  • Those files are totally unrelated.

So, Git advises about files it already knows (iphone.css) and new files it detects (mobile.css), but only when files are in index or HEAD, Git starts to check their contents.

At this moment, neither "iphone.css deletion" nor mobile.css are in the index.

Add iphone.css deletion to the index:

$ git rm iphone.css

Git tells you exactly what has happened (iphone.css is deleted. Nothing more happened):

Then add the new file mobile.css:

$ git add mobile.css

This time both deletion and new file are in the index. Now Git detects the context is the same and expose it as a rename. In fact, if files are 50% similar, it will detect that as a rename that lets you change mobile.css a bit while keeping the operation as a rename.

See this is reproducible on git diff. Now that your files are in the index you must use --cached. Edit mobile.css a bit, add that to index, and see the difference between:

$ git diff --cached

and

$ git diff --cached -M

-M is the "detect renames" option for git diff. -M stands for -M50% (50% or more similarity will make Git express it as a rename), but you can reduce this to -M20% (20%) if you edit file mobile.css a lot.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
albfan
  • 12,542
  • 4
  • 61
  • 80
8

Git will recognise the file from the contents, rather than seeing it as a new untracked file

That's where you went wrong.

It's only after you add the file, that Git will recognize it from the content.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
hasen
  • 161,647
  • 65
  • 194
  • 231
3

You didn't stage the results of your Finder move. I believe if you did the move via Finder and then did git add css/mobile.css ; git rm css/iphone.css, Git would compute the hash of the new file and only then realize that the hashes of the files match (and thus it's a rename).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mike Seplowitz
  • 9,785
  • 1
  • 24
  • 23
2

In cases where you really have to rename the files manually, for example, using a script to batch rename a bunch of files, then using git add -A . worked for me.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
pckben
  • 867
  • 2
  • 9
  • 24
2

For Xcode users: If you're renaming your file in Xcode you see the badge icon change to append. If you do a commit using Xcode you will actually create a new file and lose the history.

A workaround is easy, but you have to do it before committing using Xcode:

  1. Do a Git status on your folder. You should see that the staged changes are correct:

    renamed:    Project/OldName.h -> Project/NewName.h
    renamed:    Project/OldName.m -> Project/NewName.m
    
  2. Do commit -m 'name change'

    Then go back to Xcode and you will see the badge changed from A to M and it is saved to commit future changes in using Xcode now.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
doozMen
  • 690
  • 1
  • 7
  • 14
2

Just ran into this issue - if you updated a bunch of files and don't want to do git mv all of them this also works:

  1. Rename parent directory from /dir/RenamedFile.js to /whatever/RenamedFile.js.
  2. git add -A to stage that change
  3. Rename parent directory back to /dir/RenamedFile.js.
  4. git add -A again, will re-stage vs that change, forcing git to pick up the filename change.
bryce
  • 3,022
  • 1
  • 15
  • 19
1

Tested on git 2.33 on Window 10

  1. Rename the folders & files you need in Windows Explorer.
  2. Run git add .
  3. Run git commit -m "commit message"
  4. Run git push

Git should detect the renaming operations. You can check by running git status (after committing)

enter image description here

Abdulrahman Bres
  • 2,603
  • 1
  • 20
  • 39
0

When I capitalised my folders & files I used Node.js' file-system to resolve this problem.

Don'f forget to make a commit before you start. I'm not sure how stable it is :)

  1. Create script file in project root.
// File rename.js

const fs = require('fs').promises;
const util = require('util');
const exec = util.promisify(require('child_process').exec);

const args = process.argv.slice(2);
const path = args[0];

const isCapitalized = (s) => s.charAt(0) === s.charAt(0).toUpperCase();

const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);

async function rename(p) {
  const files = await fs.readdir(p, { withFileTypes: true });
  files.forEach(async (file) => {
    const { name } = file;
    const newName = capitalize(name);
    const oldPath = `${p}/${name}`;
    const dumbPath = `${p}/dumb`;
    const newPath = `${p}/${newName}`;
    if (!isCapitalized(name) && name !== 'i18n' && name !== 'README.md') {
      // 'git mv' won't work if we only changed size of letters.
      // That's why we need to rename to dumb name first, then back to current.
      await exec(`git mv ${oldPath} ${dumbPath}`);
      await exec(`git mv ${dumbPath} ${newPath}`);
    }
    if (file.isDirectory()) {
      rename(newPath);
    }
  });
}

rename(path);

To rename all files in directory (or single file) script is simpler:

// rename.js

const fs = require('fs').promises;
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const args = process.argv.slice(2);
const path = args[0];

async function rename(p) {
  const files = await fs.readdir(p, { withFileTypes: true });
  files.forEach(async (file) => {
    const { name } = file;
    const currentPath = `${p}/${name}`;
    const dumbPapth = `${p}/dumb`;
    await exec(`git mv ${currentPath} ${dumbPath}`);
    await exec(`git mv ${dumbPath} ${currentPath}`);
    if (file.isDirectory()) {
      rename(newPath);
    }
  });
}

rename(path);
  1. Run the script with arguments
node rename.js ./frontend/apollo/queries

How to run shell script file or command using Node.js

Georgiy Bukharov
  • 356
  • 3
  • 10