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/ 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 e40153b3e43a5ed7fa00ce6bd7a576763

Mar 14, 2025 - 21:50
 0
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/

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 
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 
Date:   Wed Mar 12 10:34:20 2025 -0300

    update hello.txt

commit c62126fdb09e19fec4e71e0d9a4351784dab680e
Author: Lucas 
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  1741786460 -0300
committer Lucas  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.

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