Principais Novidades do Git 2.49

O projeto open source Git lançou a versão 2.49, com diversas funcionalidades e correções de bugs provenientes de mais de 89 colaboradores, sendo 24 deles novos. Esta atualização traz melhorias significativas desde a versão 2.48. O GitHub apresenta algumas das funcionalidades e mudanças mais interessantes introduzidas nesta versão. Vamos descobrir juntos o que há de novo no Git e como essas atualizações podem otimizar seu trabalho.

Empacotamento Acelerado com Name Hash v2

Frequentemente, discute-se 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 diversas funções, incluindo armazenamento local e ao enviar dados para ou de outro repositório Git, como em operações de fetch, clone ou push.

Armazenar objetos em packfiles oferece várias vantagens em relação ao armazenamento individual. Uma delas é a rapidez nas buscas de objetos. Ao procurar um objeto solto, o Git precisa realizar múltiplos system calls para encontrar, abrir, ler e fechar o objeto. Esses system calls podem ser agilizados com o block cache do sistema operacional, mas o acesso pseudoaleatório, devido à busca por SHA-1 (ou SHA-256) do conteúdo, não é 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 do arquivo permanece inalterada, o Git pode comprimir ainda mais, armazenando versões sucessivas como deltas de versões anteriores. Isso permite que o Git armazene as mudanças feitas em um objeto em relação a outro, em vez de múltiplas cópias de blobs quase idênticos.

O Git identifica pares de objetos adequados para armazenamento como pares delta-base comparando objetos que aparecem em paths similares. Para isso, calcula um “name hash“, um hash numérico que pondera os 16 caracteres finais não whitespace em um filepath. 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 resultar em compressão inadequada quando muitos arquivos têm o mesmo nome base, mas conteúdos diferentes, como múltiplos arquivos CHANGELOG.md para diferentes subsistemas. Git 2.49 introduz uma nova variante da função hash que considera mais da estrutura de diretórios ao calcular o hash. Cada camada da hierarquia de diretórios recebe seu próprio hash, que é deslocado para baixo e 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 levar a melhorias significativas no desempenho do empacotamento e no tamanho do pack resultante. Por exemplo, a nova função hash reduziu o tempo de reempacotamento de microsoft/fluentui de ~96 segundos 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 flag --name-hash-version nos comandos git repack ou git pack-objects na versão mais recente.

[source]

Preenchimento de Blobs Históricos em Partial Clones

Já se deparou com a seguinte mensagem ao trabalhar em um partial clone?

$ 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 partial clone clonado com --filter=blob:none. Seu repositório terá todas as trees, commits e objetos de tag anotados, mas apenas o conjunto de blobs diretamente acessíveis a partir de HEAD. Ou seja, seu clone local tem apenas os blobs necessários para um checkout completo na revisão mais recente, e carregar qualquer blob histórico irá buscar os objetos faltantes de onde você clonou o 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 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, resultando em armazenamento inchado e baixo desempenho.

Git 2.49 introduz uma nova ferramenta, git backfill, que pode buscar blobs históricos faltantes 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 melhor compressão delta nos packfiles enviados pelo servidor. Como as requisições são enviadas em lotes, podemos preencher todos os blobs faltantes 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 logo após clonar um repositório com --filter=blob:none não traz muitos benefícios, pois seria mais simples 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 só baixará 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 --filter=blob:none de um repositório usando Git 2.49!

[source, source]

Outras Atualizações e Melhorias

  • O Git utiliza compressão zlib ao escrever objetos soltos ou individuais dentro de packs. Zlib é uma biblioteca de compressão popular, focada em 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 incorpora muitas das otimizações mencionadas, remove código morto e soluções alternativas para compiladores antigos do zlib original, priorizando o desempenho. Por exemplo, zlib-ng suporta conjuntos de instruções SIMD (como SSE2 e AVX2) embutidos em seus algoritmos. Embora zlib-ng seja um substituto direto para 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 GNU Make, ou a opção zlib_backend ao compilar com Meson. Resultados experimentais 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).

    [source]

  • Esta versão marca um marco importante no projeto Git com a inclusão das primeiras partes 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 porção do código da biblioteca 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 Git, e ainda há muito trabalho a ser feito antes que isso se torne realidade, mas é um passo muito empolgante.

    [source]

  • Falando no esforço de “libificação“, outras mudanças relacionadas foram incluídas nesta versão. O esforço contínuo para abandonar 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 para suprimir 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 contra valores sem sinal, onde uma comparação como -1 < 2 (que deveria retornar verdadeiro) acaba retornando falso.

    Espera-se que você não perceba essas mudanças no uso diário do Git, mas são passos importantes para aproximar o projeto de ser usado como uma biblioteca independente.

    [source, source, source, source, source]

  • Leitores assíduos podem lembrar da cobertura do Git 2.39, onde discutimos a nova opção --expire-to do git repack. Caso seja novo por aqui, a opção --expire-to no git repack controla o comportamento de objetos inacessíveis que foram removidos do repositório. Por padrão, objetos removidos são simplesmente excluídos, mas --expire-to permite movê-los para o lado caso 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, 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 você pode experimentar com este comportamento via git gc --expire-to!

    [source]

  • Você já deve ter lido que o recurso help.autocorrect do Git é rápido demais para pilotos de Fórmula 1. Se não, aqui estão os detalhes. Se já viu algo 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. 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 booleanas significava a mesma coisa. Mas help.autocorrect desvia ligeiramente 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 que 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 autocorreção, estaria errado: ele executará o comando corrigido antes que você possa piscar os olhos. 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 isso de qualquer forma.

    [source, source]

  • Você pode 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 ramo ou tag específico, em vez de tudo. Essas opções são frequentemente usadas em CI farms quando querem clonar um ramo ou tag específico para teste.

    Mas e se você quiser clonar uma revisão específica que não está em nenhum ramo 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 depois de adicionar o repositório do qual você está buscando como um remoto.

    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 ramo ou tag apontando para ela.

    [source]

  • 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 conhece. 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 descontinuadas 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 lançado, esses recursos serão removidos completamente.

    Se quiser saber mais sobre as próximas mudanças significativas 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 chave de tempo de compilação WITH_BREAKING_CHANGES, que compila os recursos que serão removidos no Git 3.0.

    [source, source]

  • Por último, mas não menos importante, o projeto Git teve dois estagiários maravilhosos do 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 solicitações HTTP, e Seyi Kuforiji trabalhou na conversão de mais testes de unidade para usar o Clar testing framework.

    Você pode saber mais sobre seus projetos aqui e aqui. Parabéns, Usman e Seyi!

    [source, source, source, source]

O Resto do Iceberg

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


  1. É verdade. Leva cerca de 100-150 milissegundos para os humanos piscarem os olhos, e definir help.autocorrect para “1” executará o comando sugerido após esperar apenas 100 milissegundos (1 decissegundo). 

Os Destaques do Git 2.49 trazem inovações notáveis para desenvolvedores. Desde melhorias no empacotamento de objetos até novas ferramentas para partial clones e a integração de Rust, esta versão promete otimizar fluxos de trabalho e elevar a eficiência.

Este conteúdo foi auxiliado por Inteligência Artificiado, mas escrito e revisado por um humano.
Via The GitHub Blog

Leave a Comment