Die Veröffentlichung der Version 0.4 des Projekts mergiraf wurde bekanntgegeben. Dieses Projekt entwickelt einen Treiber für Git, der die Möglichkeit einer dreiseitigen Zusammenführung implementiert. Mergiraf unterstützt die Lösung verschiedener Arten von Konflikten bei Zusammenführungen und kann für verschiedene Programmiersprachen und Dateiformate verwendet werden. Es ist möglich, mergiraf als separaten Aufruf zur Behandlung der Konflikte zu nutzen, die bei der Arbeit mit dem herkömmlichen Git auftreten, oder es kann als Ersatz für den Merge-Handler in Git verwendet werden, um die Möglichkeiten von Befehlen wie merge, revert, rebase und cherry-pick zu erweitern. Der Code wird unter der GPLv3-Lizenz verbreitet. In der neuen Version wurde die Unterstützung für die Sprachen Python, TOML, Scala und Typescript hinzugefügt sowie die Leistung optimiert.
Im Folgenden finden Sie eine detaillierte Beschreibung der Probleme, die mit mergiraf gelöst werden:
Software ist ein herausragendes Beispiel für ein äußerst komplexes System. Komplexe Systeme haben ein gemeinsames Merkmal – sie sind KOMPLEX – und man kann nicht erwarten, dass das gewünschte komplexe Verhalten zufällig von selbst entsteht. Stattdessen entwickeln sich diese Systeme im Laufe der Zeit schrittweise weiter, und jede Mutation wird in jeder Phase sorgfältig überprüft. Dazu sind eine klar definierte Struktur und die entsprechenden Werkzeuge erforderlich. Die Evolution eines beliebigen komplexen Systems kann als gerichteter Baum visualisiert werden, wobei die Wurzel eine leere Menge von Funktionen darstellt und jeder Knoten – mit Ausnahme der Wurzel – das Ergebnis der Anwendung einer Mutation auf seine elterliche Funktion ist.
Im Kontext von Produkten wird jeder Knoten als 'Version' bezeichnet, die einen bestimmten Satz von Funktionen und Antifunktionen darstellt. Jede Änderung dieses Satzes gilt als Mutation, die eine Kante in unserem gerichteten azyklischen Graphen bildet. Diese Funktionen sind von Natur aus abstrakt; sie spiegeln nicht direkt die Funktionsweise physischer Systeme wider, sondern zeigen vielmehr, wie intelligente Agenten den Nutzen dieser Systeme wahrnehmen. Um Ideen in die bestehenden realen Implementierungen zu übertragen, muss man die Ärmel hochkrempeln und sich in *ausreichend* tiefliegende Details vertiefen, in einer Sprache, die es ermöglicht zu erklären, wie alles konkret funktioniert. In der Softwareentwicklung werden diese tief liegenden Details normalerweise durch den Quellcode dargestellt.
Um den Quellcode schrittweise in einen Zustand zu versetzen, der das gewünschte Verhalten zeigt, und zu dokumentieren, wie sie dorthin gelangten, stellen Programmierer ihre Arbeit in Form von Snapshots und Changesets dar. Ein Snapshot stellt einen bestimmten Zustand des Produkts mit allen niedrigstufigen Details dar, während ein Changeset den Übergang zwischen Snapshots bezeichnet. In der Regel werden Snapshots von einzelnen Changesets zu ihren Eltern erzeugt, weshalb diese Snapshots fast immer mit dem gekennzeichnet sind, was die Changesets, die sie erstellt haben, tun. Daher werden diese Begriffe oft synonym verwendet.
Manchmal gibt es Snapshots, die aus mehreren Übergängen - Fusionen von Commits - stammen. Diese sind schwer zu handhaben, weshalb sie normalerweise vermieden werden. Moderne Open-Source-Versorgungssysteme wie Git bieten recht grundlegende Möglichkeiten zur Verwaltung von Entwicklungsworkflows. Sie ermöglichen es Entwicklern, Snapshots in Form von gerichteten azyklischen Grafiken zu organisieren, sie mit Kommentaren zu annotieren und bei Bedarf deren Reihenfolge zu ändern.
Diese Funktionalität ermöglicht es Entwicklern, eine semantisch bedeutungsvolle Historie des Projekts zu erstellen, was entscheidend für Debugging und zur Beantwortung von Fragen ist wie: "Warum wurde dieses niedrigstufige Detail eingeführt (z. B. eine Variable)?", "Wie hoch ist ungefähr mein Anteil an diesem Projekt?", "Wann wurden die Implementierungen gehackt und von wem?", "Welches niedrigstufige Änderung hat diese Funktion (obwohl sie es eigentlich nicht hätte tun sollen, wir haben alles überprüft!?)"
Versionskontrollsysteme ergänzen dieses Konzept um Branches – ein niedrigstufiges Konzept, das einfach einen kontinuierlichen Abschnitt der projektbezogenen Geschichte darstellt, der für den Entwickler semantisch bedeutungsvoll ist. Branches werden in der Regel für die spezifische Implementierung von Funktionen verwendet, und manchmal werden mehrere Branches für verschiedene Implementierungskandidaten der gleichen Funktion erstellt. Durch die Verwendung von Branching-Workflows (die in der Tat Mainstream und Standard der Entwicklung sind und überall und jederzeit verwendet werden) kann jeder einzelne Entwickler effektiv mit vielen sich überschneidenden Branches eines Projekts umgehen, von denen jede in Bezug auf Fertigstellung oder Qualität unterschiedlich ist. Dies ermöglicht es den Entwicklern, die Ergebnisse ihrer und anderer Arbeiten zu kombinieren, ohne alles manuell erneut eingeben zu müssen.
In der Regel gibt es einen Hauptbranch, der das "offizielle" Produkt darstellt, und von dem aus Seitenbranches für jede Funktion abgezweigt werden, die regelmäßig (idealerweise nach jedem Commit) mit dem Hauptbranch synchronisiert werden, sodass die Entwickler mit der neuesten Version des Produkts arbeiten und gleichzeitig Funktionen einführen können, an denen sie gerade arbeiten, und Probleme, die sich aus den Aktivitäten anderer Entwickler ergeben, so früh wie möglich erkennen.
Beim Versuch, Funktionen verschiedener Snapshots zu kombinieren (was einfach das Finden eines gemeinsamen Vorfahren und das nacheinander Anwenden von die daraus resultierenden Changesets auf einen anderen umfasst, wird diese Operation als Rebase bezeichnet; Merge hingegen ist fast wie Rebase, strukturiert jedoch den Commit-Graphen anders, was es unangenehm macht, damit zu arbeiten. Daher wird versucht, Merges zugunsten von Rebases zu vermeiden) treten Probleme auf. Moderne Versionskontrollsysteme (VCS) verwenden interne Algorithmen zur Zusammenführung von Änderungen, die einfach Dateien in einzelne Zeilen unterteilen, jede Zeile als Symbol betrachten und Dateien als deren Sequenzen, und dann Algorithmen zur Zusammenführung anwenden, die aus der Bioinformatik stammen.
Leider hat diese zeilenweise Darstellung des Quellcodes nichts mit seinem Inhalt zu tun. Ihre einzige Stärke liegt in ihrer Einfachheit und Universalität. Diese Diskrepanz führt zu Konflikten und ist eine ständige Quelle von Kopfschmerzen für Entwickler. Die Behebung von Konflikten erfordert von Entwicklern, beide Versionen des Codes gründlich zu prüfen, und zwar nicht nur die Abschnitte, die der zeilenweise Vergleichsalgorithmus als „geändert“ oder „konfliktbehaftet“ kennzeichnet, sondern möglicherweise auch das gesamte Projekt.
Der Entwickler muss die Änderungen verstehen, den zusammengeführten Code manuell schreiben und alle Abweichungen beheben. Die Probleme nehmen zu, wenn das zeilenweise Werkzeug Änderungen falsch identifiziert, was häufig bei umfangreichen Änderungen geschieht, einschließlich trivialer wie der Codeformatierung. Wenn spätere Änderungen nicht auf den manuell zusammengeführten Code angewendet werden können, wird die Situation zum vollständigen Albtraum. Trotz dieser schrecklichen Fälle funktioniert der zeilenweise Algorithmus in den meisten Fällen, insbesondere wenn die Entwickler aktiv versuchen, ihm keine Probleme zu bereiten. Eine Möglichkeit, solche Probleme zu minimieren, besteht darin, eine obligatorische Verarbeitung des Quellcodes mit Canonicalization-Tools wie Black vorauszusetzen.
Natürlich ist die richtige Lösung für schreckliche Fälle (und nicht nur für diese, da der zeilenweise Algorithmus eine Heuristik ist, die trivial zu nicht funktionsfähigem Code führen kann, wenn beispielsweise ein Entwickler eine Variable umbenennt und ein anderer gleichzeitig neuen Code schreibt, der diese Variable verwendet, es jedoch keinen Merge-Patch-Konflikt gibt, aber das Ergebnis nicht funktioniert) die Verwendung eines richtigen internen Modells.
Obwohl in diesem Bereich seit etwa 30 Jahren Forschung betrieben wird und mehrere proprietäre kommerzielle Produkte geschaffen wurden, wurden diese Forschungen bis vor kurzem nicht in praktisch anwendbare Open-Source-Produkte überführt. Der Großteil der FOSS-Lösungen begann zu Beginn der 2010er Jahre zu wachsen und konzentrierte sich hauptsächlich auf die Programmiersprache Java.
Die herausragendste freie Implementierung aus dieser Zeit, GumTree, wurde von einem Forscher mit akademischem Hintergrund erstellt, ist in Java geschrieben und hat eine eigene abstrakte interne Darstellung, die treesitter vorausgeht. Sie verfügt über Backends, die sowohl auf treesitter als auch auf anderen Werkzeugen basieren, um Quellcode in abstrakte Darstellungen zu parsen. Dieses System kann nur generieren (in Form eines textuellen Ereignisprotokolls, außerdem gibt es eine API, die triviaal von jeder Programmiersprache mit Bindungen zu Java aufgerufen werden kann) und Änderungen visualisieren. Allerdings ist es für das Zusammenführen von Änderungen sowie für die Ansicht der generierten Diff-Dateien von Haus aus nicht anwendbar (obwohl es wahrscheinlich ist, dass das Laden von Diffs über die API umgesetzt werden kann).
Eine jüngere und praktischere Implementierung von difftastic ist in Rust geschrieben, basiert auf treesitter und konzentriert sich auf die Generierung von hervorgehobenen Diffs in der Konsole. Dieses System zielt ebenfalls auf die Visualisierung von Diffs ab und hat nicht das Ziel, Änderungen zusammenzuführen oder Patches anzuwenden.
Kürzlich wurde das Projekt mergiraf ins Leben gerufen und entwickelt sich aktiv weiter. Dieses in Rust geschriebene Tool (belegt 21 MiB!) basiert ebenfalls auf treesitter, der mittlerweile für Parser kontextfreier Grammatiken in Entwicklungswerkzeugen zum Standard geworden ist, genau wie LLVM für die Optimierung von Niedriglevel-Instruktionsdarstellungen. Im Gegensatz zu seinen Mitbewerbern bietet mergiraf keine Funktionen zur Erzeugung von Diffs, sondern zur automatischen Lösung von Merge-Konflikten. Unter der Haube verwendet mergiraf zur Erzeugung von Patches eine Implementierung des Algorithmus, der in GumTree verwendet wird, und zur Anwendung eine Implementierung des Algorithmus, der in spork verwendet wird, angepasst an die Strukturen von treesitter.
Die Serialisierung von Patches in Dateien, die später angewendet werden können, ist leider noch nicht umgesetzt (es ist jedoch gut möglich, dass dies durch das Parsen von Ereignisprotokollen, die von GumTree erzeugt werden, realisiert werden kann). Ein weiterer vielversprechender Ansatz zur Anwendung von Unterschieden könnte die Nutzung von Unterschieden über funktionale Refactoring-Funktionen von LSP-Servern sein, was bei der Erkennung von Konflikten auf Projektebene helfen könnte. Die Visualisierung wird nur für Konflikte unterstützt.
Beispiel für die Funktionsweise: ein gemeinsamer Vorfahr «base.py» (Einrückungen mit Tabs, überflüssige Zeile am Anfang) foo = 1 def main(): print(foo + 2 + 3) «a.py» (Einrückungen sind immer noch Tabs, 2 überflüssige Zeilen am Anfang anstelle von einer, für Debug-Ausgaben wird die Bibliothek icecream verwendet, die Klasse «baz» wurde hinzugefügt: from icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): «»»baz»»» «b.py» (Variable «foo» wurde in «bar» umbenannt, nach den Änderungen mit «black» bearbeitet, das Ergebnis sind Einrückungen mit Leerzeichen und überflüssige Zeilen wurden entfernt): bar = 1 def main(): print(bar + 2 + 3) Der Aufruf .\/mergiraf merge .\/base.py .\/a.py .\/b.py -x a.py -y b.py -s base.py -o .\/res.py ergibt folgendes Ergebnis from icecream import ic bar = 1 def main(): ic(bar + 2 + 3) class baz: def __init__(self): «»»baz»»» (Für Debug-Ausgaben wird die Bibliothek «icecream» verwendet, die Variable «foo» wurde in «bar» umbenannt, nach den Änderungen mit «black» bearbeitet, das Ergebnis sind Einrückungen mit Leerzeichen und überflüssige Zeilen wurden entfernt, eine Mischung aus Tabs und Leerzeichen für Einrückungen, die jedoch zulässig ist).
Hier wird der Nachteil des Werkzeugs deutlich. Der Stil des Dokuments wird normalerweise in «.editorconfig»-Dateien konfiguriert, und globale Stiländerungen, wie der Wechsel von Tabs zu Leerzeichen und die Annahme des «black»-Stils, wie es in «b.py» geschehen ist, werden normalerweise von Änderungen in der «.editorconfig» begleitet. Daher sollte das Werkzeug für eine korrektere Anwendung solcher Änderungen ein Konzept für den globalen Standardstil haben und in der Lage sein, Einstellungen aus der «.editorconfig» zu beziehen.
Quelle: opennet.ru
