Skip to content

Branches and Merging

Branching is where Git's design pays off. In older systems like SVN, creating a branch meant copying the entire directory tree - it was slow, expensive, and merging was painful enough that teams avoided branches. In Git, a branch is a 41-byte file containing a commit hash. Creating one is instant. Merging is a first-class operation. This changes how you work: branches become disposable tools for isolating work, not heavyweight decisions.


What Is a Branch?

A branch in Git is a movable pointer to a commit. That's it. The file .git/refs/heads/main contains the 40-character SHA-1 hash of the commit that main currently points to. When you make a new commit on main, Git updates that pointer to the new commit.

main ──→ e4f5a6b ──→ c3d4e5f ──→ a1b2c3d

When you create a new branch, Git creates a new pointer at the current commit. Both branches now point to the same commit - no files are copied:

main ──────→ e4f5a6b
feature/auth ─→ e4f5a6b

As you make commits on each branch, their pointers diverge:

main ────→ f7g8h9i ──→ e4f5a6b
feature/auth ──→ x1y2z3a ──→ e4f5a6b

HEAD points to whichever branch you currently have checked out. It's how Git knows which branch to advance when you commit.


Creating Branches

# Create a new branch at the current commit
git branch feature/search

# Create and switch to it in one step
git switch -c feature/search

# Create a branch at a specific commit
git branch hotfix/login a1b2c3d

# Create a branch from another branch
git branch feature/search-v2 feature/search

Listing Branches

# List local branches (* marks current)
git branch

# List all branches (including remote-tracking)
git branch -a

# List branches with last commit info
git branch -v

# List branches merged into current branch
git branch --merged

# List branches NOT merged into current branch
git branch --no-merged

Switching Branches: git switch vs git checkout

Git 2.23 introduced git switch as a clearer alternative to git checkout for changing branches:

# Modern (Git 2.23+)
git switch main
git switch feature/auth
git switch -c new-branch          # Create and switch

# Legacy (still works)
git checkout main
git checkout feature/auth
git checkout -b new-branch        # Create and switch

git checkout is overloaded - it switches branches, restores files, creates branches, and detaches HEAD. git switch does one thing: switch branches. Use git switch for branch operations and git restore for file operations.

Uncommitted changes and switching

If you have uncommitted changes that would conflict with the branch you're switching to, Git refuses the switch to prevent data loss. You have three options: commit the changes, stash them (git stash), or discard them (git restore .).


Renaming and Deleting Branches

# Rename current branch
git branch -m new-name

# Rename a specific branch
git branch -m old-name new-name

# Delete a branch (only if fully merged)
git branch -d feature/old

# Force delete (even if not merged - you'll lose unmerged commits)
git branch -D feature/abandoned

Recovering a deleted branch

Deleted a branch by accident? The commits still exist in the object database. Use git reflog to find the commit hash, then recreate the branch: git branch recovered-branch a1b2c3d. The Rewriting History guide covers the reflog in depth.


Merging

Merging combines the work from two branches. When you run git merge, Git takes the commits from one branch and integrates them into the current branch.

Fast-Forward Merge

A fast-forward merge happens when the target branch has no new commits since the source branch diverged. Git simply moves the branch pointer forward - no new commit is created:

Before:

main:    A ── B
                \
feature:         C ── D

git switch main
git merge feature

After:

main:    A ── B ── C ── D

Git moved main forward to point at D. The history is linear - no merge commit.

Three-Way Merge

A three-way merge happens when both branches have new commits since they diverged. Git finds their common ancestor, compares both branches against it, and creates a new merge commit with two parents:

Before:

main:    A ── B ── E
                \
feature:         C ── D

git switch main
git merge feature

After:

main:    A ── B ── E ── M
                \      /
feature:         C ── D

M is the merge commit. It has two parents: E (from main) and D (from feature). The merge commit records that these two lines of development were combined.

gitGraph
   commit id: "A"
   commit id: "B"
   branch feature
   commit id: "C"
   commit id: "D"
   checkout main
   commit id: "E"
   merge feature id: "M"

Forcing a Merge Commit

Sometimes you want a merge commit even when a fast-forward is possible, to preserve the record that a branch existed:

git merge --no-ff feature

This is common in workflows where you want the history to show that features were developed on separate branches, even if the history could be linear.


Merge Conflicts

A merge conflict happens when both branches modified the same part of the same file. Git can automatically merge changes to different files or different parts of the same file, but when two branches change the same lines, Git can't decide which version to keep.

What Causes Conflicts

  • Both branches edited the same line(s) of the same file
  • One branch deleted a file that the other modified
  • Both branches added a file with the same name but different content

Reading Conflict Markers

When a conflict occurs, Git marks the conflicting sections in the file:

<<<<<<< HEAD
const timeout = 3000;
=======
const timeout = 5000;
>>>>>>> feature/timeout-update
  • <<<<<<< HEAD to =======: your current branch's version
  • ======= to >>>>>>>: the incoming branch's version

With merge.conflictstyle = diff3 configured, Git also shows the original (base) version:

<<<<<<< HEAD
const timeout = 3000;
||||||| merged common ancestors
const timeout = 1000;
=======
const timeout = 5000;
>>>>>>> feature/timeout-update

This third section (the base) is invaluable - it shows what the code looked like before either change, making it much easier to understand the intent of both modifications.

Resolution Process

  1. Run git merge and see the conflict
  2. Open the conflicted file(s)
  3. Edit to resolve - keep one version, combine both, or write something new
  4. Remove all conflict markers (<<<<<<<, =======, >>>>>>>)
  5. Stage the resolved file(s) with git add
  6. Complete the merge with git commit
git merge feature/timeout-update
# CONFLICT: Merge conflict in config.js

# Edit config.js to resolve the conflict
# ... make your edits ...

git add config.js
git commit    # Opens editor with pre-populated merge commit message

To abort a merge in progress and return to the pre-merge state:

git merge --abort

Branch Management Practices

Naming Conventions

Most teams use prefixed branch names to categorize work:

Prefix Purpose Example
feature/ New functionality feature/user-search
bugfix/ or fix/ Bug fixes bugfix/login-timeout
hotfix/ Urgent production fixes hotfix/security-patch
release/ Release preparation release/2.1.0
chore/ Maintenance tasks chore/upgrade-deps

Keep branch names lowercase, use hyphens for spaces, and include a ticket number if your team uses issue trackers: feature/PROJ-123-user-search.

Short-Lived Branches

Branches work best when they're short-lived. A branch that lives for months accumulates merge conflicts and diverges from the mainline. Aim for branches that last days, not weeks. If a feature is large, break it into smaller branches that merge incrementally.


Exercises


Further Reading


Previous: Commits and History | Next: Remote Repositories | Back to Index

Comments