What you're seeing is that there's a very big difference between your work-tree—the area where you do your work—and actual commits.
It may help if you don't think about Git as storing changes, because it doesn't. It stores snapshots. Each commit is a full, complete copy of all of your files—well, all of the ones you've committed, but that's kind of redundant, so just think "all the files". The saved snapshots are frozen forever—well, living as long as the commit itself lives, but that's forever by default, anyway.
These snapshots are saved in a special, compressed, frozen, Git-only format. They're no good to anything but Git, so for you to work with them—on git checkout
—Git has to expand them out into a normal form, where they're unfrozen and useful, and of course you can change them around. That's your work-tree, where you will do your work.
So there are two "active" copies of every file: the frozen one in the commit, and the one you're working on. When you view things as changes, you're comparing these different copies.
Git adds an extra wrinkle here, because there's actually a third copy of every file. When Git first extracts a commit, it copies the frozen Git-only files into what Git calls, variously, the index, the staging area, or the cache. Here, files are still Git-only, but now they're not quite frozen any more. They are more sort of slushy, ready-to-freeze, and specifically, you can replace the index copy of any file with a new copy. That's what git add
does.
When you go to use git stash
, what Git really does is make a commit, or more precisely, two commits. Then it cleans out the index and work-tree so that they match the current, or HEAD
, commit. The main special thing about this stash commit is that it is not on any branch. So you can switch to another branch and use git apply
to re-extract it, regardless of which branch you've switched-to.
Switching to another branch means select that branch's tip commit as the current commit, and that branch as the current branch. Git will copy all of those files, from that snapshot, out into the index and work-tree. If you were clean before, you'll again be in a clean situation, but now the HEAD
commit is some other commit. Now you can modify the work-tree again.
As Philip Couling noted, there's a special case where two branch names identify the same commit. If we draw the commits as a chain, with two branch names pointing to the latest commit:
...--o--o--* <-- master, develop
then, in one sense, it doesn't matter whether we git checkout master
or git checkout develop
. Both identify that same last commit. The active snapshot in HEAD
, the index, and the commit will all have the same contents. Git doesn't have to do anything to the index and work-tree if you switch from commit *
to commit *
since you're not actually switching anything in them!
But that's not all that git checkout
does! In particular, checking out master
tells Git to attach the name HEAD
to the name master
, while checking out develop
tells Git to attach it to develop
instead. This affects which branch name gets updated when you make a new commit. If you are set up like this:
...--o--o--* <-- master, develop (HEAD)
and then make a new commit, Git will move the name develop
to point to the new commit:
...--o--o--o <-- master
\
* <-- develop (HEAD)
Note that HEAD
is still just attached to develop
. The trick is that develop
now identifies this new commit.
Git makes the new commit from whatever is in the index, not from what's in the work-tree. You use git add
to tell Git: copy the work-tree file over top of the index file. This prepares the file for freezing, compressing it down to the special Git-only format, using whatever's in the work-tree version right then. Then git commit
just has to flash-freeze the index copies, to make the new commit's frozen copies of all files.
So, for this particular case, git stash
made a commit, cleaned out your work-tree, then you re-attached HEAD
to the same commit but a different branch name, and then re-applied your work-tree changes. The stash-and-apply was entirely unnecessary. If develop
and master
had pointed to different commits, though, you would often need to do that stash-and-apply. (Even then, Git will let you get away with switching commits in many cases. See Checkout another branch when there are uncommitted changes on the current branch for all the gory details.)