[pt-BR] Fundamentos do Git, um guia completo
Se você já trabalha com Git diariamente, mas deseja ter uma boa compreensão dos fundamentos do Git, então este post é para você.
Aqui, você terá a chance de verdadeiramente entender a arquitetura do Git e como comandos como add, checkout, reset, commit, merge, rebase, cherry-pick, pull, push e tag funcionam internamente.
Não deixe o Git te dominar, aprenda os fundamentos do Git e domine o Git em vez disso.
Prepare-se, um guia completo sobre o Git está prestes a começar.
💡 Primeiro as coisas mais importantes
Você deve praticar enquanto lê este post.
Acompanhando, vamos primeiro criar um novo projeto chamado git-101 e depois inicializar um repositório git com o comando git init:
O CLI do Git fornece dois tipos de comandos:
plumbing, que consiste em comandos de baixo nível usados internamente pelo Git nos bastidores quando os usuários digitam comandos de alto nível
porcelain, que são os comandos de alto nível comumente usados pelos usuários do Git
Neste guia, veremos como os comandos plumbing se relacionam com os comandos porcelain que usamos no dia a dia.
⚙️ A arquitetura do Git
Dentro do projeto que contém um repositório Git, podemos verificar os componentes do Git:
Vamos nos concentrar nos principais:
.git/objects/
.git/refs
HEAD
Vamos analisar cada componente em detalhes.
💾 O Banco de Dados de Objetos
Usando a ferramenta UNIX find, podemos ver a estrutura da pasta .git/objects:
No Git, tudo é persistido na estrutura .git/objects, que é o Banco de Dados de Objetos do Git.
Que tipo de conteúdo podemos persistir no Git? Qualquer tipo.
🤔 Espere!
Como isso é possível?
Através do uso de funções hash.
🔵 Hashing for the rescue
Uma função hashmapeia dados de tamanho arbitrário e dinâmico em valores de tamanho fixo. Ao fazer isso, podemos armazenar/persistir qualquer coisa, porque o valor final terá sempre o mesmo tamanho.
Implementações ruins de funções hash podem facilmente levar a colisões, onde dois dados de tamanho dinâmico diferentes podem mapear para o mesmo valor final de hash de tamanho fixo.
SHA-1 é uma implementação bem conhecida da função hash que é geralmente segura e raramente tem colisões.
Vamos pegar, por exemplo, o hash da string `
my precious`:
|
Observação: Se você estiver usando o Linux, pode usar o comandosha1sumem vez deOpenSSL.
🔵 Comparando diferenças no conteúdo
Um bom hashing é uma prática segura onde não podemos conhecer o valor original, ou seja, fazer engenharia reversa.
Caso queiramos saber se o valor mudou, basta envolver o valor na função de hash e voilà, podemos comparar a diferença:
|
|
Se os hashes forem diferentes, então podemos assumir que o valor mudou.
Você consegue ver uma oportunidade aqui? Que tal usar SHA-1 para armazenar dados e apenas acompanhar tudo comparando hashes?
Isso é exatamente o que o Git faz internamente 🤯.
🔵 Git e SHA-1
O Git usa o SHA-1 para gerar hashes de tudo e armazena no diretório .git/objects. Simples assim!
O comando plumbinghash-object faz o trabalho:
|
Vamos comparar com a versão OpenSSL:
|
Oooops... é bastante diferente. Isso ocorre porque o Git adiciona uma palavra específica seguida pelo tamanho do conteúdo e o delimitador \0. Essa palavra é o que o Git chama de tipo do objeto.
Sim, objetos do Git têm tipos. O primeiro que vamos ver é o objeto blob.
🔵 O objeto blob
Quando enviamos, por exemplo, a string "my precious" para o comando hash-object, o Git adiciona o padrão {tipo_do_objeto} {tamanho_do_conteúdo}\0 à função SHA-1, para que fique:
blob 12\0myprecious
Então:
|
|
Yay! 🎉
🔵 Armazenando blobs no banco de dados
Mas o comando hash-object em si não persiste no diretório .git/objects. Devemos acrescentar a opção -w e o objeto será persistido:
|
### Ou, simplesmente

🔵 Lendo o conteúdo de um blob
Já sabemos que, por razões criptográficas, não é possível ler o conteúdo de um blob a partir de sua versão de hash.
🤔 Ok, mas espere.
Como o Git descobre o valor original?
Ele usa o hash como uma chave que aponta para um valor, que é o próprio conteúdo original usando um algoritmo de compressão chamado Zlib, que compacta o conteúdo e o armazena no banco de dados de objetos, economizando assim espaço de armazenamento.
O comando plumbingcat-file faz o trabalho, de forma que, dada uma chave, ele descomprime os dados compactados e obtém o conteúdo original:
No caso de você estar imaginando, isso mesmo, o Git é um banco de dados chave-valor!

🔵 Promovendo blobs
Ao usar o Git, queremos trabalhar no conteúdo e compartilhá-lo com outras pessoas.
Comumente, depois de trabalhar em vários arquivos/blobs, estamos prontos para compartilhá-los e assinar nossos nomes para o trabalho final.
Em outras palavras, precisamos agrupar, promover e adicionar metadados aos nossos blobs. Esse processo funciona da seguinte forma:
Adicionar o blob a uma área de preparação (staging area)
Agrupar todos os blobs na área de preparação em uma estrutura de árvore
Adicionar metadados à estrutura de árvore (nome do autor, data, uma mensagem semântica)
Vamos ver os passos acima em detalhes.
🔵 Área de preparação, o índice
O comando plumbingupdate-index permite adicionar um blob à área de preparação (stage) e dar um nome a ele:
--add: adiciona o blob à área de preparação, também chamada de índice--cacheinfo: usado para registrar um arquivo que ainda não está no diretório de trabalhoo hash do blob
index.txt: um nome para o blob no índice

Onde o Git armazena o índice?
No entanto, não é legível para humanos, está compactado usando Zlib.
Podemos adicionar quantos blobs quisermos ao índice, por exemplo:
Após adicionar blobs ao índice, podemos agrupá-los em uma estrutura de árvore que está pronta para ser promovida.
🔵 O objeto Tree
Ao usar o comando plumbingwrite-tree, o Git agrupa todos os blobs que foram adicionados ao índice e cria outro objeto na pasta .git/objects:
Verificando a pasta .git/objects, observe que um novo objeto foi criado:
### O novo objeto
### O blob criado anteriormente
Vamos recuperar o valor original usando cat-file para entender melhor:
### Usando a opção -t, obtemos o tipo do objeto
Isso é uma saída interessante, é bastante diferente do blob que retornou o conteúdo original.
No objeto de árvore, o Git retorna todos os objetos que foram adicionados ao índice.
100644 blob 8b73d29acc6ae79354c2b87ab791aecccf51701f index.txt
100644: o cacheinfoblob: o tipo do objetoo hash do blob
o nome do blob

Uma vez que a promoção é concluída, hora de adicionar alguns metadados à árvore, para que possamos declarar o nome do autor, a data e assim por diante.
🔵 O objeto commit
O comando plumbingcommit-tree recebe uma árvore, uma mensagem de commit e cria outro objeto na pasta .git/objects:
Que tipo de objeto é esse?
### cat-file
E qual é o seu valor?
tree 3725c: a árvore referenciadaautor/confirmador
a mensagem do commit meu commit precioso

🤯 OMG! Estou vendo um padrão aqui?
Além disso, os commits podem fazer referência a outros commits:
Onde a opção -p permite fazer referência a um commit pai:
Podemos ver que, dado um commit com um commit pai, podemos percorrer todos os commits recursivamente, através de todas as suas árvores, até chegarmos aos blobs finais.
Uma solução potencial:
E assim por diante. Bem, você chegou ao ponto.
🔵 Log for the rescue
O comando porcelaingit log resolve esse problema, percorrendo todos os commits, seus parents e árvores, nos dando uma perspectiva de uma linha do tempo do nosso trabalho.
🤯 OMG!
O Git é um banco de dados de grafos gigante e leve, baseado em chave-valor!
🔵 O Grafo do Git
Dentro do Git, podemos manipular objetos como ponteiros em grafos.

