mergiraf — herramienta orientada a AST para la fusión tripartita en Git

Se ha publicado la versión 0.4 del proyecto mergiraf, que desarrolla un controlador para Git con la implementación de la capacidad de fusión tripartita. Mergiraf soporta la resolución de varios tipos de conflictos durante la fusión y puede utilizarse para diversos lenguajes de programación y formatos de archivos. Se puede llamar a mergiraf de forma independiente para manejar los conflictos que surgen al trabajar con Git estándar, así como reemplazar el controlador de fusiones en Git para ampliar las capacidades de comandos como merge, revert, rebase y cherry-pick. El código se distribuye bajo la licencia GPLv3. La nueva versión añade soporte para los lenguajes Python, TOML, Scala y Typescript, además de realizar optimizaciones de rendimiento.

A continuación se presenta una descripción detallada de los problemas que se resuelven utilizando mergiraf:

El software es un ejemplo destacado de un sistema extremadamente complejo. Los sistemas complejos tienen una propiedad común: son COMPLICADOS, y no puedes esperar que el comportamiento complejo deseado surja por sí solo, por casualidad. En su lugar, estos sistemas evolucionan a lo largo del tiempo, paso a paso, y cada mutación es cuidadosamente verificada en cada etapa. Para lograr esto, se requiere una estructura bien definida y herramientas adecuadas. La evolución de cualquier sistema complejo se puede visualizar como un árbol dirigido, donde la raíz representa un conjunto vacío de funciones, y cada nodo—excepto la raíz—es el resultado de aplicar una mutación a su progenitor.

En el contexto de los productos, cada nodo se llama 'versión', que representa un conjunto específico de funciones y antifunções. Cualquier cambio en este conjunto se considera una mutación que forma un borde en nuestro gráfico acíclico dirigido. Estas funciones son por naturaleza abstractas; no reflejan directamente cómo funcionan los sistemas físicos, sino que demuestran cómo los agentes inteligentes perciben la utilidad de estos sistemas. Para traducir ideas a implementaciones en el mundo real, es necesario arremangarse y sumergirse en detalles *suficientemente* de bajo nivel, en un lenguaje que permita expresar y explicar cómo funciona realmente todo. En el desarrollo de software, estos niveles bajos suelen estar representados por el código fuente.

Para llevar gradualmente el código fuente a un estado que muestre el comportamiento deseado y documentar cómo se llegó a ese punto, los programadores presentan su trabajo en términos de snapshots y changesets. Un snapshot representa un estado específico del producto con todos los detalles de bajo nivel, mientras que un changeset denota la transición entre snapshots. Normalmente, los snapshots se generan a partir de cambios individuales respecto a sus padres, por lo que estos snapshots suelen estar marcados por las acciones que realizan los changesets que los crearon, por lo que estos términos a menudo se utilizan de manera intercambiable.

A veces existen snapshots que son el resultado de múltiples transiciones: fusiones de commits. Estos son difíciles de manejar, por lo que generalmente se evitan. Los sistemas modernos de control de versiones de código abierto, como Git, ofrecen capacidades bastante básicas para gestionar flujos de trabajo de desarrollo. Permiten a los desarrolladores organizar snapshots en forma de gráficos acíclicos dirigidos, anotarlos con comentarios y, si es necesario, cambiar su orden.

Esta funcionalidad permite a los desarrolladores escribir una historia del proyecto que tenga un significado semántico, lo cual es crucial para la depuración y para responder preguntas como "¿Por qué se introdujo este detalle de bajo nivel (por ejemplo, una variable)?", "¿Qué porcentaje aproximadamente es mi contribución a este proyecto?", "¿A quién comprometió la introducción de un backdoor y cuándo?", "¿Qué cambio de bajo nivel rompió esta función (aunque no debería, ¡lo comprobamos todo!?)"

Los sistemas de control de versiones complementan este concepto con la ramificación, un término de bajo nivel que simplemente significa un fragmento continuo de la historia de bajo nivel del proyecto, semánticamente significativo para el desarrollador. Las ramas se utilizan generalmente para la implementación específica de funciones, y a veces se crean múltiples ramas para diferentes candidatos a la implementación de una misma función. Al usar flujos de trabajo con ramificaciones (que son, de hecho, la norma y el estándar de desarrollo, utilizados en todas partes), cada desarrollador puede gestionar eficazmente muchas ramas conflictivas del proyecto, cada una de las cuales varía en su grado de preparación o calidad. Esto permite a los desarrolladores combinar los resultados de su trabajo y el de otros sin tener que volver a escribir todo manualmente cada vez.

Normalmente se crea una rama principal que representa el producto "oficial", desde la cual se ramifican las ramas secundarias para cada función, que se sincronizan regularmente (idealmente, después de cada confirmación) con la rama principal, lo que permite a los desarrolladores trabajar con la versión más actualizada del producto mientras implementan funciones que están desarrollando en ese momento, detectando problemas derivados del trabajo de otros desarrolladores lo más pronto posible.

Al intentar combinar funciones de varios snapshots (que simplemente significa encontrar un ancestro común y aplicar conjuntos de cambios que los generan de manera secuencial sobre otro, esta operación se llama rebase; la fusión, por otro lado, es casi como un rebase, simplemente estructura el gráfico de commits de manera diferente, lo que lo hace menos manejable, por lo que se evita la fusión en favor de los rebases), surgen problemas. Los sistemas modernos de control de versiones (VCS) utilizan algoritmos internos para fusionar cambios que simplemente dividen los archivos en líneas individuales, tratando cada línea como un símbolo y los archivos como sus secuencias, y luego aplican algoritmos para fusionarlos, que provienen de la bioinformática.

