AlgoInsight

Free lesson · Git Mastery

Branching Basics

Medium8 min readgit

Branching Basics

A note from Nikhil Joshi, Senior Software Engineer at Google Cloud Spanner.

What a Branch Actually Is

The mental model that took me longest to internalize, coming from SVN at Persistent Systems, was what a Git branch actually is. In SVN, a branch is a directory - a server-side copy of the entire codebase. Creating one is slow, switching between them is slow, and the implementation makes that slowness fundamental rather than incidental.

In Git, a branch is a forty-byte file. The file contains the hash of one commit. That's it. The branch "points at" that commit. Creating a branch creates one of these files; deleting a branch deletes one of these files. The actual codebase isn't copied anywhere.

This single design choice - branches as cheap pointers rather than as copies - is why Git's branching model is so different from older systems. Operations that were prohibitive in SVN are trivial in Git. The cultural patterns that surround branching shift dramatically as a result.

This topic covers what a branch is at the data level, what HEAD means in relation, and why branching in Git is fundamentally cheap. Topics 6.2 through 6.9 cover the specific operations.

The Pointer Model

flowchart LR
    A[Commit A] --> B[Commit B] --> C[Commit C]
    M[main pointer<br/>40 bytes] -.->|points at| C
    F[feature pointer<br/>40 bytes] -.->|points at| B
This diagram shows the pointer model. The commit history is the chain of commits. Each branch is a tiny pointer file that names one commit. Multiple branches can exist simultaneously, each pointing at a different commit, with no duplication of the underlying data. Creating a branch is just creating another pointer file; deleting a branch is just deleting the pointer file, not the commits it pointed at. The commits themselves are part of the repository's object store and are shared across all branches that reference them.

A branch is just a name plus a commit hash. The pointer can be moved - when you commit on a branch, the branch's pointer moves to the new commit. When you switch branches, your working directory updates to match the commit the new branch points at. The branches themselves are tiny; the heavy lifting is in the commit objects, which are shared.

You can see the actual pointer files inside .git:

ls .git/refs/heads/
cat .git/refs/heads/main
The refs slash heads directory inside dot git is where branch pointers live. Each branch is a file. The file's contents are the forty-character hash of the commit the branch points at. You can read these files directly with cat to see the pointer's current value. Don't edit them directly; use Git commands. But seeing the file contents demystifies what a branch actually is.

The sample output is one line, forty hex characters - the commit hash that branch points at. Nothing more.

What HEAD Is

HEAD is a special pointer that names which branch you're currently on. It's the second file you should look at:

cat .git/HEAD
The HEAD file at the root of dot git contains either a branch name - typically formatted as ref colon refs slash heads slash branch-name - or a commit hash directly. The first form is the normal state, where HEAD points at a branch. The second form is detached HEAD, where HEAD points directly at a commit without going through a branch. Topic 6.8 covers HEAD in depth; Topic 6.9 covers detached HEAD.

Sample contents:

ref: refs/heads/main

When you make a commit, two things happen: a new commit object is created, and the current branch (the one HEAD points at) gets its pointer updated to the new commit. HEAD itself doesn't change - it still points at the same branch - but the branch points at a different commit now.

This is the chain: HEAD → branch → commit. Each link is small. Each operation is fast because the data being modified is small.

Why Branches in Git Are Cheap

Three reasons branching is cheap, all flowing from the pointer model:

Creation is constant-time. Creating a branch is creating one forty-byte file. The cost doesn't depend on the size of the repository, the depth of history, or the number of files. A branch on a one-commit repository takes the same time to create as a branch on the Linux kernel's million-commit history.

Switching is mostly about working-directory updates. When you switch from one branch to another, Git compares the trees of the two branches' tip commits and updates only the files that differ. If the two branches' commits are close in history, very few files change. The "switch" feels nearly instant.

No data duplication. The commits themselves are part of the repository's object store, shared across all branches that reference them. A repository with twenty branches has the same on-disk footprint as a repository with one branch, plus twenty forty-byte pointers.

The implication: don't conserve branches. The team I worked with at Razorpay routinely had hundreds of active branches across the company - one or two per engineer plus several shared feature branches. Total disk overhead from branches themselves: a few kilobytes. The branches helped us organize work without imposing real cost.

The Default Branch

Every Git repository has a default branch - the first one created when you git init (Topic 2.1) or the one a git clone (Topic 2.2) checks out by default. Historically this was called master; modern convention has shifted to main. Topic 1.7 covered the init.defaultBranch configuration that controls the name for new repositories.

