On This Page
Git Branch: Under the Hood of Git’s Lightweight Pointer System
TL;DR: In Git, a branch is not a folder copy or a heavy snapshot of your source files. In Git’s default files reference format it is simply a lightweight, mutable reference — a 41-byte text file containing a 40-character commit SHA-1 hash and a newline character — stored inside the .git/refs/heads/ directory. Creating, renaming, or deleting branches in Git is practically instantaneous because it only alters these reference files, rather than modifying or copying the files in your working directory.
Scope note: the “41-byte file” description on this page is exact for Git’s default
filesreference backend when a branch is unpacked. References can also be packed into.git/packed-refsbygit gc, and Git supports an alternative reftable backend where branches are not individual files at all. The commands and behaviour are identical in every case — only the on-disk representation differs. See Git Refs for details.
A branch is a ref, not a copy of your files
To appreciate why Git’s branching model is so revolutionary, it helps to contrast it with older Version Control Systems (VCS) like Subversion (SVN) or Perforce. In those legacy systems, creating a branch meant making a physical copy of the entire project directory tree. If your codebase was 5 GB, creating a branch meant copying 5 GB of files on the server. This operation took significant time, wasted disk space, and discouraged developers from creating branches for small tasks.
Git does not copy files. Instead, Git organizes your project history as a Directed Acyclic Graph (DAG) of commit objects. Every commit points to its parent commit(s), creating a tree-like structure of history. In this graph:
- A Commit represents a snapshot of your project’s tracked files at a specific point in time, along with author metadata and parent links. See /git-commit for details on commit object structures.
- A Branch is simply a named reference pointing to one specific commit in this graph. It is a starting point from which Git can traverse backward through parent pointers to construct the history of that branch.
Think of a branch as a sticky note attached to a specific node in a chain of commits. As you work and create new commits on the active branch, Git automatically moves the sticky note forward to point to the latest commit. Because a branch is just a reference (or “ref”), Git branching operations only require writing or modifying a small 40-character string.
For a deeper look into the general reference system of Git, read our guide on /git-refs.
Three references are easy to confuse: refs/heads/main is your local branch; refs/remotes/origin/main is your local record of the remote branch, updated by commands such as git fetch; and HEAD identifies the branch currently checked out, or a commit directly in detached-HEAD state. A successful git push can also update the relevant remote-tracking ref.
Watching refs/heads change as you branch
To fully grasp the mechanics of Git branches, let us perform a step-by-step terminal walkthrough and observe how the .git/ directory changes during branching operations.
Step 1: Initializing a Clean Repository
Let us create a new repository, add a commit, and look at the structure of the .git/ directory.
$ mkdir git-branch-demo
$ cd git-branch-demo
$ git init
Initialized empty Git repository in /users/admin/git-branch-demo/.git/
At this stage, if we list the contents of .git/refs/heads/, we will find it completely empty:
$ ls -la .git/refs/heads/
total 0
drwxr-xr-x 2 admin staff 64 Jul 7 10:25 .
drwxr-xr-x 4 admin staff 128 Jul 7 10:25 ..
There is no default branch file created yet because no commits exist. Let’s see what .git/HEAD points to:
$ cat .git/HEAD
ref: refs/heads/main
Even though refs/heads/main does not exist on disk yet, HEAD is pre-configured to point to it. This state is known as an “unborn branch.” Let us create our first commit to bring the branch into existence.
$ echo "Hello Git Branching!" > README.md
$ git add README.md
$ git commit -m "Initial commit"
[main (root-commit) f24e93e] Initial commit
1 file changed, 1 insertion(+)
create mode 100644 README.md
Now that a commit exists, let’s inspect .git/refs/heads/ again:
$ ls -la .git/refs/heads/
total 8
drwxr-xr-x 3 admin staff 96 Jul 7 10:27 .
drwxr-xr-x 4 admin staff 128 Jul 7 10:25 ..
-rw-r--r-- 1 admin staff 41 Jul 7 10:27 main
A file named main has appeared! Let’s print the contents of this file:
$ cat .git/refs/heads/main
f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c
The file contains exactly a 40-character SHA-1 commit hash followed by a newline character. Let’s verify what this hash points to using the plumbing command git cat-file:
$ git cat-file -t f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c
commit
And let’s print the contents of the commit object to see how it represents our snapshot:
$ git cat-file -p f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c
tree 5b38a7c29375ab2b4a53239a584a22c5432a138b
author User <user@example.com> 1783420000 +0000
committer User <user@example.com> 1783420000 +0000
Initial commit
Step 2: Creating a New Branch
Now, let’s create a new branch called feature-login using the git branch command:
$ git branch feature-login
Let’s check the contents of .git/refs/heads/ to see what Git did under the hood:
$ ls -la .git/refs/heads/
total 16
drwxr-xr-x 4 admin staff 128 Jul 7 10:30 .
drwxr-xr-x 4 admin staff 128 Jul 7 10:25 ..
-rw-r--r-- 1 admin staff 41 Jul 7 10:30 feature-login
-rw-r--r-- 1 admin staff 41 Jul 7 10:27 main
A new file named feature-login has been created. Let’s read its content:
$ cat .git/refs/heads/feature-login
f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c
It contains the exact same commit hash as main! Git didn’t copy any source files. It simply created a 41-byte text file pointing to the existing commit (assuming the default unpacked files reference format). This explains why branch creation is instantaneous.
Let’s check our active branch by reading .git/HEAD:
$ cat .git/HEAD
ref: refs/heads/main
HEAD still points to main, meaning we are still on the main branch. To switch branches, we must update the HEAD reference, which is typically handled by commands like git checkout or git switch. Read more about this mechanism in /git-checkout.
Step 3: Committing on a Branch
Let’s switch to our new branch and make a change.
$ git checkout feature-login
Switched to branch 'feature-login'
Now let’s verify .git/HEAD:
$ cat .git/HEAD
ref: refs/heads/feature-login
HEAD has been updated to point to refs/heads/feature-login. Let’s create a new commit:
$ echo "Login Form UI" > login.html
$ git add login.html
$ git commit -m "Add login page"
[feature-login a7c8e9d] Add login page
1 file changed, 1 insertion(+)
create mode 100644 login.html
Let’s check the contents of our branch references now:
$ cat .git/refs/heads/feature-login
a7c8e9d8928424ef245eefb925b42d3851b2c129
$ cat .git/refs/heads/main
f24e93e25b90f46c6a7e0c4a45adbb474f8e6c7c
feature-login has moved forward to point to our new commit a7c8e9d.... However, main remains pointing to f24e93e.... If we check the history using /git-log:
$ git log --oneline --graph --all
* a7c8e9d (HEAD -> feature-login) Add login page
* f24e93e (main) Initial commit
The graph shows that feature-login has diverged. The main branch pointer is static, while feature-login moved along with HEAD. If we decide to merge these branches later, Git will read these pointers to compute their histories. Learn about this in /git-merge.
Creating, renaming, and deleting branches
The git branch command is not just for creating references; it is a versatile tool for managing, querying, and auditing the branch graph.
1. Listing and Filtering Branches
When managing large projects, you will often find yourself with dozens of local and remote branches. Git provides filters to keep this organized.
- List all branches (local and remote):
This shows local branches in one color (e.g., green) and remote-tracking branches (stored in$ git branch -a.git/refs/remotes/) in another (e.g., red). - Filter by merge status:
Before cleaning up old branches, you want to know which branches have been safely merged into your current branch:
To see branches that contain work not yet merged into your current branch:$ git branch --merged
Running$ git branch --no-mergedgit branch -don any branch in the--no-mergedlist will fail unless forced. - Verbose listing with upstream tracking:
This shows the latest commit hash, the commit subject line, and the upstream tracking status (e.g., whether your local branch is ahead or behind its remote counterpart).$ git branch -vv
2. Deletion Internals and Garbage Collection
Deleting a branch is a common task, but it works differently depending on the flags used.
- Safe Delete (
-dor--delete):
Git will prevent you from deleting a branch if it contains commits that are not reachable from your current branch or upstream branch. This prevents accidental loss of work.$ git branch -d feature-login - Force Delete (
-Dor-d --force):
This tells Git: “Delete the reference, no matter what.”$ git branch -D feature-login
What happens to the commits?
Deleting a branch removes its ref; it does not immediately erase commit objects. If no other ref reaches those commits, they become unreachable and may remain recoverable through reflogs for a time. Git can eventually prune unreachable objects, with timing affected by configuration and repository state. If the work matters, create another branch before cleanup; see Git objects for the object lifecycle.
3. Track Upstream Branches
When collaborating, your local branches track branches on a remote server (like GitHub). This tracking configuration is stored in .git/config.
To set an upstream branch for your current active branch:
$ git branch -u origin/feature-login
# Or:
$ git branch --set-upstream-to=origin/feature-login
This modifies your .git/config file to include tracking metadata:
[branch "feature-login"]
remote = origin
merge = refs/heads/feature-login
This configuration is what allows you to simply type git pull or git push without specifying the remote and branch name.
Recovering a branch you deleted by accident
Imagine you are cleaning up your workspace. You think you’ve merged your branch feature-payment into main, so you run:
$ git branch -D feature-payment
Deleted branch feature-payment (was c3b91a2).
Suddenly, you realize that feature-payment was not merged, and you just deleted days of work! Because the files are no longer visible in your editor, it looks like a disaster.
How to Recover It:
Since Git branches are only references, the commits themselves are still in the database. You just need to find the commit hash and attach a new branch reference to it.
- Search the Reflog:
The reflog records every time
HEADchanges position. Run:
Looking closely, we see that$ git reflog f24e93e HEAD@{0}: checkout: moving from feature-payment to main c3b91a2 HEAD@{1}: commit: Add Stripe Integration and webhooks f24e93e HEAD@{2}: checkout: moving from main to feature-paymentc3b91a2was the commit we made on thefeature-paymentbranch before switching back tomain. - Recreate the Branch Pointer:
Now, run
git branchfollowed by the new branch name and the hash you recovered:
Voila! Git creates a new reference file at$ git branch feature-payment c3b91a2.git/refs/heads/feature-paymentcontainingc3b91a2, restoring your work instantly.
Where branch commands go wrong
1. Thinking git branch Switches to the New Branch
Running git branch new-feature only creates the pointer. It does not update HEAD. If you start editing files immediately, you will write commits to your old branch (e.g., main). Always remember to run git checkout new-feature (or use git checkout -b new-feature to create and switch in one step).
2. Namespace Collisions (File vs Directory)
Because Git stores branch refs as files in .git/refs/heads/, they must follow your Operating System’s file path rules.
- If you have a branch named
feature, Git creates a file named.git/refs/heads/feature. - If you then try to create a branch named
feature/login, Git must create a directory namedfeatureand place a filelogininside it. - On most systems, you cannot have a file and a directory with the exact same name in the same parent folder. This results in the following error:
error: cannot lock ref 'refs/heads/feature/login': 'refs/heads/feature' exists; cannot create 'refs/heads/feature/login' - Fix: Avoid creating a generic branch named
featureif you plan to use slash namespaces likefeature/loginandfeature/payment.
3. Stale Local References to Deleted Remote Branches
When a teammate deletes a branch on the remote server, your local Git still keeps track of it in .git/refs/remotes/origin/. This can lead to confusion.
- Fix: Clean up stale remote-tracking branches by running:
$ git fetch --prune
Branch commands at a glance
| Command | Description | Git Internals Action |
|---|---|---|
git branch | Lists all local branches | Reads files inside .git/refs/heads/ |
git branch <name> | Creates a new branch reference | Creates a 41-byte file at .git/refs/heads/<name> (default unpacked files format) |
git branch -d <name> | Safely deletes a branch if merged | Deletes the ref file; checks reachability first |
git branch -D <name> | Force deletes a branch | Deletes the ref file regardless of reachability |
git branch -m <new-name> | Renames current branch | Renames the ref file in .git/refs/heads/ |
git branch -a | Lists local and remote branches | Reads .git/refs/heads/ and .git/refs/remotes/ |
git branch -vv | Shows tracking status and commits | Correlates local ref files with upstream settings |
Branch questions, answered
What does “detached HEAD” mean?
A “detached HEAD” state occurs when HEAD points directly to a specific commit hash rather than a named branch reference.
For example, .git/HEAD may contain a commit ID instead of ref: refs/heads/main. A commit made in this state is not on a branch; after switching away, it may become unreachable from named refs, though the reflog can help recover it. To keep the work, create a branch while still at that commit with git switch -c rescue-work. See Git refs for how HEAD and branch refs relate.
Can two branches point to the same commit?
Yes. Multiple branches can point to the exact same commit hash. In fact, when you create a new branch from main, both branches point to the same commit until you create a new commit on one of them. Git has no issue with multiple refs containing the same 40-character SHA-1 string.
How do I push a local branch to a remote server?
To push your branch to a remote repository and set it up to track that remote, use the command:
$ git push -u origin <branch-name>
The -u (or --set-upstream) flag tells Git to create a tracking association, updating your .git/config file so that future pushes and pulls on this branch require only git push or git pull.
Check your understanding
Loading quiz…