mergiraf — narzędzie oparte na AST do trójstronnego łączenia w Git

Opublikowano wydanie projektu mergiraf 0.4, który rozwija sterownik dla Git z możliwością trójstronnego łączenia. Mergiraf wspiera rozwiązywanie różnych rodzajów konfliktów przy łączeniu i może być używany dla różnych języków programowania oraz formatów plików. Możliwe jest zarówno osobne wywołanie mergiraf do obsługi konfliktów, które pojawiają się podczas pracy z standardowym Git, jak i zastąpienie w Git przetwornika łączenia, aby rozszerzyć możliwości takich komend jak merge, revert, rebase i cherry-pick. Kod jest rozpowszechniany na licencji GPLv3. W nowej wersji dodano wsparcie dla języków Python, TOML, Scala i Typescript, a także przeprowadzono optymalizację wydajności.

Poniżej znajduje się szczegółowy opis problemów, które można rozwiązać przy pomocy mergiraf:

Oprogramowanie jest doskonałym przykładem niezwykle złożonego systemu. Złożone systemy mają jedną wspólną cechę — są ZŁOŻONE — i nie można oczekiwać, że wymagane złożone zachowanie pojawi się samoistnie, przypadkowo. Zamiast tego te systemy ewoluują w czasie, krok po kroku, a każda mutacja jest dokładnie sprawdzana na każdym etapie. Aby to osiągnąć, potrzebna jest dobrze zdefiniowana struktura i odpowiednie narzędzia. Ewolucję każdego złożonego systemu można zwizualizować jako skierowane drzewo, gdzie korzeń reprezentuje pusty zbiór funkcji, a każdy węzeł — z wyjątkiem korzenia — jest wynikiem zastosowania mutacji do swojego rodzica.

W kontekście produktów każdy węzeł nazywany jest „wersją”, która reprezentuje określony zestaw funkcji i antyfunkcji. Jakiekolwiek zmiany tego zestawu są uważane za mutację, formującą krawędź w naszym skierowanym acyklicznym grafie. Te funkcje z natury są abstrakcyjne; nie odzwierciedlają bezpośrednio sposobów działania fizycznych systemów, a raczej pokazują, jak rozumne podmioty postrzegają użyteczność tych systemów. Aby przełożyć pomysły na istniejące w rzeczywistym świecie realizacje, należy zakasać rękawy i zanurzyć się w *wystarczająco* niskopoziomowe szczegóły, w języku, którego można użyć do wyrażenia i wyjaśnienia, w jaki sposób wszystko naprawdę działa. W rozwoju oprogramowania te niskopoziomowe szczegóły zwykle są reprezentowane przez kod źródłowy.

Aby stopniowo doprowadzić kod źródłowy do stanu, który wykazuje pożądane zachowanie, a także udokumentować, w jaki sposób to osiągnięto, programiści przedstawiają swoją pracę w kategoriach snapshotów i changestów. Snapshot to określony stan produktu z wszystkimi niskopoziomowymi szczegółami, podczas gdy changeset oznacza przejście między snapshotami. Zwykle snapshoty są generowane z pojedynczych changesetów ich rodziców, dlatego te snapshoty prawie zawsze oznaczane są przez te changesety, które je stworzyły, co sprawia, że te terminy są często używane zamiennie.

Czasami istnieją zdjęcia uzyskane w wyniku kilku przejść — scalania commitów. Praca z nimi jest trudna, dlatego zazwyczaj się ich unika. Nowoczesne systemy kontroli wersji z otwartym źródłem, takie jak Git, oferują dość podstawowe możliwości zarządzania przepływem pracy w procesie rozwoju. Umożliwiają programistom organizowanie snapshotów w postaci skierowanych grafów acyklicznych, annotowanie ich komentarzami i w razie potrzeby zmianę ich kolejności.

Ta funkcjonalność umożliwia programistom pisanie semantycznie istotnej historii projektu, co jest kluczowe dla debugowania i odpowiadania na pytania typu: „Dlaczego wprowadzono ten niskopoziomowy szczegół (np. zmienną)?”, „Jakie procentowo miałem wkład w ten projekt?”, „Kto został zhakowany przez wprowadzenie backdoora i kiedy?”, „Która niskopoziomowa zmiana zepsuła tę funkcję (choć wydaje się, że nie powinna, wszystko sprawdziliśmy!?)”

