(This should perhaps be a comment but it's way too too big and needs nice formatting :-) ... also, I appear to be in a quirky mood, if you follow some of the links...)
The key to understanding this—I think you're already there, but let's put this in the SO article to make it clear to the next reader—is this:
All git refs wind up naming (pointing to) a single commit.
This is true for tags (whether lightweight or annotated), branches, "remote branches", git "notes" references, the "stash", and so on. They all name one commit, and that's it. (Well, OK, not quite: technically, tags can name any object in the repo. In fact, that's how annotated tags work: there's a lightweight tag that names a repository object that is the annotated tag, and then the annotated tag names a commit object. That's also how HEAD
works: it usually names another ref, which then names the commit. So, sometimes you have to peel a few layers off the onion to hit the commit. Naming a blob or tree object is possible, but normally, nothing actually does this.)
(The "true name" of the commit is, of course, the SHA-1 value.)
What makes a branch reference name "special" is equally simple:
A branch reference is a name that automatically moves to the new branch tip, when new commits are added.
Specifically, a ref of the form refs/heads/branchname
points to some commit (by definition, since names point to commits). When you are "on" that branch1 and do some git operation that adds a new commit, like git commit
or git merge
or git cherry-pick
, git sticks the new commit into the repo, then re-points the name to the new commit. That's all it has to do!
What we think of as "the branch" is formed by starting at the tip—the commit to which the name points—and working backwards, using each commit's parent or parents. If the commit has one parent, it's an ordinary commit "on the branch". If it has two or more, it's a "merge", and you can follow all of its parents to find what got merged. If it has no parents at all, it's a root commit (like the initial commit in a new repo—you can actually have more than one root; git "notes" do this, for instance).
If you put seven branch labels onto one commit, you now have seven2 names for "the branch". If you clear this down to one, it's less confusing, of course, but git won't care either way. (Git only cares if you take it down to zero names. Now the branch still exists, but it's really hard to find, and it's eligible for garbage collection.)
Since we're on the topic, let's also make a note about "remote branches". (I have never been quite satisfied with the name "remote branch" but I don't have a better one, so let's just define it.) A "remote branch" is a local ref of the form refs/remotes/rname/bname
, where rname is the name of the remote (such as origin
) and bname is the branch name on the remote, i.e., the part that comes after refs/heads/
if you log in on that remote and look at the branch over there. You can't get "on" a remote branch—if you git checkout origin/master
, git gives you a "detached HEAD"—but these branch names are automatically updated: when you use git fetch
to get new commits from the remote, you also pick up the new branch-tips. In other words, instead of you moving the names to the new branch tips, you let someone else do it (on the remote) and then you pick up their latest version all at once, when you fetch from them.
1To be "on a branch", the HEAD
ref must be an "indirect" reference, something like ref: refs/heads/master
. When HEAD
is instead "detached" it contains a raw SHA-1 value. You can still add commits; they're added to an un-labeled branch. The reference in HEAD
keeps them from being garbage-collected.
2Or more, if there are tags. Let's assume there are no tags.
3There is no footnote three.