🧭 Uma bússola dentro do Git: `HEAD`

Entendendo o HEAD 🧭

Vou representar Git como um conjunto de nós que apontam uns para os outros, assim a explicação fica mais simples. Esses nós seriam os commits (Figura 1).

Grafo de 3 commits

Figura 1 - Árvore de 3 commits

O HEAD é simplesmente uma referência, um ponteiro, para um commit (geralmente o último feito, na Figura 1 é 75e274c).

Quando o commit para o qual ele aponta não é o atual (isto é, NÃO É o último commit feito, NÃO é o topo do branch) isso se chama detached HEAD (ou HEAD desacoplado, Figura 2).

HEAD detached

Figura 2 - HEAD detached

Por ser possível "desacoplar", nós podemos movimentar o HEAD para qualquer commit que a gente consiga identificar. Dois comandos que permitem fazer essa movimentação são:

  • git checkout <commit-hash>
  • git switch -d <commit-hash>

Em resumo, o HEAD fornece uma abstração, um nome, para um commit e permite fazer movimentações e operações com ele de forma mais fluida.

🔎 Verificando para onde HEAD aponta

Tá. Eu entendi o que é o HEAD, mas como eu vejo para onde ele está apontando?

Bem, isso é possível por meio de vários comandos, entre eles:

  • git log -1 (que mostra somente para onde HEAD aponta)
  • git reflog
  • git show HEAD

Que resultam em representações semelhantes a

941fb2b (HEAD -> main, origin/main) adiciona a feature

Nessa mensagem é possível ver HEAD -> main (HEAD aponta para main, que é uma branch).

Pode parecer estranho que, quando defini HEAD, não falei direito sobre branches e HEAD tá apontando para uma agora.

Long-story short (já que o escopo do post não é sobre branchs): branches também são ponteiros e também apontam para commits. Então HEAD apontar para main é o mesmo que apontar para o commit que main aponta.

Dentro do .git

Também dá para ver o HEAD acessando o diretório .git e acesando o arquivo de nome HEAD.

cat .git/HEAD -> ref: refs/heads/main

Ele aponta para main que é uma branch. Para ver para onde main aponta, fazemos

cat .git/refs/heads/main -> 75e274cf0ecdb3141e447d21e987a510b6f47f0b

Pronto: main aponta para um commit, logo HEAD aponta para um commit quando aponta para main.

Se o HEAD estiver detached ele vai apontar diretamente para o commit

cat .git/HEAD -> 75e274cf0ecdb3141e447d21e987a510b6f47f0b

🗺 Usando HEAD^ e HEAD~n para voltar no tempo

Agora, HEAD^ e HEAD~n. Esses operadores podem ser usados em branches e commits também, mas eu vejo muito mais casos em que eles vêm acompanhando o HEAD.

O HEAD^ faz referencia ao commit pai do HEAD (ou seja, o commit que vem antes). Voltando à Figura 1, o commit pai de 75e274c é o b98136f. Veja a Figura 3:

Exemplo de uso de HEAD^ e HEAD^^

Figura 3 - HEAD^ e HEAD^^

Só que ficar usando vários ^ em sequencia fica inconveniente bem rápido. Para voltar um número maior de commits, temos o HEAD~n.

Nesse operador, n é o número de commits que você quer voltar (HEAD^ e HEAD~1 são equivalentes).

Voltando commits com HEAD~{n}

Figura 4 - Voltando commits com HEAD~n

Ambos são muitos úteis quando precisamos voltar nos commits, por exemplo em um git reset HEAD~3.


Basicamente é isso. Não é muito, e nem perto de ser tudo, mas é um pouquinho de algo muito interessante e que ajuda a ter uma experiência melhor com o Git que é uma ferramenta incrível.

Complementando, já escrevi um post mais detalhado sobre o assunto:

E aproveitando, seguem outros posts que escrevi sobre o Git (e que de certa forma complementam o primeiro, já que te dão uma visão mais clara e ampla sobre como funciona um repositório do Git):

Por fim, tem também este comentário sobre reflog, esta sim a verdadeira "máquina do tempo" do Git :-)

Apenas complementando sobre HEAD^ e HEAD~. É verdade que HEAD^^ é equivalente a HEAD~2, mas na verdade existe uma diferença mais fundamental sobre o funcionamento deles.

Para explicar a diferença, temos que lembrar que um commit pode ter mais de um pai, quando este é o resultado de um merge que não teve fast-forward.

O mais comum é quando eu faço git merge branch e não há fast-forward, pois aí o resultado é um commit com dois pais: o commit para onde o HEAD apontava e o commit para onde o branch aponta. Mas nada impede que eu faça algo como git merge branch1 branch2 branch3, e neste caso o commit resultante poderá ter até quatro pais (caso não seja possível fazer fast-forward em nenhum dos branches).

E é aí que usar ^ ou ~ começa a fazer diferença. Pois o ^N é usado para obter o enésimo pai, enquanto que ~N é usado para o enésimo ancestral.

Para entender melhor, segue um exemplo retirado da documentação oficial:

G   H   I   J
 \ /     \ /
  D   E   F
   \  |  / \
    \ | /   |
     \|/    |
      B     C
       \   /
        \ /
         A

No caso, o commit A tem dois pais: B e C. O commit B tem 3 pais: D, E e F, e assim por diante.

Se eu fizer A^, A^1 ou A~1, o resultado é B. Agora, como eu chego em C a partir de A? Neste caso, eu uso A^2, ou seja, o "segundo pai de A".

Já se eu fizer A~2 ou A^^ (ou ainda A^1^1), eu chego em D. E para chegar em E, tenho que fazer algo como B^2 (o segundo pai de B), ou ainda A^^2 (pois A^ equivale a B, e depois com ^2 eu chego em E).

Essa é a diferença: A^2 é o segundo pai de A, enquanto A~2 é o ancestral de A "voltando duas gerações" (e considerando sempre o primeiro pai de cada geração).

Ainda usando o mesmo exemplo: para chegar em F, podemos fazer B^3 (o terceiro pai de B), ou A^^3 (o terceiro pai do primeiro pai de A). Para chegar em H, posso fazer algo como A~2^2 (pois com A~2 eu chego em D, e depois ^2 pega o segundo pai de D, que é H).

Enfim, esses atalhos existem porque fazem coisas diferentes: ^ olha apenas uma geração acima, e procura pelo enésimo pai (ou o primeiro, se nenhum número for especificado). Já o ~ vai subindo na "árvore genealógica", podendo olhar várias gerações anteriores.

O fato de A^^ ser equivalente a A~2 é mera consequência desta definição. Mas o intuito original não foi um ser atalho para o outro.

Poxa, muito obrigado por esses comentários. Eles complementam (e muito) o post. Vi que seu post sobre HEAD do Git foi de dois anos atrás, inclusive enquanto estudava me deparei com sua resposta do StackOverflow, obrigado de novo! Esse tipo de interação é muito legal e acredito que é um dos motivos pelos quais postar no TabNews agrega mais que em outros sites...