The problem
When two branches of a Python project managed with uv both change
dependencies, uv.lock often conflicts. Both branches add entries to the
project's own dependencies and requires-dist lists, new packages land next
to each other in sorted order, or both upgrade the same package. uv cannot
read a lockfile with conflict markers, so someone has to fix a generated file
by hand.
The usual workaround is to take one side's lockfile and run uv lock again.
That works, but it keeps only that side's pins. An upgrade made on the other
branch, for example of a transitive dependency, quietly goes back to the old
version.
Others have the same problem:
- astral-sh/uv#11185, "Automatic resolution of merge conflicts in lockfiles", open since February 2025, 28 reactions, and astral-sh/uv#5633, "Document how to resolve uv.lock merge conflicts", open since July 2024, 22 reactions.
- A GitHub code search for
uv.lock merge=returned 95 repositories that register their own driver (some are forks of one project). The ones I read keep one side and runuv lock. - Poetry has the same issue open since 2018 (python-poetry/poetry#496, 124 reactions). yarn, npm and Deno resolve such conflicts themselves.
How it works
uv-lock-merge is registered as a
git merge driver for uv.lock, so git calls it with the base, ours and theirs
versions whenever both sides changed the file. It parses all three with
tomllib and merges them:
- The header (
requires-python, markers,[options],[manifest]) is merged key by key. Lists are merged as sets: added on either side means added, removed on either side means removed. - Package entries are keyed by name, version and source. An entry changed on one side takes that side's version. An entry changed on both sides, usually the project itself, is merged field by field.
- If both branches move one package to different versions, the higher version wins and the driver prints that.
- Packages that a remaining dependency still needs are restored, and packages that nothing reaches any more are dropped.
The result is written in exactly the layout uv uses. The writer follows uv's
own lock serializer: which arrays have one element per line, which depend on
their length, when table headers appear. Parsing and writing 60 public
uv.lock files, from revision 1 to 5, gave identical bytes for every file.
Where the driver would have to guess (both sides change requires-python, or
the versions of an already forked package), it leaves a normal git conflict
and says why.
An example from the tests, with uv 0.13.7. Branch feature-a adds rich and
upgrades certifi, branch feature-b adds requests and upgrades idna:
$ git merge feature-b # with the driver
uv-lock-merge: merged uv.lock
Auto-merging pyproject.toml
CONFLICT (content): Merge conflict in pyproject.toml
Auto-merging uv.lock
Automatic merge failed; fix conflicts and then commit the result.
After fixing pyproject.toml, the lockfile has the new certifi and
idna 3.7, and uv lock --locked passes, which means uv would write the same
file. Taking either side and running uv lock keeps one of the old versions.
Testing
The offline tests cover the writer, the merge rules, the fallback to conflict
markers and the installer. The end to end tests create a repository, make two
branches with real uv lock runs, merge them through git with the driver
installed and finish with uv lock --locked. The scenarios are both branches
adding a dependency, upgrades on both branches, a removed dependency next to an
added one, both branches upgrading the same package, workspace members, a
forked resolution, a requires-python conflict and resolving during a rebase.
CI runs them with uv latest, 0.11.1 and 0.8.0.
Limitations
- It is a structural merge, not a new resolution. Two upgrades that do not fit
together are only noticed when uv resolves next, so
uv lock --lockedin CI stays useful. - Keeping the higher version is a choice and is printed for review.
- Only lockfile version 1 is supported. uv's experimental version 2 falls back to a normal conflict.
- Git only runs merge drivers configured in the git config, so every clone
needs
uv-lock-merge installonce.
The code is at github.com/Jwrede/uv-lock-merge.