Git Reset, Revert e Rebase: Guia Definitivo para Controlar seus Commits

Neste guia, você explorará as diversas maneiras de interagir com os commits no Git.

Como desenvolvedor, é comum encontrar situações onde você precisa retornar a um commit anterior, mas pode não ter certeza de como fazer isso. Mesmo que você esteja familiarizado com comandos Git como `reset`, `revert` e `rebase`, pode haver dúvidas sobre as diferenças entre eles. Vamos começar a entender o que são `git reset`, `revert` e `rebase`.

Git Reset

O `git reset` é um comando complexo, usado para desfazer alterações em seu repositório.

Considere o `git reset` como uma ferramenta de “voltar no tempo”. Com ele, você pode navegar entre diversos commits. Existem três modos principais de execução: `–soft`, `–mixed` e `–hard`. Por padrão, o comando usa o modo `mixed`. Em um fluxo de trabalho com `git reset`, três áreas internas do Git são afetadas: o `HEAD`, a área de staging (índice) e o diretório de trabalho.

O diretório de trabalho é onde seus arquivos estão localizados e onde você faz suas alterações. Você pode usar o comando `git status` para verificar quais arquivos e pastas estão presentes nesse diretório.

A área de staging, ou índice, é onde o Git rastreia e salva as modificações feitas nos arquivos. As alterações salvas são refletidas no diretório `.git`. Use `git add “nome_do_arquivo”` para adicionar um arquivo à área de staging. Ao executar `git status`, você verá quais arquivos estão na área de staging.

A ramificação atual no Git é referenciada como `HEAD`. Ela aponta para o último commit realizado na ramificação atual. É um ponteiro que se move para a nova ramificação quando você faz um checkout em outra.

Vamos entender como o `git reset` opera nos modos `hard`, `soft` e `mixed`. No modo `hard`, o `HEAD` é movido para o commit especificado, o diretório de trabalho é preenchido com os arquivos desse commit e a área de staging é redefinida. No modo `soft`, apenas o ponteiro é alterado para o commit especificado, mantendo os arquivos de todos os commits anteriores no diretório de trabalho e na área de staging. Já no modo `mixed` (o padrão), tanto o ponteiro quanto a área de staging são redefinidos.

Git Reset Hard

O objetivo do `git reset –hard` é mover o `HEAD` para o commit desejado. Ele remove todos os commits que ocorreram após o commit especificado, modificando o histórico e apontando para o commit escolhido.

Neste exemplo, adicionaremos três novos arquivos, confirmaremos as alterações e executaremos um reset total.

Como visto no comando abaixo, não há nada para confirmar no momento.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.

(use "git push" to publish your local commits)

nothing to commit, working tree clean

Agora, vamos criar três arquivos e adicionar algum conteúdo a eles.

$ vi arquivo1.txt
$ vi arquivo2.txt
$ vi arquivo3.txt

Adicione esses arquivos ao repositório existente.

$ git add arquivo*

Ao executar o comando `status` novamente, os novos arquivos criados serão exibidos.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.

(use "git push" to publish your local commits)

Changes to be committed:

(use "git restore --staged <file>..." to unstage)

new file:
arquivo1.txt

new file:
arquivo2.txt

new file:
arquivo3.txt

Antes de confirmar, veja que atualmente tenho um histórico de três commits no Git.

$ git log --oneline
0db602e (HEAD -> master) um commit a mais
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

Agora, vamos confirmar as mudanças no repositório.

$ git commit -m 'adicionando 3 arquivos'
[master d69950b] adicionando 3 arquivos
3 files changed, 3 insertions(+)
create mode 100644 arquivo1.txt
create mode 100644 arquivo2.txt
create mode 100644 arquivo3.txt

Se usarmos o comando `ls-files`, veremos que os novos arquivos foram adicionados.

$ git ls-files
demo
arquivo_teste
novo_arquivo
arquivo1.txt
arquivo2.txt
arquivo3.txt

Ao executarmos o comando `log` no git, teremos quatro commits, com o `HEAD` apontando para o commit mais recente.

$ git log --oneline
d69950b (HEAD -> master) adicionando 3 arquivos
0db602e um commit a mais
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

Se excluirmos o arquivo1.txt manualmente e executarmos `git status`, uma mensagem indicará que as alterações não estão preparadas para confirmação.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

Changes not staged for commit:

(use "git add/rm <file>..." to update what will be committed)

(use "git restore <file>..." to discard changes in working directory)

deleted:
arquivo1.txt

no changes added to commit (use "git add" and/or "git commit -a")

Agora, executaremos o comando `hard reset`.

$ git reset --hard
HEAD is now at d69950b adicionando 3 arquivos

Se verificarmos o `status` novamente, veremos que não há nada para confirmar e que o arquivo excluído retornou ao repositório. O rollback aconteceu porque depois de excluir o arquivo, não foi feito um commit. Assim, após um `hard reset`, ele voltou ao estado anterior.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

