Ha pasado lo que tenía que pasar. Bruno publicó ayer en main un commit que cambia cómo se guardan las tareas en localStorage, y esta mañana Ana ha descubierto que a los usuarios que ya tenían tareas guardadas se les vacía la lista al abrir la aplicación. El commit está en git.ejemplo.es desde hace catorce horas, Ana y Carla lo tienen descargado, y encima hay tres commits más encima suyo.

Todo lo que hemos aprendido en este módulo reescribe historial, y la regla de oro lo prohíbe expresamente en esta situación: ese commit ya es público. No se puede rebasar, no se puede eliminar con un rebase -i, no se puede hacer desaparecer.

Git tiene una respuesta para esto, y es elegante precisamente porque no lucha contra la inmutabilidad sino que la acepta: no se borra el commit, se añade otro que aplica el cambio contrario. El historial crece en lugar de encogerse, nadie tiene que forzar nada, y queda constancia pública de que aquello se deshizo y por qué.

Eso es git revert, y es la única forma segura de deshacer algo que ya está publicado. Con ella cerramos el módulo.

Contenido

  1. Qué hace git revert
  2. Revirtiendo el commit de Bruno
  3. revert frente a reset frente a --amend
  4. Revertir varios commits y rangos
  5. -n: agrupar varias reversiones en un commit
  6. Conflictos al revertir
  7. Revertir un commit de fusión: -m 1 y -m 2
  8. El problema de la rama revertida que ya no vuelve a entrar
  9. Revertir una reversión
  10. Cuándo revertir y cuándo no

  1. Qué hace git revert

git revert <sha>

Git calcula el diff que introdujo <sha>, lo invierte (lo que añadía, lo quita; lo que quitaba, lo añade) y crea un commit nuevo con ese cambio inverso sobre la punta de tu rama.

gitGraph
   commit id: "8b6d3c2"
   commit id: "c5d9b1e"
   commit id: "4f8a2e6" type: HIGHLIGHT
   commit id: "7d3a8f4"
   commit id: "e91d4a8"
   commit id: "b52c9d1"
   commit id: "3e7f1a8"

El commit 4f8a2e6 (destacado) es el que rompió las cosas. 3e7f1a8 es su reversión: deshace su efecto sin tocar nada de lo que hay en medio. Los seis commits siguen ahí, en el mismo orden, con los mismos hashes.

Las tres propiedades que lo distinguen de todo lo demás del módulo:

Propiedad git revert
¿Reescribe historial? No: solo añade
¿Cambian los hashes existentes? No, ninguno
¿Hay que forzar el push? No
¿Se puede usar sobre trabajo publicado? : es exactamente para eso
¿Queda constancia? , y es una ventaja

Ese último punto se malinterpreta a menudo. Alguien dice "quiero que desaparezca, no que quede en el historial". Pero que quede constancia es lo correcto: dentro de seis meses, cuando alguien mire por qué esa funcionalidad no está, el historial se lo dirá. Un commit borrado no explica nada; un commit revertido con un buen mensaje explica todo.

  1. Revirtiendo el commit de Bruno

Situación de partida:

git switch main
git pull
git log --oneline -6
b52c9d1 (HEAD -> main, origin/main) Añadir el filtro de pendientes
e91d4a8 Extraer la creación del elemento de tarea a su función
7d3a8f4 Devolver el foco al campo de texto tras borrar una tarea
4f8a2e6 Cambiar el formato de almacenamiento en localStorage
c5d9b1e Documentar la instalación en el README
8b6d3c2 Añadir estilos base del listado

El culpable es 4f8a2e6. Primero, mirarlo:

git show 4f8a2e6
commit 4f8a2e6...
Author: Bruno Salas <[email protected]>
Date:   Thu Jul 30 16:41:09 2026 +0200

    Cambiar el formato de almacenamiento en localStorage

diff --git a/app.js b/app.js
--- a/app.js
+++ b/app.js
@@ -8,11 +8,11 @@
 function cargarTareas() {
-  const guardadas = localStorage.getItem('tareas');
-  return guardadas ? JSON.parse(guardadas) : [];
+  const guardadas = localStorage.getItem('gestor.tareas.v2');
+  return guardadas ? JSON.parse(guardadas) : [];
 }

 function guardarTareas() {
-  localStorage.setItem('tareas', JSON.stringify(tareas));
+  localStorage.setItem('gestor.tareas.v2', JSON.stringify(tareas));
 }

