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
- Qué hace
git revert - Revirtiendo el commit de Bruno
revertfrente aresetfrente a--amend- Revertir varios commits y rangos
-n: agrupar varias reversiones en un commit- Conflictos al revertir
- Revertir un commit de fusión:
-m 1y-m 2 - El problema de la rama revertida que ya no vuelve a entrar
- Revertir una reversión
- Cuándo revertir y cuándo no
- Qué hace
git revert
git revertGit 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? | Sí: es exactamente para eso |
| ¿Queda constancia? | Sí, 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.
- Revirtiendo el commit de Bruno
Situación de partida:
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:
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 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(-)
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:
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 |
revert frente a reset frente a --amend
revert frente a reset frente a --amendLas 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 | Sí | Sí |
| ¿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? | Sí | No | No |
| Alcance | Cualquier commit del historial | Solo mueve la punta | Solo el último commit |
¿Requiere push --force? |
No | Sí | Sí |
| 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?
- Sí →
git revert. No hay alternativa segura. - No →
reseto--amendson 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 resetes mucho más de lo que cabe aquí. Tiene tres modos —--soft,--mixedy--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:resetreescribe,revertno.
- 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..HEADUn 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:
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.
-n: agrupar varias reversiones en un commit
-n: agrupar varias reversiones en un commitTres 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 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
- 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.
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.
- Revertir un commit de fusión:
-m 1 y -m 2
-m 1 y -m 2Este es el caso que hace fallar a todo el mundo la primera vez.
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:
| 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.
[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.
- 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:
"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:
[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:
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.
- 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.
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.
- 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:
- Revierte el tercer commit con un mensaje que explique el motivo.
- Demuestra que ninguno de los hashes anteriores ha cambiado.
- Demuestra que el contenido del fichero es el que había antes de ese commit, salvo lo que aportaron el cuarto y el quinto.
- Compara conceptualmente con lo que habría pasado usando
git reset --hardsobre 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
- Crea una rama con dos commits y fusiónala en
maincon--no-ff. - Añade un commit más a
main. - Revierte la fusión con
-m 1y comprueba que el contenido de la rama ha desaparecido. - Intenta fusionar la rama otra vez y observa el mensaje.
- Resuélvelo revirtiendo la reversión y comprueba que el contenido vuelve.
- 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# 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."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.
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.txtEl 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
lsfiltro.js ha desaparecido: el contenido de la rama está deshecho.
Ahí está el problema: el grafo dice que ya está integrada.
El contenido ha vuelto.
* 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 --onelinegit 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 HEADcommit 2b9e6c1...
Revertir los ficheros b, c y d
b.txt | 1 -
c.txt | 1 -
d.txt | 1 -
3 files changed, 3 deletions(-)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
pushy queda constancia pública de la decisión. - Es la única forma segura de deshacer algo publicado.
resety--amendreescriben historial y solo valen para trabajo que aún es tuyo; la lección 09-02 desarrollareseta fondo. - Admite varios commits y rangos, que revierte del más reciente al más antiguo;
-npermite acumular varias reversiones y confirmarlas como una sola decisión. - Los conflictos se resuelven como siempre, con
--continue,--skipy--abort, y aquíours/theirsno están invertidos. Un conflicto al revertir suele significar que alguien construyó encima: párate a mirar. - Revertir una fusión exige
-mpara elegir el padre principal:-m 1(la rama en la que estabas, casi siempremain) 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 rebasepara reaplicar commits sobre otra base y obtener un historial lineal, sabiendo que crea commits nuevos.git rebase -ipara reordenar, unir, dividir, renombrar y eliminar commits antes de publicarlos, con--fixup/--autosquashcomo flujo diario y--execcomo red de validación.git cherry-pickpara trasplantar commits concretos entre ramas que no se van a fusionar, con-xpara dejar rastro ygit cherrypara detectar duplicados.git stashpara apartar trabajo a medias durante minutos u horas, con-upara lo sin seguimiento ygit stash branchcuando ya no encaja.- Las etiquetas anotadas para marcar versiones de forma permanente, con SemVer,
--follow-tagsygit describe. git revertpara 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
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
