Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

git stash restoring index state of deleted and renamed files

Tags:

git

git-stash

When stashing staged deleted or renamed files, and then unstashing them, they are restored in both their deleted and non deleted state.

In the following example state:

$ git status s

A  file0
D  file1
R  file2 -> file3
?? file4

running git stash push -k -u and then git stash pop --index will leave me with in the following state:

$ git status s

A  file0
D  file1
R  file2 -> file3
?? file1
?? file2
?? file4

I would expect to end up in the original state, without the deleted files reappearing as untracked after pop.

Any way around this?

Edit: Here's a script that recreates the issue (tested on Mac OS X 10.13.2 with git 2.16.1)

#!/usr/bin/env bash

echo -e "\nInitializing a fresh git dir..."
mkdir gitStashIssue && cd $_
rm -rf * .*
git init


echo -e "\nPreparing git state for test..."
# Create files and commit them
echo 'abc' > file1
echo 'aabbcc' > file2
echo 'aaabbbccc' > file3
echo 'aaaabbbbcccc' > file4
git add .
git commit -m 'initial commit'

# Make changes and add them to stage
echo `cat file1` >> file1
echo `cat file2` >> file2
git add .

# Make another change to a staged file without
# staging it, making it partially staged
echo `cat file1` >> file1

# Delete and rename files
git rm file3
git mv file4 fileRenamed

# Add untracked file
echo "untracked" > untrackedFile

# git status -s should now show
# MM file1
# M  file2
# D  file3
# R  file4 -> fileREnamed
# ?? untrackedFile

echo -e "\nCurrent git status is:"
git status -s

echo -e "\nStasing changes..."
git stash save -u -k

# git status -s should now show
# M  file1
# M  file2
# D  file3
# R  file4 -> fileREnamed
# ?? file3
# ?? file4

echo -e "\ngit status after stashing files is:"
git status -s

echo -e "\ncleaning up deleted and renamed files..."
git clean ./ -f

echo -e "\ngit status after cleanup:"
git status -s

echo -e "\nCommiting unstashed changes..."
git commit -m 'commit unstashed changes'

# This causes a conflict in file1
# git status -s should now show
# UU file1
# ?? untrackedFile
git stash pop --index

echo -e "\ngit status after unstashing:"
git status -s
like image 492
Elia Grady Avatar asked Jan 25 '18 15:01

Elia Grady


People also ask

Can changes reverted by running git stash command be reapplied?

Re-applying your stashed changesPopping your stash removes the changes from your stash and reapplies them to your working copy. This is useful if you want to apply the same stashed changes to multiple branches.

How do I get my stashed changes back in git?

To retrieve changes out of the stash and apply them to the current branch you're on, you have two options: git stash apply STASH-NAME applies the changes and leaves a copy in the stash. git stash pop STASH-NAME applies the changes and removes the files from the stash.

Does git stash pop overwrite changes?

When the pop command runs, it's expected that files from the stash will overwrite the contents of the files in the local working tree, and the updated files will be staged in the git index. But if a git stash pop conflict arises, then the problematic files won't be added to the index.

Can you stash staged changes?

Stage all your files that you need to stash. Run git stash --keep-index . This command will create a stash with ALL of your changes (staged and unstaged), but will leave the staged changes in your working directory (still in state staged).


1 Answers

Remember: git does not track file renames. It tracks file contents.

When the index contains a deletion and a creation for different files with the same contents, git's rename detection will (probably) conclude that's a rename, and helpfully display it as such. This doesn't actually change what's in the index, though. What "really" happened was indeed a deletion-then-creation, git is just trying to display it more helpfully.

Try it yourself:

$ cp file1 boink
$ rm file1
$ git add .
$ git status

Has the same effect as:

$ git mv file1 boink
$ git status

This rename detection only works on things that have been added to the index. Do git reset now and you'll see what I mean:

$ git reset
$ git status
Changes not staged for commit:
        deleted:    file1

Untracked files:
        boink

We can use the git command git ls-files to list the files present in the index. In effect, these are the files git considers existent at the moment. When we have a rename staged as above, the output from this is:

# git ls-files
boink
file2
file3
file4

As far as the index is concerned, file1 no longer exists. We deleted it, and then we created boink. git status is friendly and shows us the results of rename detection, but remember that the index doesn't care about that.

Now we run git stash -k -u. -k tells stash not to touch the index, so it doesn't. Consider for a moment the first paragraph of the manpage for stash:

Use git stash when you want to record the current state of the working directory and the index, but want to go back to a clean working directory. The command saves your local modifications away and reverts the working directory to match the HEAD commit.

So: we have asked stash to save our local modifications (without touching the index), and revert the working directory to match the HEAD commit. file1 exists in the HEAD commit, so it comes back. But file1 is no longer in the index, because we deleted it. Hence, file1 is now an untracked file. Since we're not touching the index, the new file boink and the record that it was the result of a rename from file1 remains in the index. Hence you get the potentially surprising output from git status:

Changes to be committed:    
        renamed:    file1 -> boink

Untracked files:
        file1
like image 135
Chris Kitching Avatar answered Oct 20 '22 08:10

Chris Kitching