Straight road ahead

Fixing our mental model of Git Branches; it’s a straight line folks!

Recall

From the last part, we learnt that:

  1. git tracks your code with a pointer called HEAD
  2. When you commit, you create a snapshot of your code with a unique commit ID which is a long alphanumeric string.
  3. When you checkout a particular commit, you move the HEAD (git’s pointer) to that commit. You can move up and down your repository’s history (or commit history) which is a straight line.
  4. Tags are just human readable refs( references = names = labels) for a single commit because the commit ID is difficult to work with, for humans.

Also, till now, we’ve made six changes (and six commits) and tagged two of them.

Branches are like tags

Branches, just like tags are refs (references) that point to a particular commit. They are similar to tags, in the sense of being names (= labels) for a particular commit identified by a particular ID.

At this point, it is important to recall that the HEAD as a pointer moves forward automatically, as we add new commits. When we stopped at the last part of the tutorial, we had made six changes and our HEAD was at the sixth commit.

Let’s create a seventh change in our repository so that our git log looks like this:

Commit History after Seventh Commit

And our spreadsheet looks like this:

Spreadsheet Contents after seventh commit

At this point, the HEAD has moved automatically to this seventh commit and now refers to a the commit ID of the latest commit.

The tag 2.0 however did not move. 2.0 doesn’t suddenly point to the seventh commit. It is still (and always be) stuck at the sixth commit. You can move the HEAD pointer to the sixth commit by either checking out the tag name or checking out the commit ID. They both mean the same.

In a manner of saying, if commits are snapshots or milestones along the linear timeline of the life of your repository, tags are specially highlighted milestones or snapshots with a special name.

Branches, because you want a name for the latest commit

It’d be very stupid, if you couldn’t just ask for the latest code. Your colleague says, show me the latest code, you share a particular commit ID. You write more code and add more commits. Your colleague again asks for the latest code, you now have to share a new commit ID. If you didn’t want to share those long alphanumeric commit IDs, you’d have to tag those commits. You could end up with a lot of tags just because you don’t want to deal with those weird commit IDs.

Wouldn’t it be awesome if you or your colleague could just ask git for the latest code since it’s anyways tracking the code. This means that there’s no one better than git to know what commit ID are you talking about when you say the latest code (or commit).

For this purpose, we need a ref (reference = name = label) that moves forward with the pointer (the HEAD) as we add a new commit. This way whenever we check out this ref, it always points to the latest commit. Such a ref that moves forward with the HEAD as new commits are added is called a branch.

Let me let you in on a little secret. When you made your first commit, git automatically created a default branch and attached it to the HEAD. This default branch is called master. As we moved from the first to the seventh commit, the ref master moved with the HEAD to the latest (seventh) commit.

Being on a branch

In some cases (we really don’t need to know why, right now), sometimes you’d want to commit a change, i.e., create a new commit but not move the branch ref to this new commit. That’s when you can detach the HEAD from the branch that we’re on, ie. get off the branch. So, the branch will point to the last commit where the branch was attached to the HEAD pointer. The HEAD will still move forward, but not the branch.

So, you can get on and off a branch, in that sense. Right now, on our seventh commit, we’re on the master branch. You can check what branch we’re on right now by running:

You should get an output similar to mine:

Checking the current branch

Let’s update our spreadsheet with this information:

Spreadsheet Contents after seventh commit, with branch info

Getting off a branch/ Detaching the HEAD

Let’s detach our HEAD from this branch by running the command:

You should get a detailed error message:

Detaching HEAD

The HEAD still points to the seventh commit but is detached from the master branch. Now run git branch again and you’d see something like this:

Current branch, Detached head

Let’s make our eighth change in History.md so that the contents look like this:

And commit this change as our eighth commit:

Now, our git log would look like this:

Commit History after Eighth Commit

Let’s update the spreadsheet too:

Spreadsheet Contents after eighth commit, with detached HEAD

The HEAD is now detached and you are on no branch and off all the branches.

Creating a branch but not being on it

Let’s create a new branch called second-branch:

No messages, nothing should visibly happen. Did your HEAD get attached to this new branch? Let’s find out by running:

You should see something like this:

Creating a new branch

So, you created a new branch that points to the current commit pointed at by the HEAD. You are not on the second-branch, however. In other words, the branch is still detached from the HEAD.

Not being on the branch (or detaching the branch from the HEAD) means that when we add a new commit, the HEAD pointer will move to this new commit, but the branch won’t.

Let’s make our ninth change in History.md so that we end up with the following content:

Then make our ninth commit, so that our git log looks like this:

Commit History after Ninth Commit

Let’s update our spreadsheet too:

Spreadsheet Contents after ninth commit, with detached HEAD

Note that, right now:

  1. We are on none of the branch refs.
  2. The HEAD is on the ninth commit.
  3. We have created a new branch called second-branch but we’re not on it. So, while the  HEAD moved to the ninth commit, the branch second-branch still points to the eighth commit.