Ahí está el fallo: se cambió la clave de almacenamiento sin migrar los datos existentes. Ahora, la reversión:

git revert 4f8a2e6

Git abre el editor con un mensaje propuesto:

Revert "Cambiar el formato de almacenamiento en localStorage"

This reverts commit 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a.

Ana lo completa, porque el mensaje por defecto dice qué pero no por qué:

Revert "Cambiar el formato de almacenamiento en localStorage"

This reverts commit 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a.

El cambio de clave de localStorage deja sin tareas a quienes ya
tenían datos guardados con la clave anterior. Se revierte para
restablecer el servicio; el cambio volverá cuando incluya la
migración de los datos existentes.
[main 3e7f1a8] Revert "Cambiar el formato de almacenamiento en localStorage"
 1 file changed, 2 insertions(+), 2 deletions(-)
git log --oneline -3
git show 3e7f1a8 --stat
3e7f1a8 (HEAD -> main) Revert "Cambiar el formato de almacenamiento en localStorage"
b52c9d1 (origin/main) Añadir el filtro de pendientes
e91d4a8 Extraer la creación del elemento de tarea a su función

El diff del nuevo commit es exactamente el inverso del original:

@@ -8,11 +8,11 @@
 function cargarTareas() {
-  const guardadas = localStorage.getItem('gestor.tareas.v2');
+  const guardadas = localStorage.getItem('tareas');
   return guardadas ? JSON.parse(guardadas) : [];
 }

Y ahora lo mejor de todo:

git push
To git.ejemplo.es:equipo/gestor-tareas.git
   b52c9d1..3e7f1a8  main -> main

Un push normal, sin --force, sin --force-with-lease, sin avisar a nadie de que reescriba nada. Carla y Bruno harán git pull, recibirán un commit más y todo funcionará. Ese es el valor de revert: deshacer sin coste social.

Opciones de git revert:

Opción Qué hace
-n / --no-commit Aplica el cambio inverso al índice sin confirmar
-e / --edit Abre el editor para el mensaje (por defecto ya lo hace)
--no-edit Usa el mensaje automático sin abrir el editor
-m <n> Elige el padre principal al revertir una fusión (apartado 7)
-s / --signoff Añade la línea Signed-off-by:
--continue / --skip / --abort / --quit Control del proceso ante conflictos

  1. revert frente a reset frente a --amend

Las tres "deshacen" cosas y se confunden constantemente. La diferencia clave es si tocan el historial existente, y eso determina cuál puedes usar sobre trabajo publicado.

git revert <sha> git reset <sha> git commit --amend
Qué hace Crea un commit nuevo con el cambio inverso Mueve la rama a otro commit Sustituye el último commit
¿Reescribe historial? No
¿Cambian hashes existentes? No Los posteriores quedan huérfanos El último cambia de hash
¿El historial crece o encoge? Crece Encoge (aparentemente) Igual
Sobre trabajo publicado Sí, es lo indicado No No
Sobre trabajo local Se puede, pero suele haber algo mejor Sí, perfecto Sí, perfecto
¿Deja constancia? No No
Alcance Cualquier commit del historial Solo mueve la punta Solo el último commit
¿Requiere push --force? No
Caso típico Una funcionalidad publicada rompe algo Deshacer commits locales de hoy Corregir el mensaje o añadir un olvido al último commit

La forma de decidir, en una pregunta: ¿lo que quiero deshacer está ya en el servidor y otros lo pueden tener?

  • git revert. No hay alternativa segura.
  • Noreset o --amend son más limpios, porque el resultado no arrastra un commit de deshacer que a nadie le importa.

Un ejemplo que aclara la diferencia de resultado. Historial A → B → C, queremos quitar B:

Con revert:   A → B → C → B'        (B' deshace B; todo sigue ahí)
Con reset:    A                      (B y C dejan de estar referenciados)

Fíjate en el detalle importante del reset: también se lleva C, porque mueve la rama a A y todo lo posterior queda huérfano. Para quitar solo B conservando C hay que rebasar, no resetear.

git reset es mucho más de lo que cabe aquí. Tiene tres modos —--soft, --mixed y --hard— que se comportan de forma muy distinta según a cuáles de las tres zonas de la lección 01-03 afecten, y es la herramienta central para deshacer trabajo local. Todo eso, con las tablas y los procedimientos completos, es el tema de la lección 09-02: Deshaciendo Cambios. Aquí solo necesitábamos el contraste conceptual: reset reescribe, revert no.

  1. Revertir varios commits y rangos

git revert acepta varios commits y rangos, con la misma notación de la lección 02-06:

# Varios sueltos
git revert 4f8a2e6 7d3a8f4

# Un rango: revierte los posteriores a A hasta B (A NO incluido)
git revert A..B

# Con A incluido
git revert A^..B

# Los tres últimos commits
git revert HEAD~3..HEAD

Un detalle fundamental: Git los revierte en orden inverso, del más reciente al más antiguo. Es lo correcto y lo que evita conflictos innecesarios: si C se apoya en B, hay que deshacer C antes que B. Por eso git revert HEAD~3..HEAD produce tres commits de reversión en este orden:

Revert "Commit C"
Revert "Commit B"
Revert "Commit A"
git log --oneline -7
9c4e7b2 (HEAD -> main) Revert "Añadir el filtro de pendientes"
5f1d8a3 Revert "Extraer la creación del elemento de tarea a su función"
2a8c6f9 Revert "Devolver el foco al campo de texto tras borrar una tarea"
b52c9d1 Añadir el filtro de pendientes
e91d4a8 Extraer la creación del elemento de tarea a su función
7d3a8f4 Devolver el foco al campo de texto tras borrar una tarea
4f8a2e6 Cambiar el formato de almacenamiento en localStorage

Tres commits deshechos, tres commits de reversión, y un historial que cuenta exactamente lo que pasó. Si prefieres que sea uno solo, pasamos al siguiente apartado.

  1. -n: agrupar varias reversiones en un commit

Tres commits de reversión seguidos suelen ser ruido: lo que ocurrió conceptualmente es una decisión ("quitamos la funcionalidad del filtro"), no tres. -n (--no-commit) aplica los cambios inversos al índice sin confirmar, y tú confirmas al final:

git revert -n HEAD~3..HEAD
git status --short
M  app.js
M  estilos.css
M  index.html
git commit -m "Revertir la funcionalidad de filtro de pendientes

Se revierten los commits b52c9d1, e91d4a8 y 7d3a8f4 porque el filtro
deja el contador desincronizado cuando hay tareas ocultas. Volverá
cuando el contador se calcule sobre la lista completa."
[main 7e2c9f4] Revertir la funcionalidad de filtro de pendientes
 3 files changed, 18 insertions(+), 42 deletions(-)

Un solo commit, un solo mensaje, una sola decisión. Es casi siempre la mejor forma de revertir un conjunto de commits relacionados.

Mientras estás en medio de un revert -n, el repositorio está en un estado especial (hay un fichero .git/REVERT_HEAD). Si te arrepientes:

git revert --abort     # deshace todo lo acumulado
git revert --quit      # sale del estado pero CONSERVA lo aplicado en el índice

  1. Conflictos al revertir

Un revert es, por dentro, una fusión: Git intenta aplicar un parche inverso sobre un contexto que puede haber cambiado desde entonces. Si el código que quieres deshacer ha sido modificado después, conflictúa.

git revert 4f8a2e6
Auto-merging app.js
CONFLICT (content): Merge conflict in app.js
error: could not revert 4f8a2e6... Cambiar el formato de almacenamiento en localStorage
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run "git revert --continue".
hint: You can instead skip this commit with "git revert --skip".
hint: To abort and get back to the state before "git revert",
hint: run "git revert --abort".

La mecánica de resolución es la de siempre (lección 03-05). Lo específico:

Comando Qué hace
git revert --continue Confirma la reversión con tu resolución y sigue con la siguiente
git revert --skip Descarta esa reversión y sigue con el resto del rango
git revert --abort Cancela toda la operación y vuelve al estado inicial
git revert --quit Sale del estado de revert conservando lo aplicado

Como en el cherry-pick (lección 05-03), aquí ours y theirs NO están invertidos: ours es tu rama, theirs es el cambio inverso que se intenta aplicar. La inversión era una peculiaridad del rebase.

Y una advertencia que conviene interiorizar: cuando un revert conflictúa, párate a pensar. El conflicto te está diciendo que alguien construyó encima de lo que quieres deshacer. Revertir a la fuerza puede dejar código que llama a una función que acaba de desaparecer. Comprueba quién depende de eso antes de continuar:

