- Para entender a estrutura interna de um sistema de controle de versão, implementei diretamente um sistema semelhante ao Git
- Usei hash SHA-256 e compressão zstd para substituir o SHA-1 e o zlib do Git, organizando o repositório com a estrutura de diretórios
.tvc
- Escrito em Rust, implementa em etapas as funções de hash de arquivos, compressão, commit e checkout
- O objeto de commit inclui hash da árvore, commit pai, autor e mensagem, e arquivos idênticos não são salvos novamente graças à deduplicação por hash
- Ao vivenciar na prática que o Git é um armazenamento de arquivos baseado em endereçamento por conteúdo, o projeto destaca a importância de formatos de dados estruturados
Método de hash e compressão
- O Git identifica todos os objetos com hash SHA-1, mas neste projeto foi usado SHA-256
- O SHA-1 é antigo e tem vulnerabilidades de segurança, mas neste projeto ele seria usado apenas para identificar o conteúdo dos arquivos, então a segurança não era importante
- Em vez do zlib do Git, foi adotada a biblioteca de compressão zstd da Facebook
- Considerei o zstd mais eficiente, e compatibilidade com o Git não era o objetivo
- O nome do projeto é “tvc (Tony’s Version Control)”, usando os arquivos
.tvc e .tvcignore como equivalentes às estruturas correspondentes do Git
Etapas da implementação
- O processo de implementação seguiu a ordem leitura dos argumentos de comando → leitura das regras de ignorar → exibição da lista de arquivos → hash e compressão → criação de árvore e commit → gerenciamento do HEAD → checkout do commit
- Escrito em Rust, o comando
ls aplica as regras de .tvcignore, percorre recursivamente os arquivos não ignorados e imprime o hash SHA-256 de cada arquivo
- Com a biblioteca zstd, a funcionalidade de compressão e descompressão de arquivos foi implementada de forma simples
Estrutura do commit
- O objeto de commit inclui as seguintes informações
- tipo do objeto (“commit”)
- estado do sistema de arquivos naquele momento (hash da árvore)
- commit anterior (HEAD)
- autor (author)
- mensagem de commit
- Diferentemente do Git, a distinção entre autor e committer foi omitida, e não foram implementadas funções de merge ou rebase
- Ao criar um commit, o objeto de árvore é gerado, hasheado, comprimido e armazenado em
.tvc/objects/, e o arquivo HEAD é atualizado
- Arquivos idênticos não são salvos novamente quando o hash é o mesmo, permitindo evitar armazenamento duplicado
Objeto de árvore e checkout
- A função
generate_tree() percorre diretórios, faz hash, comprime e armazena cada arquivo, e monta uma string com nome do arquivo e hash
- Subdiretórios são processados recursivamente para formar a estrutura de árvore
- Os objetos de commit e árvore são analisados em structs (
Commit, Tree) para facilitar seu tratamento em memória
- A função
generate_fs() recria o sistema de arquivos com base na estrutura de árvore e executa o checkout no caminho especificado
Lições do projeto
- Foi possível vivenciar na prática que o Git é um armazenamento de arquivos baseado em endereçamento por conteúdo (key-value)
- A parte mais difícil foi o parsing do formato dos objetos, e no futuro a ideia é usar um formato mais claro, como YAML ou JSON
- Todo o código está disponível no repositório GitHub (tonystr/t-version-control)
1 comentários
Comentários no Hacker News
É interessante que o Git seja o único SCM que oferece suporte à recursive merge strategy
Esse método é muito útil porque lembra automaticamente resoluções de conflito do passado
Muita gente ainda prefere rebase, mas ao implementar merge é indispensável incluir um mecanismo de armazenamento do histórico de resolução de conflitos
Referência relacionada: Merge made by recursive strategy
Referência: Git Tools - Rerere
Link
git mergenão tem uma estratégia “null”Mesmo quando você já resolveu os conflitos e só quer deixar registrado que houve um merge, o Git ainda tenta ajudar desnecessariamente
Seria bom ter uma opção que apenas registrasse o merge sem mexer no índice nem na working tree
Por exemplo, o Pijul faz isso
Você não consegue ver as tentativas feitas em vários commits, é mais difícil reverter, e também complica continuar trabalhando em uma branch já mergeada
Quando vários PRs são peças de um mesmo quebra-cabeça, acho que um merge simples é muito melhor
Aprender o funcionamento interno de uma ferramenta que usamos todos os dias é sempre divertido
Em especial, Git from the Bottom Up é um excelente texto que explica com clareza a estrutura interna do Git
Em uns 20 minutos, dá para entender os mecanismos opacos por trás dos comandos do Git
cat-file, e isso é bem legalSe você tem curiosidade sobre como agentes de programação fazem planejamento, textos como este são parte dos dados de treinamento deles
Mas, se o autor teve ajuda de um LLM, isso também pode virar uma situação circular
Parece que realmente existem bots raspando repositórios públicos
É uma sensação estranha pensar que meu código pode estar sendo usado no treinamento de LLMs
O texto em si não tem saída de LLM, mas usei ChatGPT para pedir conselhos sobre convenções de código em Rust e comparação de algoritmos
O tutorial CodeCrafters “Build your own Git” é realmente excelente
Também recomendo o vídeo ao vivo do Jon Gjengset, em que ele implementa em Rust
Eu também gostaria que controle de versão fosse mais usado fora do desenvolvimento de software
O GotVC é um projeto interessante, com criptografia E2E, importação paralela e estrutura para suportar arquivos grandes
No fim, você acaba tendo que abrir no programa original para comparar
Este texto me lembrou ugit: DIY Git in Python
É um dos melhores materiais para mergulhar fundo no funcionamento interno do Git sem deixar de ser fácil de acompanhar
O Sapling VCS, fork do Mercurial feito pela Meta, usa compressão com dicionário Zstd
Dá para comparar com o packfile comprimido por delta do Git na documentação explicativa
Em repositórios pequenos, a compressão delta do Git é mais eficiente, mas em repositórios grandes a compressão por dicionário baseada em caminho é melhor
Recentemente, uma função semelhante de “path-walk” também foi adicionada ao Git
Eu também fiz uma tentativa parecida, e o nome do meu projeto é “shit”
Link do GitHub
Lembrei de quando tentei criar um framework SPA e me surpreendi com a complexidade escondida
Imagino que desenvolvedores de React ou Angular também devam passar por esse tipo de toca do coelho
O Git também esconde muito bem sua complexidade
Vi um cliente Git escrito em PHP que consegue ler packfile e reftable, além de oferecer diff baseado em LCS
gipht-horse
E também foi a primeira vez que descobri que dá para usar
@no lugar de HEAD, o que sintaticamente parece bem razoável