O projeto open source Git acaba de lançar o Git 2.49, trazendo consigo diversas novidades e correções de bugs que foram implementadas por mais de 89 colaboradores, sendo 24 deles estreantes. Para celebrar este lançamento, vamos dar uma olhada nos recursos e mudanças mais interessantes que foram introduzidas desde a última versão.
Entre os Destaques do Git 2.49, notamos melhorias na velocidade de empacotamento com o name-hash v2, o preenchimento de blobs históricos em clones parciais e a compatibilidade com a biblioteca zlib-ng. Além disso, esta versão marca um marco importante com a introdução de código Rust no projeto.
Empacotamento Aprimorado com Name-Hash v2
Já falamos sobre o modelo de armazenamento de objetos do Git, onde os objetos podem ser escritos individualmente (os chamados objetos “soltos”) ou agrupados em packfiles. O Git utiliza os packfiles em diversas funções, como no armazenamento local (ao repacotar ou executar o GC no seu repositório), e também ao enviar dados para ou receber de outro repositório Git (como ao executar fetch, clone ou push).
Armazenar objetos juntos em packfiles oferece algumas vantagens em relação a mantê-los soltos. Uma delas é que as buscas de objetos podem ser realizadas de forma muito mais rápida no armazenamento em pack. Ao procurar um objeto solto, o Git precisa fazer múltiplas chamadas ao sistema para encontrar o objeto, abri-lo, lê-lo e fechá-lo.
Essas chamadas ao sistema podem ser aceleradas usando o cache de bloco do sistema operacional, mas como os objetos são procurados por um SHA-1 (ou SHA-256) do seu conteúdo, esse acesso pseudo-randômico não é muito eficiente para o cache. Objetos soltos são armazenados individualmente, o que significa que só podemos comprimir seus conteúdos isoladamente.
Não podemos armazenar objetos como deltas de outros objetos similares que já existam no seu repositório. Imagine que você está fazendo uma série de pequenas alterações em um blob grande no seu repositório. Quando esses objetos são escritos inicialmente, cada um é armazenado individualmente e comprimido com zlib. No entanto, se a maior parte do conteúdo do arquivo permanecer inalterada entre os pares de edições, o Git pode comprimir ainda mais esses objetos.
Ele faz isso armazenando versões sucessivas como deltas de versões anteriores. Em termos gerais, isso permite que o Git armazene as mudanças feitas a um objeto (em relação a algum outro objeto) em vez de múltiplas cópias de blobs quase idênticos.
Como o Git Encontra Pares de Objetos para Delta-Base?
O Git compara objetos que aparecem em paths similares. Para fazer isso, o Git computa o que ele chama de “name hash“, que é um hash numérico ordenável que pesa mais fortemente nos 16 caracteres finais não-espaço em branco em um filepath. Essa função foi criada por Linus em 2006, e é excelente para agrupar funções com extensões similares (todas terminando em .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 que têm o mesmo basename, mas conteúdos muito diferentes, como ter muitos arquivos CHANGELOG.md
para diferentes subsistemas armazenados juntos no seu repositório. O Git 2.49 introduz uma nova variante da função hash que considera mais da estrutura de diretórios ao computar seu hash. Cada camada da hierarquia de diretórios recebe seu próprio hash, que é deslocado para baixo e então XORed no hash geral.
Isso cria uma função hash que é mais sensível ao caminho inteiro, não apenas aos 16 caracteres finais. Isso pode levar a melhorias significativas tanto no desempenho do empacotamento quanto no tamanho total do pack resultante. Por exemplo, usar a nova função hash conseguiu melhorar o tempo necessário para repacotar o microsoft/fluentui
de ~96 segundos para ~34 segundos, e reduzir o tamanho do pack resultante de 439 MiB para apenas 160 MiB.
Embora esse recurso ainda não seja compatível com o recurso de bitmaps de alcançabilidade do Git, você pode experimentá-lo usando a nova flag --name-hash-version
do git repack
ou do git pack-objects
na versão mais recente.
Preenchimento de Blobs Históricos em Clones Parciais
Já aconteceu de você estar trabalhando em um clone parcial e se deparar 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 aqui, vamos analisar um cenário de exemplo: imagine que você está trabalhando em um clone parcial que você clonou com --filter=blob:none
. Nesse caso, seu repositório terá todas as suas árvores, commits e objetos de tag anotados, mas apenas o conjunto de blobs que são imediatamente alcançáveis a partir do HEAD
.
Em outras palavras, seu clone local tem apenas o conjunto de blobs que ele precisa para preencher um checkout completo na revisão mais recente, e carregar qualquer blob histórico irá trazer os objetos faltantes de onde você clonou seu repositório. No exemplo acima, solicitamos um blame do arquivo no caminho README.md
. Para construir esse blame, precisamos ver cada versão histórica do arquivo para computar o diff em cada camada e descobrir se uma revisão modificou ou não uma determinada linha.
Vemos o Git carregando cada versão histórica do objeto uma por uma, levando a um armazenamento inchado e um desempenho ruim. O Git 2.49 introduz uma nova ferramenta, git backfill
, que pode trazer qualquer blob histórico faltante de um clone --filter=blob:none
em um pequeno número de 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 uma compressão delta muito melhor nos packfiles enviados de volta pelo servidor. Como esses pedidos são enviados em lotes em vez de um por um, podemos facilmente preencher todos os blobs faltantes em apenas alguns packs em vez de um pack por blob.
Após executar git backfill
no exemplo acima, nossa experiência se parece mais com:
$ 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 muito benefício, já que teria sido mais conveniente simplesmente 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 no seu repositório), o Git irá 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 --filter=blob:none
de um repositório usando o Git 2.49 hoje!
Outras Novidades do Git 2.49
-
O Git usa compressão impulsionada por zlib ao escrever objetos soltos, ou objetos individuais dentro de packs e assim por diante. Zlib é uma biblioteca de compressão incrivelmente popular, e enfatiza a portabilidade. Ao longo dos anos, surgiram alguns forks populares (como intel/zlib e cloudflare/zlib) que contêm otimizações não presentes no zlib original.
O fork zlib-ng combina muitas das otimizações feitas acima, além de remover código morto e soluções alternativas para compiladores históricos do zlib original, colocando ainda mais ênfase no 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çãozlib_backend
ao construir com o Meson. Resultados experimentais iniciais mostram 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 as primeiras partes de código Rust sendo incluídas. 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 porção do código da biblioteca do Git, respectivamente.
O projeto Git tem evoluído seu código para ser mais orientado a biblioteca, fazendo coisas como substituir funções que saem do programa por outras que retornam um inteiro e deixam o chamador decidir sair ou não, limpar vazamentos de memória, etc. 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 uma realidade, mas este é um passo muito animador ao longo do caminho.
-
Falando do esforço de “libificação“, houve algumas outras mudanças relacionadas que entraram 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 orepository
fornecido em vez de usar o global.Esta versão também viu muito esforço sendo colocado em silenciar 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, digamos, valores negativos com sinal com valores sem sinal, onde uma comparação como-1 < 2
(que deveria retornar true) acaba retornando false.Esperamos que você não note essas mudanças no seu uso diário do Git, mas elas são passos importantes ao longo do caminho para trazer o projeto mais perto de poder ser usado como uma biblioteca independente.
-
Leitores de longa data devem se lembrar da nossa cobertura do Git 2.39 onde discutimos a nova opção
--expire-to
dogit repack
. Caso você seja novo por aqui ou precise de uma atualização, a opção--expire-to
nogit repack
controla o comportamento de objetos inacessíveis que foram removidos do repositório. Por padrão, os objetos removidos são simplesmente deletados, mas--expire-to
permite que você os mova para o lado caso queira mantê-los para fins de backup, etc.git repack
é um comando de nível relativamente baixo, e a maioria dos usuários provavelmente interagirá com o recurso de coleta de lixo do Git através dogit gc
. Em grande parte, ogit gc
é um wrapper em torno da funcionalidade que é implementada nogit repack
, mas até esta versão, ogit 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ê agora pode experimentar com este comportamento através degit gc --expire-to
! -
Você pode ter lido que o recurso
help.autocorrect
do Git é rápido demais para pilotos de Fórmula Um. Caso você não tenha lido, aqui estão os detalhes. 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. Mas 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 de valor booleano significava a mesma coisa. Mas
help.autocorrect
desvia ligeiramente 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 ele sugira 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 correção automática, você estaria errado: ele executará o comando corrigido antes que você possa sequer piscar os olhos. O Git 2.49 muda a convenção dehelp.autocorrect
para interpretar “1” como outros comandos de valor booleano, 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 decissegundo, 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 que você clone 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 eles 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, 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 remote.
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 remotes, você pode saber que o comando
git remote
usa a configuração do seu repositório para armazenar a lista de remotes que ele conhece. Você pode não saber que existiam, na verdade, 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 foram há muito tempo depreciadas e substituídas pelo mecanismo baseado em configuração que conhecemos hoje. Mas 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 eventualmente lançado, esses recursos serão removidos completamente.Se você quiser saber mais sobre as próximas mudanças radicais do Git, você pode ler tudo sobre elas em
Documentation/BreakingChanges.adoc
. Se você realmente quiser viver na vanguarda, você pode construir o Git com a chave de tempo de compilaçãoWITH_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 agente de usuário 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.
Você pode aprender mais sobre seus projetos aqui e aqui. Parabéns, Usman e Seyi!
O Resto do Iceberg
Essa é apenas uma amostra das mudanças da versão mais recente. Para mais, confira as notas de lançamento para 2.49, ou qualquer versão anterior no repositório Git.
É importante notar que este conteúdo foi auxiliado por Inteligência Artificial, mas escrito e revisado por um humano.
Via The GitHub Blog