Branches & merging

Status: đźź© COMPLETE Last updated: 2026-06-20 Plain-English tagline: Branches let you work on changes in parallel without touching the main code. Merging combines them back. The core mechanism that makes Git collaboration possible.


In plain English

A branch in Git is a parallel line of development. Think of it like a copy of your project that you can experiment in. You make commits on the branch; the main code stays untouched. When you’re happy, you merge the branch back into main — the changes become part of the official history.

If the experiment doesn’t work out, you delete the branch. No harm done. The main code never saw your half-finished attempt.

This is why Git is “cheap” for experimentation. Branches are instant to create, instant to switch between, and free to abandon. Use them liberally.


Why it matters

Without branches:

  • Every change you make goes directly onto the main code.
  • Half-finished work is visible to everyone (and gets deployed if you’re unlucky).
  • Two people working on different features have nothing to do but step on each other.
  • You can’t quickly fix an urgent bug if you’re in the middle of unrelated work.

With branches:

  • You work on a feature on its own branch, isolated from main.
  • You can switch back to main instantly for an urgent fix on a separate branch.
  • Two people work on different branches without conflict until merge time.
  • Half-finished work stays on the branch — main is always shippable.

Modern workflow assumes branches. Vercel previews work because Vercel knows what’s on which branch. Pull requests work because PRs merge one branch into another. Code review works because the diff is one branch’s commits vs another’s.


The mental model

Imagine a timeline:

                          o ── feature/dark-mode
                         /
main:  A ── B ── C ── D
                         \
                          o ── o ── feature/search
  • The horizontal line is main. Each o is a commit.
  • feature/dark-mode branches off at commit D.
  • feature/search also branches off, separately.
  • The two features develop in parallel.
  • Eventually, each branch gets merged back into main.

Branches are just labels pointing at commits. When you make a new commit on a branch, the label moves forward. It’s not heavyweight; the underlying file storage is shared.


Common branch naming conventions

You can name a branch anything, but conventions help:

  • main — the production / latest stable branch (formerly master — most projects renamed it after 2020)
  • develop or dev — sometimes used for integration before main (less common in 2026)
  • feature/<name> — new features (e.g. feature/dark-mode)
  • fix/<name> or bugfix/<name> — bug fixes
  • hotfix/<name> — urgent production fixes
  • chore/<name> — non-feature work (refactoring, deps, etc.)
  • docs/<name> — documentation
  • release/<version> — release prep (uncommon for solo / small teams)

The slash is just convention — GitHub treats them as flat names but renders them with folder structure in the UI. Pick a convention and stick to it.


Creating, switching, deleting branches

# Create a branch (doesn't switch to it)
git branch feature/dark-mode
 
# Create AND switch to a branch (most common)
git switch -c feature/dark-mode
# or older equivalent:
git checkout -b feature/dark-mode
 
# Just switch to an existing branch
git switch main
 
# List all branches
git branch
git branch -a              # include remote branches
 
# Rename current branch
git branch -m new-name
 
# Delete a branch (only if it's been merged)
git branch -d feature/old
 
# Force-delete (even if unmerged — careful)
git branch -D feature/old

Pushing a branch to GitHub

# First time pushing this branch — set the upstream
git push -u origin feature/dark-mode
 
# After that, plain push works
git push

The -u (or --set-upstream) tells Git “this local branch tracks the remote branch with the same name.” After this, git push and git pull know what to do.

To delete a branch on the remote:

git push origin --delete feature/dark-mode

Merging — bringing changes back

When your branch is ready, you merge it into main:

git switch main         # go to the destination
git pull                # make sure main is up to date
git merge feature/dark-mode
git push                # push the merged main

Git tries to combine the changes automatically. Two outcomes:

Fast-forward merge (the clean case)

If main hasn’t changed since you branched off, Git just moves main’s pointer forward to include your commits. No new “merge commit.” History looks linear.

Before:        A ── B ── C ── D  (main)
                              \
                               E ── F  (feature)

After:         A ── B ── C ── D ── E ── F  (main = feature)

Merge commit (the integration case)