Blobs são instantâneos de dados/arquivos
Trees são conjuntos de blobs ou outras árvores
Commits fazem referência a árvores e/ou outros commits, adicionando metadados
Isso é muito legal e tudo mais. Mas usar sha1 no comando git log pode ser trabalhoso.
Que tal dar nomes aos hashes? É aí que entram as Referências.
Referências do Git
As referências estão localizadas na pasta .git/refs:
🔵 Dando nomes aos commits
Podemos associar qualquer hash de commit a um nome arbitrário localizado em .git/refs/heads, por exemplo:
Agora, vamos usar o git log usando a nova referência:
Ainda melhor, o Git fornece o comando plumbingupdate-ref, para que possamos usá-lo para atualizar a associação de um commit a uma referência:
Parece familiar, não é mesmo? Sim, estamos falando de branches.
🔵 Branches
Branches são referências que apontam para um commit específico.
Como as branches representam o comando update-ref, o hash do commit pode ser alterado a qualquer momento, ou seja, uma referência de branch é mutável.

Por um momento, vamos pensar em como funciona um git log sem argumentos:
🤔 Hmmm...
Como o Git sabe que minha branch atual é a "main"?
🔵 HEAD
A referência HEAD está localizada em .git/HEAD. É um único arquivo que aponta para uma referência de cabeça (branch):
Da mesma forma, usando um comando porcelain:
Usando o comando plumbingsymbolic-ref, podemos manipular para qual branch a HEAD aponta:
### Verificar a branch atual
Assim como update-ref nas branches, podemos atualizar a HEAD usando symbolic-ref a qualquer momento.

Na imagem abaixo, vamos mudar nossa HEAD da branch main para a branch fix:

Sem argumentos, o comando git log percorre o commit raiz que é referenciado pela branch atual (HEAD):
Até agora, aprendemos a arquitetura e os principais componentes do Git, juntamente com os comandos plumbing, que são mais baixo nível.
Agora é hora de associar todo esse conhecimento com os comandos de alto nível que usamos diariamente.
🍽️ Porcelain, os comandos de alto nível
O Git traz mais comandos de alto nível que podemos usar sem a necessidade de manipular objetos e referências diretamente.
Esses comandos são chamados de comandos porcelain.
🔵 git add
O comando git add recebe arquivos no diretório de trabalho como argumentos, salva-os como blobs no banco de dados e os adiciona ao index.

Em resumo, git add:
executa
hash-objectpara cada arquivo argumentoexecuta
update-indexpara cada arquivo argumento
🔵 git commit
git commit recebe uma mensagem como argumento, agrupa todos os arquivos previamente adicionados ao index e cria um objeto commit.
Primeiro, ele executa write-tree:

Em seguida, ele executa commit-tree:

🕸️ Manipulando ponteiros no Git
Os seguintes comandos porcelain são amplamente utilizados, manipulando as referências do Git nos bastidores.
Supondo que acabamos de clonar um projeto onde a HEAD está apontando para a branch main, que aponta para o commit C1:

Como podemos criar uma nova branch a partir da HEAD atual e mover a HEAD para esta nova branch?
🔵 git checkout
Ao usar o git checkout com a opção -b, o Git criará uma nova branch a partir da atual (HEAD) e moverá a HEAD para esta nova branch.
### HEAD
### Cria uma nova branch "fix" usando o mesmo SHA-1 de referência
#### da HEAD atual
### HEAD
Qual comando plumbing é responsável por mover a HEAD? Exatamente, symbolic-ref.

Em seguida, fazemos algum trabalho na branch fix e depois executamos git commit, que adicionará um novo commit chamado C3:

Ao executar git checkout, podemos alternar a HEAD entre diferentes branches:

Às vezes, podemos querer mover o commit para o qual uma branch aponta.
Já sabemos que o comando plumbingupdate-ref faz isso:
Na linguagem do porcelain, apresento a você o git reset.
🔵 git reset
O comando porcelaingit reset executa internamente o update-ref, então só precisamos fazer:
Mas como o Git sabe qual branch mover? Bem, o git resetmove a branch para a qual a HEAD está apontando.

E quando há diferenças entre as revisões? Ao usar o reset, o Git move o ponteiro mas mantém todas as diferenças na área de preparação (index).
Verificando com git status:
()
A revisão do commit foi alterada nabranch fixe todas as diferenças forammovidas para o index.
Ainda assim, o que devemos fazer se quisermos resetar E descartar todas as diferenças? Basta adicionar a opção --hard:

Ao usar git reset --hard, quaisquer diferenças entre as revisões serão descartadas e elas não aparecerão no index.
💡 Dica de ouro sobre mover uma branch
Caso queiramos executar o plumbing
update-ref em outra branch, não é necessário fazer checkout da branch como é necessário no git reset.
Podemos usar o comando porcelaingit branch -f source target:
Nos bastidores, ele executa um git reset --hard na branch de origem. Vamos verificar para qual commit a branch main está apontando:
Também confirmamos que a branch fix ainda está apontando para o commit 369cd:
Fizemos um "git reset" sem mover a HEAD!

Não é raro, em vez de mover um ponteiro de branch, queremos aplicar um commit específico à branch atual.
Conheça o cherry-pick.
🔵 git cherry-pick
cherry-pick é um comando porcelain que nos permite aplicar um commit arbitrário na branch atual.
Considere o seguinte cenário:

main aponta para C3 - C2 - C1
fix aponta para C5 - C4 - C2 - C1
HEAD aponta para fix
Na branch fix, estamos sem o commit C3, que está sendo referenciado pela branch main.
Podemos aplicá-lo executando git cherry-pick C3:

Observe que:
o commit C3 será clonado em um novo commit chamado C3'
esse novo commit fará referência ao commit C5
fix moverá o ponteiro para C3'
HEAD continua apontando para fix
Depois de aplicar as alterações, o grafo será representado da seguinte forma:

Existe outra maneira de mover o ponteiro de uma branch. Consiste em aplicar um commit arbitrário de outra branch, mas mesclar as diferenças, se necessário.
Você não está errado, estamos falando de git merge aqui.
🔵 git merge
Vamos descrever o seguinte cenário:

main aponta para C3 - C2 - C1
fix aponta para C4 - C3 - C2 - C1
HEAD aponta para main
Queremos aplicar a branch fix na branch atual (main), também conhecido como realizar um git merge fix.
Observe que a branch fix contém todos os commits pertencentes à branch main (C3 - C2 - C1), tendo apenas um commit à frente da main (C4).
Nesse caso, a branch main será "encaminhada", apontando para o mesmo commit da branch fix.
Esse tipo de merge é chamado de fast-forward, como descrito na imagem abaixo:

Quando o fast-forward não é possível
Às vezes, a estrutura atual do nosso estado na árvore não permite o fast-forward. Veja o cenário abaixo:

Nesse caso, a branch que será mesclada - branch fix no exemplo acima - não contém um ou mais commits da branch atual (main): o commit C3.
Portanto, o fast-forward não é possível.
No entanto, para que a mesclagem seja bem-sucedida, o Git realiza uma técnica chamada Snapshotting, composta pelas seguintes etapas.
Prime
iro, o Git busca o próximo parente comum entre as duas branches, neste exemplo, o commit C2.

Em segundo lugar, o Git tira um snapshot do target, que é o commit da branch C3:

Terceiro, o Git tira um snapshot do source, que é o commit da branch C5:

Por fim, o Git cria automaticamente um commit de mesclagem (C6) e o aponta para dois pais respectivamente: C3 (target) e C5 (source):

Você já se perguntou por que sua árvore Git exibe alguns commits que foram criados automaticamente?
Não se engane, esse processo de mesclagem é chamado de mesclagem de três vias, ou three-way merge!

A seguir, vamos explorar outra técnica de mesclagem em que o fast-forward não é possível, mas, em vez de snapshotting e commit automático de mesclagem, o Git aplica as diferenças em cima da branch source.
Sim, esse é o git rebase.
🔵 git rebase
Considere a seguinte imagem:

main aponta para C3 - C2 - C1
fix aponta para C5 - C4 - C2 - C1
HEAD aponta para fix
Queremos rebase a branch main na branch fix, executando git rebase main. Mas como funciona o git rebase?
👉 git reset
Primeiro, o Git executa um git reset main, onde a branch fix apontará para o mesmo ponteiro da branch main: C3 - C2 - C1.

Neste momento, os commits C5 - C4 não têm referências.
👉 git cherry-pick
Em segundo lugar, o Git executa um git cherry-pick C5 na branch atual:

Observe que, durante o processo de cherry-pick, os commits cherry-pickados são clonados, portanto, o hash final será alterado: C5 - C4 se torna C5' - C4'.
Depois do cherry-pick, podemos ter o seguinte cenário:

👉 git reset novamente
Por último, o Git realizará um git reset C5', para que o ponteiro da branch fix seja movido de C3 para C5'.
O processo de rebase está concluído.

Até agora, trabalhamos com branches locais, ou seja, em nossa máquina. Hora de aprender como trabalhar com branches remotas, que estão sincronizadas com repositórios remotos na internet.
🌐 Branches remotas
Para trabalhar com branches remotas, precisamos adicionar um remote ao nosso repositório local, usando o comando porcelaingit remote.
Os remotes estão localizados na pasta .git/refs/remotes:
🔵 Buscar do remoto
Como sincronizar a branch remota com nossa branch local?
O Git fornece duas etapas:
👉 git fetch
Usando o comando porcelaingit fetch origin main, o Git fará o download da branch remota e a sincronizará com uma nova branch local chamada origin/main, também conhecida como branch upstream.

👉 git merge
Após buscar e sincronizar a branch upstream, podemos executar um git merge origin/main e, como a upstream está à frente da nossa branch local, o Git aplicará com segurança um merge fast-forward.

No entanto, fetch + merge pode ser repetitivo, pois estaríamos sincronizando as branches local/remota várias vezes ao dia.
Mas hoje é nosso dia de sorte, e o Git fornece o comando git pull, que realiza o fetch + merge em nosso nome.
👉 git pull
Com o git pull, o Git executará o fetch (sincronizar o remoto com a branch upstream) e, em seguida, mesclará a branch upstream na branch local.

Ok, vimos como baixar/transferir alterações do remoto. Por outro lado, como enviar alterações locais para o remoto?
🔵 Enviar para o remoto
O Git fornece um comando porcelain chamado git push:
👉 git push
Ao executar git push origin main, primeiro o Git enviará as alterações para o remoto:

Em seguida, o
Git mesclará a branch upstream origin/main com a branch local main:

No final do processo de push, temos a seguinte imagem:

Onde:
O remoto foi atualizado (alterações locais enviadas para o remoto)
main aponta para C4
origin/main aponta para C4
HEAD aponta para main
🔵 Dando nomes imutáveis para commits
Até agora, aprendemos que as branches são simplesmente referências mutáveis para commits, é por isso que podemos mover o ponteiro de uma branch a qualquer momento.
No entanto, o Git também fornece uma maneira de dar referências imutáveis, que não podem ter seus ponteiros alterados (a menos que você as exclua e as crie novamente).
As referências imutáveis são úteis quando queremos rotular/marcar commits que estão prontos para algum lançamento de produção, por exemplo.
Sim, estamos falando das tags.
👉 git tag
Usando o comando porcelaingit tag, podemos dar nomes aos commits, mas não podemos executar reset ou qualquer outro comando que possa alterar o ponteiro.

É bastante útil para a versão de lançamentos. As tags estão localizadas na pasta .git/refs/tags:
Se quisermos alterar o ponteiro da tag, precisamos excluí-la e criar outra com o mesmo nome.
💡 Git reflog
Por último, mas não menos importante, há um comando chamado git reflog que mantém todas as alterações que fizemos em nosso repositório local.
É bastanteútil se quisermos voltar e avançar na linha do tempo do Git. Juntamente com reset, cherry-pick e similares, é uma ferramenta poderosa se quisermos dominar o Git.
Conclusão
Que jornada longa!
Este artigo foi um pouco longo, mas pude abordar os principais tópicos que considero importantes para entender sobre o Git.
Espero que, depois de ler este artigo, você se sinta mais confiante ao usar o Git, resolver conflitos diários e situações complicadas durante um processo de merge/rebase.
Siga-me no Twitter e confira meu blog leandronsp.com, onde também escrevo alguns artigos técnicos.
Até mais!
Este artigo é uma tradução by ChatGPT do meu artigo original Git fundamentals, a complete guide.