Guide

If objects are the database, refs are the names you use. A ref is a named pointer to a commit. Branches, tags and HEAD are all refs. Once you see that, branching, checking out and resetting stop looking like magic.

Concept

Before you start: a commit hash is the ID of a commit object (see Git's object store). A ref holds that hash, or the name of another ref.

A branch is a file with a hash in it

Bash
$ cat .git/refs/heads/main
607d042a5cb0663df9073d24a108e5d3cb5ca3a8

$ cat .git/HEAD
ref: refs/heads/main

That is the whole implementation of a branch: a file of about 41 bytes. Creating a branch writes one file. That is why branching in Git is instant regardless of repository size. Older systems such as Subversion copied directories.

  • refs/heads/* are your local branches.
  • refs/remotes/origin/* are remote-tracking branches. They hold your last-known copy of the server's branches. git fetch updates them and never touches your local branches.
  • refs/tags/* are tags. A lightweight tag is just a ref. An annotated tag is a ref that points at a tag object.

HEAD is where you are

HEAD normally names a branch. This is a symbolic ref. When you commit, Git writes the new commit with the current HEAD commit as its parent, then moves the branch that HEAD names to the new commit.

Detached HEAD means HEAD holds a commit hash directly instead of a branch name. This happens after git checkout <sha>, and during some rebases. Commits made there are not on any branch, which is why Git warns you.

The reflog: your safety net

Every time a ref moves, whether from a commit, reset, rebase, checkout, merge or pull, Git appends a line to a local log called the reflog:

Bash
$ git reflog
d2c7d31 HEAD@{0}: rebase (finish): returning to refs/heads/feature
d2c7d31 HEAD@{1}: rebase (pick): Add retries
dfcebc5 HEAD@{2}: rebase (start): checkout main
f8d2642 HEAD@{3}: checkout: moving from main to feature
dfcebc5 HEAD@{4}: commit: Bump timeout

HEAD@{3} means "where HEAD was three moves ago". Here, f8d2642 is the feature branch's tip from before the rebase. The rebase created new commits, but the old ones still exist, just unreferenced. Unreachable objects survive until garbage collection prunes them. By default, reflog entries for unreachable commits are kept for 30 days, and for reachable ones for 90 days.

The reflog is local. It is not pushed, so it cannot save work that only ever existed on someone else's machine.

Common mistakes

  • Deleting a branch that has unmerged work. Its tip is in the reflog, so you can get it back with git branch <name> <sha>.
  • Assuming the reflog is shared. It lives only on your machine.
  • Waiting too long to recover. Unreachable commits are pruned after the retention window.

Check yourself

  • You ran git reset --hard HEAD~3 and lost three commits from view. How do you get them back? Run git reflog, find the entry from before the reset, and point a branch or git reset --hard at that hash.
  • Why is creating a branch instant? It writes one small file holding a commit hash. No files are copied.

Key takeaways

  • A branch is a file with a commit hash. HEAD names the current branch, or holds a hash when detached.
  • Remote-tracking branches are your last-known view of the server, updated by fetch.
  • The reflog records every pointer move for 30 to 90 days, so almost any committed work can be recovered locally.

Further learning

Next, see what each way of combining branches does to these pointers, in merge, rebase and squash.

For the tools that work with this model, read Pro Git: Git Internals.