Executando verificação de segurança...
15
kht
7 min de leitura ·

O que é o HEAD do Git?

Este post é mais um da série sobre Git que estou escrevendo. Os anteriores são:

Primeiramente, temos que entender o que é um repositório do Git. E para isso, este artigo é bem esclarecedor, pois explica que no fundo um repositório do Git é um DAG (Directed Acyclic Graph - Grafo Acíclico Dirigido). Não vou entrar nos detalhes da definição matemática (que pode ser vista aqui), mas basicamente podemos pensar em um repositório do Git como um conjunto de nós apontando uns para os outros.

Alguns desses nós são os commits, e eles apontam para outros nós. Alguns desses outros nós são outros commits (no caso, cada commit aponta para o(s) seu(s) "pai(s)" - sim, commits que são resultados de merges podem ter mais de um "pai"), outros nós são as árvores de diretórios (que por sua vez apontam para os arquivos e subdiretórios, etc). Se quiser se aprofundar nos detalhes internos desta estrutura, leia "Como o Git grava os conjuntos de modificações do repositório?":

repositório Git

Todos os nós são identificados por seu hash (aqueles "códigos gigantes" como por exemplo 618ce9936d015012ae55b95ac6afcc286d02682d). Porém, alguns commits podem ter um "nome"/label associado: são os branches. Sim, no fundo, um branch é simplesmente um nome associado a um commit específico (ou, como é mais comumente definido, um ponteiro para um commit).

Por exemplo, no diagrama abaixo, o branch master aponta para o "segundo commit", o branch new_branch aponta para o "terceiro commit", e o "primeiro commit" não tem nenhum branch apontando para ele.

+-----------------+     +----------------+
| primeiro commit | <-- | segundo commit |  <-- master
+-----------------+     +----------------+
                                 ↑
                        +-----------------+
                        | terceiro commit | <-- new_branch
                        +-----------------+

O HEAD, no caso, é simplesmente um ponteiro para um commit específico (que pode ou não estar sendo apontado por um branch). É claro que, para simplificar, costuma-se dizer que o HEAD aponta para o "branch atual", mas na verdade ele pode apontar para qualquer commit, inclusive algum que não esteja sendo referenciado por nenhum branch (mais detalhes abaixo, na seção "HEAD não é exatamente o branch atual").

De qualquer forma, é assim que o Git sabe onde adicionar o próximo commit. Por exemplo, se meu repositório estivesse como o diagrama acima, e eu criasse um novo commit, onde ele seria adicionado?

Se o HEAD estivesse apontando para o branch master, ele seria adicionado depois do "segundo commit":

Adicionar commit no branch master
+-----------------+     +----------------+     +---------------+
| primeiro commit | <-- | segundo commit | <-- | quarto commit | <-- master (HEAD)
+-----------------+     +----------------+     +---------------+
                                 ↑
                        +-----------------+
                        | terceiro commit | <-- new_branch
                        +-----------------+

Mas se o HEAD estivesse apontando para o branch new_branch, ele seria adicionado depois do "terceiro commit":

Adicionar commit no branch new_branch
+-----------------+     +----------------+
| primeiro commit | <-- | segundo commit |  <-- master
+-----------------+     +----------------+
                                 ↑
                        +-----------------+
                        | terceiro commit |
                        +-----------------+
                                 ↑
                        +-----------------+
                        |  quarto commit  | <-- new_branch (HEAD)
                        +-----------------+

E para mudar o HEAD (ou seja, para mudar o branch para o qual ele aponta), usamos git checkout [branch]. Portanto, git checkout master (ou git switch master, para Git >= 2.23.0) faz o HEAD apontar para o branch master, enquanto git checkout new_branch (ou git switch new_branch, para Git >= 2.23.0) faz o HEAD apontar para o branch new_branch.


Existem várias formas de ver qual é o valor do HEAD. Uma é verificar o conteúdo do arquivo .git/HEAD (que fica na raiz do repositório). Por exemplo, criei um repositório de teste, fiz um commit e o conteúdo deste arquivo é:

ref: refs/heads/master

Ou seja, o HEAD está apontando para o branch master. No caso, ele está apontando para outro arquivo interno, que seria .git/refs/heads/master. E se verificarmos o conteúdo deste arquivo, veremos o hash do respectivo commit:

618ce9936d015012ae55b95ac6afcc286d02682d

Claro que também dá para usar git status (que na primeira linha diz "On branch master"), ou git show HEAD, que retorna várias informações, inclusive o hash do commit para o qual o HEAD aponta, ou git log -1, etc.


E ao mudar para outro branch (git checkout new_branch, ou git switch new_branch, para Git >= 2.23.0), o conteúdo do arquivo .git/HEAD passou a ser:

ref: refs/heads/new_branch

Ou seja, agora o HEAD passou a apontar para o branch new_branch (e no arquivo .git/refs/heads/new_branch pode-se ver o hash do commit para o qual este branch está apontando). E novos commits serão adicionados neste branch.


