Estrutura interna do Git – Parte 2

Não exatamente uma sequencia, porém ainda ficaram alguns pontos que eu acho muito interessante do Git que levarei em conta quando precisar desenvolver algo.

Além dos comandos normalmente utilizados, que para quem utiliza o bash completion possui autocompletamento, existem vários comandos “ocultos”, porém documentados. O comando git cat-file do texto anterior é um exemplo. Isso ocorre pela forma que os comandos do Git foram feitos, existe um conjunto de comandos básicos para executar pequenas tarefas, assim como a filosofia UNIX (uma única coisa, porém bem feita). Ou seja, comandos para adicionar arquivos na estrutura do Git, recuperá-los, calcular diferenças, aplicá-las, lidar com braches e outros.

No capítulo 9 do Pro Git temos um exemplo melhor desses comandos, e até como fazer um commit sem utilizar o git commit. Isso mostra que até mesmos comandos internos são implementados utilizando outros comandos, dividindo o problema em partes menores.

Agora imagina que você está desenvolvendo um sistema que utiliza alguma parte do Git, como o GitHub para visualizar o código, ou o OpenShift para receber o código e fazer deploy da aplicação. Como existem várias operações você não precisa desenvolver algo para ler a estrutura do Git, pode simplesmente reutilizar esses comandos, ou seja, a API já está pronta, basta adaptá-la para a sua linguagem.

Porém não precisamos ir muito longe para tirarmos proveito disso. No meu texto Verificador de estilo de código no VIM, comentei sobre os verificadores de estilo e podemos utilizá-las em conjunto com o Git. Imagine que seu projeto tem um estilo que todo o código deve ser seguido, porém as vezes alguém acaba deixando passar um espaço a mais, troca espaço por tab. No Git podemos fazer a validação do código é permitir o commit apenas caso ele esteja de acordo com o estilo.

Para fazer essa verificação precisamos utilizar outro recurso chamado de Hook, que são scripts executados em determinados momentos (consulte o capítulo 7.3 do Pro Git). Inicialmente fiz a verificação apenas local para lembrar caso tenha alguma coisa fora do padrão, mas ainda possível forçar o commit se assim for desejado. Como os projetos tinham vários arquivos fora do estilo, queria verificar apenas os alertas referentes as minhas mudanças, até por questão de performance, se eu não alterei o arquivo não preciso ser lembrado que ele tem algum erro, deixando a saída até menos poluída e posso verificar todos os arquivos caso esteja atrás de erros.

Primeira coisa, preciso saber quais os arquivos que foram alterados no commit, para tanto utilizei o comando git diff –raw –cached, uma informação um pouco mais bruta dos arquivos selecionados com git add. Porém eu posso ter executado o git add e alterar o arquivo novamente antes de fazer o commit, então não posso simplesmente fazer a validação do código do arquivo no diretório, para isso utilizei a hash do git diff para pegar o arquivo que será commitado com git show e fazer a validação em cima dele.

Quem quiser ver como tudo ficou, tenho esse código no meu GitHub (https://github.com/eduardoklosowski/codelint), está escrito em Python, porém tenho que trabalhar melhor a documentação e ter decidido fazer tudo em inglês não ajudou, porém assim que estudar melhor as formas de documentação e opções que tenho para compartilhá-la irei fazer isso, mas se alguém for usar e tiver alguma dúvida deixe nos comentários, até ficarei feliz em saber que alguém está utilizando (testando). Report de bugs, melhorias no código ou funcionalidades novas são bem vindas. Pode ser utilizado com outras ferramentas de controle de versão, só não tenho conhecimento e necessidade para isso agora, mas pode ser facilmente desenvolvido já que o Git foi feito a parte também.

Depois de criar os arquivos de configuração em ~/.config/codelint, preparo tudo para o commit e executo git codelint, isso iniciará os validadores. Um detalhe que todos os comandos no PATH que começam com git-, exemplo git-codelint podem ser executados como comandos do git dessa forma e com autocompletamento.

Agora só falta colocar no Hook executado antes do commit, o pre-commit, seu conteúdo é simples.

#!/usr/bin/env bash
git-codelint

Caso queira verificar os arquivos antes de commitar posso executar git codelint e quando der o git commit será verificado, se tiver algum erro o commit será interrompido para correção, ou posso forçar com git commit -n.

Anúncios

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.