nothing to commit, working tree clean

O log do Git agora se apresenta da seguinte forma:

$ git log
commit d69950b7ea406a97499e07f9b28082db9db0b387 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 19:53:31 2020 +0530

adicionando 3 arquivos

commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

um commit a mais

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

novo commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

teste

O `hard reset` tem o objetivo de apontar para um commit específico, atualizando tanto o diretório de trabalho quanto a área de staging. Vamos a outro exemplo. Atualmente, o histórico de commits é:

Aqui, executaremos o comando com `HEAD^`, indicando que desejamos redefinir para o commit anterior (um commit atrás).

$ git reset --hard HEAD^
HEAD is now at 0db602e um commit a mais

Como pode ser visto, o ponteiro `HEAD` mudou de `d69950b` para `0db602e`.

$ git log --oneline
0db602e (HEAD -> master) um commit a mais
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

Ao verificar o `log`, o commit `d69950b` desapareceu e o `HEAD` agora aponta para o SHA `0db602e`.

$ git log
commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

um commit a mais

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

novo commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

teste

Ao executar `ls-files`, você notará que os arquivos `arquivo1.txt`, `arquivo2.txt` e `arquivo3.txt` não estão mais no repositório, pois este commit e seus arquivos foram removidos pelo `hard reset`.

$ git ls-files
demo
arquivo_teste
novo_arquivo

Git Soft Reset

Agora, vamos a um exemplo de `soft reset`. Imagine que os três arquivos foram adicionados novamente como antes e confirmamos as mudanças. O `git log` se apresentará conforme abaixo. Observe que “soft reset” é o último commit e o `HEAD` aponta para ele.

$ git log --oneline
aa40085 (HEAD -> master) soft reset
0db602e um commit a mais
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

Os detalhes do commit podem ser visualizados usando o comando abaixo.

$ git log
commit aa400858aab3927e79116941c715749780a59fc9 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 21:01:36 2020 +0530

soft reset

commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

um commit a mais

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

novo commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

teste

Agora, usando o `soft reset`, queremos retornar a um commit anterior, com SHA `0db602e085a4d59cfa9393abac41ff5fd7afcb14`.

Para isso, executaremos o comando a seguir. Não é necessário o SHA completo, apenas os seis primeiros caracteres são suficientes.

$ git reset --soft 0db602e085a4

Ao executar `git log` novamente, vemos que o `HEAD` foi redefinido para o commit especificado.

$ git log
commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

um commit a mais

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

novo commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

teste

A diferença aqui é que os arquivos do commit `(aa400858aab3927e79116941c715749780a59fc9)`, onde adicionamos três arquivos, ainda permanecem no diretório de trabalho. Eles não foram excluídos. Por isso, é preferível usar o `soft reset` em vez do `hard reset`. Não há risco de perder arquivos no modo `soft`.

$ git ls-files
demo
arquivo_teste
arquivo1.txt
arquivo2.txt
arquivo3.txt
novo_arquivo

Git Revert

No Git, o comando `revert` é usado para reverter mudanças, ou seja, desfazer alterações. É similar ao comando `reset`, mas a diferença é que ele cria um novo commit para retornar a um commit específico. Resumidamente, o `git revert` é como um “commit de desfazer”.

O comando `git revert` não exclui dados durante o processo de reversão.

Vamos adicionar três arquivos e executar um `git commit` para um exemplo de revert.

$ git commit -m 'adicionando 3 arquivos novamente'
[master 812335d] adicionando 3 arquivos novamente
3 files changed, 3 insertions(+)
create mode 100644 arquivo1.txt
create mode 100644 arquivo2.txt
create mode 100644 arquivo3.txt

O log exibirá o novo commit.

$ git log --oneline
812335d (HEAD -> master) adicionando 3 arquivos novamente
0db602e um commit a mais
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

Agora, vamos reverter para um dos commits anteriores, digamos “59c86c9 novo commit”. Executaremos o seguinte comando:

$ git revert 59c86c9

Um editor de texto será aberto, mostrando os detalhes do commit para o qual estamos revertendo e permitindo nomear o novo commit. Após salvar e fechar o arquivo, o comando será executado.

Revert "novo commit"

This reverts commit 59c86c96a82589bad5ecba7668ad38aa684ab323.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Your branch is ahead of 'origin/master' by 4 commits.
# (use "git push" to publish your local commits)
#
# Changes to be committed:
# modified: arquivo_teste

Após salvar e fechar o arquivo, esta é a saída:

$ git revert 59c86c9
[master af72b7a] Revert "novo commit"
1 file changed, 1 insertion(+), 1 deletion(-)

Para implementar as alterações necessárias, ao contrário do `reset`, o `revert` cria um novo commit. Ao verificar o log novamente, um novo commit será visível devido à operação de reversão.

