Using Feature Branches Correctly Without Merge Hell
AI generated
git
HEAD
Git · Feature Branches · Merge Strategy · Team Workflow
Using Feature Branches Correctly Without Merge Hell
Short branches, frequent syncs, clean merges

Long lived feature branches quietly grow into tangled merge conflicts and cost development teams valuable time on every release. This article shows in practical terms how to keep feature branches short and focused, sync regularly with main, detect oversized branches early and split them cleanly, and use a clear branch per ticket workflow to permanently avoid merge hell in Magento and PHP projects.

13 min. read Branch Per Ticket · Rebase · Code Review Git · GitLab · GitHub · PHP Teams

1. What merge hell is and why feature branches are meant to prevent it

Merge hell is the moment when a feature branch, after weeks of parallel development, has to be merged into main and dozens of files are in conflict at once. Instead of a controlled integration, a days long fight against contradictory changes breaks out, and nobody can say with confidence which version of which file is correct anymore. Ironically, the exact opposite is the actual purpose of feature branches: they are meant to isolate work so it can be tested and reviewed independently, not to drift away from main for weeks.

In PHP and Magento projects the problem gets worse because of generated files like di.xml caches, Composer lockfiles, and frequently edited layout XML files, which are almost guaranteed to collide across several parallel branches. Treating feature branches as a tool for isolation rather than a storage bin for months of work prevents merge hell from the start. The following eight sections cover concrete principles and workflows that keep branches small, current, and mergeable.

2. Principle 1: keep feature branches short and focused

The first and most important principle: a feature branch should represent exactly one coherent concern, not several. A branch that simultaneously contains a new payment module, a checkout bugfix, and a refactor of the ProductRepository can neither be reviewed meaningfully nor merged cleanly. The rule of thumb: if the change cannot be described in a single sentence, it probably belongs in more than one branch.

A branch's lifespan should ideally be one to three days, at most a week for larger features. The longer a branch diverges from main, the bigger the diff grows, the more commits land in main in the meantime, and the more likely conflicts become at merge time. A short lived feature branch is not a sign of shallow work, it is a sign of disciplined scope management: small, clearly bounded changes that can be reviewed, merged, and deleted quickly.

3. The branch per ticket workflow in practice

The branch per ticket workflow ties every feature branch to exactly one ticket in the issue tracker, whether that's Jira, GitLab Issues, or GitHub Issues. The naming convention feature/TICKET-123-short-desc or fix/TICKET-456-checkout-error makes it immediately obvious which branch belongs to which task, without anyone having to look it up in the tracker. Many trackers such as Jira and GitLab automatically recognize the ticket ID in the branch name and link commits and pull requests directly on the ticket.

In practice this means: a ticket is only moved into progress once a branch exists for it, and a branch is never created without an associated ticket. That implicitly forces small units of work, since tickets themselves are usually cut small. A Git alias for the correct naming prevents typos that would otherwise break later automation such as CI triggers based on branch prefixes.


#!/usr/bin/env bash
# Create a branch for a ticket, always based on the latest main
git checkout main
git pull origin main
git checkout -b feature/TICKET-123-add-payment-retry

# Push the branch and set upstream tracking immediately
git push -u origin feature/TICKET-123-add-payment-retry

# Naming convention keeps CI and issue trackers in sync
# feature/TICKET-123-short-desc for new features
# fix/TICKET-456-short-desc for bugfixes

4. Principle 2: sync regularly with main

The second principle: sync regularly with main instead of waiting until the end. A feature branch that pulls in the latest changes from main every day sees conflicts in small, digestible pieces, instead of experiencing them at the end as one huge, unmanageable pile. Two mechanisms are available: git rebase origin/main and git merge origin/main.

Rebase rewrites your own commits on top of the current state of main and produces a linear, clean history without merge commits. The downside: for branches already pushed, rebase requires a force push, which is dangerous on branches shared with others. Merge creates an extra merge commit but never rewrites existing commits, making it safer for branches with multiple contributors. The pragmatic recommendation: rebase for private, not yet shared feature branches, merge for branches with multiple contributors.


#!/usr/bin/env bash
# Sync a feature branch with the latest main via rebase
git fetch origin
git rebase origin/main

# If commits were already pushed, force-push with lease for safety
git push --force-with-lease origin feature/TICKET-123-add-payment-retry

# Alternative: merge instead of rebase for branches with multiple contributors
# git merge origin/main

5. When a feature branch has grown too large

An oversized feature branch shows clear signs long before the merge is due. The number of changed lines is a first indicator: a diff of more than 400 to 500 lines is barely fully graspable for most reviewers. The number of days a branch has been open is a second indicator: branches that live longer than a week almost inevitably accumulate conflicts with colleagues working in parallel.

A third, often overlooked indicator is the number of files that potentially collide with other open branches. git diff main...feature/TICKET-123 --stat quickly shows how many files and lines are affected. A look at git log main..feature/TICKET-123 --oneline | wc -l shows how many commits have piled up. If a branch contains more than ten thematically unrelated commits, that's a strong signal that several concerns got mixed together and the branch should be split.

6. Splitting an oversized branch into smaller pull requests

A branch that has grown too large can almost always be split after the fact, even though that requires more discipline than staying small from the start. The approach: individual, self contained commits are pulled from the large branch into new, smaller branches with git cherry-pick, so each one can be reviewed and merged independently. Every new sub branch gets its own ticket and its own pull request.

Feature flags are the key enabler for this strategy: when a large feature is hidden behind a flag like if (FeatureFlags::isEnabled('new_checkout_flow')), individual pieces of it can be merged without exposing unfinished functionality to real users. That decouples merging from releasing and allows continuously integrating small, verified increments into main, instead of risking one giant merge at the end of the feature cycle.


#!/usr/bin/env bash
# Split an oversized branch into smaller, independently mergeable branches
git checkout main
git checkout -b feature/TICKET-123-checkout-validation

# Cherry-pick only the commits relevant to this smaller scope
git cherry-pick a1b2c3d
git cherry-pick e4f5g6h

# Push and open a focused pull request for just this slice
git push -u origin feature/TICKET-123-checkout-validation

7. Resolving conflicts early and often instead of at the end

Resolving conflicts early is cheaper than trying to avoid them altogether. Anyone working with git fetch origin && git rebase origin/main daily sees conflicts in small doses: usually one or two files whose context is still fresh in mind. Anyone who instead skips syncing for weeks ends up facing a conflict across twenty files at once, where the original context of many changes has long been forgotten.

The practical flow for a conflict: git status shows the affected files, the conflict markers are resolved manually, the cleaned up file is staged with git add, and git rebase --continue resumes the rebase with the next commit. A merge tool like git mergetool or the conflict view in PhpStorm speeds up this process considerably, especially for generated files like Composer lockfiles, where a clean regeneration is often faster than resolving by hand.


#!/usr/bin/env bash
# Resolve conflicts during a rebase, one file at a time
git status

# After manually fixing conflict markers in the affected files
git add app/code/Mironsoft/Checkout/Model/Validator.php
git rebase --continue

# Abort and start over if the conflict resolution goes wrong
# git rebase --abort

8. Code review practices against stagnating branches

Small pull requests are not only easier to merge, they are also faster to review. A pull request under 200 changed lines gets reviewed noticeably more thoroughly than one over 1000 lines, where reviewers tend to only skim by experience. Small PRs are therefore not just a merge hell prevention tool, they are a quality tool for the whole review process.

A fast review turnaround prevents branches from sitting unreviewed for days while main keeps growing. A team standard like "reviews within four hours" keeps branches short lived, because authors get timely feedback and can merge quickly. Draft pull requests signal that a branch is still in progress, while still allowing early feedback on architecture before too much code gets written in the wrong direction and has to be reworked entirely later.

9. Cleanup, hygiene, and the direct comparison

Part of the hygiene of a healthy Git workflow is consistently deleting merged branches, both locally with git branch -d and on the remote with git push origin --delete. Orphaned branches otherwise pile up over months and make it harder to see which branches still represent active work. git fetch --prune removes local references to branches already deleted on the remote and keeps the local view clean.

The main branch should be protected with branch protection rules: no direct pushes, mandatory review before merge, a passing CI pipeline as a prerequisite. A practical checklist before every merge: the branch is younger than a week, the diff is under 500 lines, it has been rebased onto the current main within the last 24 hours, CI is green, and there is at least one review approval. The table below compares bad and good feature branch habits directly.


#!/usr/bin/env bash
# Clean up branches after they have been merged into main
git branch -d feature/TICKET-123-add-payment-retry
git push origin --delete feature/TICKET-123-add-payment-retry

# Remove local references to branches already deleted on the remote
git fetch --prune

# List merged branches that are safe to delete
git branch --merged main | grep -v "main"
Habit Bad practice Good practice Effect
Branch lifespan Branch lives for weeks Branch lives 1 to 3 days Small diff, fewer conflicts
Synchronization No rebase, conflicts at the end Daily rebase onto main Conflicts in small doses
Pull request size One giant pull request Several small, focused PRs Faster, more thorough reviews
Ticket linkage Branch with no ticket reference Branch per ticket with a clear convention Traceability across the whole team
After the merge Merged branches are left lying around Branches are deleted immediately A tidy repository
main protection main unprotected, direct pushes allowed main protected, review and CI required No untested changes land in main

In modern Git teams, these habits aren't a matter of style, they are measurable factors for lead time and code quality. Consistently applying the recommendations from the table and enforcing them technically through branch protection rules prevents merge hell structurally instead of merely hoping to avoid it.

Mironsoft

Git workflow consulting, code review process setup, and developer training for PHP teams

Ready to establish feature branches without merge hell?

We analyze your Git workflow, set up a practical branch per ticket process, and train your team on rebase strategies, branch protection, and code review practices for Magento and PHP projects.

Workflow audit

Analysis of existing branching strategies and merge conflict hotspots

Process setup

Setting up branch per ticket, branch protection, and CI gates

Team training

Hands on training in rebasing, conflict resolution, and code review practices

10. Summary

Using feature branches correctly mostly means keeping them small, short lived, and focused on a single topic. The branch per ticket workflow prevents multiple concerns from getting mixed together uncontrolled. Regularly rebasing or merging with main ensures conflicts show up in small, manageable doses instead of one unmanageable merge hell at the end. When a branch still grows too large, cherry picking and feature flags allow splitting it after the fact into smaller, independently mergeable units.

The long term lever is team culture: fast code reviews, consistently deleting merged branches, and a protected main branch with a CI gate ensure these principles don't just exist on paper but are actually practiced day to day. Teams that establish this discipline spend noticeably less time resolving conflicts and more time on the actual feature.

Feature Branches Without Merge Hell - The Essentials at a Glance

Short lived branches

Feature branches live 1 to 3 days, at most a week. One concern per branch, clearly scoped.

Branch per ticket

Every branch belongs to exactly one ticket, naming convention feature/TICKET-123-short-desc.

Regular rebasing

Daily git fetch && git rebase origin/main, conflicts in small doses instead of at the end.

Cleanup & protection

Delete merged branches immediately, protect main with branch protection and a CI gate.

11. FAQ: Using Feature Branches Correctly

1What is merge hell and how does it happen?
Happens when a feature branch is developed independently from main for weeks and dozens of files collide at once during the merge. Regular synchronization prevents it from the start.
2How long should a feature branch live at most?
Ideally one to three days, at most a week for larger features. The longer the divergence from main, the bigger the diff and the more likely conflicts become.
3What is a branch per ticket workflow?
Every branch is tied to exactly one ticket, identified by feature/TICKET-123-short-desc. Forces small, traceable units of work.
4Should I rebase or merge to sync with main?
Rebase for private branches for a linear history. Merge for branches with multiple contributors, since existing commits stay untouched.
5How do I know a feature branch has grown too large?
A diff over 400 to 500 lines, a lifespan over a week, or more than ten thematically unrelated commits are strong signals.
6How do I split an oversized feature branch?
With git cherry-pick, pull self contained commits into new, smaller branches that can be reviewed and merged independently.
7What do feature flags have to do with merge hell?
They decouple merging from releasing: code behind a flag can be merged in small increments without exposing unfinished functionality to users.
8What's the most efficient way to resolve merge conflicts?
Frequent rebasing keeps conflicts small. git status shows affected files, git add stages the fix, git rebase --continue resumes.
9How fast should a code review happen?
A few hours up to one day at most. Small pull requests under 200 lines get reviewed more thoroughly and faster than huge PRs.
10Why should I delete merged branches right away?
Orphaned branches make it harder to see which branches represent active work. git branch -d, git push origin --delete, and git fetch --prune keep the repository clean.