Desafortunadamente, esta representación línea por línea del código fuente no tiene nada que ver con su contenido. Su única ventaja es que es simple y universal. La discrepancia conduce a conflictos, siendo una fuente constante de dolores de cabeza para los desarrolladores. Resolver conflictos requiere que el desarrollador examine detenidamente ambas versiones del código, no solo las secciones que el algoritmo de comparación línea por línea señala como "modificadas" o "en conflicto", sino, posiblemente, todo el proyecto.

El desarrollador debe comprender los cambios, escribir manualmente el código combinado y eliminar cualquier discrepancia. Los problemas aumentan cuando la herramienta línea por línea identifica incorrectamente los cambios, lo que ocurre a menudo en grandes modificaciones, incluyendo triviales, como el reformateo del código. Si los cambios subsiguientes no pueden aplicarse al código combinado manualmente, la situación se convierte en una pesadilla completa. A pesar de estos casos aterradores, en la mayoría de las ocasiones, el algoritmo línea por línea funciona, especialmente si los desarrolladores se esfuerzan activamente por no causarle problemas. Una forma de minimizar estos problemas es hacer que el procesamiento de los fuentes sea obligatorio a través de herramientas de canonización, como black.

Por supuesto, la solución correcta a los casos aterradores (y en general, no solo para ellos, el algoritmo línea por línea es una heurística, puede llevar trivialmente a un código que no funciona, por ejemplo, un desarrollador renombró una variable, mientras que otro escribió un nuevo código utilizando esa variable. No habrá un conflicto de fusión/rebase aquí, pero el resultado se volverá inoperativo) es el uso de un modelo interno adecuado.

A pesar de que la investigación en este campo ha estado en curso durante aproximadamente 30 años, y ha dado lugar a la creación de varios productos comerciales propietarios, hasta hace poco, estos estudios no se habían traducido en productos prácticos aplicables de código abierto. La mayoría de las soluciones de software libre comenzaron a desarrollarse a principios de la década de 2010, y se centraron principalmente en el lenguaje Java.

La implementación más destacada de esa época, GumTree, fue creada por un investigador con formación académica, está escrita en Java, tiene su propia representación interna abstracta, anterior a treesitter, y cuenta con backend tanto basados en treesitter como en otras herramientas para analizar código fuente en representaciones abstractas. Este sistema solo puede generar (en forma de un registro de eventos en texto, también hay una API que se puede invocar fácilmente desde cualquier lenguaje de programación que tenga bindings para Java) y visualizar cambios. Sin embargo, para fusionar cambios, así como para ver los archivos diff generados, no es apto de forma predeterminada (aunque es probable que la carga de los diffs pueda implementarse a través de la API).

Una implementación más joven y práctica de difftastic está escrita en Rust, se basa en treesitter y se centra en la generación de diffs resaltados en la consola. Este sistema también está orientado a la visualización de diffs y en absoluto tiene como objetivo la fusión de cambios o la aplicación de parches.

Recientemente ha surgido y está en activo desarrollo el proyecto mergiraf. Esta herramienta, escrita en Rust (¡ocupa 21 MiB!) también se basa en treesitter, que se ha convertido en un estándar para analizadores de gramáticas libres de contexto en herramientas de desarrollo, así como LLVM lo es para la optimización de representaciones de instrucciones de bajo nivel. A diferencia de sus competidores, mergiraf ofrece funciones no para la generación de diffs, sino para la resolución automática de conflictos de fusión. Detrás de mergiraf, se utiliza una implementación del algoritmo de generación de parches utilizado en GumTree, y para su aplicación, se utiliza una implementación del algoritmo de spork, adaptadas para estructuras de treesitter.

Lamentablemente, la serialización de parches en archivos que pueden aplicarse posteriormente no está implementada (aunque es bastante probable que se pueda lograr mediante el análisis de registros de eventos generados por GumTree). Otro método prometedor para aplicar las diferencias puede ser hacerlo no mediante parches, sino a través de la funcionalidad de refactorización de los servidores LSP, lo que podría ayudar en la detección de conflictos a nivel de proyecto. La visualización se admite solo para conflictos.

Ejemplo de trabajo: ancestro común «base.py» (sangrías con tabulaciones, línea adicional al inicio) foo = 1 def main(): print(foo + 2 + 3) «a.py» (las sangrías siguen siendo con tabulaciones, 2 líneas adicionales al inicio en lugar de una, para la impresión de depuración se utiliza la biblioteca icecream, se ha añadido la clase «baz»: from icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): «»»baz»»» «b.py» (la variable «foo» ha sido renombrada a «bar», procesada con «black» después de los cambios, como resultado las sangrías son con espacios y se han eliminado líneas adicionales): bar = 1 def main(): print(bar + 2 + 3) La llamada .\/mergiraf merge .\/base.py .\/a.py .\/b.py -x a.py -y b.py -s base.py -o .\/res.py da como resultado lo siguiente from icecream import ic bar = 1 def main(): ic(bar + 2 + 3) class baz: def __init__(self): «»»baz»»» (para la impresión de depuración se utiliza la biblioteca «icecream», la variable «foo» ha sido renombrada a «bar», procesada con «black» después de los cambios, como resultado las sangrías son con espacios y se han eliminado líneas adicionales, mezcla de tabulaciones y espacios para la sangría, pero en una forma permitida).

Aquí también se puede ver la desventaja de la herramienta. El estilo del documento generalmente se configura en archivos «.editorconfig», y los cambios globales de estilo, como cambiar tabulaciones por espacios y adoptar el estilo de black, como se hizo en «b.py», generalmente vienen acompañados de cambios en «.editorconfig». Por lo tanto, para una aplicación más correcta de dichos cambios, la herramienta debe tener un concepto de estilo global «por defecto» y ser capaz de extraer configuraciones de «.editorconfig».

Fuente: opennet.ru

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster