DEV Community

Lucas Ribeiro da Luz
Lucas Ribeiro da Luz

Posted on

Git e seu funcionamento interno

Git e seu funcionamento interno

Este post é uma introdução sobre o funcionamento interno do Git. Aqui vamos entender melhor como funciona sua implementação e os comandos básicos que usamos no dia-a-dia.

Minha recomendação é que durante a leitura você pratique o que está sendo passado aqui.

Espero que gostem. Boa leitura!

Comandos Plumbing e Porcelain

Os comandos do Git são divididos em duas categorias: plumbing e porcelain. Plumbing se refere a comandos de nível mais baixo e porcelain são comandos de nível mais alto. Comandos porcelain são conhecidos por nós: add, commit, clone, etc. Já os comandos plumbing ficam por debaixo do capô do Git e não são comumente usados manualmente. Apesar disso o Git faz uso intensivo de comandos plumbing ao usarmos os comandos porcelain.

Diretório .git

Vamos começar pelo começo de tudo. Quando inicializamos um novo repositório Git com git init, o Git cria um diretório .git com os seguintes diretórios e arquivos:

$ ls -F1 .git
config
description
HEAD
hooks/
info/
objects/
refs/
Enter fullscreen mode Exit fullscreen mode

O aquivo description é utilizado pelo GitWeb. O aquivo config contém configurações específicas do projeto. O diretório info contém padrões globais que são ignorados e que não são rastreados pelo .gitignore. O diretório hooks contém hook scripts.

Agora vamos ao que nos interessa neste artigo. O diretório objects é onde são armazenados todo o conteúdo que está sendo gerenciado pelo Git. O diretório refs armazena referências a commits (branches). Por fim, o aquivo HEAD aponta para a branch atual que será usada para os commits. Vamos ver em detalhes como o Git opera internalmente cada uma destas seções.

git add e git commit

Vamos analisar o que acontece quando fazemos um commit de forma comum usando comandos porcelain como o git add e git commit:

$ echo "Hello, world!" > hello.txt
$ git add hello.txt
$ git commit -m "add hello.txt"
Enter fullscreen mode Exit fullscreen mode

Lembra que falei que todo conteúdo gerenciado pelo Git, fica armazenado em .git/object? Então, vamos analisar o que tem dentro depois que foi criado o commit:

$ find .git/objects/
...
.git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b -- blob
.git/objects/ec/947e3dd7a7752d078f1ed0cfde7457b21fef58 -- tree
.git/objects/c6/2126fdb09e19fec4e71e0d9a4351784dab680e -- commit
Enter fullscreen mode Exit fullscreen mode

Perceba que, após o commit, foram criados três diretórios. Cada diretório representa um objeto Git, que neste caso são: blob, tree e commit.

Armazenamento de objetos

Antes de continuar, é importante entendermos como o Git armazena seus objetos. Como dito anteriormente tudo é armazenado em .git/objects/ e para fazer isso, o Git utiliza de funções hash.

Uma funções hash mapeia dados de tamanho dinâmico para valores de tamanho fixo. Implementações ruins podem facilmente levar a colisões, onde dois dados de tamanhos diferentes podem ser mapeados para um mesmo hash.

Quando criamos o commit anteriormente, vimos que três objetos foram gerados, e todo foram criados usado usando uma função hash. É isso que significa os nomes extensos como, por exemplo 065c57718c8acfe576b5ea5358cf3a8f461c69e2. O nome do algoritmo usado pelo Git é SHA-1. Além disso, como o conteúdo original de um hash não pode lido, ele é utilizado como chave para um algoritmo de compactação com zlib. Isso significa que podemos dizer que o Git é um banco de dados chave-valor!

Uma última informação importante é que é comum objetos blob e tree serem gerados com o mesmos hash caso o conteúdo seja o mesmo, mas commits não. Isso acontece pois um commit leva em consideração outras informações para criar o hash.

Objeto blob

O objeto blob representa um conteúdo qualquer. Neste caso e sendo o caso mais comum, representa o conteúdo de um arquivo. Para criar este objeto vamos usar o comando plumbing git hash-object:

$ echo "Hello, Git!" > hello.txt
$ git hash-object -w hello.txt
670a245535fe6316eb2316c1103b1a88bb519334
Enter fullscreen mode Exit fullscreen mode

Está aqui o primeiro objeto que é criado pelo Git. Perceba que é o mesmo tipo de objeto que foi criado anteriormente quando fizemos o commit de forma comum. A opção -w indica para o Git armazenar este objeto em .git/objects. O hello.txt é o nome do arquivo:

$ find .git/objects/
...
.git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b -- blob
.git/objects/ec/947e3dd7a7752d078f1ed0cfde7457b21fef58 -- tree
.git/objects/c6/2126fdb09e19fec4e71e0d9a4351784dab680e -- commit
.git/objects/67/0a245535fe6316eb2316c1103b1a88bb519334 -- blob (novo)
Enter fullscreen mode Exit fullscreen mode

Existe um comando Git na qual podemos ver qual o conteúdo original de qualquer objeto Git, que é o git cat-file:

$ git cat-file -p 670a245535fe6316eb2316c1103b1a88bb519334 
Hello, Git!
Enter fullscreen mode Exit fullscreen mode

Antes de seguir para o próximo tópico vamos analisar melhor o hash criado pelo Git. Como explicado o Git utiliza SHA-1 para criar um hash. Poderíamos então, criar um hash Git válido a partir de um algoritmo SHA-1? A resposta é: sim!:

$ echo -e "Hello, Git!" | openssl sha1
e40153b3e43a5ed7fa00ce6bd7a576763b88dab2
Enter fullscreen mode Exit fullscreen mode

Mas... espere um pouco, não foi o mesmo hash criado pelo Git! O que aconteceu? Além do Git criar o hash baseado no conteúdo, ele adiciona algumas informações importantes antes do conteúdo principal. Este conteúdo segue o padrão object-type {content-size}\0. Vamos adicionar isso no inicio do conteúdo e ver se será gerado um hash igual:

$ echo -e "blob 12\0Hello, Git!" | openssl sha1
670a245535fe6316eb2316c1103b1a88bb519334
Enter fullscreen mode Exit fullscreen mode

Agora sim, um hash válido!

Objeto tree

O objeto tree representa vários objetos blob e/ou outras trees. Ele registra o conteúdo de arquivos, adicionando mais informações referentes aos objetos blob. Para este objeto existem dois comandos: git update-index e git write-tree:

O comando git update-index adiciona informações sobre o cache. No caso, 100644 representa um aquivo comum e também adiciona um nome ao objeto blob que está sendo inserido nesse novo objeto tree, que normalmente faz referência ao arquivo onde está o conteúdo.

$ git update-index --add --cacheinfo 100644 670a245535fe6316eb2316c1103b1a88bb519334 hello.txt
Enter fullscreen mode Exit fullscreen mode

Por fim, vamos executar o comando git write-tree que vai de fato criar um novo objeto tree. Nesta etapa podemos adicionar quantos objetos blob queremos, antes de executar git write-tree:

$ git write-tree
d3ec8a0f5950fb1f73ce0d1ed55cd6fa7afcdeb9
Enter fullscreen mode Exit fullscreen mode

Pronto agora temos mais um objeto criado! Podemos verificar em .git/objects/ que foi criado mais um objeto:

$ find .git/objects/
...
.git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b -- blob
.git/objects/ec/947e3dd7a7752d078f1ed0cfde7457b21fef58 -- tree
.git/objects/c6/2126fdb09e19fec4e71e0d9a4351784dab680e -- commit
.git/objects/67/0a245535fe6316eb2316c1103b1a88bb519334 -- blob (novo)
.git/objects/d3/ec8a0f5950fb1f73ce0d1ed55cd6fa7afcdeb9 -- tree (novo)
Enter fullscreen mode Exit fullscreen mode

Objeto commit

Este objeto representa um commit, nele está presente informações úteis que são usadas para identificar as mudanças que foram realizadas. Para este objeto vamos usar o comando git commit-tree, ele recebe como parâmetro um objeto tree, o commit anterior e a mensagem descrevendo o commit:

$ git commit-tree d3ec8a0f5950fb1f73ce0d1ed55cd6fa7afcdeb9 -p bdacb9cd59348d39994e4cb392b734f9236bd859 -m "update hello.txt"
Enter fullscreen mode Exit fullscreen mode

Devemos ficar atentos ao parâmetro -p que indica o commit anterior. Isso deve ser referenciado pois o histórico de commits funcionam com um commit fazendo referência ao commit anterior.

$ find .git/objects/
...
.git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b -- blob
.git/objects/ec/947e3dd7a7752d078f1ed0cfde7457b21fef58 -- tree
.git/objects/bd/acb9cd59348d39994e4cb392b734f9236bd859 -- commit
.git/objects/67/0a245535fe6316eb2316c1103b1a88bb519334 -- blob (novo)
.git/objects/d3/ec8a0f5950fb1f73ce0d1ed55cd6fa7afcdeb9 -- tree (novo)
.git/objects/12/7f0bcadb277747bfa75cc14014c91941d26d94 -- commit (novo)
Enter fullscreen mode Exit fullscreen mode

Sim, este é um objeto commit válido, assim como o gerado pelo git commit. Agora, se executarmos git log, veremos algo interessante:

$ git log
commit c62126fdb09e19fec4e71e0d9a4351784dab680e (HEAD -> main)
Author: Lucas <email@gmail.com>
Date:   Wed Mar 12 10:26:28 2025 -0300

    add hello.txt
Enter fullscreen mode Exit fullscreen mode

Qual o motivo de aparecer apenas o primeiro commit criado? Simples: refs! Perceba que o primeiro commit aponta para a branch main e em nosso último commit feito, em momento algum adicionamos qualquer referência a branch main.

Referências

Referências são utilizadas para que diversos commits apontem para um mesmo contexto. Para adicionar o nosso commit a referência main podemos usar o comando git update-ref:

$ git update-ref refs/heads/main 127f0bcadb277747bfa75cc14014c91941d26d94
Enter fullscreen mode Exit fullscreen mode

Agora, vamos executar git log para ver o seu comportamento:

$ git log
commit 127f0bcadb277747bfa75cc14014c91941d26d94 (HEAD -> main)
Author: Lucas <email@gmail.com>
Date:   Wed Mar 12 10:34:20 2025 -0300

    update hello.txt

commit c62126fdb09e19fec4e71e0d9a4351784dab680e
Author: Lucas <email@gmail.com>
Date:   Wed Mar 12 10:26:28 2025 -0300

    add hello.txt
Enter fullscreen mode Exit fullscreen mode

Pronto, temos a branch main apontando para o último commit feito. Perceba que a branch main aponta apenas para o último commit feito, ele não aponta para os commits anteriores. Por isso que é importante e fundamental que quando criamos um commit devemos passar a referência ao commit anterior.

Para demonstrar melhor este funcionamento, podemos ver com git cat-file que o commit 127f0bcadb277747bfa75cc14014c91941d26d94 tem um campo chamado parent fazendo referência ao commit anterior:

$ git cat-file -p 127f0bcadb277747bfa75cc14014c91941d26d94

tree d3ec8a0f5950fb1f73ce0d1ed55cd6fa7afcdeb9
parent c62126fdb09e19fec4e71e0d9a4351784dab680e
author Lucas <email@gmail.com> 1741786460 -0300
committer Lucas <email@gmail.com> 1741786460 -0300

update hello.txt
Enter fullscreen mode Exit fullscreen mode

Encerramento

Aposto que não tinha ideia de como é interessante o funcionamento interno do Git, certo? Mas isso não é tudo! Ainda há conteúdos que não foram abordados neste artigo, como Refspec, Transfer Protocolos, Environment Variables e muito mais...

Após terminar essa leitura, recomendo fortemente que busque pelo livro gratuito no site oficial do Git chamado Pro Git onde será abordado todo o funcionamento do Git desde o nível básico até o mais avançado. Para este artigo usei de base parte do capítulo: 10. Git Internals

Espero que tenham gostado e espero que este artigo tenha dado uma visão mais ampla sobre o Git. Fica como recomendação e ideia de projeto, fazer uma implementação simples do Git na sua linguagem preferida.

Me siga nas minhas redes: X, Linkedin e GitHub.

AWS Q Developer image

Your AI Code Assistant

Ask anything about your entire project, code and get answers and even architecture diagrams. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Start free in your IDE

Top comments (0)

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay