Search code examples
gitgithubgit-mv

Odd `git mv` behaviour


Last month I started to contribute to a GitHub repository by forking the corresponding repo, creating a feature branch and then submitting a pull request. While repeating that process for a couple of days, I ran into a weird problem when renaming files with the pre-installed Linux command mv and also with the Git command git mv.

The actual problem is, that depending on when you move/rename a file with git mv, when you git add it and at what point you edit the renamed file, you either get:

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

        renamed:    somethingelse -> something

Or this:

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

        new file:   something
        deleted:    somethingelse

To demonstrate this, I have written a test:

#!/bin/bash

# To my knowledge, this “problem” only occurs with new files in a Git repo
printf "COMMAND: mkdir -v gitrepo\n\n"
mkdir -v gitrepo

printf "\nCOMMAND: cd gitrepo\n\n"
cd gitrepo

printf "\nCOMMAND: git init\n\n"
git init

printf "\nCOMMAND: git status\n\n"
git status

printf "\nCOMMAND: touch something\n\n"
touch something

printf "\nCOMMAND: git status\n\n"
git status

printf "\nCOMMAND: git add something\n\n"
git add something

printf "\nCOMMAND: git status\n\n"
git status

printf '\nCOMMAND: git commit -m "Added something"\n\n'
git commit -m "Added something"

printf "\nCOMMAND: git status\n\n"
git status

printf "\nCOMMAND: git mv something somethingelse\n\n"
git mv something somethingelse

printf "\nCOMMAND: git status\n\n"
git status

# Type in the following on line 1: First line of code
printf "\nCOMMAND: vim somethingelse\n\n"
vim somethingelse

printf "\nCOMMAND: git status\n\n"
git status

printf "\nCOMMAND: git add somethingelse\n\n"
git add somethingelse

printf "\nCOMMAND: git status\n\n"
git status

printf '\nCOMMAND: git commit -m "Renamed something to somethingelse and edited somethingelse"\n\n'
git commit -m "Renamed something to somethingelse and edited somethingelse"

printf "\nCOMMAND: git status\n\n"
git status

printf "\nCOMMAND: git mv somethingelse something\n\n"
git mv somethingelse something

printf "\nCOMMAND: git status\n\n"
git status

# If you add something to the first line, the rename will not be detected by Git
# However, if you instead create 2 newlines and fill line 3 with new code,
# the rename gets detected for whatever reason
printf "\nCOMMAND: vim something\n\n"
vim something

printf "\nCOMMAND: git status\n\n"
git status

printf "\nCOMMAND: git add something\n\n"
git add something

printf "\nCOMMAND: git status\n\n"
git status

printf '\nCOMMAND: git commit -m "Renamed somethingelse to something and edited something"\n\n'
git commit -m "Renamed somethingelse to something and edited something"

printf "\nCOMMAND: git status\n\n"
git status

cd .. && rm -fr gitrepo && printf "\nREMOVED gitrepo folder\n"
printf "\nDONE.\n"

For some reason, this mostly affects “new files” and not the ones which already exist in a repository. If you clone my fork of the Spoon-Knife repository with git clone https://github.com/christianheinrichs/Spoon-Knife.git for example and then apply the work flow of the linked test script, you will see that in most cases you will be able to rename the README.md file to README for example, edit it and it will still count as a rename instead of a new file/deleted split.

Although I could reproduce the new file/deleted behavior on the cloned Spoon-Knife fork repo, I am not exactly sure how I did that and believe me when I say that I tried to figure it out.

So what exactly is going on here that I don't understand?

See: https://gist.github.com/christianheinrichs/e50bfdd5eec70a606fa6ce4a88c5951b#file-git_mv-test-sh-L65


Solution

  • git does not keep a flag saying "this newname file was initially called oldname file" :

    git mv oldname newname
    
    # is exactly equivalent to :  
    
    mv oldname newname
    git rm oldname
    git add newname
    

    When displaying the status of a file, git tries to guess if it was a rename or a delete + add by comparing the content of the files, and seeing how similar they are.

    So : if you start by git mv a file, and then edit the file, depending on how much the file is modified, git may or may not be able to see that it all started with a mv.

    See also the answer to this question : How does Git know that file was renamed?