Understanding conflict markers, using merge tools, avoiding chaos
Merge conflicts are part of everyday life for every Git team and scare off many developers unnecessarily. This article explains how to read conflict markers, when a graphical merge tool like VS Code, Meld or PhpStorm pays off, how git rerere resolves recurring conflicts automatically, and which strategies help teams reduce the number of conflicts in feature branches from the start.
Table of Contents
- 1. Why merge conflicts happen and when Git cannot auto-merge
- 2. Anatomy of conflict markers: reading <<<<<<<, =======, >>>>>>>
- 3. Resolving conflicts manually in a text editor, step by step
- 4. Using a graphical merge tool: VS Code's 3-way merge editor
- 5. Meld and the PhpStorm merge dialog, configuring git mergetool
- 6. Conflicts in git merge, git rebase and git cherry-pick: what differs
- 7. Aborting safely: git merge --abort, git rebase --abort, git cherry-pick --abort
- 8. Preventing conflicts from the start: rebasing, small PRs, feature flags
- 9. git rerere: recording conflict resolutions and replaying them automatically
- 10. Summary
- 11. FAQ
1. Why merge conflicts happen and when Git cannot auto-merge
Git resolves most merges without human intervention, because the three-way merge algorithm compares the changes of two branches against a shared ancestor commit and combines them line by line, as long as the changed regions do not overlap. A merge conflict only arises when both branches have changed the same line or the same code block differently and Git cannot make an unambiguous decision about which version is valid.
Besides overlapping text changes, structural conflicts also count: one branch deletes a file while the other keeps editing it; both branches rename the same file differently; or a file is created in the same location once as a directory and once as a regular file. Git reports such cases as conflicts too, even when not a single character in the text content collides. The cause then lies in the directory structure, not in the line content, which means git status needs to be read more carefully in these cases than for a classic content conflict.
2. Anatomy of conflict markers: reading <<<<<<<, =======, >>>>>>>
As soon as Git cannot resolve a conflict automatically, it writes both competing versions directly into the affected file and marks them with special character sequences. <<<<<<< HEAD marks the start of the current state on your own, checked-out branch. The separator ======= divides this section from the incoming change. >>>>>>> branch-name marks the end and names the branch or commit that brings in the change.
It is important to understand: these markers are valid file content, not comments. A PHP linter or compiler reports a syntax error on unresolved markers, because <<<<<<< HEAD is simply invalid code. Committing a file without removing all markers risks a broken build. A quick grep -rn '<<<<<<<' . before every commit after a merge reliably catches forgotten markers before they reach the CI pipeline.
<?php
// getDiscountedPrice() after a merge conflict: both branches changed the method
class PriceCalculator
{
public function getDiscountedPrice(float $price, float $percentage): float
{
<<<<<<< HEAD
// Local change: round the discount to two decimal places
$discount = round($price * ($percentage / 100), 2);
return $price - $discount;
=======
// Incoming change: enforce a minimum discount of 0 EUR
$discount = $price * ($percentage / 100);
return max(0.0, $price - $discount);
>>>>>>> feature/minimum-discount
}
}
3. Resolving conflicts manually in a text editor, step by step
Anyone resolving conflicts manually opens the file Git marks as "both modified," visible via git status, and decides for each conflict block which code stays: their own version, the incoming version, a combination of both, or an entirely new, third solution. It is crucial to fully remove all three marker lines, not just the content between them. An overlooked leftover ======= leads to a syntax error that often only surfaces in the CI pipeline, not locally when saving.
After editing, git add <file> marks the file as resolved, regardless of whether changes were actually made. Git tracks this status per file. Only once git status no longer lists any files under "unmerged paths" can the merge be completed with git commit. For extensive conflicts, running git diff without arguments during an ongoing merge helps, because it automatically uses a diff mode that shows both conflict sides against the common ancestor, making the actual change clearer than the raw file content with markers.
4. Using a graphical merge tool: VS Code's 3-way merge editor
VS Code automatically detects conflict markers when opening a file and shows inline buttons above every conflict block: "Accept Current Change," "Accept Incoming Change," "Accept Both Changes" and "Compare Changes." For more complex cases, the built-in 3-way merge editor opens a three-column view with your own state on the left, the incoming change on the right, and the result in the middle, while the common ancestor is optionally shown as a fourth reference column.
The big advantage over pure text editing: syntax highlighting stays intact in all three columns, and changes can be adopted with a click instead of manual copy-paste, which avoids mistakes especially in deeply nested PHP classes or long merge conflicts in layout XML files. After resolving all blocks, VS Code shows a summary of open conflicts in the source control sidebar before the commit button even becomes clickable, an extra safety net against forgotten markers.
5. Meld and the PhpStorm merge dialog, configuring git mergetool
Meld is a standalone, cross-platform diff and merge tool using the same three-column logic as VS Code, but is usually invoked via git mergetool from the terminal instead of directly inside an editor. PhpStorm ships its own merge dialog that appears automatically on a conflict during git pull or git merge, and in addition to the pure three-way view shows a live preview of the resulting code with full PHP syntax checking, an advantage over pure text editors that cannot detect a syntactically invalid intermediate state.
For git mergetool to launch the desired program, it must be registered in the global or project-local .gitconfig. Without configuration, Git interactively asks which tool to use on every invocation, which is impractical in scripts or CI environments. After every resolved conflict, git mergetool creates a *.orig backup file by default, which can be disabled with mergetool.keepBackup = false.
; ~/.gitconfig: register merge tools and set default behavior
[merge]
tool = meld
[mergetool "meld"]
cmd = meld "$LOCAL" "$BASE" "$REMOTE" --output "$MERGED"
trustExitCode = true
[mergetool "phpstorm"]
cmd = phpstorm merge "$LOCAL" "$REMOTE" "$BASE" "$MERGED"
trustExitCode = true
[mergetool]
keepBackup = false
prompt = false
6. Conflicts in git merge, git rebase and git cherry-pick: what differs
With git merge, a conflict occurs exactly once, because Git combines the entire history of both branches into a single merge commit. All conflict blocks of a file appear bundled in one pass. With git rebase, by contrast, Git replays each commit of the rebased branch individually onto the new base commit, which means the same logical conflict can reoccur across several consecutive commits, especially unpleasant with long commit chains that overlap in changes to the same file.
git cherry-pick behaves like a mini-rebase of a single commit when it comes to conflicts: the chosen commit is applied against the current HEAD, and a conflict only affects that one commit's content, not the entire branch history. In all three cases, the resolution step stays identical, edit the file, git add, then continue, but the continuation command differs: git commit for merge, git rebase --continue for rebase, git cherry-pick --continue for cherry-pick.
# Start an interactive rebase that can cause conflicts across several commits
git rebase main
# Git stops at the first conflict and shows the affected commit
# CONFLICT (content): Merge conflict in src/Model/PriceCalculator.php
# Edit the file in an editor or merge tool, then mark it as resolved
git add src/Model/PriceCalculator.php
# Apply the next commit in the rebase sequence
git rebase --continue
# If the same conflict occurs again on another commit: repeat
git status
git add src/Model/PriceCalculator.php
git rebase --continue
7. Aborting safely: git merge --abort, git rebase --abort, git cherry-pick --abort
Not every started conflict has to be resolved to the end. Anyone who notices the wrong branch was merged, or that the resolution is too complex for the current moment, can abort cleanly at any time. git merge --abort resets the working area exactly to the state before the git merge command, as long as no manual commit exists yet. The same principle applies to git rebase --abort, which restores the original branch tip and discards all commits from the rebase sequence already applied successfully.
git cherry-pick --abort works identically for an ongoing cherry-pick, while git cherry-pick --quit ends the conflict state without resetting the working area, useful when partial changes have already been adopted manually. A common mistake is reaching for git reset --hard instead of --abort: reset does not know about the merge or rebase state and can leave inconsistent intermediate states behind, while the specific abort commands always restore the exact correct prior state.
# Adopt a single commit from a hotfix branch into main
git cherry-pick a1b2c3d
# Conflict in exactly this one commit
# error: could not apply a1b2c3d... Fix null pointer in checkout flow
# Resolve the conflict manually or with a merge tool, then continue
git add src/Controller/CheckoutController.php
git cherry-pick --continue
# If it turns out the commit does not fit after all: abort cleanly
git cherry-pick --abort
8. Preventing conflicts from the start: rebasing, small PRs, feature flags
The most effective conflict prevention happens long before the actual merge: anyone who brings their own feature branch up to date with main daily via git pull --rebase or git fetch && git rebase origin/main always works with a small, manageable diff instead of weeks of accumulated, overlapping changes. The longer a branch lives without being synced with main, the more exponentially likely and complex later conflicts become.
Small, focused pull requests additionally reduce the surface area for conflicts, because they change fewer files and fewer lines at once. A PR with 50 changed lines collides less often than one with 2,000. Feature flags solve the problem at a structural level: instead of developing a large feature branch separately from main for weeks, the new code is merged into main directly, but disabled behind a flag. That keeps the history linear and short, and conflicts arise against a constantly up-to-date main branch instead of an outdated divergence point.
9. git rerere: recording conflict resolutions and replaying them automatically
git rerere (reuse recorded resolution) remembers how a particular conflict was resolved once and applies the same resolution automatically as soon as the same conflict occurs again, typically when repeatedly rebasing a long-lived feature branch against main, where the same conflict spots pop up again on every rebase. The feature is enabled project-wide with git config rerere.enabled true, after which Git records every manually resolved conflict in the .git/rr-cache directory.
On a repeated, identical conflict, Git applies the recorded resolution automatically and marks the file with "Resolved by 'rerere'" in git status. A quick check with git diff is still worthwhile before committing the result. rerere is especially valuable for recurring conflicts in generated files like composer.lock or in translation files, where the same manual resolution would otherwise have to be redone by hand on every rebase.
# Enable rerere globally so resolutions are recorded permanently
git config --global rerere.enabled true
git config --global rerere.autoupdate true
# Start a rebase with a known, recurring conflict
git rebase main
# Resolve the conflict manually, once
git add composer.lock
git rebase --continue
# On the next rebase with the same conflict: rerere applies the resolution automatically
git rebase main
# Resolved 'composer.lock' using previous resolution.
# Inspect recorded resolutions
git rerere diff
The overview below summarizes which habits tend to cause conflicts and which practices have proven themselves in Git teams instead.
| Area | Conflict-prone practice | Conflict-reducing practice | Tool / command |
|---|---|---|---|
| Branch lifespan | Weeks-long feature branch without syncing | Daily rebase onto main | git pull --rebase |
| PR size | 2,000+ lines in one PR | Focused PRs under 400 lines | Smaller, more frequent commits |
| Conflict resolution | Resolving blindly without context | Reusing a recorded resolution | git config rerere.enabled true |
| Divergence strategy | Long branch running parallel to main | Feature flags directly in main | Feature flag system |
| Uncertainty during a conflict | git reset --hard as a workaround | Using the correct abort command | git merge/rebase --abort |
In practice these habits reinforce each other: short, frequently synced branches produce smaller conflicts that resolve faster, and every conflict resolved once is thanks to rerere never handled manually again. Teams that consistently apply all five points spend, in practice, only a fraction of the time on conflict resolution compared to teams with long-lived, unsynced branches.
Mironsoft
Git workflows, code reviews and merge strategies for Magento teams
Get merge conflicts under professional control?
We analyze your Git workflow, set up merge tools and rerere configuration for the whole team, and establish branch strategies that reduce conflicts in Magento and Hyva projects from the start.
Git Workflow Audit
Analysis of branch strategy, PR size and conflict frequency
Merge Strategy Consulting
Introducing feature flags, trunk-based development and rebase policies
Team Onboarding & Training
Establishing merge tools, git rerere and safe aborting across the team
10. Summary
Merge conflicts are not a sign of faulty work, they are a normal consequence of parallel development that can be resolved quickly and safely with the right tools and habits. Anyone who has internalized the meaning of the conflict markers <<<<<<<, ======= and >>>>>>> can confidently decide which code stays, both in a text editor and in graphical merge tools like VS Code, Meld or PhpStorm. The difference between conflicts in git merge, git rebase and git cherry-pick determines how often a conflict can reoccur during an operation and which command continues or safely aborts it.
In the long run, prevention pays off more than repair: rebasing onto main frequently, small pull requests and feature flags structurally reduce the number and complexity of conflicts. Where conflicts still repeat, for example in generated files like composer.lock, git rerere automatically applies the resolution worked out once, saving teams noticeable time on every further rebase.
Resolving Merge Conflicts, the essentials at a glance
Reading conflict markers
<<<<<<< HEAD is your own state, ======= separates, >>>>>>> marks the incoming change. All three lines must be removed.
Using merge tools
VS Code, Meld and PhpStorm offer 3-way views. Configure git mergetool in your .gitconfig.
Aborting safely
git merge/rebase/cherry-pick --abort always restores the correct prior state, never use reset --hard.
Preventing & automating
Frequent rebasing, small PRs, feature flags and git rerere reduce conflicts structurally.