Principais novidades do Git 2.49

O projeto de código aberto Git lançou a versão 2.49 com diversas novidades e correções de bugs, resultado do trabalho de 89 colaboradores, sendo 24 deles novos. Esta Atualização do Git 2.49 traz melhorias significativas desde a versão 2.48. Vamos explorar os recursos e mudanças mais interessantes desta versão.

## Empacotamento mais rápido com name-hash v2

Frequentemente, discutimos o modelo de armazenamento de objetos do Git, onde os objetos podem ser escritos individualmente (objetos “soltos”) ou agrupados em packfiles. O Git usa packfiles em diversas funções, incluindo armazenamento local e ao enviar dados para outro repositório Git (como fetching, clonagem ou pushing).

Armazenar objetos em packfiles oferece vantagens sobre o armazenamento individual. A busca de objetos é mais rápida no armazenamento em pack. Ao procurar um objeto solto, o Git faz várias chamadas ao sistema para encontrá-lo, abri-lo, lê-lo e fechá-lo. Essas chamadas podem ser aceleradas com o block cache do sistema operacional, mas o acesso pseudoaleatório não é eficiente para o cache, já que os objetos são procurados por SHA-1 (ou SHA-256) de seus conteúdos.

Objetos soltos são comprimidos isoladamente, sem a possibilidade de armazená-los como deltas de outros objetos similares. Por exemplo, ao fazer pequenas mudanças em um blob grande, cada objeto é armazenado individualmente e comprimido com zlib. Se a maior parte do conteúdo do arquivo permanece inalterada, o Git pode comprimir ainda mais esses objetos, armazenando versões sucessivas como deltas de versões anteriores. Assim, o Git armazena as mudanças feitas em um objeto (em relação a outro) em vez de múltiplas cópias de blobs quase idênticos.

Como o Git determina quais pares de objetos são bons candidatos para armazenar como pares delta-base? Uma forma é comparar objetos que aparecem em paths similares. O Git faz isso computando um “name hash“, um hash numérico que pesa mais nos 16 caracteres finais não-espaço em um filepath. Essa função agrupa funções com extensões similares (.c, .h, etc.) ou arquivos movidos de um diretório para outro (a/foo.txt para b/foo.txt).

No entanto, a implementação atual do name-hash pode levar a uma compressão ruim quando muitos arquivos têm o mesmo nome base, mas conteúdos diferentes, como múltiplos arquivos CHANGELOG.md para diferentes subsistemas. A Atualização do Git 2.49 introduz uma nova variante da função hash que considera mais da estrutura de diretórios ao computar o hash. Cada camada da hierarquia de diretórios tem seu próprio hash, que é deslocado para baixo e então XORed no hash geral. Isso cria uma função hash mais sensível ao path completo, não apenas aos 16 caracteres finais.

Isso pode levar a melhorias significativas no desempenho do empacotamento e no tamanho do pack resultante. Por exemplo, a nova função hash conseguiu melhorar o tempo de repack de microsoft/fluentui de aproximadamente 96 segundos para 34 segundos, e reduziu o tamanho do pack de 439 MiB para 160 MiB.

Embora esse recurso ainda não seja compatível com os reachability bitmaps do Git, você pode experimentá-lo usando a nova flag --name-hash-version do git repack ou git pack-objects na versão mais recente.

## Preenchimento de blobs históricos em partial clones

Já se deparou com a seguinte saída ao trabalhar em um partial clone?

$ git blame README.md
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (1/1), 1.64 KiB | 8.10 MiB/s, done.
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (1/1), 1.64 KiB | 7.30 MiB/s, done.
[...]

Para entender o que aconteceu, vamos analisar um cenário:

Imagine que você está trabalhando em um partial clone clonado com --filter=blob:none. Seu repositório terá todas as suas trees, commit e objetos de tag anotados, mas apenas o conjunto de blobs imediatamente acessíveis a partir do HEAD. Em outras palavras, seu clone local tem apenas os blobs necessários para popular um checkout completo na última revisão, e carregar qualquer blob histórico irá buscar os objetos ausentes de onde você clonou o repositório.

No exemplo acima, solicitamos um blame do arquivo no path README.md. Para construir esse blame, precisamos ver cada versão histórica do arquivo para calcular o diff em cada camada e determinar se uma revisão modificou uma linha. No entanto, o Git carrega cada versão histórica do objeto individualmente, resultando em armazenamento inchado e baixo desempenho.

O Git 2.49 introduz uma nova ferramenta, git backfill, que pode buscar todos os blobs históricos ausentes de um clone --filter=blob:none em pequenos lotes. Essas solicitações usam a nova path-walk API (também introduzida no Git 2.49) para agrupar objetos que aparecem no mesmo path, resultando em uma compressão delta muito melhor nos packfiles enviados pelo servidor. Como essas solicitações são enviadas em lotes, podemos preencher facilmente todos os blobs ausentes em poucos packs, em vez de um pack por blob.

