Novidades do Git 2.49

O projeto de código aberto Git acaba de lançar o Git 2.49, trazendo diversas novidades e correções de bugs de mais de 89 colaboradores, sendo 24 deles novos. A última atualização sobre as novidades do Git foi quando a versão 2.48 foi lançada. Para celebrar este lançamento recente, confira os destaques do Git 2.49 com as funcionalidades e mudanças mais interessantes desde a última versão.

Empacotamento Mais Rápido com name-hash v2

Frequentemente, discutimos o modelo de armazenamento de objetos do Git, onde os objetos podem ser escritos individualmente (conhecidos como objetos “soltos”) ou agrupados em packfiles. O Git utiliza packfiles em diversas funções, incluindo armazenamento local (ao repacotar ou executar o GC no seu repositório), bem como ao enviar dados de ou para outro repositório Git (como fetching, clonagem ou push).

Armazenar objetos juntos em packfiles oferece algumas vantagens em relação ao armazenamento individual como objetos soltos. Uma delas é que as buscas de objetos podem ser realizadas muito mais rapidamente no armazenamento em pack. Ao procurar um objeto solto, o Git precisa fazer múltiplas chamadas de sistema para encontrar o objeto desejado, abri-lo, lê-lo e fechá-lo. Essas chamadas de sistema podem ser agilizadas usando o block cache do sistema operacional, mas como os objetos são procurados por um SHA-1 (ou SHA-256) de seus conteúdos, este acesso pseudo-aleatório não é muito eficiente para o cache.

O mais interessante é que, como os objetos soltos são armazenados individualmente, só podemos comprimir seus conteúdos de forma isolada, e não podemos armazenar objetos como deltas de outros objetos semelhantes que já existam no seu repositório. Por exemplo, 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 inicialmente escritos, eles são cada um armazenado individualmente e comprimidos com zlib. Mas se a maior parte do conteúdo do arquivo permanece inalterada entre pares de edições, o Git pode comprimir ainda mais esses objetos armazenando versões sucessivas como deltas de versões anteriores. Em termos gerais, isso permite que o Git armazene as alterações feitas a um objeto (relativamente a algum outro objeto) em vez de múltiplas cópias de blobs quase idênticos.

Mas como é que o Git determina quais pares de objetos são bons candidatos para armazenar como pares de delta-base? Uma forma útil é comparar objetos que aparecem em paths semelhantes. O Git faz isso hoje computando o que chama de “name hash“, que é efetivamente um hash numérico ordenável que pondera mais fortemente os últimos 16 caracteres não-espaço em branco num filepath. Esta função vem de Linus desde 2006, e destaca-se no agrupamento de funções com extensões semelhantes (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 existente de name-hash pode levar a uma compressão insatisfatória quando existem muitos arquivos que têm o mesmo nome base, mas conteúdos muito diferentes, como ter muitos arquivos CHANGELOG.md para diferentes subsistemas armazenados em conjunto no seu repositório. O Git 2.49 introduz uma nova variante da função hash que leva mais em conta a estrutura de diretórios ao calcular seu hash. Entre outras mudanças, cada camada da hierarquia de diretórios obtém seu próprio hash, que é deslocado para baixo e depois XORed no hash geral. Isso cria uma função hash que é mais sensível a todo o path, não apenas os 16 caracteres finais.

Isto pode levar a melhorias significativas tanto no desempenho do empacotamento, mas também no tamanho geral do pack resultante. Por exemplo, usar a nova função hash foi capaz de melhorar o tempo que levou para repacotar microsoft/fluentui de aproximadamente 96 segundos para aproximadamente 34 segundos, e reduzir o tamanho do pack resultante de 439 MiB para apenas 160 MiB.

Embora este recurso ainda não seja compatível com o recurso de reachability bitmaps do Git, você pode experimentá-lo usando a nova flag --name-hash-version do git repack ou do git pack-objects através da última versão.

Preenchimento Retroativo 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.
[...]

O que aconteceu aqui? Para entender a resposta a essa pergunta, vamos trabalhar em um cenário de exemplo:

Suponha que você está trabalhando em um clone parcial que você clonou com --filter=blob:none. Neste caso, seu repositório terá todas as suas trees, commit e objetos de tag anotada, mas apenas o conjunto de blobs que são imediatamente alcançáveis a partir do HEAD. Ou seja, seu clone local tem apenas o conjunto de blobs que ele precisa para preencher um checkout completo na revisão mais recente, e carregar quaisquer blobs históricos irá falhar em quaisquer objetos ausentes de onde você clonou seu repositório.

No exemplo acima, pedimos um blame do arquivo no path README.md. Para construir esse blame, no entanto, precisamos ver cada versão histórica do arquivo para calcular o diff em cada camada para descobrir se uma revisão modificou ou não uma determinada linha. Mas aqui vemos o Git carregando cada versão histórica do objeto uma por uma, levando ao armazenamento inchado e ao mau desempenho.

O Git 2.49 introduz uma nova ferramenta, git backfill, que pode preencher quaisquer blobs históricos ausentes de um clone --filter=blob:none em um pequeno número de batches. Essas requisições usam a nova path-walk API (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 de volta do servidor. Como essas requisições são enviadas em batches em vez de um por um, podemos facilmente preencher todos os blobs ausentes em apenas alguns packs em vez de um pack por blob.

Depois de 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, uma vez que teria sido mais conveniente simplesmente clonar o repositório sem um filtro de objeto habilitado em primeiro lugar. Ao usar a opção --sparse do comando backfill (o padrão sempre que o recurso de sparse checkout é habilitado em seu repositório), o Git apenas baixará 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 Atualizações e Melhorias

Além dos principais destaques do Git 2.49, esta versão inclui várias outras atualizações e melhorias notáveis, como:

  • O Git usa compressão zlib ao escrever objetos soltos, ou objetos 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) que contêm otimizações não presentes no zlib original.

    O fork zlib-ng junta 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 agora construir o Git com zlib-ng passando ZLIB_NG ao construir com o GNU Make, ou a opção zlib_backend ao construir com Meson. Os primeiros resultados experimentais mostram um aumento de velocidade de aproximadamente 25% ao imprimir o conteúdo de todos os objetos no repositório Git (de aproximadamente 52,1 segundos para aproximadamente 40,3 segundos).

  • Esta versão marca um marco importante no projeto Git com as primeiras partes de código Rust sendo incorporadas. 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 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 tira proveito desse 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 em torno de toda a interface da biblioteca do Git, e ainda há muito mais trabalho a ser feito em todo o projeto antes que isso possa se tornar uma realidade, mas este é um passo muito animador ao longo do caminho.

  • Por falar no esforço de “libificação“, houve um punhado de 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 o repository fornecido em vez de usar o global.

    Esta versão também viu muito esforço sendo colocado em silenciar avisos -Wsign-compare, que ocorrem quando um valor assinado é comparado com um não assinado. Isso pode levar a um comportamento surpreendente ao comparar, digamos, valores assinados negativos com não assinados, onde uma comparação como -1 < 2 (que deveria retornar verdadeiro) acaba retornando falso em vez disso.

    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 aproximar o projeto de ser usado como uma biblioteca independente.

  • Leitores de longa data devem se lembrar da cobertura do Git 2.39 onde discutimos a nova opção --expire-to do git repack. Caso você seja novo por aqui ou precise de uma atualização, nós ajudamos você. 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 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 do git gc. Em grande parte, o git gc é um wrapper em torno da funcionalidade que é 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 agora experimentar com este comportamento através do git gc --expire-to!

  • Você pode ter lido que o recurso help.autocorrect do Git é rápido demais para pilotos de Fórmula Um. Caso não tenha, 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 exatamente à convenção de outras opções semelhantes. Por exemplo, em outras partes do Git, especificar valores como “true”, “yes”, “on” ou “1” para configurações com valor booleano 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 sugere 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 mesmo que você possa piscar os olhos. O Git 2.49 muda a convenção de help.autocorrect para interpretar “1” como outros comandos com valor booleano, e números positivos maiores que 1 como faria antes. Embora você não possa especificar que quer 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 dadas, estas opções permitem que você clone o histórico de um repositório até uma branch ou tag específica em vez de tudo. Estas opções são frequentemente usadas em CI farms quando querem clonar uma branch ou tag específica para teste.

    Mas e se você quiser clonar uma revisão específica que não está em nenhuma 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 depois de 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 branch ou tag apontando para ela.

  • Por falar 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. Você pode não saber que havia realmente 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 já foram há muito tempo descontinuadas e substituídas pelo mecanismo baseado em configuração com o qual estamos familiarizados 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 eventualmente, estes recursos serão removidos completamente.

    Se você quiser aprender mais sobre as próximas mudanças importantes do Git, você pode ler tudo sobre elas em Documentation/BreakingChanges.adoc. Se você realmente quiser viver na ponta da faca, você pode construir o Git com o switch 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 maravilhosos estagiários Outreachy que recentemente concluíram seus projetos! Usman Akinyemi trabalhou na adição de suporte para incluir informações do 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.

    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 última versão. Para mais informações, confira as notas de lançamento para 2.49, ou qualquer versão anterior no repositório Git.

Os destaques do Git 2.49 são apenas uma parte das muitas melhorias e correções que foram implementadas. Para uma visão completa, é recomendável consultar as notas de lançamento detalhadas.

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

Leave a Comment

Exit mobile version