È stato rilasciato il progetto mergiraf 0.4, che sviluppa un driver per Git con implementazione della funzionalità di merge a tre vie. Mergiraf supporta la risoluzione di vari tipi di conflitti durante il merge e può essere utilizzato per diversi linguaggi di programmazione e formati di file. È possibile utilizzare mergiraf come richiamo separato per gestire i conflitti che sorgono nell'uso di Git standard, nonché sostituirlo in Git come gestore di merge per ampliare le funzionalità di comandi come merge, revert, rebase e cherry-pick. Il codice è distribuito sotto licenza GPLv3. Nella nuova versione è stata aggiunta la supporto per i linguaggi Python, TOML, Scala e Typescript, e sono state effettuate ottimizzazioni delle prestazioni.
Di seguito è fornita una descrizione dettagliata dei problemi risolti con mergiraf:
Il software è un brillante esempio di un sistema estremamente complesso. I sistemi complessi hanno una caratteristica in comune: sono COMPLESSI, e non ci si può aspettare che il comportamento complesso desiderato emerga da solo, per caso. Al contrario, questi sistemi evolvono nel tempo, passo dopo passo, e ogni mutazione è attentamente verificata in ogni fase. Per raggiungere questo obiettivo è necessaria una struttura ben definita e gli strumenti appropriati. L'evoluzione di qualsiasi sistema complesso può essere visualizzata come un albero orientato, dove la radice rappresenta un insieme vuoto di funzioni e ogni nodo, ad eccezione della radice, è il risultato dell'applicazione di una mutazione al proprio genitore.
Nel contesto dei prodotti, ogni nodo è chiamato "versione", che rappresenta un insieme specifico di funzioni e anti-funzioni. Qualsiasi modifica a questo insieme è considerata una mutazione, creando un arco nel nostro grafo diretto e aciclico. Queste funzioni, per loro natura, sono astratte; non riflettono direttamente i modi in cui funzionano i sistemi fisici, ma piuttosto dimostrano come gli agenti razionali percepiscono l'utilità di tali sistemi. Per tradurre le idee in implementazioni esistenti nel mondo reale, è fondamentale rimboccarsi le maniche e tuffarsi in dettagli *sufficientemente* a basso livello, nella lingua che consente di esprimere e spiegare esattamente come funziona tutto. Nello sviluppo software, questi livelli bassi sono generalmente rappresentati dal codice sorgente.
Per portare gradualmente il codice sorgente in uno stato che manifesti il comportamento richiesto e documentare come ci siano arrivati, i programmatori presentano il loro lavoro in termini di snapshot e changeset. Uno snapshot rappresenta uno stato specifico del prodotto con tutti i dettagli di basso livello, mentre un changeset indica la transizione tra gli snapshot. Di solito, gli snapshot sono generati da singoli changeset ai loro genitori, quindi questi snapshot vengono quasi sempre etichettati con ciò che i changeset hanno creato, ed è per questo che questi termini vengono spesso usati in modo intercambiabile.
A volte esistono snapshot ottenuti da più transizioni — fusioni di commit. Sono difficili da gestire, quindi di solito vengono evitati. I moderni sistemi di controllo versione open source, come Git, offrono funzionalità piuttosto basilari per la gestione dei flussi di lavoro di sviluppo. Permettono ai programmatori di organizzare gli snapshot in grafi aciclici diretti, di annotarli con commenti e, se necessario, di modificarne l'ordine.
Questa funzionalità consente agli sviluppatori di scrivere una storia di progetto semanticamente significativa, fondamentale per il debug e per rispondere a domande come "Perché è stata introdotta questa parte a basso livello (ad es., una variabile)?", "Qual è approssimativamente la mia percentuale di contributo a questo progetto?", "Chi ha compromesso le implementazioni di backdoor e quando?", "Quale cambiamento a basso livello ha rotto questa funzione (anche se non avrebbe dovuto, abbiamo controllato tutto!?)"
I sistemi di controllo della versione si integrano con il concetto di ramo, un'idea a basso livello che rappresenta semplicemente un frammento continuo della cronologia del progetto, semanticamente significativo per lo sviluppatore. I rami vengono generalmente utilizzati per implementazioni specifiche di funzionalità; talvolta, vengono creati più rami per diversi candidati all'implementazione della stessa funzionalità. Utilizzando flussi di lavoro basati su rami (che sono in realtà la norma e il standard di sviluppo, usati ovunque e da chiunque), ogni sviluppatore può gestire efficacemente molti rami conflittuali del progetto, ognuno dei quali differisce per grado di prontezza o qualità. Questo consente agli sviluppatori di combinare i risultati del proprio lavoro e di quello degli altri senza dover riscrivere manualmente ogni volta tutto.
Di solito viene creata una branch principale che rappresenta il prodotto "ufficiale", da cui si diramano branch secondarie per ogni funzionalità, che vengono regolarmente (idealmente dopo ogni commit) sincronizzate con la branch principale. Questo consente agli sviluppatori di lavorare sempre con l'ultima versione del prodotto, mentre implementano le funzionalità su cui stanno attualmente lavorando, rilevando tempestivamente eventuali problemi causati dalle attività di altri sviluppatori.
Durante il tentativo di combinare le funzionalità di diversi snapshot (che consiste semplicemente nell'individuare un antenato comune e nell'applicare sequenzialmente i changeset generati sopra l'altro, quest'operazione è chiamata rebase; il merge, invece, è simile a un rebase, ma struttura il grafo dei commit in modo diverso, rendendo più difficile la manipolazione, per cui si cerca di evitare i merge a favore dei rebase) si presentano problemi. I moderni sistemi di controllo versione (VCS) utilizzano algoritmi interni per unire le modifiche che semplicemente suddividono i file in singole righe, trattando ogni riga come un simbolo e i file come sequenze di questi simboli, e quindi applicano algoritmi per unirli, provenienti dalla bioinformatica.
Purtroppo, questa rappresentazione riga per riga del codice sorgente non ha nulla a che fare con il suo contenuto. La sua unica qualità è la semplicità e l'universalità. La discrepanza comporta conflitti, rappresentando una costante fonte di problemi per gli sviluppatori. Risolvere i conflitti richiede che lo sviluppatore esamini attentamente entrambe le versioni del codice, e non solo le sezioni contrassegnate dall'algoritmo di confronto riga per riga come "modificate" o "in conflitto", ma forse l'intero progetto.
Lo sviluppatore deve comprendere le modifiche, scrivere manualmente il codice unificato e risolvere eventuali discrepanze. I problemi aumentano quando lo strumento riga per riga identifica erroneamente le modifiche, spesso un problema con i cambiamenti significativi, inclusi quelli triviali come il riformattare il codice. Se le modifiche successive non possono essere applicate al codice unificato manualmente, la situazione diventa un completo incubo. Nonostante i casi spaventosi, nella maggior parte dei casi, l'algoritmo riga per riga funziona, specialmente se gli sviluppatori si sforzano attivamente di non creargli problemi. Un modo per minimizzare tali problemi è richiedere l'elaborazione dei sorgenti tramite strumenti di canonizzazione, come black.
Certo, la soluzione corretta per casi spaventosi (e in generale, non solo per loro, l'algoritmo ligneare è un'euristica, può trivialmente portare a codice non funzionante, ad esempio un programmatore ha rinominato una variabile mentre un altro ha scritto un nuovo pezzo di codice che utilizza quella variabile; non ci sarà conflitto di merging/rebase, ma il risultato diventerà non funzionante) è l'uso di un modello interno corretto.
Nonostante siano in corso ricerche in questo campo da circa 30 anni, che hanno portato alla creazione di diversi prodotti commerciali proprietari, fino a tempi recenti queste ricerche non erano ancora state trasformate in prodotti praticamente applicabili con codice sorgente aperto. La maggior parte delle soluzioni open source ha iniziato a svilupparsi all'inizio degli anni 2010 ed è stata principalmente focalizzata sul linguaggio Java.
La realizzazione open source più notevole di quel periodo, GumTree, è stata creata da un ricercatore con una formazione accademica, è scritta in Java e ha una sua rappresentazione interna astratta, precedente a treesitter. Ha backend sia basati su treesitter che su altri strumenti per il parsing del codice sorgente in rappresentazioni astratte. Questo sistema è in grado di generare (sotto forma di log testuale degli eventi, è disponibile anche un'API che può essere facilmente richiamata da qualsiasi linguaggio di programmazione che abbia binding per Java) e visualizzare le modifiche. Tuttavia, per unire le modifiche e per visualizzare i file diff generati, non è applicabile di default (tuttavia, è probabile che il caricamento dei diff possa essere implementato tramite API).
Una realizzazione più giovane e praticabile di difftastic è scritta in Rust, è basata su treesitter e si concentra sulla generazione di diff evidenziati nella console. Questo sistema è anche orientato alla visualizzazione dei diff e non ha come obiettivo la fusione delle modifiche o l'applicazione di patch.
Di recente è emerso e si sta sviluppando attivamente il progetto mergiraf. Questo strumento, scritto in Rust (occupa 21 MiB!), si basa anch'esso su treesitter, che è diventato uno standard per i parser di grammatiche context-free negli strumenti di sviluppo, così come LLVM è diventato per l'ottimizzazione delle rappresentazioni a basso livello delle istruzioni. A differenza dei concorrenti, mergiraf offre funzionalità non per la generazione di diff, ma per la risoluzione automatica dei conflitti di merge. Sotto il cofano, mergiraf utilizza per la generazione delle patch un'implementazione dell'algoritmo usato in GumTree e per l'applicazione un'implementazione dell'algoritmo usato in spork, adattate per le strutture di treesitter.
La serializzazione delle patch in file che possono essere applicati successivamente non è attualmente implementata (ma è molto probabile che possa essere realizzata attraverso il parsing dei log eventi generati da GumTree). Un altro modo promettente per applicare le differenze potrebbe essere l'uso delle differenze non tramite patch, ma attraverso la funzionalità di refactoring dei server LSP, il che potrebbe aiutare nella rilevazione dei conflitti a livello di progetto. La visualizzazione è supportata solo per i conflitti.
Esempio di lavoro: antenato comune «base.py» (indentazione con tabulazioni, riga extra all'inizio) foo = 1 def main(): print(foo + 2 + 3) «a.py» (indentazione ancora con tabulazioni, 2 righe extra all'inizio invece di una, per la stampa di debug utilizzata la libreria icecream, aggiunta la classe «baz»: from icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): «»»baz»»» «b.py» (variabile «foo» rinominata in «bar», elaborato con «black» dopo le modifiche, con indentazione ora in spazi e righe extra rimosse): bar = 1 def main(): print(bar + 2 + 3) Chiamata ./mergiraf merge ./base.py ./a.py ./b.py -x a.py -y b.py -s base.py -o ./res.py restituisce il seguente risultato from icecream import ic bar = 1 def main(): ic(bar + 2 + 3) class baz: def __init__(self): «»»baz»»» (per la stampa di debug utilizzata la libreria «icecream», variabile «foo» rinominata in «bar», elaborato con «black» dopo le modifiche, con indentazione ora in spazi e righe extra rimosse, mix di tabulazioni e spazi per l'indentazione, ma nella forma consentita).
Qui si evidenzia un limite dello strumento. Lo stile del documento è solitamente configurato nei file «.editorconfig», e modifiche globali allo stile, come il passaggio da tabulazioni a spazi e l'adozione dello stile black, come avvenuto in «b.py», sono generalmente accompagnate da cambiamenti in «.editorconfig». Pertanto, per applicare in modo più corretto tali modifiche, lo strumento dovrebbe avere un concetto di stile globale «di default» e essere in grado di estrarre le impostazioni da «.editorconfig».
Fonte: opennet.ru