HEAD não é exatamente o "branch atual"

Como já dito acima, o HEAD não aponta para o "branch atual". Esta é uma simplificação para o caso de uso mais comum, quando o HEAD está apontando para um branch, no qual novos commits podem ser adicionados.

Mas nada impede que o HEAD aponte para um commit que não está sendo apontado por nenhum branch. Por exemplo, se eu tiver este repositório:

+-----------------+     +----------------+
|     618ce99     |     |     4dc1fe0    |
| primeiro commit | <-- | segundo commit |  <-- master (HEAD)
+-----------------+     +----------------+

O HEAD aponta para o branch master (cujo hash é 4dc1fe0 - estou usando o "hash encurtado" para simplificar). Se eu fizer git checkout 618ce99 (ou git switch 618ce99 --detach, para Git >= 2.23.0), vai aparecer a seguinte mensagem:

Note: switching to '618ce99'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 618ce99 first

Isso quer dizer que agora estamos no estado detached: o HEAD aponta para um commit que não está sendo apontado por nenhum branch. Tanto que o conteúdo do arquivo .git/HEAD não é mais "refs: etc..." e sim o próprio hash do commit 618ce99. Ou seja, o HEAD não está apontando para nenhum branch.

Como a mensagem acima diz, ainda é possível adicionar commits e criar um novo branch a partir deste commit, ou simplesmente descartar tudo sem impactar os demais branches já existentes. Aliás, é isso que acontece se você fizer git checkout qualquer_outro_branch (ou git switch qualquer_outro_branch, para Git >= 2.23.0): as alterações feitas no detached HEAD serão descartadas (embora ainda seja possível recuperá-las).

Obs: repare que as mensagens acima dizem para usar git switch, pois estou usando uma versão acima da 2.23.0. Para versões anteriores, a mensagem será diferente, já que o comando switch não existia.


Ou seja, o HEAD não necessariamente aponta para um branch. O fato deste ser o caso de uso mais comum não quer dizer que seja o único possível. Mas talvez seja só excesso de pedantismo da minha parte, afinal todo mundo entende quando você explica que o HEAD é um ponteiro para o "branch atual" (e o detached HEAD seria um "caso especial").

De qualquer forma, o HEAD sempre aponta para um commit específico, seja diretamente, no caso de ser detached, ou indiretamente, no caso dele estar apontando para um branch.

Sim, o apontamento para o branch é indireto: o HEAD aponta para um branch, que por sua vez aponta para um commit. Usando o mesmo repositório como exemplo:

+-----------------+     +----------------+
|     618ce99     |     |     4dc1fe0    |
| primeiro commit | <-- | segundo commit |  <-- master (HEAD)
+-----------------+     +----------------+

O conteúdo do arquivo .git/HEAD é ref: refs/heads/master, e o conteúdo do arquivo .git/refs/heads/master é o hash do commit 4dc1fe0.

Ao adicionar um novo commit no master, o repositório fica assim:

+-----------------+     +----------------+     +-----------------+
|     618ce99     |     |     4dc1fe0    |     |     630d06b     |
| primeiro commit | <-- | segundo commit | <-- | terceiro commit |  <-- master (HEAD)
+-----------------+     +----------------+     +-----------------+

E no caso, o conteúdo do arquivo .git/HEAD continua sendo ref: refs/heads/master, e somente o conteúdo do arquivo .git/refs/heads/master é modificado, para conter o hash do novo commit criado (630d06b). Em outras palavras, o branch é modificado, o HEAD não (ele continua apontando para o mesmo branch).

Talvez seja só meu pedantismo falando mais alto de novo, pois se dissermos que o HEAD está apontando para o "commit atual" de determinado branch, todo mundo entende. Mas de forma geral, o HEAD aponta para o branch, e o commit para o qual o branch está apontando é que muda a cada commit.

Na verdade, isso vale para qualquer operação que mude o commit para o qual o branch aponta. Por exemplo, se eu fizer git reset --hard 618ce99, o master apontará para o primeiro commit e o repositório ficará assim:

+-----------------+
|     618ce99     |
| primeiro commit |  <-- master (HEAD)
+-----------------+

Nesse caso, os demais commits ficarão dangling (a menos, é claro, que que eles sejam alcancáveis através de algum branch: por exemplo, se tiver um branch apontando para o terceiro commit, tanto este quanto o segundo serão alcancáveis e não serão dangling). E novamente, o conteúdo do arquivo .git/HEAD continua sendo ref: refs/heads/master, e somente o conteúdo do arquivo .git/refs/heads/master é modificado, pois agora contém o hash do commit 618ce99.

Baseado na minha resposta no Stack Overflow

Carregando publicação patrocinada...
1

obg, entendi melhor sobre HEAD, que pra mim nunca fazia sentido, mt obg, agora ficou claro.
como vc tinha dito, todo mundo entendia que o HEAD apontava para o branch atual, porém isso não está certo, pq se a branch estiver apontando para um commit anterior e antigo, o HEAD estaria "desatualizado", enfim.