O projeto open source Git lançou a versão 2.49, que inclui novas funcionalidades e correções de erros feitas por mais de 89 colaboradores, sendo 24 deles novos. Esta atualização traz melhorias significativas desde o lançamento da versão 2.48. Vamos explorar os recursos e mudanças mais interessantes introduzidas nesta versão.
## Empacotamento Mais Rápido com Name Hash v2: Destaques do Git 2.49
Já falamos sobre 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 várias funções, incluindo armazenamento local e ao enviar dados para outro repositório Git.
Armazenar objetos em packfiles tem algumas vantagens sobre armazená-los individualmente. Uma delas é que as buscas de objetos são muito mais rápidas. Ao procurar um objeto solto, o Git precisa fazer várias chamadas ao sistema para encontrá-lo, abri-lo, lê-lo e fechá-lo.
Essas chamadas podem ser agilizadas com o block cache do sistema operacional, mas como os objetos são procurados por um SHA-1 (ou SHA-256) do seu conteúdo, esse acesso pseudo-aleatório não é muito eficiente para o cache.
Objetos soltos são comprimidos de forma isolada, impedindo o uso de deltas de outros objetos semelhantes. Imagine que você está fazendo pequenas alterações em um blob grande. Inicialmente, cada versão é armazenada individualmente e comprimida com zlib. Se grande parte do conteúdo permanece igual, o Git pode usar deltas para comprimir versões sucessivas, armazenando apenas as mudanças em relação a versões anteriores.
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 similares. O Git faz isso calculando um “name hash“, que é um hash numérico ordenável que considera os 16 caracteres finais não-vazios de um caminho. Essa função agrupa funções com extensões similares ou arquivos movidos de um diretório para outro.
A implementação atual do name-hash pode levar a uma compressão ruim quando há muitos arquivos com o mesmo nome base, mas conteúdos diferentes, como vários arquivos CHANGELOG.md. O Git 2.49 introduz uma nova variante da função hash que considera mais da estrutura do diretório ao calcular o hash. Cada camada do diretório 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 caminho completo, não apenas aos 16 caracteres finais. Essa mudança pode melhorar significativamente o desempenho do empacotamento e reduzir o tamanho do pack resultante. Por exemplo, a nova função hash diminuiu o tempo de reempacotamento de microsoft/fluentui de 96 para 34 segundos, e o tamanho do pack de 439 MiB para 160 MiB.
Embora essa funcionalidade ainda não seja compatível com os reachability bitmaps do Git, você pode experimentá-la com a nova flag –name-hash-version nos comandos git repack ou git pack-objects.
## Preenchimento de Blobs Históricos em Clones Parciais
Já se deparou com a seguinte saída ao trabalhar em um clone parcial?
“`
$ 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 trees, commits e objetos de tag anotados, mas apenas o conjunto de blobs que são imediatamente 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.
No exemplo acima, solicitamos 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 determinar se uma revisão modificou uma determinada linha. O Git acaba carregando cada versão histórica do objeto individualmente, inchando o armazenamento e prejudicando o desempenho.
O Git 2.49 introduz uma nova ferramenta, git backfill, que pode preencher os blobs históricos ausentes de um clone –filter=blob:none em pequenos lotes. Essas requisições usam a nova API path-walk (também introduzida no Git 2.49) para agrupar objetos que aparecem no mesmo caminho, resultando em uma compressão delta melhor nos packfiles enviados pelo servidor. Como essas requisições são enviadas em lotes, podemos preencher todos os blobs ausentes em poucos packs.
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 um filtro de objeto. Ao usar a opção –sparse do comando backfill, o Git baixa apenas os blobs que aparecem dentro do seu sparse checkout, evitando objetos que você não faria checkout de qualquer forma.
Experimente executar git backfill em qualquer clone –filter=blob:none de um repositório usando o Git 2.49 hoje mesmo!
## Outras Novidades do Git 2.49
O Git usa compressão zlib ao escrever objetos soltos ou dentro de packs. zlib é uma biblioteca de compressão popular, com foco em portabilidade. Ao longo dos anos, surgiram forks populares, como intel/zlib e cloudflare/zlib, que contêm otimizações não presentes na versão original.
O fork zlib-ng incorpora muitas dessas otimizações, remove código morto e soluções alternativas para compiladores antigos, focando ainda mais no desempenho. Ele suporta conjuntos de instruções SIMD, como SSE2 e AVX2, em seus algoritmos principais. Embora zlib-ng seja um substituto direto para zlib, o projeto Git precisou atualizar sua camada de compatibilidade.
No Git 2.49, você pode compilar o Git com zlib-ng passando ZLIB_NG ao construir com o GNU Make, ou a opção zlib_backend ao construir com Meson. Testes experimentais mostraram um aumento de velocidade de 25% ao imprimir o conteúdo de todos os objetos no repositório Git, de 52,1 segundos para 40,3 segundos.
Esta versão marca um marco importante no projeto Git, com a inclusão dos primeiros trechos de código 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 da biblioteca do Git, respectivamente.
O projeto Git tem evoluído seu código para ser mais orientado a bibliotecas, substituindo funções que encerram o programa por funções que retornam um inteiro e permitem que o chamador decida se deve sair ou não, limpando vazamentos de memória, etc. Esta versão aproveita esse trabalho para fornecer uma crate Rust proof-of-concept que envolve parte da API config.h do Git.
Este não é um wrapper completo para toda a interface da biblioteca do Git, e ainda há muito trabalho a ser feito antes que isso se torne realidade, mas é um passo importante.
Outras mudanças relacionadas ao esforço de “libificação” incluem a migração de variáveis globais como the_repository, com muitos comandos usando o repository fornecido. Além disso, houve um esforço para eliminar avisos -Wsign-compare, que ocorrem quando um valor com sinal é comparado a um sem sinal, levando a comportamentos inesperados.
Embora essas mudanças possam não ser notadas no uso diário, elas são passos importantes para tornar o projeto utilizável como uma biblioteca independente.
Usuários mais antigos podem se lembrar da opção –expire-to no comando git repack, que controla o comportamento de objetos inacessíveis removidos do repositório. Por padrão, esses objetos são simplesmente deletados, mas –expire-to permite movê-los para o lado para fins de backup.
Como git repack é um comando de baixo nível, a maioria dos usuários interage com o recurso de coleta de lixo do Git através de git gc. Em grande parte, git gc é um wrapper em torno da funcionalidade implementada em git repack, mas até esta versã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, permitindo experimentar esse comportamento via git gc –expire-to.
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
“`
Você usou o recurso de autocorreção do Git. No entanto, suas opções de configuração não correspondem à convenção de outras opções similares. 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 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 o comando sugerido após esperar esse número de decisegundos.
Definir help.autocorrect para “1” não habilita o comportamento de autocorreção, mas executa o comando corrigido antes que você perceba. 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.
Você deve estar ciente das várias opções do comando git clone, como –branch ou –tag. Essas opções permitem clonar o histórico de um repositório até um ramo ou tag específico, em vez de todo o repositório. Essas opções são frequentemente usadas em CI farms para clonar um ramo ou tag específico para testes.
Mas e se você quiser clonar uma revisão específica que não está em nenhum ramo ou tag? Antes do Git 2.49, a única opção era inicializar um repositório vazio e buscar uma revisão específica após adicionar o repositório de origem como um remoto. O Git 2.49 introduz um método 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 ramo ou tag apontando para ela.
O comando git remote usa a configuração do seu repositório para armazenar a lista de remotos que ele conhece. No entanto, existiam dois mecanismos anteriores para armazenar remotos antes dos arquivos de configuração. Inicialmente, os remotos eram configurados via arquivos separados em $GIT_DIR/branches. Poucas semanas depois, a convenção mudou para usar $GIT_DIR/remote.
Ambas as convenções foram descontinuadas e substituídas pelo mecanismo baseado em configuração que conhecemos hoje. No entanto, o Git manteve o suporte para eles ao longo dos anos como parte de sua compatibilidade com versões anteriores. No Git 3.0, esses recursos serão removidos completamente.
Se você quiser saber mais sobre as próximas mudanças que quebrarão a compatibilidade no Git, consulte Documentation/BreakingChanges.adoc. Para experimentar a versão mais recente, você pode compilar o Git com a opção WITH_BREAKING_CHANGES, que remove os recursos que serão removidos no Git 3.0.
Por fim, o projeto Git teve dois estagiários Outreachy que concluíram seus projetos recentemente. Usman Akinyemi trabalhou na adição de suporte para incluir informações uname no user agent do Git ao fazer requisições HTTP, e Seyi Kuforiji trabalhou na conversão de mais testes de unidade para usar o Clar testing framework.
A nova versão do Git traz diversas melhorias e novidades, desde o empacotamento mais rápido com name-hash v2 até o preenchimento de blobs históricos em clones parciais. Além disso, a inclusão de código em Rust e outras otimizações internas prometem um Git mais eficiente e preparado para o futuro.
Este conteúdo foi auxiliado por Inteligência Artificial, mas escrito e revisado por um humano.
Via The GitHub Blog