Systemy kontroli wersji uzupełniają tę koncepcję gałęzi — niskopoziomowego pojęcia, które po prostu oznacza ciągły fragment niskopoziomowej historii projektu, semantycznie znaczący dla programisty. Gałęzie są zazwyczaj wykorzystywane do konkretnej implementacji funkcji, czasami tworzy się kilka gałęzi dla różnych kandydatów do implementacji tej samej funkcji. Korzystając z procesów roboczych z gałęziowaniem (które są faktycznie mainstreamowe i standardem rozwoju, używanym wszędzie), każdy pojedynczy programista może efektywnie zarządzać wieloma konfliktującymi gałęziami projektu, z których każda różni się stopniem gotowości lub jakości. Umożliwia to programistom łączenie wyników swojej pracy z pracą innych bez ręcznego przepisywania wszystkiego za każdym razem.

Zazwyczaj tworzy się główną gałąź, reprezentującą "oficjalny" produkt, od której odgałęziają się boczne gałęzie dla każdej funkcji, które regularnie (w idealnym przypadku — po każdym commicie) synchronizują się z główną gałęzią, co pozwala programistom pracować z najnowszą wersją produktu, jednocześnie wdrażając funkcje, które aktualnie rozwijają, wykrywając problemy, wynikające z działań innych programistów, jak najwcześniej.

Podczas próby łączenia funkcji różnych snapshotów (co jest po prostu znalezieniem wspólnego przodka i stosowaniem zmian, które je generują, jedna po drugiej, ta operacja nazywa się rebase, natomiast scalenie — to prawie jak rebase, po prostu strukturyzuje wykres commitów w inny sposób, przez co staje się on mniej wygodny do manipulacji, dlatego od scalania stara się unikać na rzecz rebase) pojawiają się problemy. Nowoczesne systemy kontroli wersji (VCS) wykorzystują wewnętrzne algorytmy łączenia zmian, które po prostu dzielą pliki na pojedyncze wiersze, traktują każdy wiersz jako symbol, a pliki — jako ich sekwencje, a następnie stosują algorytmy do ich łączenia, które pochodzą z bioinformatyki.

Niestety, takie liniowe przedstawienie kodu źródłowego nie ma nic wspólnego z jego treścią. Jego jedyną zaletą jest to, że jest proste i uniwersalne. Rozbieżności prowadzą do konfliktów, będących stałym źródłem bólu głowy dla programistów. Rozwiązanie konfliktów wymaga od programisty dokładnego zbadania obu wersji kodu, a nie tylko sekcji oznaczonych przez liniowy algorytm porównania jako „zmienione” lub „konfliktujące”, ale być może także całego projektu.

Programista musi zrozumieć zmiany, ręcznie napisać połączony kod i rozwiązać wszelkie niespójności. Problemów przybywa, gdy narzędzie liniowe błędnie identyfikuje zmiany, co często zdarza się przy dużych zmianach, w tym trywialnych, takich jak formatowanie kodu. Jeśli późniejsze zmiany nie mogą zostać zastosowane do ręcznie połączonego kodu, sytuacja zamienia się w kompletny koszmar. Mimo przerażających przypadków, w większości przypadków algorytm liniowy działa, zwłaszcza jeśli programiści starają się aktywnie nie stwarzać mu problemów. Jednym ze sposobów minimalizacji takich problemów jest obowiązkowe przetwarzanie źródeł za pomocą narzędzi kanonikalizacji, takich jak black.

Oczywiście właściwym rozwiązaniem przerażających przypadków (i ogólnie, nie tylko dla nich) jest stosowanie prawidłowego modelu wewnętrznego. Algorytm liniowy to heurystyka, która w trywialny sposób może prowadzić do nie działającego kodu, na przykład, gdy jeden programista zmienił nazwę zmiennej, a drugi w tym czasie napisał fragment nowego kodu wykorzystującego tę zmienną. Nie będzie konfliktu łączenia/przemieszczania, ale wynik stanie się nie działający.

Pomimo tego, że badania w tej dziedzinie prowadzone są od około 30 lat i doprowadziły do stworzenia kilku komercyjnych produktów własnościowych, badania te do niedawna nie zostały przekształcone w praktycznie zastosowalne produkty z otwartym kodem źródłowym. Główna masa rozwiązań open source zaczęła się rozwijać na początku lat 2010-tych i była głównie skoncentrowana na języku Java.

Najbardziej wyróżniająca się darmowa implementacja tego okresu, GumTree, została stworzona przez badacza z akademickim zapleczem. Napisał ją w Javie, ma swoje abstrakcyjne wewnętrzne przedstawienie, poprzedzające treesitter, ma backendy oparte zarówno na treesitter, jak i innych narzędziach do parsowania kodu źródłowego w abstrakcyjne przedstawienia. System ten potrafi jedynie generować (w formie logu tekstowego zdarzeń, istnieje również API, które można łatwo wywołać z każdego języka programowania mającego powiązania z Javą) i wizualizować zmiany. Niestety, do łączenia zmian, a także do przeglądania generowanych plików diff, nie jest on od razu zastosowalny (choć prawdopodobnie ładowanie diffów można zrealizować poprzez API).

Młodsza i bardziej praktyczna implementacja difftastic napisana w Rust ma oparcie w treesitter i koncentruje się na generacji podświetlonych diffów w konsoli. System ten również wykierowany jest na wizualizację diffów i w ogóle nie ma na celu łączenia zmian ani stosowania łatek.

Niedawno zadebiutował i intensywnie rozwija się projekt mergiraf. To narzędzie napisane w Rust (zajmujące 21 MiB!) jest również oparte na treesitter, który stał się takim samym standardem dla parserów gramatyk kontekstowo-wolnych w narzędziach deweloperskich, jak LLVM dla optymalizacji niskopoziomowych reprezentacji instrukcji. W przeciwieństwie do konkurencji, mergiraf oferuje funkcje nie do generowania diffów, lecz do automatycznego rozwiązywania konfliktów scalania. W tle mergiraf wykorzystuje implementację algorytmu stosowanego w GumTree do generowania poprawek, a do stosowania — implementację algorytmu wykorzystywanego w spork, dostosowaną do struktur treesitter.

Serializacja poprawek do plików, które można następnie zastosować, niestety nie została jeszcze zrealizowana (ale całkiem prawdopodobne, że może być wykonana poprzez parsowanie logów zdarzeń generowanych przez GumTree). Innym potencjalnym sposobem zastosowania różnic może być wykorzystanie ich nie poprzez łaty, ale poprzez funkcjonalność refaktoryzacji serwerów LSP, co może pomóc w wykrywaniu konfliktów na poziomie całego projektu. Wizualizacja obsługiwana jest tylko dla konfliktów.

Przykład działania: wspólny przodek „base.py” (wcięcia tabulatorami, zbędna linia na początku) foo = 1 def main(): print(foo + 2 + 3) „a.py” (wcięcia nadal tabulatorami, 2 zbędne linie na początku zamiast jednej, do debugowania używana jest biblioteka icecream, dodana klasa „baz”: from icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): „””baz””” „b.py” (zmienna „foo” została przemianowana na „bar”, przetworzona przy użyciu „black” po zmianach, w wyniku wcięcia — spacjami, a zbędne linie usunięte): bar = 1 def main(): print(bar + 2 + 3) Wywołanie .\/mergiraf merge .\/base.py .\/a.py .\/b.py -x a.py -y b.py -s base.py -o .\/res.py daje następujący wynik from icecream import ic bar = 1 def main(): ic(bar + 2 + 3) class baz: def __init__(self): „””baz””” (do debugowania używana jest biblioteka „icecream”, zmienna „foo” została przemianowana na „bar”, przetworzona przy użyciu „black” po zmianach, w wyniku wcięcia — spacjami, a zbędne linie usunięte, mieszanka tabów i spacjami dla wcięcia, ale dozwolony widok).

Tutaj widać wadę narzędzia. Styl dokumentu zazwyczaj konfiguruje się w plikach „.editorconfig” i globalne zmiany stylu, takie jak zmiana tabulatorów na spacje i przyjęcie stylu black-a, jak miało to miejsce w „b.py”, zazwyczaj wiążą się ze zmianami w „.editorconfig”. Dlatego, aby poprawnie zastosować takie zmiany, narzędzie powinno mieć koncepcję globalnego stylu „domyślnego” i umieć wciągać ustawienia z „.editorconfig”.

Źródło: opennet.ru

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster