mergiraf — инструмент, ориентиран към AST за триренедно сливане в Git

Публикувана е версия 0.4 на проекта mergiraf, който развива драйвера за Git с реализирана възможност за триренедно сливане. Mergiraf поддържа разрешаване на различни видове конфликти при сливане и може да се използва за различни програмни езици и формати на файлове. Възможно е както отделно извикване на mergiraf за обработка на конфликти, възникващи при работа с основния Git, така и замяна в Git на обработчика на сливане за разширяване на възможностите на команди като merge, revert, rebase и cherry-pick. Кодът се разпространява под лицензия GPLv3. В новата версия е добавена поддръжка за езици Python, TOML, Scala и Typescript, а също така е извършена оптимизация на производителността.

По-долу е представено подробно описание на проблемите, които се решават с помощта на mergiraf:

Софтуерът е ярък пример за изключително сложна система. Сложните системи имат едно общо свойство — те са СЛОЖНИ — и не можете да очаквате, че необходимото сложно поведение ще възникне случайно и само по себе си. Вместо това тези системи еволюират с времето, стъпка по стъпка, и всяка мутация се проверява внимателно на всяка стъпка. За да постигнете това, са необходими добре определена структура и съответни инструменти. Еволюцията на всяка сложна система може да бъде визуализирана под формата на насочено дърво, където коренът представлява празно множество функции, а всеки възел — с изключение на корена — е резултат от прилагането на мутация към родителя си.

В контекста на продуктите, всеки възел се нарича "версия", представляваща определен набор от функции и антифункции. Всяка промяна в този набор се счита за мутация, която образува ръб в нашия насочен ацикличен граф. Тези функции по същността си са абстрактни; те не отразяват директно начини за функциониране на физически системи, а по-скоро демонстрират как разумни агенти възприемат полезността на тези системи. За да преведем идеите в реализациите, съществуващи в реалния свят, е необходимо да засучем ръкави и да се потопим в *достатъчно* нискоуровневите детайли, на езика, на който можем да изразим и обясним как всичко точно работи. При разработката на софтуер тези нискоуровневи детайли обикновено се представят от исходния код.

За да доведат постепенно изходния код до състояние, което проявява желаното поведение, и да документират как са го постигнали, програмистите представят своята работа в термини като snapshot-и и changest-е. Snapshot представлява определено състояние на продукта с всички нискоуровневи детайли, докато changeset обозначава преход между snapshot-и. Обикновено snapshot-ите произлизат от единични changeset-ове спрямо техните родителски, затова тези snapshot-и почти винаги са обозначени с това, което правят changeset-ите, които са ги създали, така че тези термини често се използват взаимозаменяемо.

Понякога съществуват снимки, получени в резултат на няколко прехода — сливане на комити. С тях е трудно да се работи, затова обикновено се избягват. Съвременните системи за контрол на версиите с отворен код, като Git, предоставят доста основни възможности за управление на работните потоци на разработката. Те позволяват на разработчиците да организират снимки под формата на насочени ациклични графове, да ги анотират с коментари и при необходимост да променят реда им.

Тази функционалност позволява на разработчиците да напишат семантически значима история на проекта, което е от решаващо значение за отстраняване на грешки и отговори на въпроси като "Защо беше въведен този нискоуровнев детайл (напр. променлива)?", "Колко процента приблизително е моят принос в този проект?", "Кого хакнаха внедряванията на backdoor и кога?", "Кое нискоуровнево изменение е счупило тази функция (въпреки че не би трябвало, всичко проверихме!?)"

Системите за контрол на версиите допълват концепцията за клонове — нискоуровнево понятие, което просто означава непрекъснат фрагмент от историята на проекта, семантично значим за разработчика. Клоновете обикновено се използват за конкретни реализации на функции, а понякога се създават няколко клона за различни кандидати за реализация на една и съща функция. Използвайки работни процеси с клониране (които всъщност са основни и стандартни в разработката, използвани навсякъде), всеки отделен разработчик може ефективно да управлява много конфликтни клона на проекта, всеки от които е различен по степен на завършеност или качество. Това позволява на разработчиците да комбинират резултатите от труда си и на другите без да въвеждат всичко ръчно всеки път.

Обикновено се създава основен клон, представляващ „официалния“ продукт, от който клонят странични клонове за всяка функция, които редовно (в идеалния случай — след всяко комитване) се синхронизират с основния клон, което позволява на разработчиците да работят с най-новата версия на продукта и едновременно да внедряват функции, които в момента разработват, откривайки проблеми, произтичащи от действията на други разработчици, възможно най-рано.

При опит за комбиниране на функции от различни snapshot-ове (което просто е намиране на общ предшественик и последователно прилагане на changeset-ите, които ги произвеждат, върху друг, тази операция се нарича rebase, а сливането е почти като rebase, просто структурира графика на комитите по различен начин, в резултат на което става неудобно да се манипулира с тях, затова от сливанията се стремят да се откажат в полза на rebase) възникват проблеми. Съвременните системи за контрол на версиите (VCS) използват вътрешни алгоритми за обединяване на изменения, които просто разбиват файловете на отделни редове, разглеждат всеки ред като символ, а файловете — като техни последователности, и след това прилагат алгоритми за тяхното обединяване, произхождащи от биоинформатиката.

За съжаление, такова редово представяне на изходния код няма нищо общо с неговото съдържание. Единственото му предимство е, че е просто и универсално. Неподходящото представяне води до конфликти при, представлявайки постоянен източник на главоболия за разработчиците. Разрешаването на конфликтите изисква от разработчика внимателно проучване на двете версии на кода, и то не само на разделите, обозначени от редовния алгоритъм на сравнение като „променени“ или „конфликтни“, но, вероятно, и на целия проект.

Разработчикът трябва да разбере промените, ръчно да напише обединен код и да отстрани всякакви несъответствия. Проблемите значително нарастват, когато редовният инструмент неправилно идентифицира промените, което често се случва при значителни изменения, включително тривиални, като например повторно форматиране на кода. Ако последващи промени не могат да бъдат приложени към ръчно обединения код, ситуацията се превръща в истински кошмар. Въпреки ужасните случаи, в повечето случаи редовният алгоритъм работи, особено ако разработчиците активно се стараят да не му създават проблеми. Един от начините за минимизиране на подобни проблеми е задължителното изискване за обработка на изходниците с инструменти за канонизация, като black.

Разбира се, правилното решение на ужасните случаи (и изобщо, не само за тях, редовният алгоритъм е хевристичен, той е тривиален и може да доведе до неработещ код, например, един разработчик е преименувал променлива, а друг в същото време е написал част от нов код, използваща тази променлива, конфликт на сливане/преобазиране тук няма да има, но резултатът ще стане неработещ) е използването на правилен вътрешен модел.

Въпреки че изследванията в тази област продължават вече около 30 години и доведоха до създаването на няколко собственически търговски продукта, тези изследвания до неотдавна не бяха превърнати в практически приложими продукти с отворен код. Основната част от решенията с отворен код започна да се развива в началото на 2010-те и беше основно фокусирана върху езика Java.

Най-добрата свободна реализация от този период, GumTree, е създадена от изследователь с академичен бекграунд, написана на Java, разполага със своето абстрактно вътрешно представление, предшестващо treesitter, и има бекенди, основани както на treesitter, така и на други инструменти за парсинг на изходен код в абстрактни представяния. Тази система може само да генерира (под формата на текстов лог на събития, съществува и API, който може да се извика тривиално от всякакъв ЯП с биндинги към Java) и визуализира промени. Въпреки това, за сливане на промени или преглед на генерираните от нея diff-файлове, тя не е приложима в наличието си (въпреки че е вероятно, че зареждането на diff-ове може да бъде реализирано чрез API).

По-младата и по-практична реализация, difftastic, е написана на Rust, базирана на treesitter и е фокусирана върху генерирането на подсветени diff-ове в конзолата. Тази система също е насочена към визуализиране на diff-ове и изобщо не цели сливане на промени или прилагане на пачове.

Съвсем наскоро се появи и активно се развива проектът mergiraf. Този инструмент, написан на Rust (заема 21 MiB!), също е базиран на treesitter, който вече стана също толкова стандарт за парсери на контекстно-свободни граматики в инструменти за разработка, колкото LLVM стана за оптимизация на нискоуровневи представяния на инструкции. За разлика от конкуренцията, mergiraf предоставя функции не за генериране на diff-ове, а за автоматично разрешаване на конфликти при сливане. Под капака, mergiraf използва за генериране на пачове реализация на алгоритъма, използван в GumTree, а за прилагане — реализация на алгоритъма, използван в spork, адаптирани за структури treesitter.

Сериализацията на пачове в файлове, които могат да бъдат приложени по-късно, за съжаление, не е реализирана (но е вероятно, че може да бъде реализирана чрез парсинг на логовете на събития, генерирани от GumTree). Друг обещаващ начин за прилагане на разлики може да бъде прилагането им не чрез пачове, а чрез функционалността за рефакторинг на LSP-сервитори, което може да помогне при откриване на конфликти на ниво цял проект. Визуализацията се поддържа само за конфликти.

Пример за работа: общ родител «base.py» (отстъпи табове, излишен ред в началото) foo = 1 def main(): print(foo + 2 + 3) «a.py» (отстъпи пак табове, 2 излишни реда в началото вместо един, за отладочно печатане е включена библиотеката icecream, добавен е клас «baz»: от icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): «»»baz»»» «b.py» (променливата «foo» преименувана на «bar», обработена с помощта на «black» след изменения, в резултат отстъпите — с интервали и излишните редове изрязани): bar = 1 def main(): print(bar + 2 + 3) Извикването .\/mergiraf merge .\/base.py .\/a.py .\/b.py -x a.py -y b.py -s base.py -o .\/res.py дава следния резултат from icecream import ic bar = 1 def main(): ic(bar + 2 + 3) class baz: def __init__(self): «»»baz»»» (за отладочно печатане е включена библиотеката «icecream», променливата «foo» е преименувана на «bar», обработена с помощта на «black» след изменения, в резултат отстъпите — с интервали и излишните редове изрязани, смес от табове и интервали за отстъп, но разрешен вид).

Тук също е видима недостигата на инструмента. Стилът на документа обикновено се конфигурира в файловете «.editorconfig», и глобалните промени в стила, като смяната на табове с интервали и приемането на стила на black, както беше направено в «b.py», обикновено се съпровождат с изменения в «.editorconfig». Затова за по-точно приложение на подобни промени инструментът трябва да има концепция за глобален стил «по подразбиране» и да може да извлича настройки от «.editorconfig».

Източник: opennet.ru

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster