Search…

Branching and merging

In this series (8 parts)
  1. Git internals: how Git actually works
  2. Everyday Git: the commands that matter
  3. Branching and merging
  4. Branching strategies for teams
  5. Git rebase and history rewriting
  6. Git hooks and automation
  7. Monorepos and large repo management
  8. GitOps

Branches are Git’s killer feature. They cost almost nothing to create, switch between, or delete. This makes parallel development natural instead of risky.

From the internals article, you know a branch is a pointer to a commit. This article covers what happens when branches diverge and how to bring them back together.

Creating and switching branches

# Create a branch
git branch feature

# Switch to it
git checkout feature

# Create and switch in one step
git checkout -b feature

# Modern alternative
git switch -c feature

When you create a branch, Git writes a new ref file. When you switch to it, Git updates HEAD and rewrites your working directory to match that branch’s latest commit.

How branches diverge

When two branches receive different commits, they diverge.

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

Main and feature have diverged. Main has commit E. Feature has commits C and D.

Commit B is the common ancestor. Any merge strategy needs to reconcile the changes made after B on both branches.

Fast-forward merge

If the target branch has no new commits since the branch point, Git can simply move the pointer forward. No merge commit is needed.

# main has not moved since feature branched off
git checkout main
git merge feature
# Fast-forward: main pointer moves to feature's tip
gitGraph
commit id: "A"
commit id: "B"
branch feature
commit id: "C"
commit id: "D"
checkout main
merge feature

A fast-forward merge. Main moves to where feature is. No merge commit is created.

Fast-forward merges produce clean, linear history. Some teams enforce them with --ff-only.

git merge --ff-only feature
# Fails if fast-forward is not possible

Three-way merge

When both branches have new commits, Git performs a three-way merge. It compares the common ancestor with both branch tips and creates a merge commit with two parents.

git checkout main
git merge feature
# Merge made by the 'ort' strategy.
gitGraph
commit id: "A"
commit id: "B"
branch feature
commit id: "C"
commit id: "D"
checkout main
commit id: "E"
merge feature id: "M"

A three-way merge. Commit M has two parents: E (from main) and D (from feature).

The merge commit M records that two lines of work were combined. History preserves the fact that parallel development happened.

Rebase

Rebase takes a different approach. Instead of creating a merge commit, it replays the feature branch’s commits on top of the target branch.

git checkout feature
git rebase main
gitGraph
commit id: "A"
commit id: "B"
commit id: "E"
commit id: "C-prime" tag: "C'"
commit id: "D-prime" tag: "D'"

After rebasing feature onto main. Commits C and D are replayed as C’ and D’ with new hashes.

The original commits C and D still exist in the object store but are no longer reachable from any branch. The new commits C’ and D’ have different parent pointers and therefore different hashes.

Merge vs rebase: the trade-off

AspectMergeRebase
HistoryPreserves branching topologyCreates linear history
Merge commitYes (for three-way)No
Commit hashesUnchangedNew hashes for replayed commits
SafetySafe on shared branchesDangerous on shared branches
Conflict resolutionOncePotentially once per replayed commit

Neither is universally better. Merge preserves the full picture of how work happened. Rebase produces a cleaner log that is easier to bisect.

Merge conflicts

Conflicts occur when both branches modify the same lines in the same file. Git cannot decide which version to keep.

<<<<<<< HEAD
const port = 3000;
=======
const port = 8080;
>>>>>>> feature

The section between <<<<<<< HEAD and ======= is your current branch. The section between ======= and >>>>>>> feature is the incoming branch.

Resolving conflicts

  1. Open the conflicted file.
  2. Choose the correct version (or combine both).
  3. Remove the conflict markers.
  4. Stage the resolved file.
  5. Complete the merge.
# After editing the conflicted file
git add src/config.js
git commit
# Git opens the editor with a pre-filled merge commit message

Aborting a merge

If conflicts are too complex and you want to start over:

git merge --abort

This resets your working directory and index to the state before the merge began.

Conflict prevention strategies

Conflicts are inevitable in team environments. You can reduce their frequency.

  • Keep branches short-lived. Long-running branches diverge further and produce more conflicts.
  • Merge main into your feature branch regularly. This keeps your branch close to the target.
  • Communicate about shared files. If two people are editing the same module, coordinate.
  • Use small, focused commits. They are easier to rebase and resolve.

Octopus merges

Git can merge more than two branches at once. This is called an octopus merge.

git merge feature-a feature-b feature-c

Octopus merges create a commit with multiple parents (one per merged branch plus the current branch). They are useful when integrating several independent feature branches that do not conflict.

The octopus strategy cannot handle conflicts. If any branch conflicts with another, Git aborts and you need to merge them individually.

Practical workflow

A typical daily workflow combines branching, committing, and merging.

# Start a feature
git checkout -b feature/user-auth

# Work and commit
git add .
git commit -m "Add login endpoint"

# Keep up with main
git fetch origin
git rebase origin/main

# Push the branch
git push origin feature/user-auth

# After code review, merge on the remote (PR merge)
# Or locally:
git checkout main
git merge feature/user-auth
git push origin main

# Clean up
git branch -d feature/user-auth

Listing and managing branches

# List local branches
git branch

# List remote branches
git branch -r

# List all branches
git branch -a

# Delete a merged branch
git branch -d feature/done

# Force delete an unmerged branch
git branch -D feature/abandoned

# Prune stale remote-tracking branches
git fetch --prune

Regular cleanup prevents branch sprawl. Stale branches confuse the team and clutter the log.

Rerere: reuse recorded resolutions

If you find yourself resolving the same conflict repeatedly (common during long-running rebases), enable rerere.

git config --global rerere.enabled true

Git records how you resolved a conflict and automatically applies the same resolution if it encounters the identical conflict again.

What comes next

You now understand the mechanics of branching and merging. The next article covers branching strategies: how teams organize branches to support different release cadences and workflows. The mechanics stay the same. The strategy determines when and how you apply them.

Start typing to search across all content
navigate Enter open Esc close