resolving them properly in Magento projects instead of patching them by hand
In Magento projects with several developers working in parallel and regular module updates, a merge conflict in composer.lock is one of the most common Git problems, and also one of the most commonly resolved incorrectly. The file is machine generated, several thousand lines long, and contains a hash that must match the actual composer.json, which is why manually merging the conflict markers almost always produces a file that is inconsistent but looks superficially valid. This article walks through the correct resolution path and how such conflicts can be reduced across a team in the first place.
Table of Contents
- 1. Why composer.lock causes conflicts so often in Magento projects
- 2. Understanding the structure of composer.lock
- 3. What a classic merge conflict looks like and why manual resolution is risky
- 4. The correct path: discard composer.lock and let it be resolved fresh
- 5. Targeted updates for individual packages after the merge
- 6. Why an automatic merge driver for composer.lock is a bad idea
- 7. Validating after the merge: composer validate and platform checks
- 8. Team workflow: lock file policy, feature branches, and rebasing
- 9. CI safeguards against inconsistent lock files
- 10. Summary
- 11. FAQ
1. Why composer.lock causes conflicts so often in Magento projects
Magento projects typically pull in dozens to hundreds of Composer packages, the core system itself, numerous third party extensions, and custom modules with their own dependencies. Even the smallest change to a version constraint in composer.json, say bumping a single package by a minor version, causes Composer to rewrite large parts of composer.lock on the next composer update, because dependency resolutions can influence one another.
When several developers work on different feature branches at the same time and each updates packages independently, a conflict on merge is practically unavoidable, even when the actual content changes in composer.json do not overlap at all. Git has no concept of the file's semantic structure and treats it as plain text, which almost inevitably leads to conflicts given such a tightly interconnected data structure.
2. Understanding the structure of composer.lock
composer.lock stores, for every installed package, the exactly resolved version, the source commit hash, and every transitive dependency, split into a packages section for production dependencies and packages-dev for development dependencies. At the top of the file sits an additional content-hash field, a hash over the relevant parts of the associated composer.json, which Composer uses on every invocation to check whether the lock file still matches the current requirements file.
That very content-hash is why a manually merged conflict is problematic even when the remaining entries look correct: if the hash no longer matches composer.json exactly, Composer emits a warning on every command that the lock file is outdated, a reliable sign that something was not merged cleanly.
3. What a classic merge conflict looks like and why manual resolution is risky
When merging two branches that changed composer.lock differently, Git marks the affected blocks with the familiar <<<<<<<, =======, and >>>>>>> separators, often right in the middle of a package entry with a version number, hash, and dependency list. Anyone who removes these markers by hand and picks one of the two variants, or combines lines from both, produces a file that can be syntactically valid JSON but is inconsistent in meaning, for example when a package is referenced at a version whose dependencies no longer match the remaining entries.
The real risk is that this inconsistency does not surface immediately. composer install initially just installs exactly what the manually repaired file states, without checking internal consistency, and problems may only appear weeks later as subtle bugs traced back to an incorrectly resolved transitive dependency.
4. The correct path: discard composer.lock and let it be resolved fresh
The reliable solution deliberately ignores the conflict in composer.lock and instead lets Composer recompute the file itself. To do that, composer.lock is fully taken from one of the two branches, usually the target branch being merged into, and then a composer update --lock run performs a fresh dependency resolution based on the already correctly merged composer.json.
The --lock parameter is essential here, because it instructs Composer to recalculate only the lock file to match composer.json, without actually updating or reinstalling any packages in the vendor directory. After this step, composer.lock is guaranteed to be consistent, since it comes from a real Composer run rather than a manual text merge.
# During a merge conflict: take composer.lock from the target branch
git checkout --ours composer.lock
# or, depending on which base you want to start from
git checkout --theirs composer.lock
# composer.json is already merged correctly, recompute the lock file
composer update --lock
# Review the result and commit
git add composer.json composer.lock
git commit
5. Targeted updates for individual packages after the merge
A full composer update without specifying a package can potentially update far more packages than necessary, because Composer tries to find the latest compatible version within the ranges allowed by composer.json for everything. For a clean merge conflict that is often not what you want, the actual goal is simply to bring the lock file back into consistency with composer.json, not to introduce additional, unplanned package updates.
Anyone who only wants to update the packages actually affected specifies them explicitly and adds --with-dependencies, so Composer also resolves the transitive dependency chain of those packages, without touching the rest of the dependency tree.
# Resolve only a specific package along with its dependencies
composer update vendor/package-name --with-dependencies
# Target multiple affected packages
composer update vendor/package-a vendor/package-b --with-dependencies
6. Why an automatic merge driver for composer.lock is a bad idea
.gitattributes lets you configure a merge strategy like merge=union for specific files, which automatically combines both variants line by line on conflict. That is a poor idea for composer.lock, because a union merge simply writes both versions of a package side by side into the file, with no regard for the fact that a package can only exist at exactly one version, the result is practically always invalid or inconsistent.
A custom, specialized merge driver that tries to intelligently merge the JSON structure does not solve the core problem either, because even a syntactically correct merge of two package lists says nothing about whether the resulting combination of versions represents a valid dependency resolution at all. Only the Composer dependency resolver itself can reliably perform that check, which is why going through composer update --lock is preferable to any automation inside Git.
7. Validating after the merge: composer validate and platform checks
Before committing a resolved merge conflict, a brief validation round pays off. composer validate checks whether composer.json and composer.lock are structurally consistent with each other and whether the content-hash is correct, while composer check-platform-reqs additionally ensures that every resolved package is actually compatible with the locally installed PHP version and available PHP extensions.
For Magento projects, a trial composer install --dry-run is also worth running, simulating which packages would be installed without actually changing the vendor directory, a quick way to catch obvious inconsistencies before the merge reaches a shared branch.
# Check structure and content-hash
composer validate --strict
# Check compatibility with the local PHP environment
composer check-platform-reqs
# Simulated install without changes to the vendor directory
composer install --dry-run
8. Team workflow: lock file policy, feature branches, and rebasing
A large share of composer.lock conflicts can be avoided by making package updates their own, clearly scoped commits rather than folding them incidentally into an otherwise unrelated feature branch. A team that establishes a clear rule that Composer updates run only through dedicated, short lived branches merged promptly reduces the chance that several parallel branches touch the same lock file at once.
For longer running feature branches, regularly rebasing or merging the target branch into the feature branch helps, since it makes composer.lock conflicts surface early and in smaller, more manageable steps, rather than piling up into one large, hard to resolve conflict at the end.
9. CI safeguards against inconsistent lock files
As a final safety net, every Magento CI pipeline should include a job that runs composer install --no-dev --prefer-dist with strict error handling, ideally preceded by composer validate --strict as an earlier step. Composer aborts with a clear error message on an inconsistent content-hash instead of silently installing a potentially wrong combination of dependencies.
Such a pipeline step catches exactly the cases where a manually patched but internally inconsistent merge slipped into the target branch unnoticed, preventing a broken state from only being discovered during deployment to a production environment.
# Example CI job safeguarding the lock file
composer-check:
script:
- composer validate --strict
- composer install --no-dev --prefer-dist
| Conflict scenario | Recommended solution | Risk of resolving it wrong |
|---|---|---|
| Only composer.json changed, resulting lock conflict | Discard composer.lock, run composer update --lock |
Inconsistent content-hash, hidden version errors |
| Two branches update different packages | Targeted composer update package --with-dependencies |
Unwanted extra package updates from a full update |
| Manually merged conflict markers in composer.lock | Discard the file and let Composer regenerate it | Syntactically valid but semantically wrong dependency resolution |
| Long running feature branch with many lock changes | Regularly rebase/merge the target branch during development | One large, hard to resolve conflict at the end |
Mironsoft
Git workflows, branching strategies, and CI hooks
Chaotic Git history and unclear branching rules across the team?
We set up clean Git workflows, clarify branching strategies for the team, and automate quality checks via Git hooks and CI pipelines so the history stays traceable.
Workflow Audit
Review the existing branching strategy and merge practice for weak spots.
Hook Automation
Set up pre-commit and pre-push hooks for linting, tests, and commit conventions.
Team Training
Teach rebase, cherry-pick, and conflict resolution hands-on across the team.
10. Summary
composer.lock Conflicts at a Glance
Golden rule
Never edit composer.lock by hand after conflict markers, always regenerate it via composer update --lock
Targeted, not blanket
composer update package --with-dependencies avoids unwanted side updates
Check before commit
composer validate --strict and composer install --dry-run catch inconsistencies early
CI safeguard
composer validate plus composer install --no-dev in the pipeline catches overlooked mistakes