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/
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"
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
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
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)
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!
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
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
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
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
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)
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"
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)
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
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
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
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
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.
Top comments (0)