git log --oneline 4f8a2e6..HEAD -- app.js     # qué se ha tocado después en ese fichero
git log -S "gestor.tareas.v2" --oneline       # quién más usa lo que estoy quitando

-S es la búsqueda por pickaxe de la lección 02-06, y aquí es exactamente la herramienta adecuada.

  1. Revertir un commit de fusión: -m 1 y -m 2

Este es el caso que hace fallar a todo el mundo la primera vez.

git revert c2a8f1e
error: commit c2a8f1ea4b7d9c3e5f2a8b6d1c9e4f7a3b5d2c8e is a merge but no -m option was given.
fatal: revert failed

Por qué falla. Revertir significa aplicar el diff inverso. Pero un commit de fusión tiene dos padres, y por tanto dos diffs posibles: uno contra el primer padre y otro contra el segundo. Git no puede adivinar cuál quieres, así que se niega y te obliga a decirlo.

gitGraph
   commit id: "8b6d3c2"
   commit id: "c5d9b1e"
   branch funcionalidad/filtro-pendientes
   commit id: "a1e5c93"
   commit id: "6f2b9d4"
   checkout main
   commit id: "7d3a8f4"
   merge funcionalidad/filtro-pendientes id: "c2a8f1e"
   commit id: "e91d4a8"

Los padres del commit de fusión, en orden:

git show --format='%h %p %s' -s c2a8f1e
c2a8f1e 7d3a8f4 6f2b9d4 Fusionar funcionalidad/filtro-pendientes
Número Padre Qué es
1 7d3a8f4 La rama en la que estabas al fusionar: main
2 6f2b9d4 La rama que fusionaste: funcionalidad/filtro-pendientes

El primer padre es siempre la rama de destino, porque el commit de fusión se creó estando tú en ella. El segundo (y siguientes, en un octopus merge) son las ramas traídas.

-m 1 significa: "quédate con la línea del padre 1", es decir, deshaz todo lo que aportó la rama fusionada y conserva main como estaba. Es lo que quieres el 99 % de las veces.

git revert -m 1 c2a8f1e
[main 4d7c1a9] Revert "Fusionar funcionalidad/filtro-pendientes"
 3 files changed, 4 insertions(+), 47 deletions(-)

-m 2 haría lo contrario: deshacer lo que aportó main y conservar la rama. Es una operación rarísima y casi siempre indica que te has equivocado de comando.

La regla mnemotécnica: -m 1 = "conserva la rama principal, tira lo que traje".

Consejo práctico: antes de revertir una fusión, mira sus padres. git log --format='%h %p %s' -1 <sha> te los da en un segundo, y git show <sha>^1 / git show <sha>^2 te enseña cada uno. Con dos comandos evitas el error.

  1. El problema de la rama revertida que ya no vuelve a entrar

Aquí llega la consecuencia sutil, la que muerde semanas después, y la razón por la que revertir una fusión merece un apartado propio.

Bruno revirtió la fusión de funcionalidad/filtro-pendientes. Dos semanas después arregla el problema del contador y quiere volver a integrar la rama:

git switch main
git merge funcionalidad/filtro-pendientes
Already up to date.

"Already up to date", y en main no aparece nada del filtro. Desconcertante.

Por qué pasa. Recuerda la fusión a tres bandas de la lección 03-03: Git busca el ancestro común y compara. Pero el ancestro común de main y funcionalidad/filtro-pendientes es ahora la propia fusión revertida (c2a8f1e), porque esa fusión sigue en el historial y hace que todos los commits de la rama sean alcanzables desde main.

Desde el punto de vista del grafo, esos commits ya están integrados. Que su contenido se deshiciera después con 4d7c1a9 es irrelevante para el cálculo: Git razona sobre alcanzabilidad, no sobre contenido.

gitGraph
   commit id: "c5d9b1e"
   branch funcionalidad/filtro-pendientes
   commit id: "a1e5c93"
   commit id: "6f2b9d4"
   checkout main
   commit id: "7d3a8f4"
   merge funcionalidad/filtro-pendientes id: "c2a8f1e"
   commit id: "4d7c1a9" type: HIGHLIGHT
   commit id: "e91d4a8"

El commit destacado es la reversión. La rama está conectada al grafo de main; el contenido, no.

Y si Bruno añade commits nuevos a la rama y fusiona otra vez, el resultado es peor todavía: entran solo los commits nuevos, sobre un main al que le falta la base que aquellos esperaban. Código que llama a funciones que no existen, y ningún conflicto que lo avise.

Las tres soluciones, en orden de preferencia:

A. Revertir la reversión. La más simple y la que se usa casi siempre:

git revert 4d7c1a9
[main 9e2c6b1] Revert "Revert \"Fusionar funcionalidad/filtro-pendientes\""
 3 files changed, 47 insertions(+), 4 deletions(-)

Eso devuelve el contenido de la rama a main. A partir de ahí, los commits nuevos que Bruno haga en la rama se fusionarán con normalidad. El historial queda un poco cómico —un "Revert del Revert"— pero es correcto, honesto y seguro. Si quieres, edita el mensaje para explicarlo:

Reintegrar el filtro de pendientes

Revierte la reversión 4d7c1a9. El problema del contador está
corregido en 8f3d2c7, así que la funcionalidad vuelve a entrar.

B. Rehacer la rama sobre el main actual. Si la rama era corta, crear una rama nueva desde main y traer los commits con git cherry-pick (lección 05-03) da un historial más limpio:

git switch -c funcionalidad/filtro-pendientes-v2 main
git cherry-pick a1e5c93 6f2b9d4
# ... corregir el problema del contador ...

Los commits nuevos tienen hashes nuevos, no son alcanzables desde main, y la fusión posterior funciona con normalidad.

C. Forzar la fusión ignorando el ancestro. Existe, pero casi nunca es buena idea; la mencionamos para que la reconozcas si la ves:

git merge --no-commit --no-ff funcionalidad/filtro-pendientes

La lección de fondo, y merece subrayarse:

Revertir una fusión deshace el contenido, no la topología. El grafo sigue diciendo que esa rama se integró.

Por eso, cuando una rama entera hay que quitarla y se sabe que volverá, muchos equipos prefieren revertir los commits individuales en lugar de la fusión, o directamente no fusionar hasta estar seguros. Y por eso conviene saber esto antes de revertir una fusión, no después.

  1. Revertir una reversión

Ya lo hemos usado en el apartado anterior, pero conviene enunciarlo: una reversión es un commit normal y corriente, así que se puede revertir.

git revert <sha-de-la-reversión>

El resultado es que el cambio original vuelve. Es la forma canónica de decir "me precipité al deshacer esto".

Y como es simétrico, funciona tantas veces como quieras: revertir la reversión de la reversión deshace otra vez. Cada paso es un commit nuevo, nadie fuerza nada y el historial cuenta la secuencia completa de decisiones. Poco elegante, pero rigurosamente seguro.

  1. Cuándo revertir y cuándo no

Situación Herramienta
Un commit publicado rompe algo git revert
Una fusión publicada hay que deshacerla git revert -m 1
Quiero deshacer commits locales que no he enviado git reset (lección 09-02)
Me equivoqué en el mensaje del último commit (sin publicar) git commit --amend (lección 02-04)
Quiero quitar un commit del medio de una rama sin publicar git rebase -i con drop (lección 05-02)
Quiero descartar cambios sin confirmar git restore (lección 02-04)
Quiero volver a probar una versión antigua sin cambiar nada git switch --detach <sha> (lección 03-02)

Y dos consideraciones de criterio que no son técnicas:

Revierte pronto. Si algo está roto en main, revertir primero y diagnosticar después es casi siempre lo correcto. Un main roto bloquea a todo el equipo; el commit revertido no se pierde y se puede reintroducir arreglado. Es una decisión de servicio, no de orgullo.

Escribe por qué. El mensaje automático (This reverts commit ...) dice qué se deshizo pero no por qué. Añadir tres líneas explicando el motivo y qué haría falta para volver a intentarlo convierte una reversión en información útil dentro de seis meses. Las convenciones de mensajes son el tema de la lección 08-01, pero este caso concreto vale la pena mencionarlo aquí: la reversión sin explicación es una de las entradas más frustrantes que se puede encontrar en un historial.

Errores Comunes y Consejos

Error 1: usar reset en lugar de revert sobre trabajo publicado. Es la vía rápida al escenario del apartado 9 de la lección 05-01: push --force, compañeros con historiales divergentes y una tarde de coordinación. Si está en el servidor, revert.

Error 2: intentar revertir una fusión sin -m. Git falla con un mensaje claro. Recuerda: -m 1 en el 99 % de los casos.

Error 3: confundir el orden de los padres. -m 1 es la rama en la que estabas (normalmente main); -m 2 es la que trajiste. Comprueba con git show --format='%h %p %s' -s <sha> antes de decidir.

Error 4: dar por hecho que tras revertir una fusión la rama volverá a entrar sola. No lo hará: Already up to date. Hay que revertir la reversión o rehacer la rama.

Error 5: aceptar el mensaje por defecto sin más. Dice qué, no por qué. Treinta segundos de explicación ahorran una investigación futura.

Error 6: revertir un commit que otros construyeron encima sin comprobar dependencias. Puedes dejar el código llamando a algo que ya no existe. Usa git log -S y git log <sha>..HEAD -- <fichero> antes.

Error 7: revertir en la rama equivocada. git revert actúa sobre la rama actual. Comprueba con git status antes de lanzarlo, sobre todo si vienes de mirar otra rama.

Consejo 1: mira siempre el commit antes de revertirlo. git show <sha> cuesta tres segundos y te dice si el cambio es autocontenido o si arrastra medio proyecto.

Consejo 2: para varios commits relacionados, usa -n y confirma una sola vez. Una decisión, un commit.

Consejo 3: si el revert conflictúa, léelo como una señal. Alguien construyó encima. Puede que la salida correcta no sea revertir sino corregir hacia delante.

Consejo 4: revertir es reversible. Si te precipitas, git revert sobre la reversión lo devuelve todo. Nada de lo que hagas con este comando es irrecuperable.

Consejo 5: en main, revertir primero y pensar después. Restablecer el servicio es prioritario; el diagnóstico puede hacerse con calma en una rama.

Ejercicios

Ejercicio 1: revertir un commit publicado

En un repositorio con cinco commits, de los cuales el tercero introduce un fallo:

  1. Revierte el tercer commit con un mensaje que explique el motivo.
  2. Demuestra que ninguno de los hashes anteriores ha cambiado.
  3. Demuestra que el contenido del fichero es el que había antes de ese commit, salvo lo que aportaron el cuarto y el quinto.
  4. Compara conceptualmente con lo que habría pasado usando git reset --hard sobre el segundo commit (no hace falta ejecutarlo en la rama principal: hazlo en una copia).

Ejercicio 2: revertir una fusión y volver a integrarla

  1. Crea una rama con dos commits y fusiónala en main con --no-ff.
  2. Añade un commit más a main.
  3. Revierte la fusión con -m 1 y comprueba que el contenido de la rama ha desaparecido.
  4. Intenta fusionar la rama otra vez y observa el mensaje.
  5. Resuélvelo revirtiendo la reversión y comprueba que el contenido vuelve.
  6. Dibuja (o describe) el grafo resultante.

Ejercicio 3: agrupar reversiones

Con una rama que tenga cuatro commits sobre main, revierte los tres últimos en un único commit usando -n. Verifica con git show --stat que el commit resultante contiene la reversión de los tres, y con git diff que el estado del proyecto coincide con el que había tras el primero.

Soluciones

Solución 1:

mkdir /tmp/practica-revert && cd /tmp/practica-revert
git init -b main
printf 'linea 1\n' > f.txt && git add . && git commit -m "Uno"
printf 'linea 1\nlinea 2\n' > f.txt && git commit -am "Dos"
printf 'linea 1\nlinea 2\nERROR\n' > f.txt && git commit -am "Tres (el fallo)"
printf 'linea 1\nlinea 2\nERROR\nlinea 4\n' > f.txt && git commit -am "Cuatro"
printf 'linea 1\nlinea 2\nERROR\nlinea 4\nlinea 5\n' > f.txt && git commit -am "Cinco"

git log --oneline
7c2e9b4 Cinco
3f8a1d6 Cuatro
9b5c7e2 Tres (el fallo)
1d4f8a3 Dos
6e2b9c7 Uno
# 1. Revertir el tercero
git revert 9b5c7e2 --no-edit
git commit --amend -m "Revert \"Tres (el fallo)\"

This reverts commit 9b5c7e2.

La linea ERROR se coló por un pegado accidental y rompe el parseo
del fichero. Se revierte para restablecer el comportamiento."
# 2. Los hashes anteriores no han cambiado
git log --oneline
8a1f6c3 Revert "Tres (el fallo)"
7c2e9b4 Cinco
3f8a1d6 Cuatro
9b5c7e2 Tres (el fallo)
1d4f8a3 Dos
6e2b9c7 Uno

Los cinco commits originales conservan sus hashes; solo se ha añadido uno arriba.

# 3. El contenido
cat f.txt
linea 1
linea 2
linea 4
linea 5

Ha desaparecido ERROR y se conservan las líneas de Cuatro y Cinco.

# 4. Comparación con reset, en una copia
git switch -c copia-reset 7c2e9b4
git reset --hard 1d4f8a3
git log --oneline
cat f.txt
1d4f8a3 Dos
6e2b9c7 Uno
linea 1
linea 2

El reset se ha llevado por delante Tres, Cuatro y Cinco: mueve la rama, no deshace un cambio concreto. Y si esa rama estuviera publicada, el push sería rechazado y haría falta forzarlo.

Solución 2:

mkdir /tmp/practica-revert-merge && cd /tmp/practica-revert-merge
git init -b main
echo "base" > f.txt && git add . && git commit -m "Base"

git switch -c funcionalidad/filtro
echo "filtro parte 1" > filtro.js && git add . && git commit -m "Filtro parte 1"
echo "filtro parte 2" >> filtro.js && git commit -am "Filtro parte 2"

git switch main
git merge --no-ff funcionalidad/filtro -m "Fusionar funcionalidad/filtro"
echo "otra cosa" > otro.txt && git add . && git commit -m "Otro trabajo en main"

git log --oneline --graph
* 5c1e8f2 (HEAD -> main) Otro trabajo en main
*   9d3b7a4 Fusionar funcionalidad/filtro
|\
| * 2f8c6e1 (funcionalidad/filtro) Filtro parte 2
| * 7a4d9b3 Filtro parte 1
|/
* 1e6f2c8 Base
# 3. Revertir la fusión
git show --format='%h %p %s' -s 9d3b7a4
git revert -m 1 9d3b7a4 --no-edit
ls
9d3b7a4 1e6f2c8 2f8c6e1 Fusionar funcionalidad/filtro
f.txt  otro.txt

filtro.js ha desaparecido: el contenido de la rama está deshecho.

# 4. Intentar fusionar otra vez
git merge funcionalidad/filtro
Already up to date.

Ahí está el problema: el grafo dice que ya está integrada.

# 5. Revertir la reversión
git log --oneline -1
git revert HEAD --no-edit
ls
cat filtro.js
4b8e2c9 Revert "Fusionar funcionalidad/filtro"
f.txt  filtro.js  otro.txt
filtro parte 1
filtro parte 2

El contenido ha vuelto.

# 6. El grafo
git log --oneline --graph
* 8f2d6a1 (HEAD -> main) Revert "Revert "Fusionar funcionalidad/filtro""
* 4b8e2c9 Revert "Fusionar funcionalidad/filtro"
* 5c1e8f2 Otro trabajo en main
*   9d3b7a4 Fusionar funcionalidad/filtro
|\
| * 2f8c6e1 (funcionalidad/filtro) Filtro parte 2
| * 7a4d9b3 Filtro parte 1
|/
* 1e6f2c8 Base

Ocho commits que cuentan la historia completa: se integró, se quitó, se volvió a poner. Nada se ha reescrito y nadie ha forzado nada.

Solución 3:

mkdir /tmp/practica-revert-n && cd /tmp/practica-revert-n
git init -b main
echo "base" > f.txt && git add . && git commit -m "Base"

echo "a" > a.txt && git add . && git commit -m "Añadir a"
echo "b" > b.txt && git add . && git commit -m "Añadir b"
echo "c" > c.txt && git add . && git commit -m "Añadir c"
echo "d" > d.txt && git add . && git commit -m "Añadir d"

git log --oneline
9c2f7e4 Añadir d
5a8d1b6 Añadir c
3e7b9c2 Añadir b
8f4c2a9 Añadir a
1d6e8f3 Base
# Revertir los tres últimos en un solo commit
git revert -n HEAD~3..HEAD
git status --short
D  b.txt
D  c.txt
D  d.txt
git commit -m "Revertir los ficheros b, c y d

Se revierten 3e7b9c2, 5a8d1b6 y 9c2f7e4: el enfoque de un fichero
por letra no era el previsto. Volverá con la estructura correcta."

git show --stat HEAD
commit 2b9e6c1...
    Revertir los ficheros b, c y d

 b.txt | 1 -
 c.txt | 1 -
 d.txt | 1 -
 3 files changed, 3 deletions(-)