Após executar git backfill no exemplo acima, a experiência se torna mais eficiente:

$ git clone --sparse --filter=blob:none git@github.com:git/git.git[...] # downloads historical commits/trees/tags
$ cd git
$ git sparse-checkout add builtin
[...] # downloads current contents of builtin/
$ git backfill --sparse
[...] # backfills historical contents of builtin/
$ git blame -- builtin/backfill.c
85127bcdeab (Derrick Stolee 2025-02-03 17:11:07 +0000   1) /* We need this macro to access core_apply_sparse_checkout */
85127bcdeab (Derrick Stolee 2025-02-03 17:11:07 +0000   2) #define USE_THE_REPOSITORY_VARIABLE
85127bcdeab (Derrick Stolee 2025-02-03 17:11:07 +0000   3)
[...]

Executar git backfill imediatamente após clonar um repositório com --filter=blob:none não traz muitos benefícios, já que seria mais conveniente clonar o repositório sem um filtro de objeto habilitado. Ao usar a opção --sparse do comando backfill (o padrão sempre que o recurso de sparse checkout está habilitado), o Git só baixa os blobs que aparecem dentro do seu sparse checkout, evitando objetos que você não faria checkout de qualquer maneira.

Para experimentar, execute git backfill em qualquer clone --filter=blob:none de um repositório usando o Git 2.49!

## Outras melhorias e atualizações

* O Git usa compressão zlib ao escrever objetos soltos, ou objetos individuais dentro de packs. zlib é uma biblioteca de compressão popular, com ênfase na portabilidade. Ao longo dos anos, surgiram forks populares (como intel/zlib e cloudflare/zlib) com otimizações não presentes no zlib original.

O fork zlib-ng mescla muitas dessas otimizações, remove código morto e adaptações para compiladores históricos do zlib original, enfatizando ainda mais o desempenho. Por exemplo, o zlib-ng tem suporte para conjuntos de instruções SIMD (como SSE2 e AVX2) embutidos em seus algoritmos principais. Embora o zlib-ng seja um substituto direto para o zlib, o projeto Git precisou atualizar sua camada de compatibilidade para acomodar o zlib-ng.

No Git 2.49, você pode construir o Git com zlib-ng passando ZLIB_NG ao construir com o GNU Make, ou a opção zlib_backend ao construir com Meson. Resultados experimentais mostram um aumento de velocidade de aproximadamente 25% ao imprimir o conteúdo de todos os objetos no repositório Git (de aproximadamente 52,1 segundos para 40,3 segundos).
* Esta versão marca um marco importante no projeto Git com as primeiras partes do código Rust sendo integradas. Especificamente, esta versão introduz duas crates Rust: libgit-sys e libgit, que são wrappers de baixo e alto nível em torno de uma pequena parte do código da biblioteca do Git, respectivamente.

O projeto Git tem evoluído seu código para ser mais orientado a biblioteca, substituindo funções que encerram o programa por funções que retornam um inteiro e deixam o chamador decidir se deve sair ou não, além de corrigir vazamentos de memória. Esta versão aproveita esse trabalho para fornecer uma crate Rust de prova de conceito que envolve parte da API config.h do Git.

Este não é um wrapper completo em torno de toda a interface da biblioteca Git, e ainda há muito trabalho a ser feito antes que isso se torne realidade, mas é um passo emocionante nessa direção.
* Falando no esforço de “libificação“, outras mudanças relacionadas foram incluídas nesta versão. O esforço contínuo para se afastar de variáveis globais como the_repository continua, e muitos mais comandos nesta versão usam o repository fornecido em vez do global.

Esta versão também viu muito esforço sendo dedicado a eliminar avisos de -Wsign-compare, que ocorrem quando um valor com sinal é comparado com um sem sinal. Isso pode levar a um comportamento surpreendente ao comparar, por exemplo, valores negativos com sinal contra valores sem sinal, onde uma comparação como -1 < 2 (que deveria retornar verdadeiro) acaba retornando falso.

Espera-se que essas mudanças não sejam notadas no uso diário do Git, mas são passos importantes para aproximar o projeto de ser usado como uma biblioteca independente.
* Os leitores antigos podem se lembrar da cobertura do Git 2.39, onde discutimos a nova opção --expire-to do git repack. A opção --expire-to no git repack controla o comportamento de objetos inacessíveis que foram removidos do repositório. Por padrão, os objetos removidos são simplesmente excluídos, mas o --expire-to permite movê-los para o lado caso você queira mantê-los para fins de backup.