You can confirm 3 above by running

Doing this will get you an output similar to this, as your HEAD pointer moves to the second-branch which is still pointing to the eighth commit:

Checking out the second-branch

It basically says that your ninth commit is not on any branch. At this point, if you look at the git log, you’d get the whole commit history up to the eighth commit:

Commit History at second-branch

Let’s get back to our latest (ninth) commit by checking out the commit id:

This should get you an output similar to this:

Checking out latest commit

Fixing detached HEAD by getting on a branch

Git doesn’t like being in a state of detached HEAD like this. It’s nothing to worry about. As we’ve seen very clearly, your commit history, repository and code are just fine. You’re just not on a branch.

To fix this you need to get on a branch. The best way to do that is  to create a new branch third-branch and get on it by checking it out, like this:

Or, use a shortcut command that’ll both create a new branch and get you on it (that is attach it to the HEAD):

Now, if you run git branch, you are on the third-branch and no longer in a detached HEAD state:

Checking out third-branch

In fact, now we have three branches, each of them pointing to a different commit ID (snapshot) of the linear timeline of the repository. The code or repository never got divided into three branches. You can see what commit ID does each branch point to by running:

It’d give you an output similar to this:

Branches and commit IDs

It is still a straight line, there have been no tree-like branches.

Let’s update our spreadsheet too:

Spreadsheet after the third-branch

Let’s tag this ninth commit with 3.0:

And list all our branches and tags:

List of all refs

You can notice that both tag 3.0 and branch third-branch point to the same commit. Let’s update our spreadsheet:

Spreadsheet after tag 3.0

Now, let’s add another commit, our tenth one by modifying the History.md file with the tenth change to get a git log as follows:

Commit History after tenth commit

Let’s see where the branch third-branch points to. Earlier, we saw that it points to the ninth commit also pointed at by the tag 3.0:

List of all refs after the tenth commit

You can see that the ref third-branch has moved along with the HEAD to the tenth commit and doesn’t point to the ninth commit anymore. Let’s update our spreadsheet too:

Spreadsheet after tenth commit

Let’s make our eleventh and last change and commit again to get our log:

Log after tenth commit

And update our spreadsheet:

Spreadsheet after eleventh commit

Finally, let’s check what commits do our refs point to:

List refs after eleventh commit

The branch third-branch has moved again to point to the eleventh commit.

If there are no branches, what are we merging then?

Let’s perform a merge and see. First, checkout our second-branch:

Now merge our third-branch into the second-branch

We have not merged a tree’s two branches into one. It doesn’t look or work like a river that branched out and now the branches are rejoining. What is actually happening is you are merging the code at one particular snapshot (commit) of your repository’s linear timeline (commit history) that is pointed to by the ref third-branch with the code at another particular snapshot pointed to by the ref second-branch.

Merging branches

Everything is still a straight line, nothing’s a tree.

Let’s look at where all our refs are pointing at:

List refs after merge

Do note that even though both second-branch and third-branch refer to the same commit ID (eleventh commit), we’re right now off the third-branch and on the second-branch.

The HEAD is now attached to the second-branch branch and any further commit will make the second-branch ref move to this new commit ID whereas the third-branch branch will continue to point at the eleventh commit. Both these branches did not become one.

Still a straight line, not a tree!

In summary

  • Both tags and branches are just references to specific commits (snapshots).
  • Tags are fixed references.
  • Branches are moving references; they move forward with the HEAD, if we’re on a branch.
  • You can checkout a snapshot of your code using the commit ID or a tag referring to the commit ID or a branch referring to the commit ID.
  • Merging branches just means merging code from one snapshot (commit) with another. The refs still point to the commit IDs they were pointing to.

Let’s go back to the quote from the beginning:

When we start out as developers, using git for the first time, we are shown diagrams of git commits forming chains that “branch” outwards (see Scott Chacon’s excellent Pro Git Book: http://git-scm.com/doc). When we look into the underlying implementation of git we find data structures known as “trees”. Then we hear that there is such a thing as a git branch, and suddenly we instinctively feel we know what that is: it must surely bear a similarity to the branch of a tree, else why would they have named it a “branch”.

And that must mean…

  • Branches are made up of a load of commits joined together, right?
  • I can move up and down a branch, right?
  • Two people can be at different points on the same branch, right?
  • I can delete a branch by deleting the commits that make up the branch, right?
  • I guess I can move a branch if I break the links between commits or something, right?
  • When two branches merge they become one single branch, right?

No! No! No! No! No! No!
Git branches – Is your mental model wrong?

Now that you have a better mental model for git branches, you can empathise with the author’s “No! No! No! No! No! No!”. Go read that post again now.

The repository with all the commit history, tags and branches as mentioned in this tutorial can be found here: https://github.com/BaapWP/Git-Model-Tutorial/

What do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.