As três árvores do Git

Quando trabalhamos com os comandos reset e checkout do Git, podemos utilizar um modelo mental em que o Git gerencia três árvores: HEAD, Index e Working Directory. Neste texto pretendo mostrar como estas árvores afetam nosso dia-a-dia com a ferramenta e como manipulá-las.

A HEAD é um apontador para o branch atual, que por sua vez é um apontador para o último commit naquele branch.

Para exemplificar, imagine que você acabou de clonar um repositório e este tem os seguintes branches: master e branch1. Assim que você o clona, você está no branch master, portanto a sua HEAD aponta para o branch master.

C  < master  < HEAD
|
B   D  < branch1
| _/
|/
A

E no momento que executamos o comando git checkout branch1, estamos mudando o apontamento da HEAD para o branch branch1.

C  < master
|
B   D  < branch1  < HEAD
| _/
|/
A

Index

O Index é seu próximo commit, ou seja, os arquivos adicionados na área de stagging com o comando git add e antes do comando git commit. O comando git status exibe os arquivos que irão para o próximo commit.

Working Directory

Também referenciado como working tree, o working directory representa os arquivo em seu diretório, na qual você os edita para adicioná-los ao seu index. Então ao mudar de branch com o comando git checkout, o Git irá mudar o apontamento da HEAD e irá modificar os arquivos em seu diretório.

Fluxo de trabalho

O fluxo de trabalho consiste em modificar um arquivo, que modifica seu working directory, adicioná-lo ao seu index (git add) e realizar o commit (git commit), que irá mudar o apontamento do seu branch atual, ou seja, o branch em que a HEAD está apontando.

Por fim, ao mudar de branch (mudar o apontamento da HEAD) com o comando git checkout, o Git irá atualizar seu working directory com os arquivos do branch.

Working Directory      Index               HEAD
       |                 |                   |
       | <---------- git checkout ---------- |
       |                 |                   |
       | --- git add --> |                   |
       |                 |                   |
       |                 | -- git commit --> |

Detalhando o processo

A partir do momento em que iniciamos um repositório no Git com git init, o branch master é criado automaticamente e não temos nenhum commit, assim a HEAD aponta para o branch master, que não aponta para um commit específico.

? < master
      ^
    head

Então você começa seu trabalho e cria um novo arquivo index.html. Com isso você modificou seu working directory.

Working Directory      Index      HEAD
       |
   index.html

Ao executar o comando git add index.html, estamos colocando o arquivo index.html em nosso index.

Working Directory      Index      HEAD
                         |
                     index.html

Se adicionarmos outro arquivo chamado footer.html, nosso working directory é modificado novamente.

Working Directory      Index      HEAD
        |                |
   footer.html       index.html

Em seguida, para adicionar este arquivo em nosso próximo commit, devemos adicioná-lo em nosso index com o comando git add footer.html.

Working Directory      Index      HEAD
                         |
                     index.html
                     footer.html

Agora é hora de realizarmos nosso commit com o comando git commit -m "Mensagem".

Working Directory      Index      HEAD
                                   |
                                96b7f8c
                                   |
                                index.html
                                footer.html

reset

Durante o detalhamento do processo já vimos como modificar as três árvores, porém existem outros comandos que as modificam. O primeiro comando que veremos é o reset, que tem três modos.

reset –mixed

Imagine que você tem alguns commits e sua HEAD está apontando para o último commit.

5d50d76  < master  < HEAD
   |
   |
f831ed8
$ git log --children --decorate
commit 51d7b8b43f6a7742f6a015dec50076d934f1e79f (HEAD -> master)
Author: João Vitor Retamero <joaovretamero@gmail.com>
Date:   Sat Nov 2 17:54:34 2019 -0300

    Arquivo 3

commit 5d50d7664780f91c91d480e1a6b185fdc71c7733 51d7b8b43f6a7742f6a015dec50076d934f1e79f
Author: João Vitor Retamero <joaovretamero@gmail.com>
Date:   Sat Nov 2 17:54:10 2019 -0300

    Arquivo 2

commit f831ed83ecc8e25cd4103fecdcb61ce2649cad93 5d50d7664780f91c91d480e1a6b185fdc71c7733
Author: João Vitor Retamero <joaovretamero@gmail.com>
Date:   Sat Nov 2 17:53:55 2019 -0300

    Arquivo 1

Usando o comando git reset --mixed 5d50d76, você mudará o apontamento do branch em que a HEAD aponta.

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    arquivo3.txt

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

O comando git reset sem argumentos é equivalente a executar o comando git reset --mixed

51d7b8b
   |
   |
5d50d76  < master  < HEAD
   |
   |
f831ed8

Um ponto interessante sobre modo --mixed é que além de modificar a HEAD, também modifica o Index, então o comando desfaz seu commit (conceitualmente) e desfaz o Index, ou seja, te deixa e um estado onde você acabou de fazer sua alteração no working directory.

reset –soft

Se executar o comando git reset --soft 5d50d76, o Git irá modificar a HEAD mas não o Index, ou seja, te deixa em um estado onde você modificou seu working directory e adicionou os arquivos no Index.

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   arquivo3.txt

reset –hard

Os dois modos anteriores modificam algumas coisas, mas não são perigosos por você ainda ter seu working directory preservado. O modo --hard irá alterar as três árvores do Git, então mudará sua HEAD, Index e working directory.

$ git reset --hard 5d50d76
HEAD is now at 5d50d76 Arquivo 2
$ git status
On branch master
nothing to commit, working tree clean

Executando git reset --hard 5d50d76 te deixará em um estado onde você acabou de realizar o commit que você especificou, perdendo todas as alterações após o commit.

$ git log --children --decorate
commit 5d50d7664780f91c91d480e1a6b185fdc71c7733 (HEAD -> master)
Author: João Vitor Retamero <joaovretamero@gmail.com>
Date:   Sat Nov 2 17:54:10 2019 -0300

    Arquivo 2

commit f831ed83ecc8e25cd4103fecdcb61ce2649cad93 5d50d7664780f91c91d480e1a6b185fdc71c7733
Author: João Vitor Retamero <joaovretamero@gmail.com>
Date:   Sat Nov 2 17:53:55 2019 -0300

    Arquivo 1

checkout

O comando checkout é muito similar ao reset --hard, ou seja, modifica as três árvores. A diferença é que o reset irá mover o branch que a HEAD aponta, já o checkout irá mover apenas a HEAD.

Então veja a situação abaixo:

51d7b8b  < master  < HEAD
   |
   |
5d50d76   9ef3499  < develop
   |        |
   | ______/
   |/
f831ed8

Usando o comando git checkout develop, irá mudar o apontamento da HEAD assim como as três árvores para que estejam sincronizadas com a nova situação.

51d7b8b  < master
   |
   |
5d50d76   9ef3499  < develop  < HEAD
   |        |
   | ______/
   |/
f831ed8

Conclusão

Neste texto vimos um conceito importante do Git e que usamos muito no dia-a-dia: as três árvores. Vimos também como as três árvores interagem entre si. Também vimos como modificá-las e as diferenças entre os comandos.

Espero que da próxima vez que usar o Git, você possa entender melhor o que está acontecendo com seu repositório.

Até mais.