mergiraf — uma ferramenta orientada a AST para mesclagem tripla no Git

Foi lançada a versão 0.4 do projeto mergiraf, que desenvolve um driver para Git com a implementação de mesclagens tridimensionais. O Mergiraf suporta a resolução de vários tipos de conflitos durante a mesclagem e pode ser utilizado para diversas linguagens de programação e formatos de arquivo. É possível tanto chamar o mergiraf separadamente para lidar com conflitos que surgem ao trabalhar com o Git padrão, quanto substituir o manipulador de mesclagens do Git para expandir as funcionalidades de comandos como merge, revert, rebase e cherry-pick. O código é distribuído sob a licença GPLv3. A nova versão inclui suporte para as linguagens Python, TOML, Scala e Typescript, além de otimizações de desempenho.

Abaixo está uma descrição detalhada dos problemas resolvidos com o mergiraf:

O software é um exemplo claro de um sistema extremamente complexo. Sistemas complexos têm uma característica comum — eles são COMPLEXOS — e você não pode esperar que o comportamento complexo desejado surja por si só, aleatoriamente. Em vez disso, esses sistemas evoluem ao longo do tempo, passo a passo, e cada mutação é meticulosamente testada em cada etapa. Para alcançar isso, uma estrutura bem definida e as ferramentas apropriadas são necessárias. A evolução de qualquer sistema complexo pode ser visualizada como uma árvore direcionada, onde a raiz representa um conjunto vazio de funções, e cada nó — exceto a raiz — é o resultado da aplicação de uma mutação a seu pai.

No contexto dos produtos, cada nó é chamado de "versão", representando um determinado conjunto de funções e anti-funções. Qualquer alteração nesse conjunto é considerada uma mutação, formando uma aresta em nosso gráfico acíclico direcionado. Essas funções são, por sua natureza, abstratas; elas não refletem diretamente os modos de funcionamento dos sistemas físicos, mas demonstram como agentes inteligentes percebem a utilidade desses sistemas. Para traduzir ideias em implementações existentes no mundo real, é necessário arregaçar as mangas e mergulhar em detalhes *suficientemente* de baixo nível, em cuja linguagem pode-se expressar e explicar como exatamente tudo funciona. No desenvolvimento de software, esses níveis baixos geralmente são representados pelo código-fonte.

Para trazer gradualmente o código-fonte para um estado que reflita o comportamento desejado e documentar como isso foi alcançado, os programadores apresentam seu trabalho em termos de snapshots e changesets. Um snapshot representa um determinado estado do produto com todos os detalhes em nível baixo, enquanto changeset se refere à transição entre snapshots. Normalmente, os snapshots são gerados por changesets únicos em relação a seus pais, portanto, esses snapshots geralmente são rotulados pelo que os changesets que os criaram fazem, sendo esses termos frequentemente usados de forma intercambiável.

Às vezes, existem snapshots resultantes de várias transições — fusões de commits. Eles são difíceis de trabalhar, por isso costumam ser evitados. Sistemas modernos de controle de versão de código aberto, como o Git, oferecem capacidades bastante básicas para gerenciar fluxos de trabalho de desenvolvimento. Eles permitem que os desenvolvedores organizem snapshots na forma de grafos acíclicos direcionados, anotem-nos com comentários e, quando necessário, alterem sua ordem.

Essa funcionalidade permite que os desenvolvedores escrevam uma história semanticamente significativa do projeto, o que é crucial para depuração e para responder a perguntas como “Por que este detalhe em nível baixo (ex., variável) foi introduzido?”, “Qual é aproximadamente a porcentagem da minha contribuição para este projeto?”, “Quem comprometeu as implementações de backdoor e quando?”, “Qual alteração em nível baixo quebrou essa função (embora não deveria, nós verificamos tudo!?)”

Os sistemas de controle de versão complementam isso com o conceito de ramificação — um conceito de baixo nível que simplesmente significa um fragmento contínuo da história de baixo nível do projeto, semanticamente significativo para o desenvolvedor. As ramificações são geralmente usadas para implementações específicas de funcionalidades, e às vezes várias ramificações são criadas para diferentes candidatos a implementar a mesma funcionalidade. Utilizando fluxos de trabalho ramificados (que na verdade são o padrão e a norma de desenvolvimento, usados em toda parte), cada desenvolvedor pode gerenciar efetivamente várias ramificações conflituosas do projeto, cada uma com diferentes graus de prontidão ou qualidade. Isso permite que os desenvolvedores combinem os resultados de seus trabalhos e dos outros sem ter que reescrever tudo manualmente a cada vez.

Normalmente, uma ramificação principal é criada, representando o produto "oficial", a partir da qual ramificações laterais são criadas para cada funcionalidade, que são regularmente (idealmente — após cada commit) sincronizadas com a ramificação principal, permitindo que os desenvolvedores trabalhem com a versão mais recente do produto enquanto implementam as funcionalidades que estão desenvolvendo, detectando problemas resultantes das ações de outros desenvolvedores o mais cedo possível.

Ao tentar combinar funcionalidades de diferentes snapshots (o que é simplesmente encontrar um ancestral comum e aplicar os changesets que os geram, de maneira sequencial sobre o outro, essa operação é chamada de rebase, enquanto a mesclagem é quase como rebase, apenas estrutura o gráfico de commits de maneira diferente, resultando em uma manipulação inconveniente, portanto, a mesclagem é evitada em favor do rebase), problemas surgem. Sistemas modernos de controle de versão (VCS) usam algoritmos internos de mesclagem de mudanças que simplesmente quebram arquivos em linhas separadas, tratando cada linha como um símbolo e os arquivos como suas sequências, e então aplicam algoritmos para unificá-los, que têm suas origens na bioinformática.

Infelizmente, essa representação linha a linha do código-fonte não tem relação com seu conteúdo. Sua única vantagem é que é simples e universal. A divergência leva a conflitos, sendo uma fonte constante de dor de cabeça para desenvolvedores. Resolucionar conflitos exige que o desenvolvedor examine cuidadosamente ambas as versões do código, não apenas as seções rotuladas pelo algoritmo de comparação linha a linha como "alteradas" ou "conflitantes", mas talvez todo o projeto.

O desenvolvedor deve compreender as alterações, escrever manualmente o código combinado e corrigir quaisquer divergências. Os problemas aumentam quando a ferramenta de linha a linha identifica as alterações incorretamente, o que acontece com frequência em grandes mudanças, incluindo triviais, como o reformatamento do código. Se alterações subsequentes não puderem ser aplicadas ao código combinado manualmente, a situação se torna um pesadelo total. Apesar dos casos aterradores, na maioria das vezes, o algoritmo linha a linha funciona, especialmente se os desenvolvedores se esforçam ativamente para não dificultar sua vida. Uma forma de minimizar esses problemas é a exigência obrigatória de processamento dos fontes com ferramentas de canonicização, como o black.

É claro que a solução correta para casos aterradores (e não apenas para eles, o algoritmo linha a linha é uma heurística e pode trivialmente resultar em código não funcional, por exemplo, um desenvolvedor renomeia uma variável, enquanto outro escreve um novo trecho de código usando essa variável, não haverá um conflito de mesclagem/rebase, mas o resultado se tornará não funcional) é o uso de um modelo interno adequado.

Apesar de as pesquisas nessa área terem sido realizadas há cerca de 30 anos e resultarem na criação de vários produtos comerciais proprietários, essas pesquisas só recentemente se tornaram produtos práticos aplicáveis com código aberto. A maioria das soluções de software livre começou a se desenvolver no início dos anos 2010 e foi focada principalmente na linguagem Java.

A realização mais notável e livre daquele período, GumTree, foi criada por um pesquisador com formação acadêmica, escrita em Java, possui sua própria representação interna abstrata, anterior ao treesitter, e tem backends tanto baseados em treesitter quanto em outras ferramentas para analisar código-fonte em representações abstratas. Este sistema só é capaz de gerar (na forma de um log de eventos em texto, também possui uma API que pode ser facilmente chamada de qualquer linguagem de programação com bindings para Java) e visualizar alterações. No entanto, para mesclar alterações ou visualizar os arquivos diff gerados, ele não é aplicável por padrão (embora, provavelmente, o upload dos diffs possa ser realizado através da API).

Uma implementação mais jovem e mais prática do difftastic é escrita em Rust, baseada em treesitter, e focada na geração de diffs destacados no console. Este sistema também tem como objetivo a visualização de diffs e não visa a mesclagem de alterações ou a aplicação de patches.

Recentemente surgiu e está em desenvolvimento ativo o projeto mergiraf. Esta ferramenta escrita em Rust (ocupa 21 MiB!) também baseia-se no treesitter, que se tornou um padrão para parsers de gramáticas livres de contexto em ferramentas de desenvolvimento, assim como o LLVM se tornou padrão para otimização de representações de instruções de baixo nível. Ao contrário dos concorrentes, mergiraf oferece funções não para gerar diffs, mas para resolver automaticamente conflitos de mesclagem. Internamente, o mergiraf utiliza uma implementação do algoritmo usado no GumTree para gerar patches e uma implementação do algoritmo utilizado no spork, adaptada às estruturas do treesitter.

A serialização de patches em arquivos que podem ser aplicados posteriormente, infelizmente, não foi implementada (mas é bem possível que possa ser realizada através da análise de logs de eventos gerados pelo GumTree). Outra maneira promissora de aplicar as diferenças pode ser através da funcionalidade de refatoração dos servidores LSP, o que pode ajudar na detecção de conflitos em nível de projeto. A visualização é suportada apenas para conflitos.

Exemplo de funcionamento: ancestral comum «base.py» (indentação por tabulação, linha extra no início) foo = 1 def main(): print(foo + 2 + 3) «a.py» (indentação ainda por tabulação, 2 linhas extras no início em vez de uma, para impressão de depuração foi utilizada a biblioteca icecream, foi adicionado a classe «baz»: from icecream import ic foo = 1 def main(): ic(foo + 2 + 3) class baz: def __init__(self): «»»baz»»» «b.py» (variável «foo» renomeada para «bar», processada com «black» após alterações, resultando em indentação — com espaços e linhas extras removidas): bar = 1 def main(): print(bar + 2 + 3) Chamada .\/mergiraf merge .\/base.py .\/a.py .\/b.py -x a.py -y b.py -s base.py -o .\/res.py dá o seguinte resultado from icecream import ic bar = 1 def main(): ic(bar + 2 + 3) class baz: def __init__(self): «»»baz»»» (para impressão de depuração foi utilizada a biblioteca «icecream», a variável «foo» foi renomeada para «bar», processada com «black» após alterações, resultando em indentação — com espaços e linhas extras removidas, mistura de tabulações e espaços para indentação, mas visual permitido).

Aqui também está visível a desvantagem da ferramenta. O estilo do documento geralmente é configurado em arquivos «.editorconfig», e alterações globais de estilo, como mudar tabulações para espaços e adotar o estilo do black, como foi feito em «b.py», normalmente acompanham mudanças no «.editorconfig». Portanto, para a aplicação mais precisa de tais mudanças, a ferramenta deve ter um conceito de estilo global «padrão» e ser capaz de puxar configurações do «.editorconfig».

Fonte: opennet.ru

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster