O projeto open source Git acaba de lançar o Git 2.49, trazendo consigo diversas novidades e correções de erros provenientes de mais de 89 colaboradores, sendo 24 deles estreantes. Para celebrar este lançamento recente, vamos dar uma olhada nas funcionalidades e mudanças mais interessantes desde a última atualização.
Empacotamento Mais Rápido com Name Hash v2
Em diversas publicações, abordamos 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 várias funções, incluindo armazenamento local (ao reempacotar ou executar o GC no seu repositório), e ao enviar dados para ou receber de outro repositório Git (como fetching, cloning ou pushing).
Armazenar objetos juntos em packfiles oferece algumas vantagens em relação ao armazenamento individual como “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-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 de forma isolada. Assim, não é possível armazenar objetos como deltas de outros objetos similares já existentes no repositório. Imagine que você está fazendo uma série de pequenas mudanças em um blob grande no seu repositório. Inicialmente, esses objetos são armazenados individualmente e comprimidos com zlib. No entanto, se a maior parte do conteúdo do arquivo permanece inalterada entre os pares de edições, o Git pode comprimir ainda mais esses objetos, armazenando versões sucessivas como deltas das anteriores. Isso permite que o Git armazene as mudanças feitas em um objeto (relativamente a outro objeto) em vez de múltiplas cópias de blobs quase idênticos.
Mas como o Git determina quais pares de objetos são bons candidatos para armazenar como pares delta-base? Uma forma é comparar objetos que aparecem em caminhos similares. O Git faz isso computando o que chama de “name hash“, que é efetivamente um hash numérico ordenável que pondera mais fortemente os 16 caracteres finais não-espaço em branco em um filepath. Essa função, criada por Linus em 2006, é 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 existente de name-hash pode levar a uma compressão ruim quando há muitos arquivos com o mesmo nome base, mas conteúdos muito diferentes, como vários 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 mais sensível ao caminho completo, e não apenas aos 16 caracteres finais.
Isso pode levar a melhorias significativas tanto no desempenho do empacotamento quanto no tamanho geral do pack resultante. Por exemplo, usar a nova função hash conseguiu melhorar o tempo para reempacotar microsoft/fluentui
de cerca de 96 segundos para cerca de 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 git pack-objects
através da versão mais recente.
Preenchimento Retroativo de Blobs Históricos em Clones Parciais
Já aconteceu de você estar trabalhando em um clone parcial e receber 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, vamos analisar um cenário:
Imagine 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 anotados, mas apenas o conjunto de blobs que são imediatamente acessíveis a partir de HEAD
. Em outras palavras, seu clone local tem apenas o conjunto de blobs necessários para popular um checkout completo na revisão mais recente, e carregar quaisquer blobs históricos irá buscar quaisquer objetos faltantes de onde você clonou seu repositório.
No exemplo acima, pedimos um blame do arquivo no caminho README.md
. Para construir esse blame, no entanto, 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. Mas aqui 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 buscar quaisquer blobs históricos faltantes de um clone --filter=blob:none
em um pequeno número de lotes. Essas requisições usam a nova path-walk API (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 essas requisições são enviadas em lotes em vez de uma por uma, 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 [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 muito benefício, pois 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 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 mesmo!
Outras Atualizações e Melhorias no Git 2.49
Além das grandes novidades, o Git 2.49 também inclui diversas outras melhorias e correções que visam otimizar o desempenho e a experiência do usuário. Estas mudanças, embora menores, contribuem para um ecossistema Git mais robusto e eficiente.
O Git utiliza compressão alimentada por zlib ao escrever objetos soltos, ou objetos individuais dentro de packs, entre outros. zlib é uma biblioteca de compressão incrivelmente popular, com ênfase em 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 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ê agora pode 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. Resultados experimentais iniciais mostram um aumento de velocidade de cerca de 25% ao imprimir o conteúdo de todos os objetos no repositório Git (de cerca de 52,1 segundos para cerca de 40,3 segundos).
Esta versão marca um marco importante no projeto Git com as primeiras partes de código em Rust sendo incluídas. 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 biblioteca, fazendo coisas como substituir funções que encerram o programa por outras que retornam um inteiro e deixam o chamador decidir se deve sair ou não, além de limpar vazamentos de memória, etc. Esta versão aproveita esse trabalho para fornecer um proof-of-concept crate Rust 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 este é um passo muito empolgante 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 o repository
fornecido em vez de usar o global.
Esta versão também viu muito esforço sendo dedicado a eliminar 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.
Esperamos que você não note estas 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 podem se lembrar da nossa 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 te ajudamos. A opção --expire-to
no git repack
controla o comportamento de objetos inacessíveis que foram podados do repositório. Por padrão, objetos podados são simplesmente deletados, mas --expire-to
permite que você os mova para o lado caso queira mantê-los para fins de backup, etc.
O git repack
é um comando de nível relativamente baixo, e a maioria dos usuários provavelmente irá 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 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ê agora pode 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 1. 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 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 um pouco 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 ele sugira após esperar muitos decissegundos.
Então, enquanto você pode ter pensado que definir help.autocorrect
para “1” habilitaria o comportamento de autocorreção, você estaria errado: ele irá, em vez disso, executar o comando corrigido antes que você possa sequer piscar os olhos. O Git 2.49 muda a convenção de help.autocorrect
para interpretar “1” como outros comandos booleanos, e números positivos maiores que 1 como faria antes. Embora você não possa especificar que quer o comportamento de autocorreção em exatamente 1 decissegundo, você provavelmente nunca quis isso de qualquer maneira.
Você pode estar ciente das várias opções do git clone
como --branch
ou --tag
. Quando fornecidas, estas opções permitem que você clone o histórico de um repositório até um branch ou tag específico em vez de tudo. Estas opções são frequentemente usadas em CI farms quando 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 de onde 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 um branch 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. Você pode não saber que existiam 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 há muito tempo foram descontinuadas e substituídas pelo mecanismo baseado em configuração que conhecemos hoje. Mas o Git manteve 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 disruptivas 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çã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 completaram seus projetos! Usman Akinyemi trabalhou na adição de suporte para incluir informações 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.
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 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 incluem melhorias significativas no desempenho, novas funcionalidades e a introdução de código Rust. Essas atualizações visam otimizar a experiência do usuário e preparar o Git para o futuro.
Primeira: Este conteúdo foi auxiliado por Inteligência Artificiado, mas escrito e revisado por um humano.
Via The GitHub Blog