version control
GitHub tips
BitBucket strategies
merge conflicts
collaboration tools

How to avoid merge-commit hell on GitHub/BitBucket

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Merge-commit hell usually happens when long-lived branches drift too far apart and every integration adds another noisy merge node to the history. The problem is not that merge commits exist at all. The problem is uncontrolled branch lifetime, inconsistent integration habits, and unclear merge policy.

On GitHub and Bitbucket, the cleanest way to avoid that chaos is to keep branches short-lived, integrate often, and choose a merge strategy deliberately instead of mixing merge, rebase, and squash at random.

Keep feature branches small and short-lived

The biggest win is reducing branch lifetime. A feature branch that lives for two days is much easier to integrate than one that lives for three weeks.

Good habits include:

  • open pull requests early
  • merge frequently into the main integration branch
  • avoid giant "feature dump" branches
  • split large work into reviewable slices

Small branches reduce both the probability and the cost of conflicts. They also make pull requests easier to review.

Rebase locally before opening or updating a pull request

If your team wants a mostly linear history, rebase your feature branch onto the latest main before merging it.

bash
git checkout feature/login-cleanup
git fetch origin
git rebase origin/main

If conflicts appear, resolve them locally, continue the rebase, and then update the remote branch:

bash
git add .
git rebase --continue
git push --force-with-lease

--force-with-lease is important because it protects against overwriting someone else's remote work accidentally.

This approach keeps the pull request branch current without generating repeated "merge main into feature" commits.

Choose one PR merge policy and enforce it

GitHub and Bitbucket both let you choose how pull requests land:

  • merge commit
  • squash merge
  • rebase merge

The trouble starts when a team uses all three without a shared rule.

A common clean policy is:

  • developers rebase feature branches locally
  • pull requests are merged with squash or rebase
  • direct merge commits into main are restricted

That gives a readable history while still allowing local iterative commits during development.

Use branch protection and CI to reduce emergency merges

Many ugly merge histories are a symptom of rushed integration. Branch protection helps by forcing a more predictable path:

  • require pull requests
  • require passing CI
  • require up-to-date branches before merge
  • limit who can push directly to main

These settings are available on both GitHub and Bitbucket in different forms, and they prevent the "just merge it quickly" behavior that often creates tangled history.

Avoid rebasing shared collaboration branches casually

Rebasing is excellent for private feature branches. It is risky for branches that multiple people already share because rebasing rewrites commit history.

Safe rule:

  • rebase your own feature branch
  • avoid rebasing a branch that teammates are already building on

If several people collaborate on one branch, either coordinate very carefully or accept that a normal merge may be the safer choice.

Merge from main into a branch only when policy requires it

Some teams regularly run:

bash
git merge origin/main

inside a feature branch to keep it updated. That works, but if it happens repeatedly across many branches, history becomes cluttered quickly.

If your team prefers clean history, rebasing local feature branches is usually better than repeatedly merging the main branch into them. The important part is consistency across the team.

Common Pitfalls

The most common mistake is letting feature branches live too long. No merge strategy can save a branch that has drifted for weeks.

Another issue is mixing policies. One developer rebases, another merges main into the branch every day, and the platform uses merge commits on PR completion. The result is history that nobody can read comfortably.

Teams also misuse force-push. If you rebase, use --force-with-lease, not plain --force.

Finally, do not optimize only for pretty history. A clean graph matters, but predictable collaboration rules matter more.

Summary

  • Short-lived feature branches are the strongest defense against merge-commit hell.
  • Rebase local feature branches before merging if your team wants linear history.
  • Pick one pull-request merge policy and enforce it consistently.
  • Use branch protection and CI to prevent rushed, messy integrations.
  • Rebase private branches freely, but be careful with shared branches because rebasing rewrites history.

Course illustration
Course illustration

All Rights Reserved.