Estrutura interna do Git

Existem diversos tutoriais e guias sobre como utilizar o Git e seu funcionamento, como por exemplo o Try Git e o Pro Git. Todos esses materiais auxiliam nos primeiros passos com a ferramenta, porém quando se começa a trabalhar com recursos mais avançados, principalmente branches e merge/rebase as coisas tentem a ficarem mais confusas para quem está iniciando.

Eu particularmente li o livro Pro Git, aprendi muito sobre o funcionamento e seus recursos. Como faz algum tempo quase um ano que fiz esta leitura, irei fazer um resumo para relembrar e compartilhar com vocês sobre a parte interna do Git, que foi o mais importante, no meu caso, para aprender ou imaginar como os demais recursos funcionam. Como exemplo irei utilizar um código publicado do meu Git Hub (https://github.com/eduardoklosowski/webprint), assim todos podem visualizar os mesmos resultados.

Talvez o mais importante a se entender é que o Git possui um sistema para armazenar objetos, que na verdade é tudo o que está na sua estrutura interna, como arquivos, diretórios e commits, podendo ser acessados por uma hash SHA1. Um exemplo prático, na data de publicação deste texto, o último commit deste repositório era o “cb584a1992fef3c286c5b36d2aba1c66bfe9b3a2”, o que significa que existe um commit que pode ser acessado com essa hash dentro da estrutura do Git, podendo ser visualizado com o comando git cat-file -p cb584a1992fef3c286c5b36d2aba1c66bfe9b3a2:

tree 73e7173b7f13853343b3bbad766f2b54852e8995
parent ff12eb157275a886feb9a039819cfc067e7292d5
author Eduardo Klosowski 1406755582 -0300
committer Eduardo Klosowski 1406755582 -0300

Correção da exclusão da imagem

Além das informações de quem fez as modificações, quem fez o commit, suas respectivas datas e mensagem, temos duas informações importantes. A primeira é o “parent”, seu valor é o hash do commit anterior, que pode ser verificado com um git log, é possível seguir esse valor dos commits até primeiro, que não fará referência a outro justamente por ser o primeiro. Se em algum comento tiver uma ramificação no histórico dos commits, o ponto de união apresentará mais de um parent, justamente para mostrar essa união.

A segunda informação importante e o “tree” que é um objeto de diretório, assim como existe na maioria dos sistemas de arquivos, é possível considerar essa estrutura interna do Git como um sistema de arquivos, porém específica para controle de versão, existem outros tipos de objetos além de arquivos e diretórios, mas não quem é o domo, data de criação e modificação por exemplo, isso torna as vezes mais rápido colocar um arquivo na estrutura do Git que no sistema de arquivos do HD por exemplo. Utilizando o mesmo comando para visualizar o objeto do diretório (git cat-file -p 73e7173b7f13853343b3bbad766f2b54852e8995) temos:

100644 blob 7821b4a1a8bc6e6d29368e25bb1d2a4b42f3754e .gitignore
100644 blob bcca729f3ce131d32dbe9af737e8d9ed049fe25d LICENSE
100644 blob d45ebab4a0fb6137cd519a6448cb4313a8e0ba2a README.md
100755 blob 3f3fdf92237f708dd3494b63b7fce452d406d870 manage.py
040000 tree f6e01702b079c29f3798ed54349c2218e4131fc3 webprint

Essas informações, pela ordem das colunas são: permissões do objeto (os últimos 3 dígitos são a mesmas permissões do EXT4 por exemplo), o tipo de objeto (lembrando que tree é outro diretório e blob são arquivos), o hash do objeto referenciado e finalmente seu nome.

Os arquivos também podemos acessar pelo hash, no caso git cat-file -p bcca729f3ce131d32dbe9af737e8d9ed049fe25d teremos conteúdo do arquivo “LICENSE” desse commit específico. Quem já trabalhou com ponteiros e estruturas em C já deve ter entendido como o Git funciona internamente, já que é a mesma lógica, de ter referências (ponteiros) para outros objetos (estruturas).

Sendo um pouco atento você poderá notar que o Git não gravou a diferença entre os commits em sua estrutura em nenhum momento, o motivo é simples, ele não faz isso. Toda vez que precisar a diferença os objetos são acessados e calculados na hora, pode ser um pouco mais lento que simplesmente ter o resultado já armazenados, porém se você tiver vários commits entre os objetos que for comparar é mais simples ter os dois e calcular a diferença que juntar todas as diferenças de cada commit.

Outro ponto importante é que cada commit tem referência para todos os arquivos que estão no projeto naquele momento, assim como um “snapshot”, então mesmo que algum arquivo ou diretório não tenham sofrido mudanças, eles estarão em ambos os commits, porém como não houve mudanças, os dois commits (ou mais) podem apontar para os mesmos objetos da estrutura, assim já otimizando o espaço e evitando objetos duplicados.

Agora se você tiver um arquivo com centenas de linhas e alterar apenas uma, você terá uma cópia completa do arquivo com apenas essa uma linha alterada dentro da estrutura do Git, a princípio parece desperdício de espaço, porém mesmo esse arquivo exista dentro da estrutura do Git, não quer dizer que ele existirá no sistema de arquivos, quando existem vários objetos semelhantes, o Git junta esses objetos em um “pack”, um único arquivo para esses vários objetos, sendo elegido um para estar na integra dentro do pack e os demais apenas as diferenças.

Então é verdade que o Git guarda diferenças de arquivos, porém no sistema de arquivos e não em sua estrutura de objetos, assim não tendo o problema de arquivos quase duplicados. Outro ponto importante que não é necessariamente a primeira ou última versão do arquivo que estará na integra no pacote, podendo ser uma versão intermediária que gerarias diferenças menores, reduzindo ainda mais o tamanho do pack no sistema de arquivos.

Então para calcular a diferença de dois arquivos com vários commits de modificação, na pior das hipóteses terá que ler dois arquivos do pack, aplicar duas diferenças e calcular as diferenças entre eles, em vez de somar as diferenças de cada commit, ganhando assim mais desempenho.

Sobre os branches, nada mais são que referências aos commits, ou seja, guardam o hash de algum commit, que podem ser visualizados dentro do diretório “.git” do projeto com o comando cat .git/refs/heads/master por exemplo.

Todos os processos que alteram o histórico do Git, como rebase e cherry-pick são alterações nessas referências, porém se os commits forem alterados, serão gerados novos commits com outros hashs, os antigos não deixam de existir de imediato, apenas não tem mais nenhuma referência apontando direta ou indiretamente para eles, por isso não aparecem mais, porém ainda estão na estrutura do Git e podem ser acessados pelos seus hash, a menos que tenham sido removidos automaticamente pelo Git depois de algum tempo ou com o comando git prune.

Esse é um assunto extenso, e só é possível se dominar com o uso do Git, porém entender isso me ajudou e ajuda muito quando preciso fazer algo no Git. Recomendo a leitura do Pro Git para todos, já que ele vai do básico ao avançado, é gratuito e está em português, além de mostrar muito mais exemplos como esse descrito aqui.

Anúncios

3 comentários sobre “Estrutura interna do Git

  1. Estou tentando manter um texto por semana sempre que possível e eu estiver bem para escrever. Infelizmente acredito que ano que vem será um pouco mais difícil manter essa regularidade com esses tipos de textos, mas só esperando para saber.

    O piratas nunca foi embora, só tivemos alguns problemas com tempo, Metade está na faculdade e as vezes surge algum procedimento fora de horário em algum servidor de cliente, então juntar todo mundo é um pouco complicado, além da edição.

    Curtir

Comente

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s