mergiraf — ein AST-orientiertes Tool für Dreifach-Zusammenführungen in Git

Die Projektveröffentlichung mergiraf 0.4 hat die Entwicklung eines Git-Treibers vorangetrieben, der die Möglichkeit zur dreiseitigen Zusammenführung bietet. Mergiraf unterstützt die Lösung verschiedener Arten von Konflikten bei Zusammenführungen und kann für verschiedene Programmiersprachen und Dateiformate verwendet werden. Es kann sowohl als separater Aufruf von mergiraf zur Verarbeitung von Konflikten, die bei der Verwendung von Standard-Git auftreten, als auch als Ersatz des Zusammenführungsbearbeiters in Git genutzt werden, um die Fähigkeiten von Befehlen wie merge, revert, rebase und cherry-pick zu erweitern. Der Code wird unter der GPLv3-Lizenz veröffentlicht. In dieser neuen Version wurde die Unterstützung für die Programmiersprachen Python, TOML, Scala und Typescript hinzugefügt sowie die Leistung optimiert.

Nachfolgend finden Sie eine detaillierte Beschreibung der Probleme, die mit mergiraf gelöst werden:

Software ist ein strahlendes Beispiel für ein extrem komplexes System. Komplexe Systeme haben ein gemeinsames Merkmal: Sie sind KOMPLIZIERT – und man kann nicht erwarten, dass das gewünschte komplexe Verhalten von selbst entsteht. Stattdessen entwickeln sich diese Systeme im Laufe der Zeit, Schritt für Schritt, wobei jede Mutation an jedem Schritt sorgfältig geprüft wird. Hierfür sind eine gut definierte Struktur und die entsprechenden Werkzeuge erforderlich. Die Evolution eines komplexen Systems kann als gerichteter Baum visualisiert werden, dessen Wurzel eine leere Menge an Funktionen darstellt und jeder Knoten, mit Ausnahme der Wurzel, das Ergebnis einer Mutation seines Elternteils ist.

Im Kontext von Produkten wird jeder Knoten als „Version“ bezeichnet, die einen bestimmten Satz von Funktionen und Antifunktionen darstellt. Jede Veränderung dieses Sets wird als Mutation angesehen, die eine Kante in unserem gerichteten azyklischen Graphen bildet. Diese Funktionen sind von ihrer Natur her abstrakt; sie spiegeln nicht direkt die Funktionsweisen physischer Systeme wider, sondern zeigen vielmehr, wie intelligente Agenten den Nutzen dieser Systeme wahrnehmen. Um Ideen in reale Weltimplementierungen zu übersetzen, muss man die Ärmel hochkrempeln und in *ausreichend* niedrig-levelige Details eintauchen, in einer Sprache, die es ermöglicht zu erklären und zu zeigen, wie genau alles funktioniert. In der Softwareentwicklung sind diese niedrig-leveligen Details üblicherweise durch den Quellcode dargestellt.

Um den Quellcode schrittweise in einen Zustand zu bringen, 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 ist ein bestimmter Zustand des Produkts mit allen niedrig-leveligen Details, während ein Changeset den Übergang zwischen Snapshots bezeichnet. Üblicherweise entstehen Snapshots aus Einzelchangesets ihrer Eltern, weshalb diese Snapshots fast immer durch das gekennzeichnet sind, was die Changesets, die sie erstellt haben, tun, weshalb diese Begriffe oft austauschbar verwendet werden.

Es gibt manchmal Snapshots, die aus mehreren Übergängen entstanden sind – durch das Zusammenführen von Commits. Diese sind schwer zu handhaben, weshalb sie normalerweise vermieden werden. Moderne Open-Source-Versionierungssysteme wie Git bieten sehr grundlegende Möglichkeiten zur Verwaltung des Entwicklungsprozesses. Sie ermöglichen es Entwicklern, Snapshots in Form von gerichteten azyklischen Graphen zu organisieren, sie mit Kommentaren zu versehen und bei Bedarf ihre Reihenfolge zu ändern.

Diese Funktionalität ermöglicht es Entwicklern, eine semantisch bedeutungsvolle Geschichte des Projekts zu schreiben, was für die Fehlersuche und die Beantwortung von Fragen wie „Warum wurde dieses niedrig-levelige Detail (z. B. eine Variable) eingeführt?“, „Wie viel Prozent meines Beitrags fließen ungefähr in dieses Projekt ein?“, „Wann und wer hat die Backdoor-Implementierungen gehackt?“, „Welches niedrig-levelige Änderung hat diese Funktion kaputt gemacht (obwohl sie es eigentlich nicht sollte, wir haben alles überprüft!)“ entscheidend ist.