O git repack é um comando de baixo nível, e a maioria dos usuários interage com o recurso de coleta de lixo do Git através do git gc. Em grande parte, o git gc é um wrapper em torno da funcionalidade implementada no git repack, mas até esta versão, o git gc não expunha sua própria opção de linha de comando para usar --expire-to. Isso mudou no Git 2.49, onde agora é possível experimentar esse comportamento através de git gc --expire-to!
* O recurso help.autocorrect do Git é rápido demais para pilotos de Fórmula 1. Se você já viu uma saída como:

$ git psuh
git: 'psuh' is not a git command. See 'git --help'.

The most similar command is
push

... então você usou o recurso de correção automática do Git. No entanto, suas opções de configuração não correspondem à convenção de outras opções semelhantes. Por exemplo, em outras partes do Git, especificar valores como "true", "yes", "on" ou "1" para configurações booleanas significava a mesma coisa. Mas o help.autocorrect desvia um pouco dessa tendência: ele tem significados especiais para "never", "immediate" e "prompt", mas interpreta um valor numérico para significar que o Git deve executar automaticamente qualquer comando que sugere após esperar muitos décimos de segundo.

Portanto, embora você possa ter pensado que definir help.autocorrect para "1" habilitaria o comportamento de correção automática, você estaria errado: ele executará o comando corrigido antes que você possa piscar os olhos. O Git 2.49 muda a convenção de help.autocorrect para interpretar "1" como outros comandos booleanos, e números positivos maiores que 1 como faria antes. Embora você não possa especificar que deseja o comportamento de correção automática em exatamente 1 décimo de segundo, você provavelmente nunca quis de qualquer maneira.
* Você pode estar ciente das várias opções do git clone, como --branch ou --tag. Quando fornecidas, essas opções permitem clonar o histórico de um repositório até um branch ou tag específico em vez de tudo. Essas opções são frequentemente usadas em CI farms quando desejam clonar um branch ou tag específico para teste.

Mas e se você quiser clonar uma revisão específica que não está em nenhum branch ou tag em seu repositório, o que você faz? Antes do Git 2.49, a única coisa que você podia fazer era inicializar um repositório vazio e buscar uma revisão específica após adicionar o repositório do qual você está buscando como um remoto.

O Git 2.49 introduz um método muito mais conveniente para completar as opções --branch e --tag, adicionando uma nova opção --revision que busca o histórico até a revisão especificada, independentemente de haver ou não um branch ou tag apontando para ele.
* Falando em remotos, você pode saber que o comando git remote usa a configuração do seu repositório para armazenar a lista de remotos que ele conhece. Você pode não saber que existiam realmente dois mecanismos diferentes que precederam o armazenamento de remotos em arquivos de configuração. Nos primeiros dias, os remotos eram configurados através de arquivos separados em $GIT_DIR/branches. Algumas semanas depois, a convenção mudou para usar $GIT_DIR/remote em vez do diretório /branches.

Ambas as convenções foram há muito tempo descontinuadas e substituídas pelo mecanismo baseado em configuração com o qual estamos familiarizados hoje. Mas o Git manteve o suporte para eles ao longo dos anos como parte de sua compatibilidade com versões anteriores. Quando o Git 3.0 for lançado, esses recursos serão removidos completamente.

Se você quiser aprender mais sobre as próximas mudanças disruptivas do Git, você pode ler tudo sobre elas em Documentation/BreakingChanges.adoc. Se você realmente quer viver na vanguarda, você pode construir o Git com a chave de tempo de compilação WITH_BREAKING_CHANGES, que compila recursos que serão removidos no Git 3.0.
* Por último, mas não menos importante, o projeto Git teve dois estagiários Outreachy maravilhosos que recentemente concluíram seus projetos! Usman Akinyemi trabalhou na adição de suporte para incluir informações uname no user agent do Git ao fazer solicitações HTTP, e Seyi Kuforiji trabalhou na conversão de mais testes de unidade para usar o Clar testing framework.

Você pode aprender mais sobre seus projetos aqui e aqui. Parabéns, Usman e Seyi!

## O resto do iceberg

Esta é apenas uma amostra das mudanças da versão mais recente. Para mais informações, consulte as notas de versão do 2.49, ou qualquer versão anterior no repositório Git.

A Atualização do Git 2.49 traz diversas melhorias, desde o empacotamento mais rápido com name-hash v2 até o preenchimento de blobs históricos em partial clones. Além disso, a inclusão de código Rust e a otimização da compressão zlib-ng marcam avanços significativos no projeto Git. Para desenvolvedores e usuários, essas atualizações significam um Git mais eficiente, rápido e preparado para o futuro.

Primeira: Este conteúdo foi auxiliado por Inteligência Artificiado, mas escrito e revisado por um humano.

Segunda: Via The GitHub Blog

Leave a Comment

Exit mobile version