If main has had new commits since you branched, Git creates a “merge commit” that combines both lines:

Before:        A ── B ── C ── D ── G ── H  (main)
                              \
                               E ── F  (feature)

After:         A ── B ── C ── D ── G ── H ── M  (main)
                              \             /
                               E ── F ───────

The merge commit M has two parents (the tips of both lines being merged). The history is a graph, not a line.

Conflict (the messy case)

If both branches changed the same lines of the same file, Git can’t auto-merge. You get conflicts, which you have to resolve manually. See Resolving merge conflicts.


The three merge strategies in PRs

When merging a PR on GitHub, you typically have three choices:

Merge commit (default)

Preserves the branch history with a merge commit. Linear-ish but you see the branch structure.

Squash and merge

All the commits on the branch get collapsed into ONE commit on main. Loses fine-grained history; main stays linear and clean.

Many teams prefer squash because:

  • Main’s history reads like a sequence of features, not a mess
  • Reverting a feature = reverting one commit
  • Forces the PR author to write a coherent merge message

Rebase and merge

Re-applies the branch’s commits onto main without a merge commit. Linear history but preserves individual commits.

For your projects, squash and merge is usually the right default unless there’s a specific reason to keep individual commits.


When to branch

Branch for any unit of work that could fail or take multiple commits. Specifically:

  • Every feature
  • Every bug fix
  • Every experiment
  • Every dependency update worth its own PR

Don’t branch for:

  • One-line typo fixes you’ll commit directly to main (in personal projects only — and only if you’re confident)
  • Drafts that won’t survive 5 minutes

For solo work, you might commit small things directly to main. For team work, always branch + PR.


A concrete example: building a feature

# Start on main, make sure it's current
git switch main
git pull
 
# Create a branch
git switch -c feature/dark-mode
 
# ... write code ...
 
git add lib/theme.ts
git commit -m "Add theme provider with light/dark detection"
 
# ... more code ...
 
git add components/ThemeToggle.tsx app/layout.tsx
git commit -m "Add ThemeToggle component to navbar"
 
# ... more code ...
 
git add app/globals.css
git commit -m "Add dark-mode color variables and utility classes"
 
# Push to GitHub
git push -u origin feature/dark-mode
 
# Open a PR (via GitHub UI or gh CLI)
gh pr create --title "Add dark mode" --body "..."
 
# After review/CI passes, merge in GitHub UI (squash)
 
# Clean up locally
git switch main
git pull
git branch -d feature/dark-mode

Three commits, one branch, one PR, one merge. Clean.


Common gotchas

  • Branched off the wrong base. Always start from up-to-date main: git switch main && git pull, THEN git switch -c new-branch. Otherwise you might branch off an old main.

  • Forgot to push before deleting locally. git branch -D feature/x doesn’t delete the remote. If you also want it gone on GitHub: git push origin --delete feature/x.

  • Merging main into your feature instead of the other way. Sometimes you want main’s recent changes in your feature branch (to avoid conflicts later). Run git switch feature/x then git merge main. This brings main’s changes INTO the feature. It’s a common confusion — the destination is your current branch.

  • Long-lived branches. Branches that live for weeks accumulate divergence with main. Conflicts pile up. Merge or rebase regularly to stay current.

  • “Detached HEAD.” If you git checkout a specific commit hash (not a branch), you’re not on any branch. Commits you make are easy to lose. Switch to a branch before committing.

  • Branch names with special chars. Slashes are fine. Spaces, special characters, leading dots — avoid.

  • The remote branch and local branch can drift. If you push, then someone (or another machine) pushes more, your next push fails. Pull first.

  • Renaming the default branch. Renaming master → main for an existing repo requires updates to local clones, CI configs, and protected branch rules. GitHub provides a one-click flow for this.

  • Branch protection rules surprise you. If main is protected, you can’t push directly — only via PR. This is intentional for shared projects.

  • Merging when behind. If main has moved since you branched and you try to merge, expect potential conflicts. Pull main into your branch first, resolve any conflicts there, then merge to main.


See also


Sources