Versionskontrollsysteme ergänzen dies durch das Konzept des Zweigs – ein niedrig-leveliges Konzept, das einfach einen kontinuierlichen Abschnitt der niedrig-leveligen Geschichte des Projekts bedeutet, der für den Entwickler semantisch bedeutungsvoll ist. Zweige werden üblicherweise für die spezifische Implementierung von Funktionen verwendet, manchmal werden mehrere Zweige für verschiedene Implementierungskandidaten derselben Funktion erstellt. Durch die Verwendung von Workflows mit Verzweigungen (die tatsächlich Mainstream und Entwicklungsstandard sind und überall verwendet werden) kann jeder einzelne Entwickler viele konfliktbeladenen Zweige des Projekts effektiv verwalten, die sich jeweils in ihrem Grad an Fertigstellung oder Qualität unterscheiden. Dies ermöglicht es Entwicklern, die Ergebnisse ihrer eigenen und fremder Arbeiten zu kombinieren, ohne jedes Mal alles von Hand überarbeiten zu müssen.

In der Regel wird ein Hauptbranch erstellt, der das „offizielle“ Produkt repräsentiert, von dem abzweigende Branches für jede Funktion ausgehen. Diese werden regelmäßig (idealerweise nach jedem Commit) mit dem Hauptbranch synchronisiert, was den Entwicklern ermöglicht, mit der neuesten Version des Produkts zu arbeiten und gleichzeitig Funktionen zu implementieren, an denen sie gerade arbeiten, wodurch Probleme, die durch die Aktionen anderer Entwickler entstehen, so früh wie möglich erkannt werden.

Beim Versuch, Funktionen verschiedener Snapshots zu kombinieren (was lediglich das Finden eines gemeinsamen Vorfahren und die schrittweise Anwendung der daraus resultierenden Changesets auf einen anderen bedeutet), wird dieser Vorgang als Rebase bezeichnet. Das Zusammenführen hingegen ist fast wie ein Rebase, strukturiert jedoch den Commit-Graphen anders, was die Manipulation erschwert; daher wird versucht, Merges zugunsten von Rebases zu vermeiden. Moderne Versionskontrollsysteme (VCS) verwenden interne Algorithmen zur Zusammenführung von Änderungen, die Dateien in einzelne Zeilen zerlegen, jede Zeile als Zeichen betrachten und die Dateien als deren Sequenzen, und anschließend Algorithmen zur Zusammenführung anwenden, die ihren Ursprung in der Bioinformatik haben.

Leider hat diese zeilenweise Darstellung des Quellcodes nichts mit dessen Inhalt zu tun. Ihr einziges Merkmal ist, dass sie einfach und universell ist. Diese Inkonsistenz führt zu Konflikten und ist eine ständige Quelle von Kopfzerbrechen für Entwickler. Die Konfliktlösung erfordert von den Entwicklern, dass sie beide Versionen des Codes sorgfältig prüfen, und zwar nicht nur die Abschnitte, die vom zeilenweisen Vergleichsalgorithmus als „geändert“ oder „konfliktierend“ gekennzeichnet sind, sondern möglicherweise auch das gesamte Projekt.

Der Entwickler muss die Änderungen verstehen, den zusammengeführten Code manuell schreiben und alle Inkonsistenzen beseitigen. Die Probleme nehmen zu, wenn das zeilenweise Tool Änderungen falsch identifiziert, was häufig bei größeren Änderungen, einschließlich trivialer wie der Formatierung des Codes, geschieht. Wenn nachfolgende Änderungen nicht auf den manuell zusammengeführten Code angewendet werden können, wird die Situation zum totalen Albtraum. Trotz der erschreckenden Fälle funktioniert der zeilenweise Algorithmus in den meisten Fällen, insbesondere wenn die Entwickler aktiv versuchen, ihm keine Probleme zu bereiten. Eine der Möglichkeiten, solche Probleme zu minimieren, besteht darin, die Verarbeitung der Quellcodes mit Kanonalisierungswerkzeugen wie Black zwingend vorzuschreiben.

Natürlich ist die richtige Lösung für erschreckende Fälle (und im Allgemeinen nicht nur für diese; der zeilenweise Algorithmus ist eine Heuristik, die trivialerweise zu nicht funktionsfähigem Code führen kann, wenn beispielsweise ein Entwickler eine Variable umbenannt hat, während ein anderer gleichzeitig einen neuen Codeabschnitt geschrieben hat, der diese Variable verwendet. Es wird keinen Merge- oder Rebase-Konflikt geben, aber das Ergebnis wird nicht funktionsfähig sein) die Verwendung eines richtigen internen Modells.

