Skip to content
This page has been auto-translated and may contain errors.View in English

Deshacer cambios y commits en Git

docs.scrimba.com

Siempre hay algo que deshacer. Escribiste una edición que no querías, intentaste una corrección que empeorló las cosas, o hiciste commit de algo que preferirías que nadie viera. Git mantiene un registro de casi todas las versiones de tu proyecto que ha visto, así que la mayoría de los errores son más reversibles de lo que parecen en el momento. Qué herramienta usar depende de cuán lejos ha llegado el error: todavía en tu editor, ya preparado, ya confirmado, o ya enviado a algún lugar donde un compañero de equipo puede verlo. Este capítulo recorre cada etapa en ese orden.

¿Qué son push y pull?

Push envía tus commits a una copia compartida del repositorio desde la que tus compañeros también trabajan, y pull trae sus commits hacia ti. Remotes and GitHub cubre ambos en detalle; para este capítulo, "enviado" significa que el commit ha salido de tu máquina.

Descartar un cambio antes de confirmarlo

Digamos que todavía estás rediseñando styles.css en weather-app desde the commit loop, y el nuevo diseño resulta ser una mala idea. No lo has preparado, no lo has confirmado. Quieres que el archivo vuelva al estado anterior, nada más. El comando exacto para ese trabajo es git restore:

bash
$ git status
On branch main
Changes not staged for commit:
        modified:   styles.css

$ git restore styles.css

$ git status
On branch main
nothing to commit, working tree clean

git restore <file> descarta una edición no confirmada y devuelve el archivo a su último estado guardado. Si ya habías preparado el cambio con git add antes de cambiar de opinión, git restore simple no lo tocará, ya que el archivo ahora está en el área de preparación en lugar de solo en tu directorio de trabajo. Primero sácalo del área de preparación, luego restáuralo:

bash
$ git add styles.css
$ git restore --staged styles.css
$ git restore styles.css

Dos movimientos separados para dos áreas separadas: git restore --staged saca un archivo del área de preparación, y git restore solo descarta la edición en tu directorio de trabajo. git restore solo alcanza el trabajo no confirmado, una vez que un cambio está confirmado, un comando diferente toma el relevo.

Los tutoriales y scripts antiguos usan git checkout -- <file> para el trabajo exacto que git restore <file> hace hoy. checkout solía servir tanto para "cambiar de rama" como para "restaurar un archivo", lo que era lo suficientemente confuso como para que Git dividiera los dos en switch y restore. Todavía encontrarás checkout en material antiguo, reconócelo, pero usa restore en cualquier cosa que escribas ahora. También puedes descartar cada cambio no preparado en la carpeta actual a la vez con git restore ., lo que vale la pena hacer una pausa antes de ejecutar, ya que limpia cada archivo de una sola vez.

El deshacer de restore se detiene firmemente en el directorio de trabajo. Un commit es un objeto guardado que permanece hasta que la recolección de basura de Git lo elimina, por eso un commit malo es recuperable a través del reflog incluso después de un reset forzado. Una edición no confirmada nunca se convirtió en un objeto guardado en primer lugar: restore verifica la última versión guardada de un archivo sobre tu edición, y no hay ningún estado anterior almacenado en ningún lado para desenterrar. Ese es el caso práctico para hacer commit temprano y a menudo. Un commit desordenado es casi siempre arreglable; una edición que nunca guardaste generalmente no lo es.

JunoDescartar un cambio no confirmadogit restore <file> descarta una edición que no has confirmado y devuelve el archivo al estado anterior. Si ya preparaste el cambio con git add, primero sácalo del área de preparación con git restore --staged <file>, luego restaura el archivo si quieres que la edición desaparezca también. Esto solo funciona en cambios que no has confirmado aún, así que no puede rescatar un error que ya guardaste.
JunoDescartar un cambio no confirmadogit restore <file> es el reemplazo moderno de git checkout -- <file>, y git restore . limpia cada cambio no preparado en la carpeta a la vez, así que ejecuta git status primero para ver qué tocaría eso. Todavía verás checkout haciendo este trabajo en respuestas y scripts antiguos, es la misma idea bajo un nombre antiguo y más sobrecargado.
JunoDescartar un cambio no confirmadorestore nunca crea o toca un commit, solo sobrescribe tu directorio de trabajo con la última versión guardada de un archivo. Eso significa que no deja ningún rastro para recuperarse si te equivocas, a diferencia de casi todo lo demás en este capítulo. Haz commit pequeño y hazlo a menudo, y "¡vaya!" se convierte en un problema que reset o el reflog pueden arreglar en lugar de un cambio que nunca tuvo un punto de guardado al que volver.

Guardar trabajo con git stash

A veces no hay un error para arreglar, solo mala sincronización. Estás a mitad de editar styles.css y un compañero de equipo te necesita en otra rama ahora mismo, pero no estás listo para hacer commit del estilo a medio terminar. git stash guarda tus cambios y te da un directorio de trabajo limpio para que puedas alejarte y volver más tarde.

bash
$ git status
On branch main
Changes not staged for commit:
        modified:   styles.css

$ git stash
Saved working directory and index state WIP on main: 8f3c7d1 Cache forecast responses in localStorage

$ git status
On branch main
nothing to commit, working tree clean

La edición a styles.css no se ha ido, está guardada. Tráela de vuelta con git stash pop cuando estés listo:

bash
$ git stash pop
On branch main
Changes not staged for commit:
        modified:   styles.css

git stash es para cambios que no estás listo para confirmar aún, nunca crea un commit en tu rama.

Un único stash raramente cubre una semana real de trabajo, así que conoce el resto del conjunto de herramientas. git stash list muestra todo lo que has guardado, lo más reciente primero. git stash pop reaplica la entrada superior y la elimina de la lista; git stash apply la reaplica pero la deja en la lista, útil si quieres el mismo cambio guardado en dos ramas. Nombra un stash en el camino, así que list no es una pared de entradas sin etiquetar después:

bash
$ git stash push -m "wip: forecast card layout"
$ git stash list
stash@{0}: On main: wip: forecast card layout

Un stash está construido con la misma maquinaria de commits que todo lo demás en Git. git stash guarda tu estado preparado y no preparado como un par de objetos de commit y apunta una referencia especial, refs/stash, a ellos, por lo que git stash list se lee como un pequeño historial propio. El reflog depende de este mismo truco, una referencia que apunta a commits que ninguna rama alcanza.

JunoGuardar trabajogit stash guarda cambios que no estás listo para confirmar, así que tu directorio de trabajo queda limpio, y git stash pop los trae de vuelta después. Nada se confirma y nada se pierde, la edición se queda guardada por un tiempo. Recurro a esto constantemente siempre que un compañero me necesita en otra rama ahora mismo.
JunoGuardar trabajogit stash list muestra cada cambio guardado, pop reaplica y elimina el más reciente, apply reaplica sin eliminarlo. Nombra los stashes en el camino con push -m, una lista de stash llena de entradas sin etiquetar una semana después no ayuda a nadie, incluyéndote a ti.
JunoGuardar trabajo Un stash es un par de objetos de commit que refs/stash apunta a, sentados fuera de cualquier rama, por lo que se comporta como su propio pequeño historial. Una vez que lo ves de esa manera, stash deja de sentirse como magia, es el mismo modelo de objeto haciendo el mismo truco de nuevo.

Deshacer un commit con git revert

Digamos que el commit 8f3c7d1, "Cache forecast responses in localStorage", resulta ser incorrecto, y ya está enviado, así que un compañero también lo ha traído. Reescribir ese commit ahora cambiaría algo que alguien más ya tiene, y su copia y la tuya no estarían de acuerdo la próxima vez que hagan pull. git revert resuelve esto sin cambiar nada que ya exista: crea un commits completamente nuevo que aplica lo opuesto del cambio.

bash
$ git revert 8f3c7d1
[main 2b6f0d1] Revert "Cache forecast responses in localStorage"

El historial sigue mostrando ambos commits, el original y el que lo deshace, en orden. La copia de nadie necesita ser reformada, un compañero trae el nuevo commit de reversión como cualquier otro.

git revert siempre es seguro en un commit que otros ya tienen, porque añade un nuevo commit en lugar de cambiar uno antiguo.

Invertir un cambio que se superpone con commits posteriores puede producir un conflicto, del mismo tipo que merging and conflicts cubre: Git marca el archivo con <<<<<<<, =======, y >>>>>>> y espera a que lo resuelvas. Arregla los marcadores, git add el archivo, luego termina con git revert --continue.

bash
$ git revert 8f3c7d1
Auto-merging src/index.js
CONFLICT (content): Merge conflict in src/index.js

$ git add src/index.js
$ git revert --continue

La seguridad de revert en el historial compartido descansa en la misma idea detrás de nunca hacer rebase de una rama pública: reescribir un commit que otros ya han traído obliga a su copia y la tuya a perder sincronización, y alguien tiene que forzar su paso en la falta de coincidencia después. Revert deja cada commit existente exactamente como era. Solo añade uno nuevo, así que la copia de nadie necesita reconciliación con la tuya.

JunoDeshacer un commit con revertgit revert <commit> deshace un commit añadiendo un commit completamente nuevo que lo invierte, en lugar de borrar el original. Eso lo hace la opción segura una vez que un commit ya se comparte con alguien más, ya que nada guardado previamente se cambia. El historial mantiene un registro tanto del error como de la corrección, que me gusta más cuanto más uso Git.
JunoDeshacer un commit con revert Revert puede entrar en conflicto de la misma manera que una fusión cuando commits posteriores tocaron las mismas líneas, resuélvelo de la misma manera: arregla los marcadores, add el archivo, luego git revert --continue. Úsalo cada vez que el commit que estás deshaciendo ya está enviado o compartido.
JunoDeshacer un commit con revert Revert nunca toca un commit existente, así que es la herramienta de deshacer que juega bien con una rama de la que otros ya están extrayendo. Conviértelo en tu predeterminado en el momento en que un commit malo ha salido de tu propia máquina.

Retroceder con git reset: soft, mixed, y hard

Hay una tercera forma de deshacer un commit, git reset, y funciona diferente de revert: en lugar de añadir un nuevo commit que deshace el antiguo, reset mueve tu rama actual para que apunte a un commit diferente por completo. Eso lo hace rápido y limpio para commits que nadie más ha visto, y arriesgado para commits que han visto.

Reset es el primo más amplio y más fácil de usar mal que restore y revert arriba. No lo necesitarás para la mayoría de arreglos diarios, restore, revert, y stash cubren la gran mayoría de momentos "ayuda, deshaz esto". Lo verás en las instrucciones de otros aunque, así que aquí está el titular: reset mueve tu rama hacia atrás en el historial, y uno de sus modos, --hard, descarta el trabajo no confirmado sin confirmación. Si un comando que estás copiando de algún lado incluye git reset --hard, lee qué hace antes de ejecutarlo.

git reset <commit> mueve tu rama para que apunte a ese commit, y --soft, --mixed, y --hard deciden cuánto más se mueve con él. Imagina tres capas: tu historial de commits, donde se sienta el puntero de rama, el área de preparación, lo que está alineado para confirmar, y tu directorio de trabajo, los archivos en disco. Los hashes cortos abajo se leen de la misma manera que History cubre con git log --oneline, y el destino HEAD~1 significa "un commit antes de HEAD", la forma estándar de decir "deshacer el commit más nuevo".

bash
$ git log --oneline
8f3c7d1 Cache forecast responses in localStorage
4f2a1c9 Fix temperature conversion in Celsius-to-Fahrenheit formula
5f3d8b2 Add five-day forecast to the dashboard
1a2b3c4 Set up project structure

$ git reset --soft HEAD~1

--soft mueve el puntero de rama hacia atrás un commit y se detiene. El área de preparación y el directorio de trabajo no se tocan, así que todo desde el commit deshecho se queda preparado, listo para ser reconfirmado como quieras.

bash
$ git reset --mixed HEAD~1

--mixed es lo que se ejecuta si dejas la bandera. Mueve el puntero de rama hacia atrás y también limpia el área de preparación, así que los cambios del commit deshecho regresan como ediciones no preparadas en tu directorio de trabajo. Nada se pierde, ejecuta git add de nuevo antes de confirmar.

bash
$ git reset --hard HEAD~1

git reset --hard mueve el puntero de rama hacia atrás y sobrescribe tu directorio de trabajo para que coincida, así que cualquier trabajo no confirmado y el commit deshecho desaparecen de vista en un paso, sin que se pida confirmación. Este es el modo para hacer una pausa antes de ejecutar.

Esto también explica qué estaba haciendo git commit --amend en the commit loop: amend está cerca de un reset --soft HEAD~1 seguido inmediatamente por un nuevo commit, por lo que reemplaza tu último commit en lugar de apilarse encima.

Reset solo mueve punteros y, en modo --hard, el directorio de trabajo para que coincida. Nunca elimina un objeto de commit por completo. El commit que un reset --hard dejó atrás sigue sentado en la base de datos de objetos de Git, inaccesible desde cualquier rama, lo que importa porque hay una forma de alcanzarlo de nuevo: git reflog, lo siguiente. En una rama compartida aunque, mover tu rama hacia atrás y enviar significa force-push sobre commits que tus compañeros ya tienen, así que su siguiente pull no está de acuerdo con el historial reelaborado. Esa es la razón práctica por la que reset permanece como una herramienta de limpieza local mientras que revert cubre cualquier cosa que ya haya salido de tu máquina.

JunoRetroceder con git reset No usarás git reset en la mayoría de arreglos diarios, restore, revert, y stash ya cubren casi todo. Aún así, conoce su forma: reset mueve tu rama hacia atrás a un commit anterior, y su modo --hard limpia tu directorio de trabajo para que coincida sin confirmación. Trata cualquier comando que copies que incluya reset --hard con verdadera cautela.
JunoRetroceder con git resetgit reset --soft mantiene los cambios del commit deshecho preparados, --mixed (el predeterminado) los no prepara pero mantiene las ediciones, --hard descarta las ediciones completamente. commit --amend es básicamente un soft reset inmediatamente seguido por un nuevo commit, por lo que reemplaza el último commit en lugar de apilar encima.
JunoRetroceder con git reset Reset solo mueve punteros, y con --hard, tu directorio de trabajo para que coincida, nunca elimina el objeto de commit en sí. Ese objeto permanece en la base de datos hasta que el reflog y la recolección de basura se pongan al día con él. Envía una rama reelaborada y el siguiente pull de tus compañeros choca con un historial que ya no coincide con el suyo.

Elegir entre revert y reset

La regla que decide qué herramienta usar: ¿alguien más ya ha traído este commit? Si sí, revert. Si el commit solo existe en tu máquina, en una rama que no has enviado, reset está bien y a menudo es la corrección más limpia. Cuando no estés seguro, revert es siempre la opción segura, ya que funciona en ambos casos.

"¿Alguien lo ha traído?" realmente significa "¿es este commit accesible desde una referencia que alguien más tiene?", lo que en la práctica significa si se sienta en una rama que ha sido enviada. Un commit en una rama local que inventaste hace cinco minutos y nunca enviaste es territorio abierto para reset. En el momento en que lo envías, trátalo como compartido: resetear localmente y force-push significa coordinar con quienquiera que toque esa rama primero.

JunoElegir entre revert y reset Haz una pregunta antes de deshacer un commit: ¿alguien más ya lo ha traído? Si sí, recurre a revert, siempre es seguro. Si el commit solo existe en tu máquina, reset también funciona bien. Cuando no estés seguro de cuál se aplica, revert es el predeterminado más seguro.
JunoElegir entre revert y reset Una vez que un commit está enviado y alguien podría tenerlo, recurre a revert. Mientras sea puramente local para ti, reset está bien y a menudo es la corrección más limpia, ya que no deja una entrada "revert de un revert" sentada en el registro.
JunoElegir entre revert y reset La verdadera prueba es accesibilidad: ¿este commit se sienta en una referencia que alguien más ha traído? Local y no enviado, reset es territorio abierto. Enviado y compartido, trata el reseteo como algo a coordinar primero, ya que un force-push posterior choca con lo que tus compañeros ya tienen.

Recuperar un commit con git reflog

Un reset --hard, un rebase desordenado, o eliminar una rama por error puede parecer que un commit ha desaparecido completamente. Generalmente no ha sido así. Git mantiene un registro local de dónde han apuntado tu HEAD y ramas, llamado el reflog, y ese registro es casi siempre tu forma de volver a un commit que parece perdido.

Es poco probable que necesites esto a menudo, pero aquí está el titular: Git raramente descarta algo en el momento en que parece haberlo hecho. Si alguna vez ejecutas reset --hard y te das cuenta después de que necesitabas ese commit, muy probablemente hay una forma de recuperarlo, cubierto aquí para siempre que estés listo para profundizar.

La versión corta: git reflog lista posiciones recientes de HEAD, cada una con un hash corto, y puedes git reset --hard a cualquiera de esos hashes para volver a ese estado exacto. Se lee como un segundo historial: cada lugar donde HEAD ha estado, incluyendo commits que tus ramas ya no apuntan.

Un commit sin rama, etiqueta, o HEAD apuntando a él ya es inaccesible: a la deriva en el repositorio, todavía guardado, solo faltando una etiqueta que lleve de vuelta a él. El reflog es una lista cronológica de dónde HEAD se ha movido en tu máquina, este cambio de rama, ese reset, ese rebase, cada entrada clave por una referencia corta como HEAD@{2}. Encuentra la entrada de justo antes del error y apunta una rama de vuelta a él:

bash
$ git reflog
2b6f0d1 HEAD@{0}: reset: moving to HEAD~1
8f3c7d1 HEAD@{1}: commit: Cache forecast responses in localStorage
4f2a1c9 HEAD@{2}: commit: Fix temperature conversion in Celsius-to-Fahrenheit formula

$ git reset --hard 8f3c7d1

Ese es el commit que reset --hard parecía borrar, restaurado. Dos límites que vale la pena conocer. Primero, el reflog vive solo en tu propia máquina, push y pull nunca lo tocan, así que no puede recuperar un commit que un compañero perdió en la suya. Segundo, no dura para siempre: Git finalmente ejecuta recolección de basura y limpia commits inaccesibles una vez que han envejecido más allá de un corte predeterminado de algunas semanas, así que la red de seguridad cubre errores recientes en lugar de la vida completa del proyecto.

JunoRecuperar un commit perdido Si alguna vez ejecutas reset --hard y te das cuenta demasiado tarde de que necesitabas ese commit, probablemente sea todavía recuperable. Git mantiene un registro local llamado el reflog de dónde tu trabajo ha apuntado, y eso es generalmente suficiente para recuperar un commit que pareció desaparecido. Esto es territorio de inmersión profunda, pero es reconfortante saber que está ahí.
JunoRecuperar un commit perdidogit reflog lista posiciones recientes de HEAD con hashes cortos que puedes reset --hard para volver a ellos, así que un reset que fue demasiado lejos es generalmente recuperable. Solo vive en tu propia máquina aunque, así que no puede rescatar un commit que un compañero perdió en la suya.
JunoRecuperar un commit perdido El reflog es un registro local y cronológico de cada lugar donde HEAD ha estado, que es cómo un commit que reset --hard parecía borrar se vuelve a encontrar apuntando una rama a la entrada HEAD@{n} correcta. Envejece con recolección de basura eventualmente, y cubre solo tus propios errores recientes. Un compañero que perdió un commit necesita ejecutar la misma recuperación en su propia máquina.