Git reset
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 commits (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 estruturas de dados em árvore tradicional. No entanto, são estruturas de dados baseadas em ponto central 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 acompanhar ele 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 .
Initialized empty Git repository in /git_reset_test/.git/
$ touch reset_lifecycle_file
$ git add reset_lifecycle_file
$ git commit -m"initial commit"
[main (root-commit) d386d86] initial commit
1 file changed, 0 insertions(+), 0 deletions(-)
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 um único commit (d386d86
) da adição de reset_lifecycle_file
.
Material relacionado
Folha de consulta do Git
VER SOLUÇÃO
Aprenda a usar o Git com o Bitbucket Cloud
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 main
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 execução do 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" O 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 no próximo commit. Esta árvore é um mecanismo complexo e interno de armazenamento em cache. O Git em geral tenta ocultar do usuário os dados da implementação do Índice de staging.
Para ver com precisão o estado do Índice de staging, é preciso utilizar um comando Git menos conhecido, o git ls-files
. O comando git ls-files
é, em resumo, um utilitário de depuração para inspecionar o estado da árvore do Índice de staging.
git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 reset_lifecycle_file
Aqui, a gente executa o git ls-files
com a opção -s
ou --stage
. Sem a opção -s
, o resultado do git ls-files
é apenas uma lista de nomes de arquivos e caminhos que fazem parte do índice no momento. A opção -s
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 gente 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 commits armazena os próprios SHAs de objeto para identificar indicadores de commits e referências, e o Índice de staging tem os próprios SHAs de objeto para monitorar as 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 main Changes to be committed:
(use "git reset HEAD ..." to unstage)
modified: reset_lifecycle_file
Aqui a gente invocou o git add reset_lifecycle_file
, que adiciona o arquivo ao Índice de Staging. Invocar git status
agora exibe reset_lifecycle_file
em verde em "Alterações para fazer commit". É importante notar que o git status
não é uma representação verdadeira do Índice de Staging. A saída do comando git status
exibe alterações entre o Histórico de Commit e o Índice de Staging. Vamos examinar o conteúdo do Índice de Staging neste momento.
$ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
É possível ver que o SHA do objeto reset_lifecycle_file
foi atualizado de e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
para d7d77c1b04b5edd5acfc85de0b592449e5303770
.
Histórico de commits
A última árvore é o histórico de commits. O comando git commit
adiciona alterações a um snapshot permanente que existe no histórico de commits. Esse snapshot também inclui o estado do Índice de staging no momento do commit.
$ git commit -am"update content of reset_lifecycle_file"
[main dc67808] update content of reset_lifecycle_file
1 file changed, 1 insertion(+)
$ git status
On branch main
nothing to commit, working tree clean
Aqui, a gente cria um novo commit com uma mensagem de "update content of resetlifecyclefile"
. O conjunto de alterações foi adicionado ao histórico de commits. Executar o git status
neste momento mostra que não há alterações pendentes em nenhuma das árvores. Executar o git log
vai exibir o histórico de commits. Agora que a gente acompanhou o conjunto de alterações pelas três árvores, dá para começar a utilizar o 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 do branch atual. Para demonstrar melhor esse comportamento, considere o exemplo a seguir:
Este exemplo demonstra uma sequência de commits na ramificação main
. A referência HEAD
e a referência da ramificação main
apontam no momento para o commit d. Agora a gente vai executar e comparar ambos, o git checkout b
e o git reset b
.
git checkout b
Com o git checkout
, a referência da ramificação main
ainda está apontando para d
. A referência HEAD
foi movida e agora aponta para o commit b
. O repositório agora está em um estado "HEAD
desconectado".
git reset b
Em comparação, o git reset
move ambos, HEAD
e as refs da ramificação, para a confirmação especificada.
Além de atualizar os indicadores de ref do commit, 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 commits. 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 chamada padrão do git reset
tem argumentos implícitos de --mixed
e HEAD
. Ou seja: executar o git reset
é equivalente a executar o git reset --mixed HEAD
. Desta forma, HEAD
é o commit especificado. Ao invés de HEAD
, qualquer código de commit SHA-1 do 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 commits são atualizados para o commit especificado. Então, o Índice de staging e o Diretório de trabalho são redefinidos para corresponder ao commit especificado. 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 commits. Ou seja: qualquer trabalho pendente que estava no Índice de staging e no Diretório de trabalho é perdido.
Para demonstrar, a gente vai continuar com o repositório de exemplo com três árvores criado antes. Primeiro, a gente vai fazer algumas alterações no repositório. Execute os seguintes comandos no repositório de exemplo:
$ echo 'new file content' > new_file
$ git add new_file
$ echo 'changed content' >> reset_lifecycle_file
Esses comandos criaram um novo arquivo chamado new_file
e o adicionaram ao repositório. O conteúdo de reset_lifecycle_file
também vai ser modificado. Após realizar as alterações, vamos agora examinar o estado do repositório usando o git status
.
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD ..." to unstage)
new file: new_file
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
Aqui a gente invocou o git add reset_lifecycle_file
, que adiciona o arquivo ao Índice de Staging. Invocar git status
agora exibe reset_lifecycle_file
em verde em "Alterações para fazer commit". É importante notar que o git status
não é uma representação verdadeira do Índice de Staging. A saída do comando git status
exibe alterações entre o Histórico de Commit e o Índice de Staging. Vamos examinar o conteúdo do Índice de Staging neste momento.
$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
É possível ver que o new_file
foi adicionado ao índice. Atualizações foram realizadas em reset_lifecycle_file
, mas o SHA do Índice de staging (d7d77c1b04b5edd5acfc85de0b592449e5303770
) permanece o mesmo. Esse comportamento é esperado, já que o git add
não foi usado para realizar essas alterações no Índice de staging. Essas alterações existem no Diretório de trabalho.
Vamos executar o git status
e examinar o estado atual do repositório.
$ git reset --hard
HEAD is now at dc67808 update content of reset_lifecycle_file
$ git status
On branch main
nothing to commit, working tree clean
$ 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 o commit mais recente dc67808
. A seguir, a gente verifica o estado do repositório com o 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 feitas em reset_lifecycle_file
e a adição do new_file
foram excluídas. Não se esqueça: esta perda de dados não pode ser desfeita.
'--mixed
Este é o modo operante padrão. Os indicadores de ref são atualizados. O Índice de staging é redefinido para o estado o commit especificado. 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
On branch main
Changes to be committed:
(use "git reset HEAD ..." to unstage)
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
On branch main
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
Untracked files:
(use "git add ..." to include in what will be committed)
new_file
no changes added to commit (use "git add" and/or "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 que executar o 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 SHA do objeto 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 é o 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 este caso com o 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 main
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 para fazer commits" 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, agora executamos uma redefinição soft.
$ git reset --soft
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD ..." to unstage)
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 commits. Por padrão, o git reset
é executado com HEAD
como commit alvo. Como o Histórico de commits 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 um commit alvo que não seja HEAD
. O reset_lifecycle_file
está aguardando no Índice de staging. A gente vai criar um novo commit.
$ git commit -m"prepend content to reset_lifecycle_file"
Neste momento, o repositório deve ter três commits. A gente vai voltar para o primeiro commit. Para chegar lá, a gente vai precisar do ID do primeiro commit. 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 commits são exclusivos para cada sistema. Ou seja: o ID de commit do exemplo é diferente do que aparece na sua máquina. O ID do commit relevante para o exemplo é 780411da3b47117270c0e3a8d5dcfd11d28d04a4
. Este é o ID que corresponde ao "commit inicial". Depois de localizar esse ID, a gente o usa como alvo para o soft reset.
Antes de retroceder, primeiro a gente verifica o estado atual do repositório.
$ git status && git ls-files -s
On branch main
nothing to commit, working tree clean
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
Aqui a gente executa um comando combinado git status e
git ls-files -s
, que mostra que existem alterações pendentes no repositório e que o reset_lifecycle_file
no Índice de staging está na versão 67cc52710639e5da6b515416fd779d0741e3762e
. Com essa informação em mente, a gente vai executar um soft reset de volta para o primeiro commit.
$git reset --soft 780411da3b47117270c0e3a8d5dcfd11d28d04a4
$ git status && git ls-files -s
On branch main
Changes to be committed:
(use "git reset HEAD ..." to unstage)
modified: reset_lifecycle_file
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
O código acima executa um "soft reset" e também invoca o comando combinadogit 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 o próximo commit. Em segundo lugar, git ls-files
indica que o Índica de staging não foi alterado e retém o SHA 67cc52710639e5da6b515416fd779d0741e3762e de antes.
Para explicar melhor o que aconteceu nesta redefinição, vamos examinar o git log:
$ git log commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 Author: bitbucket Date: Thu Nov 30 16:50:39 2017 -0800 initial commit
O resultado do log agora informa que existe um único commit no Histórico de commits. Assim a gente tem uma ilustração clara do que o --soft
fez. Assim como todas as chamadas de git reset
, a primeira ação que a redefinição faz é redefinir a árvore de commits. Os exemplos anteriores com --hard
e --mixed
foram ambos em relação a HEAD
e não desfizeram o que foi realizado na Árvore de commits. Durante um soft reset, é tudo o que acontece.
Então o motivo pelo qual o git status
indica que há arquivos modificados pode gerar confusão. O --soft
não interfere no Índice de staging, portanto, as atualizações do Índice de staging foram mantidas no histórico de commits. Essa informação pode ser confirmada pelo resultado de git ls-files -s
, que mostra que o SHA de reset_lifecycle_file
não foi alterado. Como um lembrete, o git status
não mostra o estado das "três árvores", ele mostra, em resumo, uma diferença entre elas. Nesse caso, ele está mostrando que o Índice de staging está à frente das alterações no Histórico de commits, como se a gente já as tivesse realizado.
Redefinir versus reverter
Se o git revert for uma maneira “segura” de desfazer alterações, você pode pensar em git reset
como um método perigoso. Existe um risco real de perder trabalho com o git reset
. O git reset
nunca exclui um commit, no entanto, os commits podem se tornar "órfãos". Quando isso acontece, não há um caminho de acesso direto partindo de uma ref. Em geral, os commits órfãos podem ser encontradas e restauradas usando git reflog. O Git exclui para sempre quaisquer commits órfãos 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 commits é uma das "três árvores do git", as outras duas, o Índice de staging e o Diretório de trabalho, não são tão permanentes como os commits. 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 um commit público, 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 um novo commit 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
Remover um commit que outros membros da equipe continuaram desenvolvendo cria problemas sérios de colaboração. Quando eles tentarem sincronizar com seu repositório, vai parecer que um pedaço do histórico do projeto sumiu de repente. A sequência abaixo demonstra o que acontece quando você tenta redefinir um commit público. A ramificação de origin/main
é a versão do repositório central da ramificação main
local.
Quando você adiciona novos commits depois da redefinição, o Git entende que o histórico local é diferente da origin/main
. Contudo, o commit de merge, que é necessário para sincronizar os repositórios, pode confundir e frustrar a equipe.
Ou seja, tenha certeza de estar usando git reset <commit>
em um experimento local que deu errado e não em alterações publicadas. Se você precisar corrigir um commit público, o comando git revert
foi desenvolvido para este fim.
Exemplos
git reset <file>
Remova o arquivo especificado da área de staging, mas deixe o diretório de trabalho inalterado. Assim o arquivo é retirado do staging sem sobrescrever nenhuma alteração.
git reset
Redefina a área de staging para corresponder ao commit mais recente, mas deixe o diretório de trabalho inalterado. Assim todos os arquivos são retirados do staging sem sobrescrever nenhuma alteração, possibilitando reconstruir o instantâneo preparado a partir do zero.
git reset --hard
Redefina a área de staging e o diretório de trabalho para corresponder ao commit mais recente. Além de alterações desativadas, a marcação --hard
diz ao Git para substituir todas as alterações no diretório de trabalho também. Dizendo de outra maneira: essa marcação apaga todas as alterações sem commit, então só use se quiser mesmo jogar fora os desenvolvimentos locais.
git reset
Mova a ponta da ramificação atual de volta para commit
, redefina a área de staging para corresponder, mas deixe o diretório de trabalho inalterado. Todas as alterações feitas desde <commit>
vão residir no diretório de trabalho, o que permite confirmar outra vez o histórico do projeto usando instantâneos mais limpos e atômicos.
git reset --hard
Mova a ponta da ramificação atual de volta para <commit>
e redefina a área de staging e o diretório de trabalho para corresponderem. Assim não apenas as mudanças sem commit são apagadas, mas todos os commits seguintes também.
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.
# Edit both hello.py and main.py
# Stage everything in the current directory
git add .
# Realize that the changes in hello.py and main.py
# should be committed in different snapshots
# Unstage main.py
git reset main.py
# Commit only hello.py
git commit -m "Make some changes to hello.py"
# Commit main.py in a separate snapshot
git add main.py
git commit -m "Edit main.py"
Como é possível ver, o git reset
ajuda a manter o alto foco dos commits, permitindo que você retire as alterações que não estão relacionadas ao próximo commit.
Remover commits 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 fazer o commit de alguns snapshots.
# Create a new file called `foo.py` and add some code to it
# Commit it to the project history
git add foo.py
git commit -m "Start developing a crazy feature"
# Edit `foo.py` again and change some other tracked files, too
# Commit another snapshot
git commit -a -m "Continue my crazy feature"
# Decide to scrap the feature and remove the associated commits
git reset --hard HEAD~2
O comando git reset HEAD~2
move o branch atual dois commits para trás, fazendo a remoção efetiva dos dois snapshots recém-criados a partir do histórico do projeto. Este tipo de redefinição só deve ser usado em commits não publicados. Nunca execute a operação acima se você já tiver enviado, via push, os commits para um repositório compartilhado.
Resumo
Revisando, o git reset
é um comando poderoso usado para desfazer alterações locais no estado de um repositório do Git. O git reset
opera nas "três árvores do Git". Essas árvores são o Histórico de commits (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, são usados diversos comandos do Git para ajudar a demonstrar os processos de redefinição. Saiba mais sobre esses comandos em suas respectivas páginas: git status, git log, git add, git checkout, git reflog e git revert.
Compartilhar este artigo
Próximo tópico
Leitura recomendada
Marque esses recursos para aprender sobre os tipos de equipes de DevOps ou para obter atualizações contínuas sobre DevOps na Atlassian.