$ git log --oneline
af72b7a (HEAD -> master) Revert "novo commit"
812335d adicionando 3 arquivos novamente
0db602e um commit a mais
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

O `Git log` manterá todo o histórico de commits. Se você deseja remover commits do histórico, `revert` não é a melhor opção. Mas, se desejar manter as alterações no histórico, o `revert` é o comando ideal em vez de `reset`.

Git Rebase

No Git, o `rebase` é um modo de mover ou combinar commits de uma ramificação para outra. Como desenvolvedor, você raramente criará seus recursos diretamente na ramificação principal. O usual é trabalhar em sua própria ramificação (uma “ramificação de recurso”) e, quando tiver alguns commits nessa ramificação, movê-los para a ramificação principal.

Às vezes, o `rebase` pode ser um pouco confuso, pois se parece com um `merge`. O objetivo de ambos, `merge` e `rebase`, é levar os commits da ramificação de recurso para a ramificação principal (ou outra ramificação). Considere o seguinte gráfico:

Imagine que você está trabalhando em equipe com outros desenvolvedores. A situação pode se tornar realmente complexa com vários desenvolvedores trabalhando em ramificações de recursos diferentes e mesclando várias mudanças, o que pode tornar difícil rastrear os commits.

É aqui que o `rebase` se torna útil. Em vez de fazer um `git merge`, faremos um `rebase`, onde pegamos os commits da ramificação de recurso e movemos para a ramificação principal. O `rebase` pegará todos os commits da ramificação de recurso e os moverá para o topo dos commits da ramificação principal. Nos bastidores, o Git duplica os commits da ramificação de recurso na ramificação principal.

Essa abordagem mantém um gráfico de linha reta com todos os commits em sequência.

Isso facilita o rastreamento dos commits. Em uma equipe com muitos desenvolvedores, todos os commits são sequenciais. É fácil acompanhar mesmo com muitas pessoas trabalhando no mesmo projeto simultaneamente.

Vamos ver isso na prática.

A ramificação principal se parece com isso. Ela tem quatro commits.

$ git log --oneline
812335d (HEAD -> master) adicionando 3 arquivos novamente
0db602e um commit a mais
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

Vamos executar o comando abaixo para criar e mudar para uma nova ramificação chamada `feature`, a partir do segundo commit, ou seja, `59c86c9`.

(master)
$ git checkout -b feature 59c86c9
Switched to a new branch 'feature'

O log na ramificação `feature` mostra apenas dois commits que vieram da ramificação principal.

(feature)
$ git log --oneline
59c86c9 (HEAD -> feature) novo commit
e2f44fc (origin/master, origin/HEAD) teste

Vamos criar o recurso 1 e enviá-lo para a ramificação `feature`.

(feature)
$ vi recurso1.txt

(feature)
$ git add .
The file will have its original line endings in your working directory

(feature)
$ git commit -m 'recurso 1'
[feature c639e1b] recurso 1
1 file changed, 1 insertion(+)
create mode 100644 recurso1.txt

Vamos criar mais um recurso, o recurso 2, e fazer o commit na ramificação de recurso.

(feature)
$ vi recurso2.txt

(feature)
$ git add .
The file will have its original line endings in your working directory

(feature)
$ git commit -m 'recurso 2'
[feature 0f4db49] recurso 2
1 file changed, 1 insertion(+)
create mode 100644 recurso2.txt

Agora, o log da ramificação `feature` terá os dois novos commits acima.

(feature)
$ git log --oneline
0f4db49 (HEAD -> feature) recurso 2
c639e1b recurso 1
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

Agora, vamos adicionar esses dois novos recursos à ramificação principal. Para isso, usaremos o comando `rebase`. A partir da ramificação `feature`, faremos um rebase em relação à ramificação principal. Isso ancorará nossa ramificação de recurso em relação às alterações mais recentes da principal.

(feature)
$ git rebase master
Successfully rebased and updated refs/heads/feature.

Vamos verificar agora a ramificação principal.

(feature)
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

E, finalmente, rebase a ramificação principal em relação à ramificação de recurso. Isso pegará os dois novos commits da ramificação de recurso e os adicionará ao topo da ramificação principal.

(master)
$ git rebase feature
Successfully rebased and updated refs/heads/master.

Verificando o log na ramificação principal, vemos que os dois commits da ramificação de recurso foram adicionados com sucesso.

(master)
$ git log --oneline
766c996 (HEAD -> master, feature) recurso 2
c036a11 recurso 1
812335d adicionando 3 arquivos novamente
0db602e um commit a mais
59c86c9 novo commit
e2f44fc (origin/master, origin/HEAD) teste

Isso é tudo sobre os comandos `reset`, `revert` e `rebase` no Git.

Conclusão

Este artigo explorou os comandos `reset`, `revert` e `rebase` no Git. Espero que este guia detalhado tenha sido útil. Agora, você sabe como trabalhar com seus commits de acordo com suas necessidades, usando os comandos explicados.