DEV Community

Cover image for [pt-BR] Fundamentos do Git, um guia completo
Leandro Proença
Leandro Proença

Posted on • Edited on

[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:



$ mkdir git-101
$ cd git-101


Enter fullscreen mode Exit fullscreen mode

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:



$ ls -F1 .git/

HEAD
config
description
hooks/
info/
objects/
refs/


Enter fullscreen mode Exit fullscreen mode

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:



$ find .git/objects

.git/objects
.git/objects/pack
.git/objects/info


Enter fullscreen mode Exit fullscreen mode

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 hash mapeia 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`:



$ echo -e "my precious" | openssl sha1
fa628c8eeaa9527cfb5ac39f43c3760fe4bf8bed


Enter fullscreen mode Exit fullscreen mode

Observação: Se você estiver usando o Linux, pode usar o comando sha1sum em vez de OpenSSL.

🔵 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:



$ echo -e "my precious" | openssl sha1
fa628c8eeaa9527cfb5ac39f43c3760fe4bf8bed

$ echo -e "no longer my precious" | openssl sha1
2e71c9ae2ef57194955feeaa99f8543ea4cd9f9f


Enter fullscreen mode Exit fullscreen mode

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 plumbing hash-object faz o trabalho:



$ echo "my precious" | git hash-object --stdin
8b73d29acc6ae79354c2b87ab791aecccf51701f


Enter fullscreen mode Exit fullscreen mode

Vamos comparar com a versão OpenSSL:



$ echo -e "my precious" | openssl sha1
fa628c8eeaa9527cfb5ac39f43c3760fe4bf8bed


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

Então:



$ echo -e "blob 12\0my precious" | openssl sha1
8b73d29acc6ae79354c2b87ab791aecccf51701f

$ echo "my precious" | git hash-object --stdin
8b73d29acc6ae79354c2b87ab791aecccf51701f


Enter fullscreen mode Exit fullscreen mode

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:



$ echo "my precious" | git hash-object --stdin -w
8b73d29acc6ae79354c2b87ab791aecccf51701f

$ find .git/objects
...
.git/objects/8b
.git/objects/8b/73d29acc6ae79354c2b87ab791aecccf51701f

### Ou, simplesmente
$ find .git/objects -type f
.git/objects/8b/73d29acc6ae79354c2b87ab791aecccf51701f


Enter fullscreen mode Exit fullscreen mode

o primeiro objeto

🔵 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 plumbing cat-file faz o trabalho, de forma que, dada uma chave, ele descomprime os dados compactados e obtém o conteúdo original:



$ git cat-file -p 8b73d29acc6ae79354c2b87ab791aecccf51701f
my precious


Enter fullscreen mode Exit fullscreen mode

No caso de você estar imaginando, isso mesmo, o Git é um banco de dados chave-valor!

nosql

🔵 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:

  1. Adicionar o blob a uma área de preparação (staging area)

  2. Agrupar todos os blobs na área de preparação em uma estrutura de árvore

  3. 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 plumbing update-index permite adicionar um blob à área de preparação (stage) e dar um nome a ele:



$ git update-index \
    --add \
    --cacheinfo 100644 \
    8b73d29acc6ae79354c2b87ab791aecccf51701f \
    index.txt


Enter fullscreen mode Exit fullscreen mode
  • --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 trabalho

  • o hash do blob

  • index.txt: um nome para o blob no índice

o índice do Git

Onde o Git armazena o índice?



$ cat .git/index

DIRCsҚjT¸zQp    index.txtÆ
                          7CJVVÙ


Enter fullscreen mode Exit fullscreen mode

No entanto, não é legível para humanos, está compactado usando Zlib.

Podemos adicionar quantos blobs quisermos ao índice, por exemplo:



$ git update-index {sha-1} f1.txt
$ git update-index {sha-1} f2.txt


Enter fullscreen mode Exit fullscreen mode

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 plumbing write-tree, o Git agrupa todos os blobs que foram adicionados ao índice e cria outro objeto na pasta .git/objects:



$ git write-tree
3725c9e313e5ae764b2451a8f3b1415bf67cf471


Enter fullscreen mode Exit fullscreen mode

Verificando a pasta .git/objects, observe que um novo objeto foi criado:



$ find .git/objects

### O novo objeto
.git/objects/37
.git/objects/37/25c9e313e5ae764b2451a8f3b1415bf67cf471

### O blob criado anteriormente
.git/objects/8b
.git/objects/8b/73d29acc6ae79354c2b87ab791aecccf51701f


Enter fullscreen mode Exit fullscreen mode

Vamos recuperar o valor original usando cat-file para entender melhor:



### Usando a opção -t, obtemos o tipo do objeto
$ git cat-file -t 3725c9e313e5ae764b2451a8f3b1415bf67cf471
tree

$ git cat-file -p 3725c9e313e5ae764b2451a8f3b1415bf67cf471
100644 blob 8b73d29acc6ae79354c2b87ab791aecccf51701f index.txt


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode
  • 100644: o cacheinfo

  • blob: o tipo do objeto

  • o hash do blob

  • o nome do blob

o objeto de árvore

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 plumbing commit-tree recebe uma árvore, uma mensagem de commit e cria outro objeto na pasta .git/objects:



$ git commit-tree 3725c -m 'meu commit precioso'
505555f4f07d90ae14a0f2e67cba7f7b9af539ee


Enter fullscreen mode Exit fullscreen mode

Que tipo de objeto é esse?



$ find .git/objects
...
.git/objects/50
.git/objects/50/5555f4f07d90ae14a0f2e67cba7f7b9af539ee

### cat-file
$ git cat-file -t 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
commit


Enter fullscreen mode Exit fullscreen mode

E qual é o seu valor?



$ git cat-file -p 505555f4f07d90ae14a0f2e67cba7f7b9af539ee

tree 3725c9e313e5ae764b2451a8f3b1415bf67cf471
author leandronsp <leandronsp@example.com> 1678768514 -0300
committer leandronsp <leandronsp@example.com> 1678768514 -0300

meu commit precioso


Enter fullscreen mode Exit fullscreen mode
  • tree 3725c: a árvore referenciada

  • autor/confirmador

  • a mensagem do commit meu commit precioso

a árvore de commits

🤯 OMG! Estou vendo um padrão aqui?

Além disso, os commits podem fazer referência a outros commits:



$ git commit-tree 3725c -p 50555 -m 'segundo commit'
5ea578a41333bae71527db537072534a199a0b67


Enter fullscreen mode Exit fullscreen mode

Onde a opção -p permite fazer referência a um commit pai:



$ git cat-file -p 5ea578a41333bae71527db537072534a199a0b67

tree 3725c9e313e5ae764b2451a8f3b1415bf67cf471
parent 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
author leandronsp <leandronsp@gmail.com> 1678768968 -0300
committer leandronsp <leandronsp@gmail.com> 1678768968 -0300

segundo commit


Enter fullscreen mode Exit fullscreen mode

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:



$ git cat-file -p <sha1-do-primeiro-commit>
$ git cat-file -p <sha1-da-árvore-do-primeiro-commit>
$ git cat-file -p <sha1-do-commit-pai-do-primeiro-commit>
$ git cat-file -p <sha1-do-commit-pai>
...


Enter fullscreen mode Exit fullscreen mode

E assim por diante. Bem, você chegou ao ponto.

🔵 Log for the rescue

O comando porcelain git log resolve esse problema, percorrendo todos os commits, seus parents e árvores, nos dando uma perspectiva de uma linha do tempo do nosso trabalho.



$ git log 5ea57

commit 5ea578a41333bae71527db537072534a199a0b67
Author: leandronsp <leandronsp@gmail.com>
Date:   Seg Mar 13 22:42:48 2023 -0300

    segundo commit

commit 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
Author: leandronsp <leandronsp@gmail.com>
Date:   Seg Mar 13 22:35:14 2023 -0300

    meu commit precioso


Enter fullscreen mode Exit fullscreen mode

🤯 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.

o grafo do Git

  • 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:



$ find .git/refs

.git/refs/
.git/refs/heads
.git/refs/tags


Enter fullscreen mode Exit fullscreen mode

🔵 Dando nomes aos commits

Podemos associar qualquer hash de commit a um nome arbitrário localizado em .git/refs/heads, por exemplo:



echo 5ea578a41333bae71527db537072534a199a0b67 > .git/refs/heads/test


Enter fullscreen mode Exit fullscreen mode

Agora, vamos usar o git log usando a nova referência:



$ git log test

commit 5ea578a41333bae71527db537072534a199a0b67
Author: leandronsp <leandronsp@gmail.com>
Date:   Seg Mar 13 22:42:48 2023 -0300

    segundo commit

commit 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
Author: leandronsp <leandronsp@gmail.com>
Date:   Seg Mar 13 22:35:14 2023 -0300

    meu commit precioso


Enter fullscreen mode Exit fullscreen mode

Ainda melhor, o Git fornece o comando plumbing update-ref, para que possamos usá-lo para atualizar a associação de um commit a uma referência:



$ git update-ref refs/heads/test 5ea578a41333bae71527db537072534a199a0b67


Enter fullscreen mode Exit fullscreen mode

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.

branches

Por um momento, vamos pensar em como funciona um git log sem argumentos:



$ git log

fatal: sua branch atual 'main' não tem nenhum commit ainda


Enter fullscreen mode Exit fullscreen mode

🤔 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):



$ cat .git/HEAD

ref: refs/heads/main


Enter fullscreen mode Exit fullscreen mode

Da mesma forma, usando um comando porcelain:



$ git branch
* main


Enter fullscreen mode Exit fullscreen mode

Usando o comando plumbing symbolic-ref, podemos manipular para qual branch a HEAD aponta:



$ git symbolic-ref HEAD refs/heads/test

### Verificar a branch atual
$ git branch
* test


Enter fullscreen mode Exit fullscreen mode

Assim como update-ref nas branches, podemos atualizar a HEAD usando symbolic-ref a qualquer momento.

symbolic ref

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

atualizando a head

Sem argumentos, o comando git log percorre o commit raiz que é referenciado pela branch atual (HEAD):



$ git log

commit 5ea578a41333bae71527db537072534a199a0b67 (HEAD -> test)
Author: leandronsp <leandronsp@gmail.com>
Date:   Ter Mar 14 01:42:48 2023 -0300

    segundo commit

commit 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
Author: leandronsp <leandronsp@gmail.com>
Date:   Ter Mar 14 01:35:14 2023 -0300

    meu commit precioso


Enter fullscreen mode Exit fullscreen mode

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.

git add

Em resumo, git add:

  1. executa hash-object para cada arquivo argumento

  2. executa update-index para 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:

commit write tree

Em seguida, ele executa commit-tree:

commit commit-tree



$ git commit -m 'mais um commit'

[test b77b454] mais um commit
 1 arquivo alterado, 1 exclusão(-)
 exclusão do modo 100644 index.txt


Enter fullscreen mode Exit fullscreen mode

🕸️ 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:

git clone

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
$ git branch
* main

### Cria uma nova branch "fix" usando o mesmo SHA-1 de referência
#### da HEAD atual
$ git checkout -b fix
Switched to a new branch 'fix'

### HEAD
$ git branch
* fix
main


Enter fullscreen mode Exit fullscreen mode

Qual comando plumbing é responsável por mover a HEAD? Exatamente, symbolic-ref.

git checkout -b fix

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

git commmit

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

git checkout ultimate

Às vezes, podemos querer mover o commit para o qual uma branch aponta.

Já sabemos que o comando plumbing update-ref faz isso:



$ git update-ref refs/heads/fix 356c2


Enter fullscreen mode Exit fullscreen mode

Na linguagem do porcelain, apresento a você o git reset.

🔵 git reset

O comando porcelain git reset executa internamente o update-ref, então só precisamos fazer:



$ git reset 356c2


Enter fullscreen mode Exit fullscreen mode

Mas como o Git sabe qual branch mover? Bem, o git reset move a branch para a qual a HEAD está apontando.

git reset

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).



$ git reset b77b


Enter fullscreen mode Exit fullscreen mode

Verificando com git status:



$ git status

On branch fix
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        another.html
        bye.html
        hello.html

nothing added to commit but untracked files present (use "git add" to track)


Enter fullscreen mode Exit fullscreen mode

A revisão do commit foi alterada na branch fix e todas as diferenças foram movidas para o index.

Ainda assim, o que devemos fazer se quisermos resetar E descartar todas as diferenças? Basta adicionar a opção --hard:

git reset 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 porcelain git branch -f source target:



$ git branch -f main b77b


Enter fullscreen mode Exit fullscreen mode

Nos bastidores, ele executa um git reset --hard na branch de origem. Vamos verificar para qual commit a branch main está apontando:



$ git log main --pretty=oneline -n1
b77b454a9a507f839880879a895ac4f241177a28 (main) another commit


Enter fullscreen mode Exit fullscreen mode

Também confirmamos que a branch fix ainda está apontando para o commit 369cd:



$ git log fix --pretty=oneline -n1
369cd96b1f1ef6fa7de1ff2ed12e15be979dcffa (HEAD -> fix, test) add files


Enter fullscreen mode Exit fullscreen mode

Fizemos um "git reset" sem mover a HEAD!

git branch -f

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:

cenário 42

  • 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:

cherry pick A

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:

cherry pick B

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:

outro 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:

fast forward

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:

cenário 44

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.

mesclagem common parent

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

snapshot do target

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

snapshot do source

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

commit de mesclagem

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!

mesclagem de três vias

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:

cenário 55

  • 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.

rebase:reset

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:

rebase:cherry-pick

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:

rebase-cherry-pick-b

👉 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.

rebase:finish

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 porcelain git remote.



$ git remote add origin git@github.com/myaccount/myrepo.git


Enter fullscreen mode Exit fullscreen mode

Os remotes estão localizados na pasta .git/refs/remotes:



$ find .git/refs
...
.git/refs/remotes/origin
.git/refs/remotes/origin/main


Enter fullscreen mode Exit fullscreen mode

🔵 Buscar do remoto

Como sincronizar a branch remota com nossa branch local?

O Git fornece duas etapas:

👉 git fetch

Usando o comando porcelain git 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.

fetch

👉 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.

fetch merge ff

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.

git pull

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:

push A

Em seguida, o

Git mesclará a branch upstream origin/main com a branch local main:

push B

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

push end

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 porcelain git tag, podemos dar nomes aos commits, mas não podemos executar reset ou qualquer outro comando que possa alterar o ponteiro.

git tag

É bastante útil para a versão de lançamentos. As tags estão localizadas na pasta .git/refs/tags:



$ find .git/refs

...
.git/refs/tags
.git/refs/tags/v1.0


Enter fullscreen mode Exit fullscreen mode

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.



$ git reflog

369cd96 (HEAD -> fix, test) HEAD@{0}: reset: moving to main
b77b454 (main) HEAD@{1}: reset: moving to b77b
369cd96 (HEAD -> fix, test) HEAD@{2}: checkout: moving from main to fix
369cd96 (HEAD -> fix, test) HEAD@{3}: checkout: moving from fix to main
369cd96 (HEAD -> fix, test) HEAD@{4}: checkout: moving from main to fix
369cd96 (HEAD -> fix, test) HEAD@{5}: checkout: moving from fix to main
369cd96 (HEAD -> fix, test) HEAD@{6}: checkout: moving from main to fix
369cd96 (HEAD -> fix, test) HEAD@{7}: checkout: moving from test to main
369cd96 (HEAD -> fix, test) HEAD@{8}: checkout: moving from main to test
369cd96 (HEAD -> fix, test) HEAD@{9}: checkout: moving from test to main
369cd96 (HEAD -> fix, test) HEAD@{10}: commit: add files
b77b454 (main) HEAD@{11}: commit: another commit
5ea578a HEAD@{12}:


Enter fullscreen mode Exit fullscreen mode

É 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.

Top comments (3)

Collapse
 
gpxlnx profile image
Guilherme Xavier

Excelente conteúdo Leandro. Uma verdadeira aula. Tenho lido seus artigos e tenho que te dar os parabéns. Tudo muito bem detalhado e com muita qualidade.

Collapse
 
leandronsp profile image
Leandro Proença

Obrigado pelo feedback, Guilherme!

Collapse
 
rabisquim profile image
Robson Paulo

Grato pela criação desse conteúdo que é importantíssimo.