Destaques da versão 2.49 do Git

O projeto de código aberto Git acaba de lançar o Git 2.49, trazendo consigo diversas novidades e correções de bugs provenientes de mais de 89 colaboradores, dos quais 24 são novos na comunidade. Esta atualização foca em otimizações no armazenamento de objetos, melhorias no clone parcial e outras atualizações.

## Empacotamento Mais Rápido com Name Hash v2

Já abordamos em outras ocasiões o modelo de armazenamento de objetos do Git, onde os objetos podem ser escritos individualmente (conhecidos como objetos “soltos”) ou agrupados em packfiles. O Git utiliza packfiles em diversas funções, como no armazenamento local (ao repactuar ou executar o GC do seu repositório), e ao enviar ou receber dados de outro repositório Git (como em fetches, clones ou pushes).

Armazenar objetos em packfiles oferece algumas vantagens em relação a mantê-los soltos. A mais evidente é a rapidez nas buscas de objetos. Para encontrar um objeto solto, o Git precisa fazer várias chamadas ao sistema para localizá-lo, abri-lo, lê-lo e fechá-lo. Essas chamadas podem ser agilizadas com o cache de bloco do sistema operacional, mas como os objetos são encontrados por um SHA-1 (ou SHA-256) de seu conteúdo, o acesso pseudoaleatório não é muito eficiente para o cache.

Objetos soltos são comprimidos de forma isolada, impedindo o armazenamento de objetos como deltas de outros objetos semelhantes já existentes no repositório. Imagine fazer pequenas alterações em um arquivo grande. Inicialmente, cada versão é armazenada individualmente e compactada com zlib. Se grande parte do conteúdo permanece igual, o Git pode comprimir as versões armazenando apenas as diferenças em relação às anteriores. Assim, em vez de múltiplas cópias quase idênticas, o Git guarda apenas as mudanças.

Mas como o Git decide quais objetos são bons candidatos para armazenar como pares delta-base? Uma forma é comparar objetos que aparecem em caminhos parecidos. O Git faz isso hoje calculando o que chama de “name hash“, que é um hash numérico ordenável que considera os 16 últimos caracteres não-espaço em branco de um caminho de arquivo. Essa função, criada por Linus em 2006, 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).

A implementação atual do name-hash pode gerar compressão ruim quando há muitos arquivos com o mesmo nome base, mas conteúdos diferentes, como vários arquivos CHANGELOG.md para subsistemas distintos. O Git 2.49 introduz uma nova versão da função hash que considera mais da estrutura de diretórios ao calcular o hash. Cada nível do diretório tem seu próprio hash, que é deslocado para baixo e então XORed no hash geral. Isso cria uma função mais sensível ao caminho completo, não apenas aos 16 caracteres finais.

Essa novidade traz melhorias significativas tanto na performance do empacotamento quanto no tamanho final do pacote. Por exemplo, ao usar a nova função hash, o tempo para repactuar o microsoft/fluentui caiu de cerca de 96 segundos para 34 segundos, e o tamanho do pacote resultante diminuiu de 439 MiB para 160 MiB.

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

Preenchimento de Blobs Históricos em Clones Parciais

Quem já trabalhou com um clone parcial provavelmente se deparou com esta saída pouco amigável:


$ 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 clone parcial criado com --filter=blob:none. Seu repositório terá todas as árvores, commits e objetos de tag anotados, mas apenas os blobs diretamente acessíveis a partir do HEAD. Ou seja, seu clone local tem apenas os blobs necessários para um checkout completo na revisão mais recente. Carregar qualquer blob histórico fará com que objetos faltantes sejam buscados de onde você clonou o repositório.

No exemplo acima, pedimos um blame do arquivo README.md. Para construir esse blame, precisamos ver todas as versões históricas do arquivo para calcular o diff em cada camada e descobrir se uma revisão modificou uma linha específica. Vemos, então, o Git carregando cada versão histórica do objeto individualmente, resultando em armazenamento inflado e baixo desempenho.

O Git 2.49 introduz uma nova ferramenta, git backfill, que pode preencher os blobs históricos faltantes de um clone com --filter=blob:none em pequenos lotes. Esses pedidos usam a nova API path-walk (também introduzida no Git 2.49) para agrupar objetos que aparecem no mesmo caminho, resultando em melhor compressão delta nos packfiles enviados pelo servidor. Como os pedidos são enviados em lotes em vez de um por um, podemos preencher todos os blobs faltantes com poucos pacotes.

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