The default branch is special only by convention. There's nothing technically distinct about it; it's just the one the team has agreed is the canonical line of development. Other branches branch off from it; merges happen back into it; deployments come from it.

A team can choose any name for the default. Some use main. Some use master for historical reasons. Some use trunk or develop. The mechanics are identical; only the convention varies.

A Specific Story From Razorpay

At Razorpay, the move from SVN to Git was my first sustained experience with the branching model. The first month, I branched reluctantly. SVN had trained me that branches were expensive - I'd create one only when there was no alternative, and I'd merge it back quickly to avoid leaving it lying around.

About six weeks in, a senior engineer noticed I was committing experimental code straight to my feature branch without separating concerns. Her suggestion: "Why don't you branch when you have an idea you want to try?" I started doing that. By the end of the month I had maybe fifteen branches at various points of completion - some abandoned, some merged, some still active.

The realization that hit me: every one of those branches was free. The fifteen-character branch names were the entire cost. The codebase wasn't copied anywhere. The repository didn't get bigger. The branches just existed as small forty-byte pointer files, ready to be checked out when I wanted to revisit an idea or thrown away when the idea didn't pan out.

The cultural shift took a couple of months to fully absorb. By the third month I was branching freely, working on multiple ideas in parallel, and abandoning branches without hesitation when they didn't lead anywhere. That pattern - branching as a thinking tool, not a deployment milestone - is how Git's branching model intends to be used.

Branches vs Stashes

Topic 5.8 covered git stash as the way to set aside in-progress work. Branches are the alternative. Which is right depends on duration and shape:

Use stash for:

Use branches for:

The distinction is roughly: stash is for "I need to be on a clean tree for ten minutes." Branch is for "I'm developing this idea over time."

Branches in a Distributed System

A branch in your local clone is independent from a branch with the same name in someone else's clone or on the remote. The names happen to match by convention; the underlying commits may diverge.

When you push a branch to a remote, you're saying "send my version of branch X to the remote." If others have pushed to the same branch in the meantime, you'll be refused unless you've integrated their changes (typically via pull - Topic 9.4) or unless you force-push (Topic 8.4 covers when force-push is and isn't safe).

When you pull a branch from the remote, you're updating your local branch to match the remote's version. If your local branch has diverged, the integration becomes a merge or rebase.

This distributed view is important. Branches aren't shared state; they're each engineer's local view that they choose to synchronize with a remote via push and pull. Module 9 covers remotes in depth.

A Pattern at Spanner

On the Spanner team at Google, the typical engineer has between three and ten active branches at any given time. The pattern:

None of this is unusual; it's just what happens when branching is cheap and the discipline encourages parallel exploration. The branches that pan out get merged; the ones that don't get deleted. The repository remains tidy because branch deletion is just as cheap as branch creation.

What Comes Next

Topic 6.2 covers creating branches in detail. Topic 6.3 covers git switch - the modern command for switching between branches. Topic 6.4 covers git checkout for branch switching (the older equivalent). Topic 6.5 covers deleting branches. Topic 6.6 covers renaming. Topic 6.7 covers listing. Topics 6.8 and 6.9 cover HEAD in depth, including detached HEAD state.

After Module 6, Module 7 covers merging - combining work from different branches.

What Goes Wrong

Warning: Common branching-model confusions:

Treating branches as expensive. They aren't. Create them freely.

Believing branches duplicate the codebase. They don't. Only the commits are stored; branches are pointers.

Expecting local branches to automatically sync with remote. They don't. Push and pull are explicit operations.

Confusing branch names across clones. "My main" and "their main" can diverge. Pull or fetch to see what's on the remote.

Believing a branch is "the code in that folder." There's no folder. The branch is a pointer; the code is the working directory after you check out the branch.

Reading master as inherently different from other branches. It's not. It's a name with a convention attached. The mechanics are identical for any branch.

Try It

Exercise: Build the mental model:

  1. In a practice repository, run ls .git/refs/heads/ and look at the files. Each file is a branch.
  2. Run cat .git/refs/heads/main. Note that it's just a commit hash.
  3. Run cat .git/HEAD. Note that it points at a branch name in the form ref: refs/heads/main.
  4. Make a commit on main. Read cat .git/refs/heads/main again. Note that the hash has changed - the branch pointer moved.
  5. Create a branch: git branch experimental. Look at ls .git/refs/heads/. There's a new file. Cat it; it's the same hash as main.
  6. Walk through what would happen on disk if you created twenty branches: twenty 40-byte pointer files. About 800 bytes total. That's the storage cost of having twenty branches.

Back to Git Mastery