A fost lansată versiunea 0.4 a proiectului mergiraf, care dezvoltă un driver pentru Git cu implementarea posibilității de fuziune în trei direcții. Mergiraf suportă rezolvarea diferitelor tipuri de conflicte în timpul fuziunii și poate fi utilizat pentru diverse limbaje de programare și formate de fișiere. Este posibil atât un apel separat mergiraf pentru a gestiona conflictele apărute în timpul utilizării Git standard, cât și înlocuirea în Git a handler-ului de fuziune pentru a extinde funcționalitățile unor comenzi precum merge, revert, rebase și cherry-pick. Codul este distribuit sub licența GPLv3. Noua versiune adaugă suport pentru limbaje precum Python, TOML, Scala și Typescript, precum și optimizări de performanță.
Mai jos este o descriere detaliată a problemelor rezolvate cu ajutorul mergiraf:
Software-ul este un exemplu elocvent al unei sisteme extrem de complexe. Sistemele complexe au o trăsătură comună — sunt COMPLEXE — și nu poți aștepta ca comportamentul complex necesar să apară de la sine, întâmplător. În schimb, aceste sisteme evoluează în timp, pas cu pas, iar fiecare mutație este verificată cu atenție la fiecare etapă. Pentru a atinge acest lucru, sunt necesare o structură bine definită și instrumente corespunzătoare. Evoluția oricărei sisteme complexe poate fi vizualizată ca un arbore orientat, unde rădăcina reprezintă un set gol de funcții, iar fiecare nod — cu excepția rădăcinii — este rezultatul aplicării unei mutații asupra părintelui său.
În contextul produselor, fiecare nod este numit "versiune", reprezentând un anumit set de funcții și antifuncții. Orice modificare a acestui set este considerată o mutație, formând o margine în graful nostru orientat aciclic. Aceste funcții sunt în esență abstracte; nu reflectă în mod direct modul în care funcționează sistemele fizice, ci mai degrabă demonstrează cum agenții raționali percep utilitatea acestor sisteme. Pentru a traduce ideile în realizări existente în lumea reală, este necesar să ne suflecăm mânecile și să ne aruncăm în detalii *suficient de* de bază, în termenii în care să putem exprima și explica exact cum funcționează totul. În dezvoltarea software-ului, aceste detalii de bază sunt de obicei reprezentate de codul sursă.
Pentru a aduce treptat codul sursă într-o stare care manifestă comportamentul dorit și a documenta cum au ajuns acolo, programatorii își prezintă munca în termeni de snapshot-uri și changeset-uri. Un snapshot reprezintă o stare specifică a produsului cu toate detaliile sale de nivel inferior, în timp ce un changeset denotă tranziția între snapshot-uri. De obicei, snapshot-urile sunt generate de changeset-uri unice către părinții lor, astfel că aceste snapshot-uri marchează aproape întotdeauna ceea ce fac changeset-urile care le-au creat, motiv pentru care acești termeni sunt adesea folosiți interschimbabil.
Uneori există snapshot-uri obținute în urma mai multor tranziții — fuziuni de commit-uri. Este dificil de lucrat cu acestea, așa că sunt, de obicei, evitate. Sistemele moderne de control al versiunilor cu sursă deschisă, cum ar fi Git, oferă funcționalități destul de de bază pentru gestionarea fluxurilor de lucru de dezvoltare. Acestea permit dezvoltatorilor să organizeze snapshot-urile sub formă de grafuri orientate aciclice, să le comenteze și, dacă este necesar, să le schimbe ordinea.
Această funcționalitate permite dezvoltatorilor să scrie o istorie semnificativă din punct de vedere semantic a proiectului, ceea ce este crucial pentru depanare și pentru a răspunde la întrebări precum „De ce a fost introdus acest detaliu de nivel inferior (de exemplu, o variabilă)?”, „Cât de mult contribuie aproximativ la acest proiect?”, „Cine a fost compromis prin infiltrarea unei backdoor și când?”, „Ce modificare de nivel inferior a distrus această funcție (deși nu ar fi trebuit să o facă, am verificat totul!?)”
Sistemele de control al versiunilor completează această concept prin ramuri - un concept de nivel scăzut care reprezintă pur și simplu un fragment continuu al istoriei de nivel scăzut a proiectului, semnificativ din punct de vedere semantic pentru dezvoltator. Ramurile sunt de obicei utilizate pentru implementarea specifică a unor funcționalități, iar uneori se creează mai multe ramuri pentru diferite propuneri de implementare a aceleași funcționalități. Folosind fluxuri de lucru cu ramificare (care sunt de fapt mainstream și standard în dezvoltare, utilizate peste tot și în mod curent), fiecare dezvoltator poate gestiona eficient multe ramuri conflictuale ale proiectului, fiecare dintre ele având un grad diferit de finalizare sau calitate. Acest lucru permite dezvoltatorilor să combine rezultatele muncii lor cu cele ale altora fără a reintroduce tot manual de fiecare dată.
De obicei, se creează o ramură principală, care reprezintă produsul „oficial”, din care se ramifică ramuri laterale pentru fiecare funcționalitate, care sunt sincronizate regulat (ideea este, după fiecare commit) cu ramura principală, permițându-le dezvoltatorilor să lucreze cu cea mai actualizată versiune a produsului și simultan să implementeze funcții pe care le dezvoltă în acel moment, detectând problemele care rezultă din acțiunile altor dezvoltatori cât mai devreme posibil.
În încercarea de a combina funcțiile diferitelor snapshot-uri (ceea ce înseamnă pur și simplu găsirea unui strămoș comun și aplicarea seturilor de modificări care le generează, în mod consecutiv, pe un altul, această operație se numește rebase, iar fuziunea este aproape ca rebase, doar că structurează graficul commit-urilor diferit, ceea ce le face mai greu de manipulat, de aceea se caută să se evite fuziunile în favoarea rebase-urilor) apar probleme. Sistemele moderne de control al versiunilor (VCS) folosesc algoritmi interni de combinare a modificărilor, care pur și simplu fragmentează fișierele în linii separate, tratează fiecare linie ca pe un simbol, iar fișierele - ca pe secvențele lor, apoi aplică algoritmi pentru a le combina, care își au originea în bioinformatică.
Din păcate, acest format linie cu linie al codului sursă nu are nicio legătură cu conținutul său. Singurul său avantaj este că este simplu și universal. Incongruențele duc la conflicte, fiind o sursă constantă de dureri de cap pentru dezvoltatori. Rezolvarea conflictelor necesită ca dezvoltatorul să studieze cu atenție ambele versiuni ale codului, nu doar secțiunile marcate de algoritmul de comparare linie cu linie ca fiind „modificate” sau „conflictuale”, ci poate chiar întregul proiect.
Dezvoltatorul trebuie să înțeleagă modificările, să scrie manual codul fuzionat și să elimine orice incongruențe. Problemele se multiplică atunci când instrumentul linie cu linie identifică greșit modificările, ceea ce se întâmplă frecvent în cazul modificărilor mari, inclusiv a celor triviale, cum ar fi reformatarea codului. Dacă modificările ulterioare nu pot fi aplicate la codul fuzionat manual, situația devine un coșmar total. Cu toate că există cazuri îngrijorătoare, în cele mai multe cazuri, algoritmul linie cu linie funcționează, mai ales dacă dezvoltatorii fac eforturi active pentru a nu-i crea probleme. Una dintre modalitățile de a minimiza aceste probleme este cerința obligatorie de a prelucra sursele cu instrumente de canonizare, cum ar fi black.
Desigur, soluția corectă în cazurile îngrijorătoare (și nu doar pentru acestea, algoritmul linie cu linie este o euristică, care poate duce în mod trivial la cod nefuncțional, de exemplu, un dezvoltator a redenumit o variabilă, iar altul în același timp a scris un nou cod care utilizează acea variabilă; nu va exista un conflict de fuzionare/rebazare aici, dar rezultatul va deveni nefuncțional) este utilizarea unui model intern corect.
Cu toate că cercetările în acest domeniu au loc de aproximativ 30 de ani și au dus la crearea mai multor produse comerciale proprietare, aceste cercetări nu au fost transformate, până de curând, în produse practic aplicabile cu sursă deschisă. Majoritatea soluțiilor software cu sursă deschisă au început să se dezvolte la începutul anilor 2010 și au fost concentrate în principal pe limbajul Java.
Cea mai proeminentă implementare gratuită din acea perioadă, GumTree, a fost creată de un cercetător cu un fundal academic, fiind scrisă în Java, având o reprezentare internă abstractă, anterioară lui treesitter, și oferind backend-uri atât bazate pe treesitter, cât și pe alte instrumente pentru parsarea codului sursă în reprezentări abstracte. Această sistemă poate doar să genereze (sub formă de jurnal de evenimente text, existând și un API care poate fi apelat ușor din orice limbaj de programare cu bindings pentru Java) și să vizualizeze modificările. Totuși, pentru fuzionarea modificărilor, precum și pentru vizualizarea fișierelor diff generate de aceasta, nu este aplicabilă din cutie (deși este probabil ca încărcarea fișierelor diff să poată fi realizată prin API).
O implementare mai tânără și mai aplicabilă în practică a difftastic este scrisă în Rust, bazată pe treesitter, concentrându-se pe generarea de diffs evidențiate în consolă. Acest sistem este de asemenea orientat spre vizualizarea diffs și nu își propune deloc să fuzioneze modificările sau să aplice patch-uri.
Recent a apărut și se dezvoltă activ proiectul mergiraf. Acest instrument scris în Rust (ocupă 21 MiB!) este de asemenea bazat pe treesitter, care a devenit un standard pentru parserele gramaticilor context-free în instrumentele de dezvoltare, așa cum LLVM a devenit standard pentru optimizarea reprezentărilor de instrucțiuni la nivel scăzut. Spre deosebire de concurenți, mergiraf oferă funcții nu pentru generarea de diffs, ci pentru rezolvarea automată a conflictelor de fuziune. Sub capotă, mergiraf folosește o implementare a algoritmului utilizat în GumTree pentru generarea patch-urilor și o implementare a algoritmului utilizat în spork pentru aplicare, adaptate pentru structurile treesitter.
Serializarea patch-urilor în fișiere care pot fi aplicate mai târziu nu este implementată, din păcate (dar este foarte probabil ca aceasta să poată fi realizată prin parsarea jurnalelor de evenimente generate de GumTree). O altă modalitate promițătoare de a aplica diferențele ar putea fi aplicarea diferențelor nu prin patch-uri, ci prin funcționalitatea de refactoring a serverelor LSP, ceea ce ar putea ajuta la detectarea conflictelor la nivelul întregului proiect. Vizualizarea este susținută doar pentru conflicte.
Exemplu de funcționare: părintele comun „base.py” (indentare cu tab-uri, linie suplimentară la început) foo = 1 def main(): print(foo + 2 + 3) „a.py” (indentare încă cu tab-uri, 2 linii suplimentare la început în loc de una, bibliotecă icecream folosită pentru imprimarea de depanare, adăugat clasa „baz”: from icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): „””baz””” „b.py” (variabila „foo” redenumită în „bar”, procesată cu „black” după modificări, rezultatul având indentare cu spații și liniile suplimentare eliminate): bar = 1 def main(): print(bar + 2 + 3) Apelul .\/mergiraf merge .\/base.py .\/a.py .\/b.py -x a.py -y b.py -s base.py -o .\/res.py dă următorul rezultat from icecream import ic bar = 1 def main(): ic(bar + 2 + 3) class baz: def __init__(self): „””baz””” (biblioteca „icecream” folosită pentru imprimarea de depanare, variabila „foo” redenumită în „bar”, procesată cu „black” după modificări, rezultatul având indentare cu spații și liniile suplimentare eliminate, amestec de tab-uri și spații la indentare, dar aspect permis).
Aici se vede deficiența instrumentului. Stilul documentului este de obicei configurat în fișierele „.editorconfig”, iar modificările globale de stil, precum schimbarea tab-urilor cu spații și adoptarea stilului black, așa cum s-a făcut în „b.py”, sunt de obicei însoțite de actualizări în „.editorconfig”. Prin urmare, pentru a aplica modificări similare mai precis, instrumentul ar trebui să aibă o conceptie pentru stilul global „implicit” și să poată extrage setările din „.editorconfig”.
Sursa: opennet.ro
