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.
Table of Contents
- 1. What merge hell is and why feature branches are meant to prevent it
- 2. Principle 1: keep feature branches short and focused
- 3. The branch per ticket workflow in practice
- 4. Principle 2: sync regularly with main
- 5. When a feature branch has grown too large
- 6. Splitting an oversized branch into smaller pull requests
- 7. Resolving conflicts early and often instead of at the end
- 8. Code review practices against stagnating branches
- 9. Cleanup, hygiene, and the direct comparison
- 10. Summary
- 11. FAQ
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.