$ git clone --sparse --filter=blob:none [email protected]: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 logo após clonar um repositório com --filter=blob:none não traz muito benefício, já que seria mais simples clonar o repositório sem filtro de objeto. Ao usar a opção --sparse do comando backfill (o padrão quando o recurso sparse checkout está habilitado), o Git baixa apenas os blobs que aparecem dentro do seu sparse checkout, evitando objetos que você não faria checkout de qualquer forma.

Para experimentar, execute git backfill em qualquer clone com --filter=blob:none usando o Git 2.49!

Outras Melhorias e Atualizações no Git 2.49

Além das grandes novidades, o Git 2.49 inclui diversas outras melhorias e atualizações que visam otimizar o desempenho e a experiência do usuário. Destaques do Git 2.49 incluem otimizações na compressão de objetos, a introdução de código em Rust e melhorias na função de auto correção.

O Git utiliza compressão com tecnologia zlib ao escrever objetos soltos ou individuais dentro de pacotes. Zlib é uma biblioteca de compressão popular, focada em portabilidade. Ao longo dos anos, surgiram forks populares como intel/zlib e cloudflare/zlib, que contêm otimizações não presentes no zlib original.

O fork zlib-ng reúne muitas dessas otimizações, remove códigos desnecessários e soluções para compiladores antigos do zlib original, focando ainda mais no desempenho. Por exemplo, o zlib-ng tem suporte para conjuntos de instruções SIMD (como SSE2 e AVX2) embutidos em seus algoritmos. Embora o zlib-ng substitua o zlib diretamente, o projeto Git precisou atualizar sua camada de compatibilidade para acomodá-lo.

No Git 2.49, é possível compilar o Git com zlib-ng passando ZLIB_NG ao compilar com o GNU Make, ou a opção zlib_backend ao compilar com Meson. Resultados experimentais iniciais mostram um ganho de velocidade de cerca de 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 a inclusão dos primeiros códigos em Rust. 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 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 outras 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 que envolve parte da API config.h do Git.

Essa não é uma wrapper completa da interface da biblioteca Git, e ainda há muito trabalho a ser feito antes que isso se torne realidade, mas é um passo importante.

Falando no esforço de “libificação“, houve outras mudanças relacionadas nesta versão. O esforço contínuo para se afastar de variáveis globais como the_repository continua, e mais comandos nesta versão usam o repository fornecido em vez do global.

Esta versão também teve um grande esforço para eliminar os avisos -Wsign-compare, que ocorrem quando um valor assinado é comparado com um não assinado. Isso pode levar a comportamentos inesperados ao comparar, por exemplo, valores negativos assinados com não assinados, 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 mais antigos devem se lembrar da cobertura do Git 2.39, onde foi discutida 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 --expire-to permite movê-los para o lado caso você queira mantê-los para fins de backup.

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 do git gc --expire-to!

Talvez você tenha lido que 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 autocorreção do Git. Mas 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 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 sugerido após esperar esse número de decissegundos.

Então, embora você possa ter pensado que definir help.autocorrect para "1" habilitaria o comportamento de autocorreção, 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 antes. Embora você não possa especificar que deseja o comportamento de autocorreção em exatamente 1 decissegundo, provavelmente nunca quis fazer isso de qualquer forma.

Você deve conhecer as 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 querem 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 no seu repositório? 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 ela.

Falando em remotos, você deve 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 havia 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 que conhecemos hoje. Mas o Git manteve suporte para elas 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 saber mais sobre as próximas mudanças disruptivas do Git, você pode ler tudo sobre elas em Documentation/BreakingChanges.adoc. Se você realmente quiser viver na vanguarda, você pode compilar 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 maravilhosos estagiários do Outreachy que concluíram recentemente seus projetos! Usman Akinyemi trabalhou na adição de suporte para incluir informações do uname no user agent do Git ao fazer requisições HTTP, e Seyi Kuforiji trabalhou na conversão de mais testes unitários para usar o Clar testing framework.

Saiba mais sobre seus projetos aqui e aqui. Parabéns, Usman e Seyi!

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

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

Via The GitHub Blog

Leave a Comment