Obwohl seit etwa 30 Jahren in diesem Bereich geforscht wird und mehrere proprietäre kommerzielle Produkte entstanden sind, wurden diese Forschungen bis vor kurzem noch nicht in praktisch anwendbare Open-Source-Produkte umgesetzt. Der Großteil der FOSS-Lösungen begann in den frühen 2010er Jahren zu entstehen und konzentrierte sich hauptsächlich auf die Programmiersprache Java.

Die herausragendste freie Implementierung aus dieser Zeit, GumTree, wurde von einem Forscher mit akademischem Hintergrund entwickelt, ist in Java geschrieben, verfügt über eine eigene abstrakte interne Darstellung, die treesitter vorausgeht, und hat Backends, die sowohl auf treesitter basieren als auch auf anderen Werkzeugen zur Analyse des Quellcodes in abstrakte Darstellungen. Dieses System kann nur Änderungen generieren (in Form eines textuellen Ereignisprotokolls; es gibt auch eine API, die problemlos aus jeder Programmiersprache aufgerufen werden kann, die Bindungen zu Java besitzt) und visualisieren. Aber für das Zusammenführen von Änderungen und das Anschauen der von ihm generierten Diff-Dateien ist es von Haus aus nicht anwendbar (wobei es wahrscheinlich ist, dass das Laden von Diffs über die API realisiert werden kann).

Eine jüngere und in der Praxis anwendbarere Implementierung, Difftastic, wurde in Rust geschrieben, basiert auf treesitter und konzentriert sich auf die Generierung von hervorgehobenen Diffs in der Konsole. Dieses System zielt auch darauf ab, Diffs zu visualisieren, ohne das Ziel zu verfolgen, Änderungen zusammenzuführen oder Patches anzuwenden.

Vor kurzem wurde das Projekt mergiraf ins Leben gerufen und entwickelt sich aktiv. Dieses auf Rust basierende Werkzeug (nimmt 21 MiB ein!) nutzt ebenfalls treesitter, das sich mittlerweile als Standard für Parser kontextfreier Grammatiken in Entwicklungstools etabliert hat, genauso wie LLVM für die Optimierung von niedrigstufigen Instruktionsdarstellungen. Im Gegensatz zu seinen Mitbewerbern bietet mergiraf Funktionen nicht zur Generierung von Diffs, sondern zur automatischen Auflösung von Merge-Konflikten. Unter der Haube verwendet mergiraf zur Patch-Generierung eine Implementierung des Algorithmus, der in GumTree verwendet wird, und für die Anwendung eine Implementierung des in spork genutzten Algorithmus, angepasst an die Strukturen von treesitter.

Die Serialisierung von Patches in Dateien, die später angewendet werden können, ist leider nicht implementiert (es ist jedoch ganz wahrscheinlich, dass dies durch das Parsen von Ereignisprotokollen, die von GumTree generiert werden, realisiert werden kann). Eine weitere vielversprechende Methode zur Anwendung von Unterschieden könnte die Verwendung von Unterschieden über Patches hinweg sein, sondern über die Funktionalitäten von LSP-Servern für Refactoring, was bei der Erkennung von Konflikten im gesamten Projekt helfen könnte. Die Visualisierung wird nur für Konflikte unterstützt.

Beispiel: gemeinsamer Vorfahre «base.py» (mit Tabulatoren, überflüssige Zeile am Anfang) foo = 1 def main(): print(foo + 2 + 3) «a.py» (Tabulatoren bleiben, 2 überflüssige Zeilen am Anfang anstelle einer, für Debug-Ausgaben wird die Bibliothek icecream verwendet, hinzugefügt Klasse «baz»: from icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): «»»baz»»» «b.py» (Variable «foo» in «bar» umbenannt, nach Änderungen mit «black» bearbeitet, sodass die Einrückungen jetzt Leerzeichen sind und überflüssige Zeilen entfernt wurden): bar = 1 def main(): print(bar + 2 + 3) 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 wurde die Bibliothek «icecream» verwendet, die Variable «foo» wurde in «bar» umbenannt, nach Änderungen mit «black» bearbeitet, sodass die Einrückungen jetzt Leerzeichen sind und überflüssige Zeilen entfernt wurden, Mischungen von Tabulatoren und Leerzeichen für die Einrückung, aber eine zulässige Erscheinung).

Hier zeigt sich der Nachteil des Werkzeugs. Der Dokumentstil wird normalerweise in «.editorconfig»-Dateien konfiguriert, und globale Stiländerungen, wie das Wechseln von Tabulatoren zu Leerzeichen und die Annahme des Stils von black, wie in «b.py» durchgeführt, werden üblicherweise von Änderungen in der «.editorconfig» begleitet. Daher sollte das Tool für eine genauere Anwendung solcher Änderungen ein Konzept für den globalen Stil «standardmäßig» haben und in der Lage sein, Einstellungen aus «.editorconfig» zu übernehmen.

Quelle: opennet.ru

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster