O comando git reset é uma ferramenta complexa e versátil para desfazer alterações. Ele tem três formas principais de invocação. Estas formas correspondem aos argumentos --soft, --mixed, --hard da linha de comandos. Cada um dos três argumentos corresponde a um mecanismo de gerenciamento do estado interno do Git: a árvore de confirmação (HEAD), o índice de staging e o diretório de trabalho.

Git reset e as três árvores do Git

Para compreender com clareza o uso do git reset, primeiro a gente precisa entender os sistemas de gerenciamento de estado interno do Git. Às vezes, estes mecanismos são chamados de "três árvores" do Git. Árvore pode ser um nome inadequado, pois elas não são de fato estruturas tradicionais de dados em árvore. No entanto, são estruturas de dados baseadas em nó e indicador que o Git utiliza para monitorar a linha do tempo das edições. A melhor forma de demonstrar estes mecanismos é criar um conjunto de alterações em um repositório e o acompanhar até as três árvores.

Para começar, a gente vai criar um novo repositório com os comandos abaixo:

 $ mkdir git_reset_test $ cd git_reset_test/ $ git init . Inicializou repositório do Git vazio em /git_reset_test/.git/ $ touch reset_lifecycle_file $ git add reset_lifecycle_file $ git commit -m"initial commit" [master (root-commit) d386d86] initial commit 1 arquivo alterado, 0 inserções(+), 0 exclusões(-) create mode 100644 reset_lifecycle_file

O código de exemplo acima cria um novo repositório do Git com um único arquivo vazio, reset_lifecycle_file. Neste momento, o repositório de exemplo tem uma única confirmação (d386d86) da adição de reset_lifecycle_file.

O diretório de trabalho

A primeira árvore que a gente vai examinar é o "Diretório de trabalho". Esta árvore está sincronizada com o sistema de arquivos local e representa as alterações imediatas feitas no conteúdo de arquivos e diretórios.


$ echo 'hello git reset' > reset_lifecycle_file
$ git status 
On branch master 
Changes not staged for commit: 
(use "git add ..." to update what will be committed) 
(use "git checkout -- ..." to discard changes in working directory) 
modified: reset_lifecycle_file

No repositório de demonstração, a gente modifica e adiciona conteúdo a reset_lifecycle_file. A invocação de git status mostra que o Git tem conhecimento das alterações no arquivo. Estas alterações agora fazem parte da primeira árvore, o "Diretório de trabalho". Git status pode ser usado para exibir as alterações no Diretório de trabalho. Elas são exibidas em vermelho com um prefixo "modified".

Índice de staging

A próxima é a árvore "Índice de staging". Esta árvore está monitorando as alterações no Diretório de trabalho que foram promovidas pelo git add e que devem ser armazenadas na próxima confirmação. Esta árvore é um mecanismo complexo e interno de caching. O Git em geral tenta ocultar do usuário os dados da implementação do Índice de staging.

Para visualizar com precisão o estado do Índice de staging, a gente precisa utilizar um comando do Git menos conhecido, o git ls-files. O comando git ls-files é em essência um utilitário de depuração para inspecionar o estado da árvore Índice de staging.

 git ls-files -s 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 reset_lifecycle_file

Aqui, a gente executa git ls-files com a opção -s ou --stage. Sem a opção -s, o resultado de git ls-files é apenas uma lista de nomes de arquivos e caminhos que fazem parte do índice no momento. A opção -s exibe metadados adicionais para os arquivos do Índice de staging. Estes metadados são os bits do modo, o nome do objeto e o número de preparação do conteúdo preparado. Aqui a geste está interessado no nome do objeto, o segundo valor (d7d77c1b04b5edd5acfc85de0b592449e5303770). Este é um código SHA-1 padrão de objeto do Git. É um código do conteúdo dos arquivos. O Histórico de confirmações armazena os próprios SHAs de objeto para identificar indicadores de confirmações e referências e o Índice de staging tem os próprios SHAs de objeto para monitorar versões dos arquivos no índice.

Em seguida, a gente vai promover o reset_lifecycle_file modificado no Índice de staging.


$ git add reset_lifecycle_file 

$ git status 

On branch master Changes to be committed: 

(use "git reset HEAD ..." to unstage) 

modified: reset_lifecycle_file

Aqui a gente invoca git add reset_lifecycle_file, que adicionou o arquivo ao Índice de staging. A invocação de git status agora exibe reset_lifecycle_file em verde sob "Alterações a serem confirmadas". É importante observar que git status não é uma representação verdadeira do Índice de staging. O resultado do comando git status exibe as alterações entre o Histórico de confirmações e o Índice de staging. A gente vai examinar o conteúdo do Índice de staging neste momento.

 $ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

A gente pode ver que o SHA do objeto para reset_lifecycle_file foi atualizado de e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 para d7d77c1b04b5edd5acfc85de0b592449e5303770.

Histórico de confirmações

A última árvore é o Histórico de confirmações. O comando git commit adiciona as alterações a um instantâneo permanente que reside no Histórico de confirmações. Este instantâneo também inclui o estado do Índice de staging no momento da confirmação.

 $ git commit -am"update content of reset_lifecycle_file" [master dc67808] update content of reset_lifecycle_file 1 file changed, 1 insertion(+) $ git status On branch master nothing to commit, working tree clean

Aqui, a gente vai criar uma nova confirmação com uma mensagem de "update content of resetlifecyclefile". O conjunto de alterações foi adicionado ao Histórico de confirmações. A invocação de git status neste momento exibe que não existem alterações pendentes a nenhuma das árvores. A execução de git log exibe o Histórico de confirmações. Agora que a gente viu este conjunto de alterações pelas três árvores, a gente pode começar a utilizar git reset.

Como funciona

Na superfície, o git reset tem comportamento semelhante ao git checkout. Enquanto o git checkout opera apenas no indicador de referência HEAD, o git reset move o indicador de referência HEAD e o indicador de referência da ramificação atual. Para demonstrar melhor esse comportamento, considere o exemplo a seguir:

Este exemplo demonstra uma sequência de confirmações no branch principal. A referência HEAD e o branch principal apontam no momento para a confirmação d. Agora a gente vai executar e comparar ambos, git checkout b e git reset b.

git checkout b

Com git checkout, o branch principal ainda está apontando para d. A referência HEAD foi movida e agora aponta para a confirmação b. O repositório agora está em um estado "HEAD separado".

git reset b

Em comparação, git reset move ambos, HEAD e as refs de ramificação, para a confirmação especificada.

Além de atualizar os indicadores de ref da confirmação, o git reset modifica o estado das três árvores. A modificação do indicador de ref sempre ocorre e é atualizada na terceira árvore, a árvore de confirmação. Os argumentos --soft, --mixed e --hard orientam como modificar as árvores do Índice de staging e do Diretório de trabalho.

Opções principais

A invocação padrão de git reset tem argumentos implícitos de --mixed e HEAD. Isso significa que executar git reset é equivalente a executar git reset --mixed HEAD. Desta forma, HEAD é a confirmação especificada. Ao invés de HEAD, qualquer código de confirmação SHA-1 de Git pode ser usado.

--hard

Esta é a opção mais direta, usada e PERIGOSA. Quando --hard é transmitido, os indicadores de ref do Histórico de confirmações são atualizados para a confirmação especificada. Então, o Índice de staging e o Diretório de trabalho são redefinidos para corresponder à confirmação especificada. Quaisquer alterações prévias pendentes no Índice de staging e no Diretório de trabalho são redefinidas para corresponder ao estado da árvore de confirmação. Isso significa que qualquer trabalho pendente que estava no Índice de staging e no Diretório de trabalho é perdido.

Para demonstrar isso, a gente vai continuar com o repositório de exemplo das três árvores de antes. Primeiro, a gente vai fazer algumas alterações no repositório. Execute os comandos a seguir no repositório de exemplo:

$ echo 'new file content' > new_file $ git add new_file $ echo 'changed content' >> reset_lifecycle_file

Estes comandos criaram um novo arquivo chamado new_file e o adicionaram ao repositório. Além disso, o conteúdo de reset_lifecycle_file é modificado. Com estas alterações realizadas, agora a gente vai examinar o estado do repositório usando git status.

$ git status No branch principal Alterações a serem confirmadas: (use "git reset HEAD .." para despreparar) new file: new_file Alterações não preparadas para confirmação: (use "git add ..." para atualizar o que vai ser confirmado) (use "git checkout -- ..." para descartar as alterações no diretório de trabalho) modified: reset_lifecycle_file

A gente pode ver que agora existem alterações pendentes no repositório. A árvore do Índice de staging tem uma alteração pendente para a adição de new_file e o Diretório de trabalho tem uma alteração pendente para as modificações em reset_lifecycle_file.

Antes de continuar, a gente vai examinar também o estado do Índice de staging:

$ git ls-files -s 100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

A gente pode ver que new_file foi adicionado ao índice. A gente fez atualizações em reset_lifecycle_file, porém o SHA do Índice de staging (d7d77c1b04b5edd5acfc85de0b592449e5303770) permanece igual. Este comportamento é esperado, pois a gente não usa git add para promover estas alterações para o Índice de staging. Estas alterações existem no Diretório de trabalho.

Agora, a gente vai executar um git reset --hard e examinar o novo estado do repositório.

$ git reset --hard HEAD agora está no conteúdo atualizado dc67808 de reset_lifecycle_file $ git status No branch principal nada a confirmar, árvore de trabalho limpa $ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

Aqui a gente executou um "hard reset" usando a opção --hard. O Git exibe o resultado indicando que HEAD está apontando para a confirmação mais recente dc67808. A seguir, a gente verifica o estado do repositório com git status. O Git indica que não existem alterações pendentes. A gente também examina o estado do Índice de staging e vê que ele foi redefinido para um ponto antes de new_file ser adicionado. As modificações que a gente fez em reset_lifecycle_file e a adição de new_file foram destruídas. Esta perda de dados não pode ser desfeita, é imprescindível lembrar disso.

--mixed

Este é o modo operante padrão. Os indicadores de ref são atualizados. O Índice de staging é redefinido para o estado da confirmação especificada. Quaisquer alterações desfeitas no Índice de staging são movidas para o Diretório de trabalho. Vamos continuar.

$ echo 'new file content' > new_file $ git add new_file $ echo 'append content' >> reset_lifecycle_file $ git add reset_lifecycle_file $ git status No branch principal Alterações a serem confirmadas: (use "git reset HEAD ..." para despreparar) new file: new_file modified: reset_lifecycle_file $ git ls-files -s 100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file 100644 7ab362db063f9e9426901092c00a3394b4bec53d 0 reset_lifecycle_file

No exemplo acima, a gente fez algumas modificações no repositório. Outra vez, a gente adicionou um new_file e modificou o conteúdo de reset_lifecycle_file. Estas alterações foram, então, aplicadas ao Índice de staging com git add. Com o repositório neste estado, agora a gente executa a redefinição.

$ git reset --mixed $ git status No branch principal Alterações não preparadas para confirmação: (use "git add ..." para atualizar o que vai ser confirmado) (use "git checkout -- ..." para descartar as alterações no diretório de trabalho) modified: reset_lifecycle_file Arquivos não rastreados: (use "git add ..." para incluir no que vai ser confirmado) new_file nenhuma alteração adicionada à confirmação (use "git add" e/ou "git commit -a") $ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

Aqui a gente executou um "mixed reset". Para reiterar, --mixed é o modo padrão e tem o mesmo efeito da execução de git reset. Examinando o resultado de git status e git ls-files, a gente pode ver que o Índice de staging foi redefinido para um estado em que reset_lifecycle_file é o único arquivo no índice. O objeto SHA para reset_lifecycle_file foi redefinido para a versão anterior.

O importante a lembrar aqui é que git status mostra que existem modificações em reset_lifecycle_file e que existe um arquivo não rastreado: new_file. Este é comportamento explícito de --mixed. O Índice de staging foi redefinido e as alterações pendentes foram movidas para o Diretório de trabalho. Compare isso com o caso do --hard reset, no qual o Índice de staging foi redefinido e o Diretório de trabalho também foi redefinido, perdendo estas atualizações.

--soft

Quando o argumento --soft é transmitido, os indicadores de ref são atualizados e a redefinição para aí. O Índice de staging e o Diretório de trabalho permanecem intocados. Este comportamento pode ser difícil de demonstrar com clareza. A gente vai continuar com o repositório de demonstração e preparar ele para um soft reset.


$ git add reset_lifecycle_file 

$ git ls-files -s 

100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file 

$ git status 

On branch master 

Changes to be committed: 

(use "git reset HEAD ..." to unstage) 

modified: reset_lifecycle_file 

Untracked files: 

(use "git add ..." to include in what will be committed) 

new_file

Aqui, a gente utilizou outra vez git add para promover o reset_lifecycle_file modificado no Índice de staging. A gente confirmou que o índice foi atualizado com o resultado git ls-files. O resultado de git status agora exibe as "Alterações a serem confirmadas" em verde. O new_file dos exemplos anteriores está solto no Diretório de trabalho como um arquivo não monitorado. A gente executa rm new_file para excluir o arquivo, já que ele não é mais necessário nos exemplos seguintes.

Com o repositório neste estado, a gente executa um soft reset.

 $ git reset --soft $ git status No branch principal Alterações a serem confirmadas: (use "git reset HEAD ..." para despreparar) modified: reset_lifecycle_file $ git ls-files -s 100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

A gente executou um "soft reset". Examinar o estado do repositório com git status e git ls-files mostra que nada mudou. Este é o comportamento esperado. Um soft reset apenas redefine o Histórico de confirmações. Por padrão, git reset é invocado com HEAD como confirmação alvo. Como o Histórico de confirmações já estava localizado em HEAD e a gente fez a redefinição explícita para HEAD, nada aconteceu de fato.

Para entender e utilizar melhor o --soft, a gente precisa de uma confirmação alvo que não seja HEAD. reset_lifecycle_file está aguardando no Índice de staging. A gente vai criar uma nova confirmação.

$ git commit -m"prepend content to reset_lifecycle_file"

Neste momento, o repositório deve ter três confirmações. A gente vai voltar para a primeira confirmação. Para isso, a gente vai precisar do ID da primeira confirmação. Ele pode ser encontrado examinando o resultado de git log.

$ git log commit 62e793f6941c7e0d4ad9a1345a175fe8f45cb9df Author: bitbucket  Date: Fri Dec 1 15:03:07 2017 -0800 prepend content to reset_lifecycle_file commit dc67808a6da9f0dec51ed16d3d8823f28e1a72a Author: bitbucket  Date: Fri Dec 1 10:21:57 2017 -0800 update content of reset_lifecycle_file commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 Author: bitbucket  Date: Thu Nov 30 16:50:39 2017 -0800 initial commit

Os IDs do Histórico de confirmações são exclusivos para cada sistema. Isso significa que o ID de confirmação do exemplo é diferente do que aparece na sua máquina. O ID de confirmação relevante para o exemplo é 780411da3b47117270c0e3a8d5dcfd11d28d04a4. Este é o ID que corresponde à "confirmação inicial". Depois de localizar este ID, a gente o usa como alvo para o soft reset.

Antes de voltar no tempo, primeiro a gente verifica o estado atual do repositório.

 $ git status && git ls-files -s No branch principal nada a confirmar, árvore de trabalho limpa 100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

Aqui a gente executa um comando combinado de git status e git ls-files -s, isto mostra que existem alterações pendentes no repositório e reset_lifecycle_file no Índice de staging está na versão de 67cc52710639e5da6b515416fd779d0741e3762e. Com isto em mente, a gente vai executar um soft reset de volta para a primeira confirmação.

$git reset --soft 780411da3b47117270c0e3a8d5dcfd11d28d04a4 $ git status && git ls-files -s No branch principal Alterações a serem confirmadas: (use "git reset HEAD ..." para despreparar) modified: reset_lifecycle_file 100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

O código acima executa um "soft reset" e também invoca o comando combinado git status e git ls-files, que resulta no estado do repositório. A gente pode examinar o resultado do estado do repositório e observar algumas coisas interessantes. Em primeiro lugar, git status indica que existem modificações em reset_lifecycle_file e as destaca indicando as alterações preparadas para a próxima confirmação. Em segundo lugar, git ls-files indica que o Índice de staging não foi alterado e retém o SHA 67cc52710639e5da6b515416fd779d0741e3762e de antes.

Para explicar melhor o que aconteceu nessa redefinição, a gente vai examinar a confirmação git log:

$ git log 780411da3b47117270c0e3a8d5dcfd11d28d04a4 Author: bitbucket  Date: Thu Nov 30 16:50:39 2017 -0800 initial commit

O resultado do log agora exibe que existe uma única confirmação no Histórico de confirmações. Isso ajuda a ilustrar com clareza o que o --soft fez. Assim como todas as invocações de git reset, a primeira ação que a redefinição faz é redefinir a árvore de confirmações. Os exemplos anteriores com --hard e --mixed foram ambos em relação a HEAD e não moveram a Árvore de confirmações de volta no tempo. Durante um soft reset, isso é tudo o que acontece.

Por isso, pode causar confusão do motivo de git status indicar que existem arquivos modificados. O --soft não toca no Índice de staging, então as atualizações no Índice de staging seguiram a gente de volta no tempo pelo Histórico de confirmações. Isso pode ser confirmado pelo resultado de git ls-files -s mostrando que o SHA para reset_lifecycle_file está inalterado. Como lembrete, o git status não exibe o estado das "três árvores", ele mostra, em essência, uma diferença entre elas. Nesse caso, ele está exibindo que o Índice de staging está à frente das alterações no Histórico de confirmações, como se a gente já as tivesse preparado.

Redefinir versus reverter

Se o git revert é uma forma "segura" de desfazer alterações, você pode pensar no git reset como um método perigoso. Existe um risco real de perder trabalho com git reset. O git reset nunca exclui uma confirmação, no entanto, as confirmações podem se tornar "órfãs", o que significa que não vai existir um caminho direto de uma ref para as acessar. Em geral, estas confirmações órfãs podem ser encontradas e restauradas usando git reflog. O Git exclui em caráter permanente quaisquer confirmações órfãs após executar o coletor de lixo interno. Por padrão, o Git é configurado para executar o coletor de lixo a cada 30 dias. O Histórico de confirmações é uma das "três árvores do git", as outras duas, Índice de staging e Diretório de trabalho, não são tão permanentes como as confirmações. Tome cuidado ao usar essa ferramenta, pois é um dos únicos comandos Git que tem o potencial de perder trabalho.

Enquanto a reversão foi desenvolvida para desfazer com segurança uma confirmação pública, o git reset foi desenvolvido para desfazer alterações locais do Índice de staging e do Diretório de trabalho. Por causa dos objetivos distintos, os dois comandos são implementados com diferenças: a redefinição remove por completo um conjunto de alterações, enquanto a reversão mantém o conjunto de alterações original e usa uma nova confirmação para aplicar a ação de desfazer.

Não faça a redefinição no histórico público

Você nunca deve usar git reset quando algum instantâneo após tiver sido colocado em um repositório público. Depois de publicar uma confirmação, você precisa supor que outros desenvolvedores dependem dela.

Remover uma confirmação que outros membros da equipe continuaram desenvolvendo cria problemas sérios de colaboração. Quando eles tentarem sincronizar com o repositório, vai parecer que um pedaço do histórico do projeto desapareceu do nada. A sequência abaixo demonstra o que acontece quando você tenta redefinir uma confirmação pública. O branch de origem/principal é a versão do repositório central do branch principal local.

Assim que você adiciona novas confirmações após a redefinição, o Git entende que o histórico local é diferente da origem/mestre, e a confirmação de mesclagem necessária para sincronizar os repositórios pode confundir e frustrar a equipe.

Ou seja, tenha certeza de estar usando git reset em um experimento local que deu errado, não em alterações publicadas. Se você precisar corrigir uma confirmação pública, o comando git revert foi desenvolvido para este fim.

Exemplos

 git reset 

Remova o arquivo especificado da área de staging, porém deixe o diretório de trabalho inalterado. Isto desprepara um arquivo sem substituir qualquer mudança.

 git reset

Redefina a área de staging para corresponder à confirmação mais recente, mas deixe o diretório de trabalho inalterado. Isto desprepara todos os arquivos sem substituir qualquer mudança, dando a você a oportunidade de reconstruir o instantâneo preparado desde o início.

 git reset --hard

Redefina a área de staging e o diretório de trabalho para corresponder à confirmação mais recente. Além de despreparar as mudanças, o sinalizador --hard diz ao Git para substituir também todas as mudanças no diretório de trabalho. Em outras palavras: isto apaga todas as alterações não confirmadas, então, tenha certeza de que quer mesmo jogar fora os desenvolvimentos locais antes de usar o sinalizador.

 git reset  

Mova a ponta da ramificação atual de volta para commit, redefina a área de staging para corresponder, mas não mexa no diretório de trabalho. Todas as alterações feitas desde o residem no diretório de trabalho, que deixa você reconfirmar o histórico do projeto usando instantâneos mais limpos e simples.

 git reset --hard  

Mova a ponta da ramificação atual de volta para e redefina ambos, a área de staging e o diretório de trabalho, para corresponder. Isto apaga não apenas as alterações não confirmadas, mas, também, todas as confirmações posteriores.

Despreparar um arquivo

O comando git reset é encontrado com frequência ao preparar o instantâneo pretendido. O exemplo a seguir supõe que você tem dois arquivos chamados hello.py e main.py que você já adicionou ao repositório.

# Edite hello.py e main.py # Prepare tudo no diretório atual git add . # As mudanças em hello.py e main.py # devem ser confirmadas em instantâneos diferentes # Desprepare main.py git reset main.py # Apenas confirme hello.py git commit -m "Faça algumas alterações em hello.py" # Confirme main.py em um instantâneo separado git add main.py git commit -m "Edite main.py"

Como você pode ver, git reset ajuda você a manter confirmações muito focadas, ao permitir que você desprepare alterações que não estão relacionadas à próxima confirmação.

Remover confirmações locais

O próximo exemplo mostra um caso de uso mais avançado. Ele demonstra o que acontece quando você trabalha por um tempo em um novo experimento, mas decide jogar ele fora por completo depois de confirmar alguns instantâneos.

# Crie um novo arquivo chamado "foo.py" e adicione alguns códigos a ele # Confirme ele no histórico do projeto git add foo.py git commit -m "Comece a desenvolver uma funcionalidade doida" # Edite "foo.py" outra vez e altere alguns arquivos monitorados também # Confirme outro instantâneo git commit -a -m "Continuar minha funcionalidade doida" # Decida descartar a funcionalidade e remova os commits associados git reset --hard HEAD~2

O comando git reset HEAD~2 move a ramificação atual para trás em duas confirmações, removendo de fato os dois instantâneos que a gente acabou de criar do histórico do projeto. Este tipo de redefinição deve ser usado apenas para confirmações não publicadas. Nunca execute a operação acima se você já tiver colocado as confirmações em um repositório compartilhado.

Resumo

Revisando, o git reset é um comando poderoso usado para desfazer alterações locais no estado de um repositório Git. O git reset opera nas "três árvores do Git". Estas árvores são o Histórico de confirmações (HEAD), o Índice de staging e o Diretório de trabalho. Existem três opções da linha de comando que correspondem às três árvores. As opções --soft, --mixed e --hard podem ser transmitidas para git reset.

Neste artigo a gente usa diversos comandos do Git para ajudar a demonstrar os processos de redefinição. Saiba mais sobre estes comandos nas páginas individuais em: git status, git log, git add, git checkout, git reflog, e git revert.

Pronto(a) para aprender o git reset?

Tente este tutorial interativo.

Comece agora mesmo