O projeto open source Git lançou a versão 2.49, trazendo diversas novidades e correções de bugs feitas por mais de 89 colaboradores, sendo 24 deles novos. Esta atualização é um marco importante, e vamos explorar os destaques do Git 2.49 que foram introduzidos desde a última versão, a 2.48. Prepare-se para conhecer as melhorias que tornam o Git ainda mais eficiente e poderoso.
## Empacotamento Acelerado com Name Hash v2
Já discutimos em diversas ocasiões sobre o modelo de armazenamento de objetos do Git, onde os objetos podem ser escritos individualmente (objetos “soltos”) ou agrupados em packfiles. O Git utiliza packfiles em várias funções, como no armazenamento local, ao repacotar ou executar o GC do seu repositório, e também ao enviar dados para ou receber de outro repositório Git, como em operações de fetch, clone ou push.
Armazenar objetos em packfiles oferece algumas vantagens em relação a mantê-los soltos. Uma delas é a rapidez nas buscas de objetos. Ao procurar um objeto solto, o Git precisa fazer várias chamadas de sistema para encontrá-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) do seu conteúdo, o acesso pseudo-randômico não é muito eficiente para o cache.
Além disso, objetos soltos são comprimidos isoladamente, impedindo que sejam armazenados como deltas de outros objetos similares já existentes no repositório. Imagine que você está fazendo pequenas alterações em um blob grande no seu repositório. Inicialmente, cada objeto é armazenado individualmente e comprimido com zlib. No entanto, se a maior parte do conteúdo do arquivo permanece inalterada entre as edições, o Git pode comprimir ainda mais esses objetos, armazenando as versões sucessivas como deltas das anteriores.
Mas como o Git identifica quais pares de objetos são bons candidatos para armazenar como pares delta-base? Uma forma útil é comparar objetos que aparecem em paths similares. O Git faz isso computando o que chama de “name hash“, um hash numérico ordenável que prioriza os 16 caracteres finais não-vazios em um filepath. Essa função, criada por Linus em 2006, é excelente para agrupar funções com extensões similares (.c
, .h
, etc.) ou arquivos que foram movidos de um diretório para outro (a/foo.txt
para b/foo.txt
).
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
para diferentes subsistemas armazenados no mesmo repositório. O Git 2.49 introduz uma nova variante da função hash que considera mais a estrutura do diretório ao computar o hash. Cada camada da hierarquia de diretórios recebe seu próprio hash, que é deslocado para baixo e, em seguida, aplicado um XOR no hash geral. Isso cria uma função hash mais sensível ao path completo, não apenas aos 16 caracteres finais.
Essa melhoria pode resultar em ganhos significativos tanto na performance do empacotamento quanto no tamanho final do pack. Por exemplo, a nova função hash conseguiu reduzir o tempo de repacotamento de microsoft/fluentui
de aproximadamente 96 segundos para 34 segundos, e diminuiu o tamanho do pack resultante de 439 MiB para 160 MiB.
Embora essa funcionalidade ainda não seja compatível com os reachability bitmaps do Git, você pode testá-la usando a nova flag --name-hash-version
nos comandos git repack
ou git pack-objects
, disponível na versão mais recente.
## Preenchimento de Blobs Históricos em Clones Parciais
Já se deparou com a seguinte mensagem 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
. Neste caso, seu repositório terá todas as suas 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 terá apenas os blobs necessários para popular um checkout completo na revisão mais recente. Tentar carregar qualquer blob histórico resultará no carregamento dos 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 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. No entanto, vemos o Git carregando cada versão histórica do objeto individualmente, o que causa inchaço no armazenamento e baixa performance.
O Git 2.49 introduz uma nova ferramenta, git backfill
, que pode carregar todos os blobs históricos ausentes de um clone com --filter=blob:none
em um pequeno número de lotes. Essas requisições utilizam a nova API path-walk (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 requisições são enviadas em lotes em vez de uma por vez, podemos preencher todos os blobs ausentes com apenas alguns packs, em vez de um pack por blob.
Após executar git backfill
no exemplo acima, a experiência se torna algo como:
$ 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 muitos benefícios, já que seria mais fácil clonar o repositório sem um filtro de objeto habilitado. Ao usar a opção --sparse
do comando backfill (o padrão quando o recurso sparse checkout está habilitado no seu repositório), o Git baixará apenas 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 de um repositório com --filter=blob:none
usando o Git 2.49!
## Outras Melhorias e Correções
Além das grandes novidades, o Git 2.49 também inclui diversas outras melhorias e correções que aprimoram a experiência do usuário e a performance do sistema. Vamos explorar algumas delas:
### Suporte para zlib-ng
Como mencionado anteriormente, o Git utiliza compressão zlib para escrever objetos soltos ou objetos individuais dentro de packs. Zlib é uma biblioteca de compressão incrivelmente popular, com ênfase na portabilidade. Ao longo dos anos, surgiram algumas forks populares (como intel/zlib e cloudflare/zlib) que contêm otimizações não presentes no upstream zlib.
O fork zlib-ng reúne muitas dessas otimizações, além de remover código morto e workarounds para compiladores antigos do upstream zlib, priorizando 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 acomodá-lo.
No Git 2.49, você pode 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).
### Introdução do Código Rust
Esta versão marca um marco importante no projeto Git com a inclusão dos primeiros trechos de código Rust. Especificamente, esta versão introduz duas crates Rust: libgit-sys e libgit, que são wrappers de baixo e alto nível, respectivamente, em torno de uma pequena parte do código da biblioteca do Git.
O projeto Git tem evoluído seu código para ser mais orientado a bibliotecas, substituindo funções que encerram o programa por outras que retornam um número inteiro e deixam para o chamador a decisão de sair ou não, além de corrigir memory leaks, entre outras coisas. 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 do Git, e ainda há muito trabalho a ser feito em todo o projeto antes que isso se torne realidade, mas é um passo muito interessante nessa direção.
### Esforços de “Libification” Contínuos
Ainda sobre o esforço de “libification“, outras mudanças relacionadas foram incluídas nesta versão. O movimento contínuo para se afastar de variáveis globais como the_repository
continua, e muitos outros comandos nesta versão usam o repository
fornecido em vez de usar o global.
Esta versão também viu muito esforço sendo dedicado a eliminar os avisos -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 com 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.
### Opção --expire-to
no git gc
Para quem acompanha as novidades do Git há mais tempo, deve se lembrar da versão 2.39, que introduziu a opção --expire-to
no comando git repack
. Essa opção controla o comportamento de objetos inacessíveis que foram removidos do repositório. Por padrão, esses objetos são simplesmente deletados, mas com --expire-to
, é possível movê-los para um local separado, caso queira mantê-los para fins de backup, por exemplo.
No entanto, 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 comando 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, onde agora é possível experimentar esse comportamento com git gc --expire-to
!
### Correção do help.autocorrect
Talvez você tenha lido que o recurso help.autocorrect
do Git é rápido demais até para pilotos de Fórmula 1. Se não, aqui estão os detalhes. Se 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ê já usou o recurso de autocorreção do Git. No entanto, suas opções de configuração não correspondem totalmente à 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 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 o comando sugerido após esperar essa quantidade de decissegundos.
Então, embora você possa ter pensado que definir help.autocorrect
como "1" habilitaria o comportamento de autocorreção, estaria enganado: ele executará o comando corrigido antes que você possa piscar os olhos. O Git 2.49 altera a convenção de help.autocorrect
para interpretar "1" como outros comandos booleanos, e números positivos maiores que 1 como faria antes. Embora não seja mais possível especificar que você deseja o comportamento de autocorreção em exatamente 1 decissegundo, provavelmente nunca foi essa a intenção.
### Nova Opção --revision
no git clone
O Git possui diversas opções de git clone
como --branch
ou --tag
. Quando fornecidas, essas opções permitem clonar o histórico de um repositório até uma branch ou tag específica, em vez de clonar tudo. Essas opções são frequentemente usadas em CI farms quando se deseja clonar uma branch ou tag específica para testes.
Mas e se você quiser clonar uma revisão específica que não está em nenhuma 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 remote.
O Git 2.49 introduz um método muito mais conveniente para complementar 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 uma branch ou tag apontando para ela.
### Remoção de Mecanismos de Remotes Obsoletos
O comando git remote
usa a configuração do seu repositório para armazenar a lista de remotes que ele conhece. No entanto, existiam dois mecanismos diferentes que precederam o armazenamento de remotes em arquivos de configuração. Nos primórdios, os remotes 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 já foram descontinuadas e substituídas pelo mecanismo baseado em configuração que conhecemos hoje. No entanto, o Git manteve o suporte a 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 por completo.
Se quiser saber mais sobre as próximas mudanças disruptivas do Git, você pode ler tudo sobre elas em Documentation/BreakingChanges.adoc
. Se realmente quiser viver no limite, você pode compilar o Git com a opção de compilação WITH_BREAKING_CHANGES
, que remove os recursos que serão removidos no Git 3.0.
### Estagiários Outreachy
Por último, mas não menos importante, 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 de 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.
## O Resto do Iceberg
Esses são apenas alguns exemplos das mudanças presentes na versão mais recente. Para mais informações, confira as notas de lançamento da versão 2.49 ou de qualquer versão anterior no repositório Git. Os destaques do Git 2.49 representam um avanço significativo para a ferramenta, trazendo otimizações e novos recursos que beneficiam tanto desenvolvedores quanto usuários.
Primeira: Este conteúdo foi auxiliado por Inteligência Artificial, mas escrito e revisado por um humano.
Via The GitHub Blog