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.