2

I'm trying to start using git branches, but there is something i don't understand.

What started as a small change on my master branch is growing into a large feature, so i'd like to move this to a new branch called dev so I can still do minor changes on the master branch and merge the dev branch when it's finished.

I've tried all kind of methods but i'm taking this one as example because i have unexpected behavior.

So with git stash all uncommitted changes are moved to the stash and the master is reverted back to it's last commit state. Then a new "dev" branch is created and switched to it. with stash apply the saved stash is applied back to this branch and all changes are back. Now when I checkout master I see all changed files are present again. I expected that only the dev branch had these changes and when switched back to master it would have the state as the last commit. I'm not sure if this is normal behavior and my expectations are wrong (with more questions) or should the master branch have stayed in de last commit state?

4 Answers4

2

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.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • This was very helpful in understanding and insight in how git works. This actually cleared a few other things I was wondering about. – Fluxlicious Jan 08 '19 at 17:41
1

In most contexts you will get an error if you try to checkout while you have uncommitted changes. This will happen if the two branches (the one you're on and the one you're checking out) don't point to the same commit. To get between the two branches retaining the uncommitted changes you need to use git stash as you have done.

When you create a branch, it points to the commit you're currently on. So both dev and master point to the same commit. In this special case, git will let you switch branches freely because the checkout will change no files.

This is most useful for your use case... You start making changes but forget to start a new branch. In this case you can just create the new branch and switch to it without using git stash.


Edit:

If the changes are not there after a git-stash then they will not be there after a commit to another branch. If you really wanted to test this, there's nothing to stop you doing this:

git checkout dev
git add *
git commit -m 'Test commit'
git checkout master

# look around

git checkout dev
git reset HEAD~

The last line pops off your last commit and leaves the files there as uncommitted changes. Just don't push the test commit.

Philip Couling
  • 13,581
  • 5
  • 53
  • 85
  • Okay, that makes sense. Since they are both pointing to the same commit they see the same changes. So then if i commit on the dev branch after `git stash pop` then they should be pointing to different commits and when i switch back to the master branch i should not see the changes from the stash. Only thing is i don't feel comfortable to commit a partial changes on the dev branch... Is there no other way? – Fluxlicious Jan 08 '19 at 16:58
0

I generally second Philip's analysis.

Just wanted to add this : If you're on your newly created branch and need to checkout back to master, expecting NOT to find your changes here, you'll need to commit these changes on the new branch before making the switch :

# just after your stash apply on the new branch
git commit -a -m"temporary message"
git checkout master

Then if you want to go on working on the new branch, undo this temporary commit, work until you're done, and commit "for real" :

git checkout dev
# the following line will undo the commit but keep your changes in working tree
git reset --soft HEAD^
# work some more
git commit -a -m "real final message"
Romain Valeri
  • 19,645
  • 3
  • 36
  • 61
  • 1
    Thank you, this literally answers my question to Philip.So yes, I should perform a commit after I unstashed the changes in a new branch. – Fluxlicious Jan 08 '19 at 17:37
0

Git only stores what you've committed. Since you haven't committed those changes, they aren't stored in either branch. Stash is a special way of creating a commit that doesn't belong to any particular branch and includes both staged and unstaged changes (there are various options to modify this behavior); but when you apply or pop a stash, it doesn't automatically create a new commit on your branch.

Git is nicely sensitive about losing work. When you asked git to checkout master, it noticed that you had uncommitted changes. It did a quick check in the background to see if you'd lose any work when moving from dev to master. Since both branches had the same history (and thus there were no differences in the files between the two branches), git knew that there would be no conflicts and thus your uncommitted changes were not overwritten or "lost". It simply preserved them. This is a handy feature when you realize you're working on the wrong branch... you can attempt to switch branches and take all of your uncommitted work with you so long as there are no conflicts. Note, again, that the uncommitted changes are not stored with either branch until you commit them.

JDB
  • 25,172
  • 5
  • 72
  • 123