# El estado coincide con el que había tras "Añadir a"
git diff 8f4c2a9 HEAD
ls
(sin salida: son idénticos)
a.txt  f.txt

Un solo commit de reversión y un estado idéntico al del primer commit de la serie, sin haber tocado ningún hash.

Conclusión

git revert es la pieza que faltaba: la forma de deshacer que respeta la regla de oro. Lo esencial:

  • Revertir no borra: añade. Git crea un commit nuevo con el cambio inverso. Ningún hash existente cambia, no hace falta forzar el push y queda constancia pública de la decisión.
  • Es la única forma segura de deshacer algo publicado. reset y --amend reescriben historial y solo valen para trabajo que aún es tuyo; la lección 09-02 desarrolla reset a fondo.
  • Admite varios commits y rangos, que revierte del más reciente al más antiguo; -n permite acumular varias reversiones y confirmarlas como una sola decisión.
  • Los conflictos se resuelven como siempre, con --continue, --skip y --abort, y aquí ours/theirs no están invertidos. Un conflicto al revertir suele significar que alguien construyó encima: párate a mirar.
  • Revertir una fusión exige -m para elegir el padre principal: -m 1 (la rama en la que estabas, casi siempre main) o -m 2 (la que trajiste, rarísimo).
  • Revertir una fusión deshace el contenido, no la topología. La rama sigue siendo alcanzable, así que volver a fusionarla dice Already up to date. Se resuelve revirtiendo la reversión, o rehaciendo la rama con cherry-pick.
  • Todo es reversible: una reversión es un commit normal y se puede revertir a su vez.
  • Y el criterio: en main, revierte primero y diagnostica después, y explica siempre por qué.

Cierre del módulo 5

Con esta lección cierras el bloque de operaciones avanzadas. Repasando lo que ha cambiado en tu forma de trabajar:

  • git rebase para reaplicar commits sobre otra base y obtener un historial lineal, sabiendo que crea commits nuevos.
  • git rebase -i para reordenar, unir, dividir, renombrar y eliminar commits antes de publicarlos, con --fixup/--autosquash como flujo diario y --exec como red de validación.
  • git cherry-pick para trasplantar commits concretos entre ramas que no se van a fusionar, con -x para dejar rastro y git cherry para detectar duplicados.
  • git stash para apartar trabajo a medias durante minutos u horas, con -u para lo sin seguimiento y git stash branch cuando ya no encaja.
  • Las etiquetas anotadas para marcar versiones de forma permanente, con SemVer, --follow-tags y git describe.
  • git revert para deshacer lo publicado sin reescribir nada.

Y sobre todas ellas, la regla de oro: no reescribas historial que ya has publicado y otros pueden tener. Rebase e interactivo, antes de publicar. Cherry-pick y revert, siempre seguros. Es la línea que separa el uso experto del uso temerario.

Lo que viene

El equipo ya sabe construir el historial y manipularlo con criterio. Lo que le falta ahora es aprender a sacarle partido.

Porque el historial de gestor-tareas es una base de datos enorme de información que hasta ahora apenas hemos consultado. En ella está escrito quién escribió cada línea y por qué, en qué commit exacto dejó de funcionar algo que antes funcionaba, y qué comprobaciones debería pasar cada commit antes de existir. Y hay problemas nuevos: el proyecto empieza a depender de una biblioteca interna que vive en su propio repositorio, y Carla necesita trabajar en dos ramas a la vez sin pasarse el día haciendo stash.

En el módulo 6: Herramientas y Técnicas de Git veremos los hooks para automatizar comprobaciones en cada commit y en cada envío, git bisect para encontrar por búsqueda binaria el commit exacto que introdujo un fallo, git blame para reconstruir la historia de cada línea, los alias y los formatos avanzados de git log para consultar todo eso con comodidad, los submódulos para componer proyectos a partir de varios repositorios, y git worktree para tener varias copias de trabajo del mismo repositorio a la vez.

Empezamos por la automatización, en la lección 06-01: Usando Git Hooks.

Dominando Git: De Principiante a Avanzado

Módulo 1: Introducción a Git

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

Módulo 6: Herramientas y Técnicas de Git

Módulo 7: Estrategias de Colaboración y Flujo de Trabajo

Módulo 8: Mejores Prácticas y Consejos de Git

Módulo 9: Solución de Problemas y Depuración

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados