Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are unstaged changes still present after checking out a different branch?

I'm observing something that is not consistent with what I know about the git checkout command. While on a branch other than master, I make some modifications to a tracked file; without staging and committing those changes, I run

git checkout master

Git complies without batting an eyelid; but what is even more surprising is that all the modifications I made on that branch are still present! Here is an example reproducing the situation:

mkdir myrepo
cd myrepo
git init
touch README # create a new file
git add .
git commit -m "initial commit"
git checkout -b new-branch
echo "foo" >> README
git checkout master  

At this stage, the README file in my working tree contains the foo line, even though I added it while on the other branch (new-branch). I was expecting README to be empty, like the version recorded in the tip of master. Why is the foo line still there after checking out master?

like image 862
Sajad Rastegar Avatar asked Sep 19 '14 17:09

Sajad Rastegar


People also ask

Why are changes in one branch visible in another branch git?

git commit takes a snapshot of all the tracked files in the index as a commit. A branch is a ref that points to a commit. In your case, the changes are still in the work tree. The branch doesn't know about them yet.

What happens to uncommitted changes when you switch branches?

If you have modifications to a file that is identical between two branches, switch from one branch to the other will not require a stash. If the file is different on your other branch, Git will not let you switch branches, as this would destroy your uncomitted changes.

How do I discard changes in git?

There are two Git commands a developer must use in order to discard all local changes in Git, remove all uncommited changes and revert their Git working tree back to the state it was in when the last commit took place. The commands to discard all local changes in Git are: git reset –hard. git clean -fxd.


2 Answers

At first sight, your question seemed pretty uninteresting, but it made me realize that git checkout is not as simple an operation as it seems. Thanks for asking it :)

As I understand it, your question is: why are the uncommitted changes, i.e. the added foo line, still present in the working tree after checking out master? If you look up the git-checkout man page, you'll find the following description:

git checkout <branch>

    To prepare for working on <branch>, switch to it by updating the
    index and the files in the working tree, and by pointing HEAD at
    the branch. Local modifications to the files in the working tree
    are kept, so that they can be committed to the <branch>.

However, this description seems to contradict what's happening in your example. You're not the first one to be confused by it; see this discussion. In it, Junio Hamano, the maintainer of Git, clarifies what git checkout <commit-ish> does in the presence of local modifications:

The principle is that we allow you to check out a different branch when you have local changes to the working tree and/or to the index, as long as we can make the index and the working tree pretend as if you reached that locally modified state, starting from a clean state of the branch you are checking out.


Exegesis of Junio Hamano's response

What happens still wasn't clear to me, so I conducted a few experiments to fix ideas, and here is my interpretation of Junio Hamano's reply. First, let me introduce some terms: let

  • Cs denote the source commit, i.e. the commit that HEAD points to before the checkout operation,
  • Is denote the state of the staging area,
  • Ws denote the state of the working tree,
  • Ct denote the target commit, i.e. the commit that we're attempting to check out.

My understanding is that, when invoked, git checkout, for each tracked file, gets the following diffs,

  • diff(Cs,Is) (which you can see in the output of git diff --staged),
  • diff(Cs,Ws) (which you can see in the output of git diff),

and checks whether those changes apply cleanly on the target commit, Ct. If there is any conflict, the checkout operation is aborted. Otherwise, HEAD moves to Ct, and the states of the staging area (Is) and working tree (Ws) are preserved.

Application to your example

Before git checkout master

Right after the line

git commit -m "initial commit"

in your example, your repo looks like this:

enter image description here

The pages next to the commit, staging area, and working tree, symbolizes the contents of your README file in the correspondings three "Git areas".

"During" git checkout master

Then you run

git checkout master

So, under the assumption that my interpretation of Junio's anwer is correct, what happens here?

Because both the current branch, new-branch, and the branch to checked out, master, point to the same commit A, both Cs and Ct (using my terminology) correspond to A. In that case, of course, diff(Cs,Is) and diff(Cs,Ws) apply cleanly to Ct; no conflict here.

Therefore, the checkout operation is carried out:

  • HEAD is made to point to master,
  • the state of the staging area is preserved,
  • the state of the working tree is preserved.

After git checkout master

Because no conflit arose when checking out master, local modifications to README in the working tree have been kept, so that they can be committed to the master branch.

enter image description here

like image 88
jub0bs Avatar answered Oct 12 '22 03:10

jub0bs


Until a change is staged (using git add), it just exists in the working directory - it is not tracked by git, and therefore will exist in any branch you checkout.

like image 31
Mureinik Avatar answered Oct 12 '22 02:10

Mureinik