O projeto open source Git lançou a versão 2.49, trazendo consigo diversas novidades e correções de bugs. Mais de 89 colaboradores, incluindo 24 novos, contribuíram para esta atualização. Para celebrar este lançamento, vamos explorar os recursos e mudanças mais interessantes desde a versão 2.48. Prepare-se para descobrir como o Git 2.49 pode otimizar seu fluxo de trabalho e aprimorar a eficiência no gerenciamento de seus projetos!
Empacotamento Acelerado com Name Hash v2
Já abordamos diversas vezes 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 armazenamento local e transferência de dados entre repositórios.
Armazenar objetos em packfiles oferece vantagens em relação ao armazenamento individual. A busca por objetos é mais rápida no armazenamento em packfiles. Ao procurar um objeto solto, o Git precisa realizar múltiplas 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 é muito eficiente para o cache.
Objetos soltos são comprimidos isoladamente, impedindo o armazenamento de objetos como deltas de outros objetos similares já existentes. Por exemplo, ao fazer pequenas alterações em um blob grande, cada objeto é armazenado individualmente e comprimido com zlib. Se a maior parte do conteúdo permanecer inalterada, o Git pode comprimir ainda mais esses objetos, armazenando as versões sucessivas como deltas das anteriores.
Mas como o Git identifica os pares de objetos ideais para armazenar como pares de delta-base? Uma forma é comparar objetos que aparecem em paths similares. Para isso, o Git calcula um “name hash“, um hash numérico que valoriza os 16 últimos caracteres não-espaço em um filepath. Essa função, criada por Linus em 2006, agrupa funções com extensões similares (.c
, .h
) ou arquivos 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 muitos arquivos têm o mesmo nome base, mas conteúdos diferentes, como múltiplos arquivos CHANGELOG.md
. O Git 2.49 introduz uma nova variante da função hash que considera mais a estrutura do diretório ao calcular o hash. Cada camada da hierarquia do diretório tem seu próprio hash, que é deslocado para baixo e combinado com XOR no hash geral. Isso torna a função hash mais sensível ao path completo.
Isso pode resultar em melhorias significativas no desempenho do empacotamento e no tamanho do pack resultante. Por exemplo, a nova função hash melhorou o tempo de reempacotamento do microsoft/fluentui
de ~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 testá-lo com a 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 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, considere o seguinte 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 os blobs 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. Carregar qualquer blob histórico resultará no carregamento dos objetos ausentes do repositório original.
No exemplo acima, solicitamos um blame
do arquivo 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, levando ao inchaço do armazenamento e baixo desempenho.
O Git 2.49 introduz uma nova ferramenta, git backfill
, que pode carregar os blobs históricos ausentes de um clone com --filter=blob:none
em pequenos 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 melhor nos packfiles enviados pelo servidor. Como essas requisições são enviadas em lotes, podemos preencher 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:
$ 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
imediatamente após clonar um repositório com --filter=blob:none
não traz muitos benefícios, pois seria mais fácil clonar o repositório sem um filtro de objeto. Ao usar a opção --sparse
do comando backfill (o padrão quando o recurso sparse checkout está habilitado), 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 com --filter=blob:none
usando o Git 2.49 hoje mesmo!
Outras Novidades e Melhorias no Git 2.49
O Git utiliza compressão zlib ao escrever objetos soltos ou 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 das otimizações feitas acima, remove código morto e soluções alternativas 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) integrados 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 compilar o Git com zlib-ng passando ZLIB_NG
ao compilar com o GNU Make, ou a opção zlib_backend
ao compilar com o Meson. Resultados experimentais iniciais mostram uma aceleração 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 dois 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 bibliotecas, substituindo funções que encerram o programa por funções que retornam um inteiro e permitem que o chamador decida sair ou não, limpando vazamentos de memória, etc. Esta versão aproveita esse trabalho para fornecer um crate Rust de prova de conceito 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 nessa direção.
Em relação ao esforço de “libificação“, outras mudanças foram incluídas 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 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.
Essas mudanças podem não ser perceptíveis no uso diário do Git, mas são passos importantes para tornar o projeto mais utilizável como uma biblioteca independente.
Usuários antigos podem 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 um local separado, caso você queira mantê-los para fins de backup, etc.
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 você pode 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ê já 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: 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. 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 antes. Embora você não possa mais especificar que deseja o comportamento de autocorreção em exatamente 1 decissegundo, provavelmente nunca quis fazer isso 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é uma ramificação ou tag específica em vez de tudo. Essas opções são frequentemente usadas em CI farms quando desejam clonar uma ramificação ou tag específica para teste.
Mas e se você quiser clonar uma revisão específica que não esteja em nenhuma ramificação ou tag em 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 uma ramificação ou tag apontando para ela.
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. O que você pode não saber é que existiam 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. No entanto, o Git manteve o 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 quer 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 estagiários Outreachy maravilhosos que recentemente concluíram seus projetos! Usman Akinyemi trabalhou na adição de suporte para incluir informações uname no agente de usuário 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.
O Resto do Iceberg
Os Destaques do Git 2.49 representam apenas uma amostra das mudanças desta versão. Para mais detalhes, confira as notas de lançamento para a versão 2.49 ou qualquer versão anterior no repositório Git. Esta atualização promete otimizar o fluxo de trabalho e aprimorar a eficiência no gerenciamento de seus projetos.
Primeira: Este conteúdo foi auxiliado por Inteligência Artificiado, mas escrito e revisado por um humano.
Via The GitHub Blog