Das Problem
Wenn in einem Python-Projekt mit uv zwei Branches die Abhängigkeiten ändern,
gibt es oft einen Konflikt in uv.lock. Beide Branches ergänzen die Listen
dependencies und requires-dist des Projekts, neue Pakete landen in der
sortierten Datei nebeneinander, oder beide aktualisieren dasselbe Paket. uv
kann eine Lockdatei mit Konfliktmarkern nicht lesen, also muss jemand eine
generierte Datei von Hand reparieren.
Die übliche Umgehung: die Lockdatei einer Seite nehmen und uv lock erneut
ausführen. Das funktioniert, behält aber nur die Versionen dieser Seite. Ein
Update auf dem anderen Branch, etwa einer transitiven Abhängigkeit, fällt
unbemerkt auf die alte Version zurück.
Andere haben dasselbe Problem:
- astral-sh/uv#11185, "Automatic resolution of merge conflicts in lockfiles", offen seit Februar 2025, 28 Reaktionen, und astral-sh/uv#5633, "Document how to resolve uv.lock merge conflicts", offen seit Juli 2024, 22 Reaktionen.
- Eine GitHub-Codesuche nach
uv.lock merge=ergab 95 Repositories, die einen eigenen Driver eintragen (einige davon Forks eines Projekts). Die, die ich gelesen habe, behalten eine Seite und führenuv lockaus. - Poetry hat dasselbe Issue seit 2018 offen (python-poetry/poetry#496, 124 Reaktionen). yarn, npm und Deno lösen solche Konflikte selbst.
Wie es arbeitet
uv-lock-merge wird als
Git-Merge-Driver für uv.lock eingetragen. Git ruft es mit Basis-, eigener
und fremder Version auf, sobald beide Seiten die Datei geändert haben. Das
Werkzeug liest alle drei mit tomllib und führt sie zusammen:
- Der Kopf (
requires-python, Marker,[options],[manifest]) wird schlüsselweise zusammengeführt. Listen werden als Mengen behandelt: auf einer Seite hinzugefügt heißt hinzugefügt, auf einer Seite entfernt heißt entfernt. - Paketeinträge werden über Name, Version und Quelle identifiziert. Ein Eintrag, den nur eine Seite geändert hat, kommt von dieser Seite. Ein Eintrag, den beide geändert haben, meist das Projekt selbst, wird Feld für Feld zusammengeführt.
- Haben beide Branches ein Paket auf verschiedene Versionen gehoben, gewinnt die höhere, und der Driver meldet das.
- Pakete, die eine verbliebene Abhängigkeit noch braucht, werden wiederhergestellt; Pakete, die nichts mehr erreicht, fallen weg.
Geschrieben wird das Ergebnis genau im Layout von uv. Der Writer folgt dem
Lock-Serializer von uv: welche Arrays ein Element pro Zeile haben, welche das
von ihrer Länge abhängig machen, wann Tabellenköpfe erscheinen. 60 öffentliche
uv.lock-Dateien (Revision 1 bis 5) kamen nach Einlesen und Schreiben
byte-gleich heraus. Wo der Driver raten müsste (beide Seiten ändern
requires-python oder die Versionen eines bereits geforkten Pakets), hinterlässt
er einen normalen Git-Konflikt und nennt den Grund.
Ein Beispiel aus den Tests mit uv 0.13.7. Branch feature-a fügt rich
hinzu und hebt certifi an, Branch feature-b fügt requests hinzu und hebt
idna an:
$ 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.
Nach dem Auflösen von pyproject.toml enthält die Lockdatei das neue
certifi und idna 3.7, und uv lock --locked läuft durch, uv würde also
dieselbe Datei schreiben. Nimmt man eine Seite und führt uv lock aus, bleibt
eine der alten Versionen stehen.
Tests
Die Offline-Tests prüfen den Writer, die Merge-Regeln, den Rückfall auf
Konfliktmarker und die Installation. Die End-to-End-Tests legen ein
Repository an, erzeugen zwei Branches mit echten uv lock-Läufen, mergen sie
per Git mit eingetragenem Driver und prüfen am Ende mit uv lock --locked.
Abgedeckt sind: beide Branches fügen eine Abhängigkeit hinzu, Updates auf
beiden Branches, eine entfernte neben einer neuen Abhängigkeit, beide heben
dasselbe Paket an, Workspace-Mitglieder, eine geforkte Auflösung, ein
requires-python-Konflikt und das Auflösen während eines Rebase. Die CI
führt sie mit uv latest, 0.11.1 und 0.8.0 aus.
Grenzen
- Es ist ein struktureller Merge, keine neue Auflösung. Zwei Updates, die
nicht zusammenpassen, fallen erst bei der nächsten Auflösung durch uv auf;
uv lock --lockedin der CI bleibt sinnvoll. - Die höhere Version zu behalten ist eine Entscheidung und wird zur Prüfung ausgegeben.
- Unterstützt wird nur Lockdatei-Version 1. Die experimentelle Version 2 von uv führt zu einem normalen Konflikt.
- Git führt nur Merge-Driver aus, die in der Git-Konfiguration stehen; jeder
Klon braucht einmal
uv-lock-merge install.
Der Code liegt unter github.com/Jwrede/uv-lock-merge.