Those who are using Git but still new to the concept will no doubt have realised that files are treated differently to normal source control tools, and that they transition between different known states, according to git. I am going to try and explain these states, and give commands on how to transition between them.

I needed these commands recently because I wanted to reset my changes and hadn't really thought about how these states affect what it is that gets 'reset'.

TL;DR: you can use these commands to perform different state transitions

  • git add <file> - add all changes to a file to stage
  • git rm --cached - unstage a new file/stage a delete for an existing file, keep local filesystem copy
  • git reset <file> - unstage, keep local changes
  • git checkout . - reset unstaged changes, keep staged
  • git reset --hard - reset all staged changes, reset all unstaged changes of tracked files(new unstaged files not removed)
  • git clean -f (use git clean -n to dry run first) - removes all untracked, unstaged changes
  • git clean -f -x (use git clean -n -x to dry run first) - as above, also removes ignored files

Skip to summary for coverage.

The Details

Given a file, git will see it as one of several states: Ignored, New-Unstaged, New-PartialStaged, New-Staged, Existing-Clean, Existing-UnstagedChanges, Existing-PartialStaged, and Existing-Staged. First I will work through the states and some of the transitions, Then ill list the commands to achieve these transitions. (There is also a Deleted-Unstaged and Deleted-Staged, but ill leave those as an exercise for the reader.)

You can think or the file as having 3 versions, one commited, one on the file system, and one in a staging area. The Unstaged/PartialStaged/Staged variations just refect whether or not the file system version or staged version has changes from the commited version. Where a change from the commited version is in both the file system and the stage it is staged. When it is in the filesystem but not the stage it is unstaged. When all changes are staged, we are in the Staged state, when all changes are unstaged we are in the Unstaged State, and when it is a mix, we are in the PartialStaged State. Hopefully that helps when looking at the state transitions below.

A file is in the Ignored state when it is covered by an explicit or wildcard entry in your .gitignore, as long as it isn't already checked in to source control. To transition into this state, Add a new file that is covered by ignore. If you have have staged changes you need to unstage them but keep the changes on disk. If you have a checked in file, you need to delete the file from git but keep the changes on disk.

When you add a new file, it will start in the New-Unstaged state, as long as it isn't being ignored by .gitignore. By staging the whole file, you get to the New-Staged, or you can stage some of the lines in the file to be in the New-PartialStaged state.

When you have an existing file, it starts in the Existing-Clean state. making changes puts it into the Existing-UnstagedChanges, and staging these produces the Existing-PartialStaged and Existing-Staged, depending on if some or all changes are staged.

Hopefully by now you have learned how to move changes forward with git add <filename> and how to commit them with git commit, so next we need to get out head about these 3 commands to go the other way.

So with out uncommited changes, we want to do one of 3 things: reset both stage and filesystem files to the commited version(or remove completely if new) or unstage the changes but keep them on the filesystem (or un-add if new), or with untracked files(ignored) we want to delete them completely.

Lets start with the unstaging. We have a new file staged, and we want to unstage, but keep the changes on disk. Why? well we either want to have it ignored(staged accidentally) or it isn't going in this commit. Either way, we use git reset <file> or if it a new file we could also use git rm --cached instead. --cached means keep the filesystem changes. Note that if you have a new staged file that is now ignored, or a commited file that you want to become ignored, but deleted, then git rm --cached is the coomand for the job, as long as you already have your .gitignore updated as well.

Now if have unstaged changes and we want to reset them on disk but leave staging alone? We can use the command git checkout .. This keeps all staged changes but resets any unstaged changes. The only issue with this command, is that if you have new files, but not staged, they stick around, while new staged files get removed.

And if we want to remove all staged and local changes of tracked(unignored, non new) files? git reset --hard. Note that any new unstaged files are kept here also. You could use git add . to stage new files, then git reset --hard, or you could try this next command after reset instead.

To remove unstaged files, you can use git clean -f. Watch out though, this cannot be undone, so best to run git clean -n to test it first. This will only remove new unstaged files(basically what the above two commands miss). As a side note, if the add is staged, this won't revert any unstaged changes to the file.

One final category is the ignored files. You know, all those artifacts, bin files, test results, .user files, package artifacts etc. These don't go away in a hurry, whether its changing branches, or resetting changes, these files stick around, and some of them take up space over time. These can be removed using git clean -f -x, again remembering to run git clean -n -x first so you don't lose something you didnt mean to.

One last one for good measure: if you want to keep all your changes for later, but still reset your files, use git stash -u. The -u here is shorthand for the --include-untracked command. This quickly gets rid of all but ignored files in your repo, making it as clean as necessary, and even keeps all your changes stored away to bring them back with git stash pop. Note though that the staged status of your files might not be the same afterwards. It will collapse together the stages and unstaged changes for each file, and put them all staged or all in unstaged. (This seems to be based on whether it is new or not, but more investigation needed).

Summary

(TL;DR continued)

So lets summarize all these removals a little simpler based on the way I found it somewhere else online:

  • Type 1: Ignored File
  • Type 2: Unstaged New File
  • Type 3: Unstaged Changes (on new staged, or existing file)
  • Type 4: Staged New File(with or without unstaged changes)
  • Type 5: Staged Changes on Existing File

Commands:

  • git checkout . - Undo Type 3 only
  • git reset --hard - Undo Types 3, 4 and 5 only
  • git clean -f - Undo Type 2 only
  • git clean -f -x - Undo Types 1 and 2 only
  • git stash -u - Undo Types 2,3,4,5, and creates a stash
  • git stash -all - Undo All types, and creates a stash(stash includes ignored)

So to nuke it all?

  • git reset --hard then git clean -f -x - All, no recovery
  • git reset --hard then git clean -f - All but ignored, no recovery
  • git stash -all - All gone, with recovery
  • git stash -u - All but ignored with recovery

Thanks to Frederik Sch√łning and others for their stackoverflow answer which helped me get the commands straight to solve my problem.