La version 0.4 du projet mergiraf a été publiée, développant un pilote pour Git avec la possibilité de fusion tripartite. Mergiraf prend en charge la résolution de divers types de conflits lors des fusions et peut être utilisé pour différents langages de programmation et formats de fichiers. Il est possible d'appeler mergiraf séparément pour traiter les conflits qui surviennent lors de l'utilisation de Git standard, ou de remplacer le gestionnaire de fusions de Git afin d'étendre les fonctionnalités des commandes telles que merge, revert, rebase et cherry-pick. Le code est distribué sous licence GPLv3. La nouvelle version ajoute la prise en charge des langues Python, TOML, Scala et TypeScript, ainsi qu'une optimisation des performances.
Voici une description détaillée des problèmes résolus grâce à mergiraf :
Le logiciel est un exemple frappant d'un système extrêmement complexe. Les systèmes complexes ont une propriété commune : ils sont COMPLEXES — et vous ne pouvez pas vous attendre à ce qu'un comportement complexe apparaisse de lui-même, par hasard. Au lieu de cela, ces systèmes évoluent au fil du temps, étape par étape, et chaque mutation est soigneusement vérifiée à chaque étape. Pour y parvenir, une structure bien définie et des outils appropriés sont nécessaires. L'évolution de tout système complexe peut être visualisée sous la forme d'un arbre orienté, où la racine représente un ensemble vide de fonctions et chaque nœud — à l'exception de la racine — est le résultat d'une mutation appliquée à son parent.
Dans le contexte des produits, chaque nœud est appelé « version », représentant un ensemble défini de fonctions et d'antifonctions. Toute modification de cet ensemble est considérée comme une mutation, formant une arête dans notre graphique orienté acyclique. Ces fonctions, par nature, sont abstraites ; elles ne reflètent pas directement les modes de fonctionnement des systèmes physiques, mais montrent plutôt comment des agents raisonnables perçoivent l'utilité de ces systèmes. Pour traduire des idées en réalisations concrètes dans le monde réel, il est nécessaire de retrousser ses manches et de plonger dans des détails *suffisamment* bas niveau, dans un langage qui permet d'exprimer et d'expliquer comment tout fonctionne concrètement. Dans le développement logiciel, ces niveaux inférieurs sont généralement représentés par le code source.
Pour amener progressivement le code source dans un état qui manifeste le comportement attendu et documenter comment ils y sont parvenus, les programmeurs présentent leur travail en termes de snapshots et de changesets. Un snapshot représente un état spécifique du produit avec tous les détails de bas niveau, tandis qu'un changeset désigne la transition entre les snapshots. En règle générale, les snapshots sont générés par des changesets individuels à leurs parents, de sorte que ces snapshots sont presque toujours marqués par les changesets qui les ont créés, c'est pourquoi ces termes sont souvent utilisés de manière interchangeable.
Il existe parfois des snapshots résultant de plusieurs transitions - des fusions de commits. Ils sont difficiles à manipuler, c'est pourquoi ils sont généralement évités. Les systèmes de contrôle de version open source modernes, comme Git, offrent des capacités de gestion des flux de travail de développement plutôt basiques. Ils permettent aux développeurs d'organiser les snapshots sous forme de graphes acycliques orientés, de les annoter avec des commentaires, et de modifier leur ordre si nécessaire.
Cette fonctionnalité permet aux développeurs d'écrire une histoire sémantiquement significative du projet, ce qui est crucial pour le débogage et pour répondre à des questions telles que : « Pourquoi ce détail de bas niveau (par exemple, variable) a-t-il été introduit ? », « Quel pourcentage représente approximativement ma contribution à ce projet ? », « Qui a été piraté par l'insertion d'une porte dérobée et quand ? », « Quel changement de bas niveau a cassé cette fonction (bien que cela ne devrait pas être le cas, nous avons tout vérifié !?) »
Les systèmes de contrôle de version complètent ce concept avec celui de branche — une notion de bas niveau qui désigne simplement un fragment continu d'historique d'un projet, sémantiquement significatif pour le développeur. Les branches sont généralement utilisées pour des implémentations spécifiques de fonctionnalités, et plusieurs branches peuvent être créées pour différents candidats à l'implémentation d'une même fonctionnalité. En utilisant des flux de travail avec des branches (qui sont en fait mainstream et standard dans le développement, utilisés partout), chaque développeur peut efficacement gérer de nombreuses branches conflictuelles du projet, chacune ayant un degré de préparation ou de qualité différent. Cela permet aux développeurs de combiner les résultats de leurs efforts et de ceux des autres sans retaper manuellement tout à chaque fois.
Une branche principale est généralement créée, représentant le produit "officiel", à partir de laquelle se ramifient des branches secondaires pour chaque fonctionnalité, qui sont régulièrement (idéalement après chaque commit) synchronisées avec la branche principale, permettant aux développeurs de travailler avec la version la plus récente du produit tout en intégrant les fonctionnalités qu'ils développent actuellement, détectant les problèmes provenant des actions d'autres développeurs le plus tôt possible.
Lors de la tentative de combinaison de fonctionnalités de différents instantanés (ce qui consiste simplement à trouver un ancêtre commun et à appliquer les ensembles de modifications qui les produisent, séquentiellement, sur un autre, cette opération est appelée rebase, tandis que la fusion est presque comme un rebase, mais structure différemment le graphe des commits, rendant leur manipulation difficile, c'est pourquoi on préfère éviter les fusions au profit des rebases) des problèmes se posent. Les systèmes de contrôle de version modernes (VCS) utilisent des algorithmes internes de fusion des modifications qui décomposent simplement les fichiers en lignes distinctes, considérant chaque ligne comme un symbole et les fichiers comme leurs séquences, puis appliquent des algorithmes pour les fusionner, originaires de la bioinformatique.
Malheureusement, cette représentation ligne par ligne du code source n'a rien à voir avec son contenu. Son seul mérite est sa simplicité et son universalité. L'inadéquation entraîne des conflits, étant une source constante de maux de tête pour les développeurs. Résoudre les conflits nécessite que le développeur étudie attentivement les deux versions du code, et ce, pas seulement les sections identifiées par l'algorithme de comparaison comme « modifiées » ou « en conflit », mais peut-être aussi l'ensemble du projet.
Le développeur doit comprendre les modifications, écrire manuellement le code fusionné et résoudre toutes les incohérences. Les problèmes se multiplient lorsque l'outil ligne par ligne identifie incorrectement les modifications, ce qui arrive souvent lors de modifications importantes, y compris triviales, telles que le reformatage du code. Si les modifications ultérieures ne peuvent pas être appliquées au code fusionné manuellement, la situation devient un véritable cauchemar. Malgré ces cas terrifiants, dans la plupart des cas, l'algorithme ligne par ligne fonctionne, surtout si les développeurs s'efforcent activement de ne pas lui causer de problèmes. Une des façons de minimiser ces problèmes est l'exigence stricte de traitement des sources avec des outils de canonisation, tels que black.
Il va sans dire que la bonne solution aux cas horribles (et en général, pas seulement pour eux, l'algorithme ligne par ligne est une heuristique, il peut triviellement conduire à un code non fonctionnel, par exemple, un développeur a renommé une variable, tandis qu'un autre a écrit une nouvelle partie de code utilisant cette variable, il n'y a pas de conflit de fusion/rebasage ici, mais le résultat deviendra non fonctionnel) est d'utiliser le bon modèle interne.
Bien que des recherches dans ce domaine soient menées depuis environ 30 ans, ayant conduit à la création de plusieurs produits commerciaux propriétaires, ces recherches n'avaient jusqu'à récemment pas été transformées en produits pratiques à code source ouvert. La majorité des solutions de logiciels libres ont commencé à se développer au début des années 2010 et étaient principalement concentrées sur le langage Java.
La réalisation libre la plus remarquable de cette époque, GumTree, a été créée par un chercheur ayant un bagage académique, écrite en Java, et dispose de sa propre représentation abstraite interne, antérieure à treesitter. Elle possède des backends basés à la fois sur treesitter et sur d'autres outils pour analyser le code source en représentations abstraites. Ce système peut seulement générer (sous forme de journal des événements, avec une API facilement appelable depuis n'importe quel langage de programmation disposant de liaisons avec Java) et visualiser les changements. Cependant, pour la fusion des changements, de même que pour la consultation des fichiers diff générés, elle n'est pas adaptée en standard (il est toutefois probable que le chargement des diffs puisse être implémenté via l'API).
Une réalisation plus récente et plus pratique, difftastic, est écrite en Rust, basée sur treesitter, et se concentre sur la génération de diffs surlignés dans la console. Ce système est également orienté vers la visualisation des diffs et n'a pas pour objectif la fusion des changements ou l'application de patches.
Dernièrement, un projet appelé mergiraf a vu le jour et est en pleine expansion. Cet outil, écrit en Rust (occupant 21 MiB!), est également basé sur treesitter, qui est devenu un standard pour les analyseurs de grammaires contextuelles libres dans les outils de développement, à l'instar de ce que LLVM représente pour l'optimisation des représentations d'instructions bas niveau. Contrairement à ses concurrents, mergiraf propose des fonctionnalités non pas pour générer des diffs, mais pour résoudre automatiquement les conflits de fusion. Sous le capot, mergiraf utilise pour la génération de patches une implémentation de l'algorithme utilisé dans GumTree, et pour l'application — une implémentation de l'algorithme utilisé dans spork, adaptées aux structures de treesitter.
La sérialisation des patches dans des fichiers pouvant être appliqués ultérieurement n'est malheureusement pas implémentée (mais il est tout à fait possible qu'elle puisse être réalisée par l'analyse des journaux d'événements générés par GumTree). Une autre méthode prometteuse pour appliquer les différences pourrait être l'application des différences non pas par le biais de patches, mais via les fonctionnalités de refactoring des serveurs LSP, ce qui pourrait aider à détecter des conflits au niveau de l'ensemble du projet. La visualisation est uniquement supportée pour les conflits.
Exemple de travail : ancêtre commun « base.py » (indentation avec des tabulations, ligne supplémentaire au début) foo = 1 def main(): print(foo + 2 + 3) « a.py » (indentation toujours avec des tabulations, 2 lignes supplémentaires au début au lieu d'une, pour l'impression de débogage, la bibliothèque icecream est utilisée, ajout d'une classe « baz » : from icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): « »»baz» »» « b.py » (la variable « foo » a été renommée en « bar », traitée avec « black » après les modifications, par conséquent, les indentations sont en espaces et les lignes supplémentaires ont été supprimées) : bar = 1 def main(): print(bar + 2 + 3) L'appel .\/mergiraf merge .\/base.py .\/a.py .\/b.py -x a.py -y b.py -s base.py -o .\/res.py donne le résultat suivant from icecream import ic bar = 1 def main(): ic(bar + 2 + 3) class baz: def __init__(self): « »»baz» »» (pour l'impression de débogage, la bibliothèque « icecream » est utilisée, la variable « foo » a été renommée en « bar », traitée avec « black » après les modifications, par conséquent, les indentations sont en espaces et les lignes supplémentaires ont été supprimées, mélange de tabulations et d'espaces pour l'indentation, mais apparence autorisée).
Ici, le défaut de l'outil est évident. Le style du document est généralement configuré dans des fichiers « .editorconfig », et les changements globaux de style, comme le passage des tabulations aux espaces et l'adoption du style de black, comme cela a été fait dans « b.py », s'accompagnent généralement de modifications dans « .editorconfig ». Par conséquent, pour une application plus correcte de tels changements, l'outil doit avoir une conception pour un style global « par défaut », et être capable de tirer les paramètres de « .editorconfig ».
Source : opennet.ru
