Git branches are 'lightweight' insofar as they're simply pointers: git just points 'master' or 'development' or 'trunk' or 'myfeature' at a particular commit. When you commit afresh on a branch, the pointer advances. Consider this diagram from the git-scm docs, a stellar resource on the subject.

The 'master' branch in this diagram points to commit f30ab
. The 'testing' branch points to commit c2b9e
. HEAD
in this diagram is a special pointer: Most of the time it points at another branch (in git terminology, it is a "symbolic reference") to show the currently checked-out state of the working directory. When you issue, say, git checkout f30ab
, you put the repository into "detached HEAD" state. In other words, you move the pointer from a symbolic reference, 'testing', to a commit, f30ab
.
Take an example, one you should be able to setup yourself locally.
git init /tmp/test && cd /tmp/test ;# make a repo and cd to it
echo A > A ;# add a file
git add A && git commit -m "first commit" ;# make the first commit
echo B > B ;# add another file
git add B && git commit -m "second commit" ;# commit that one too
git checkout -b development ;# now let's checkout development
echo C > C ;# commit one more file
git add C && git commit -m "third commit" ;# and commit that final one
You've now got something like the below. I don't have omnigraffle so we're stuck with a directed graph:
* 93e71ee - (HEAD, development) third commit
/
* 6378754 - (master) second commit
* d2b4ba9 - first commit
As you can infer from the parentheses, 'master' points at commit 6378754
, 'development' points at commit 93e71ee
, and HEAD
points at 'development'. Don't take my word for it. Explore the pointers yourself:
$ cat .git/refs/heads/master ;# cat the 'master' pointer
5a744a27e01ae9cddad02531c1005df8244d188b
$ cat .git/refs/heads/development ;# now cat the 'development' one
93e71ee0a538b0e8ac548e3936f696fa4936f8dc
$ cat .git/HEAD ;# note that 'HEAD' points at 'development'
ref: refs/heads/development
$ git symbolic-ref HEAD ;# as we can also show with 'symbolic-ref'
refs/heads/development
When branches are just pointers, switching between them is trivial. One special case is HEAD
. Consider what happens when we checkout master:
$ git checkout master ;# checkout master...
$ cat .git/HEAD ;# where are we now?
ref: refs/heads/master
What about checking out a commit?
$ git checkout d2b4ba9 ;# this will throw some advice
Note: checking out 'd2b4ba9'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
$ cat .git/HEAD ;# 'HEAD' points at a commit
d2b4ba97698d7f528f9ba1e08d978a70651b3b1d
$ git symbolic-ref HEAD ;# and thus isn't a symbolic reference
fatal: ref HEAD is not a symbolic ref
What's that advice mean? It's that committing against a repository in "detached HEAD" state generates commits unreachable from any branch. When HEAD
changes (from any checkout operation, such as git checkout master
), those commits will be lost. This is easier to see in a graph:
echo D > D ;# add another file
git add D && git commit -m "fourth commit" ;# and commit it
Let's look at our graph. Note no git command will generate what you see below. I've modified existing output for the purposes of this example.
* 93e71ee - (development) third commit
/
* 6378754 - (master) second commit
/
* / 72c1f03 - (HEAD) fourth commit
|/
* d2b4ba9 - first commit
HEAD
is still detached. It points at 72c1f03
. 'master' and 'development' point where we expect, but 72c1f03
isn't reachable from any branch. That's a problem. If I want to keep 72c1f03
around, I have to give it a branch:
$ git checkout -b experimental ;# checkout 'experimental' based on '72c1f03'
$ cat .git/HEAD ;# HEAD is once again pointed at a branch
ref: refs/heads/experimental
$ git symbolic-ref HEAD ;# and is a symbolic ref
refs/heads/experimental
And the graph:
* 93e71ee - (development) third commit
/
* 6378754 - (master) second commit
/
* / 72c1f03 - (HEAD, experimental) fourth commit
|/
* d2b4ba9 - first commit
Git makes branching easy. Pushing and pulling information about pointers is much faster than pushing and pulling entire sets of files. Cutting a branch takes milliseconds. It's so easy it almost feels wrong. As a result, git allows more distributed workflow options, although it can certainly handle centralized ones, too.