La lección anterior dejó el catálogo montado y resolvió los casos de un minuto. Ahora toca el comando que aparecía en media docena de filas de aquella tabla y que llevamos cinco módulos prometiendo: git reset.
Es probablemente el comando peor entendido de Git. Tiene tres modos que hacen cosas distintas, dos formas de invocarlo que se comportan de manera completamente diferente, y una fama de peligroso que es merecida solo en uno de sus usos. La confusión no es casual: reset hace dos trabajos que en otros sistemas serían dos comandos distintos.
Esta lección lo desmonta pieza a pieza y después lo pone en contexto. Porque reset no es la respuesta a "quiero deshacer algo": es una de las respuestas, y elegir la equivocada es lo que convierte un cambio no deseado en un problema para todo el equipo. Al final tendrás una tabla de decisión que responde a la única pregunta que importa: ¿dónde está lo que quiero deshacer?
Y una advertencia que vale para toda la lección: aquí aparece el único comando de Git que destruye de verdad, sin red de seguridad posible. No es reset. Es git clean.
Contenido
- Las tres zonas, otra vez
git resetmueve la rama: la idea central- Los tres modos:
--soft,--mixed,--hard - Casos ordenados por lo que quieres deshacer
git reset <commit> -- <ruta>: la otra formagit restore: el comando moderno para el trabajo diariogit clean: el que sí destruyeORIG_HEAD: la red de seguridad inmediata- La tabla de decisión
- Las tres zonas, otra vez
Todo reset se entiende en cuanto se recuperan las tres zonas de la lección 01-03. Merece la pena tenerlas delante:
flowchart LR
WD["Directorio de trabajo<br/>(los ficheros que editas)"]
IDX["Índice / staging<br/>(.git/index)"]
HEAD["HEAD → rama → commit<br/>(la base de datos)"]
WD -->|"git add"| IDX
IDX -->|"git commit"| HEAD
HEAD -->|"git reset --hard"| WD
HEAD -->|"git reset --mixed"| IDX
Tres fotos del proyecto que pueden coincidir o no:
| Zona | Qué contiene | Dónde vive |
|---|---|---|
HEAD |
El último commit de la rama actual | .git/refs/heads/<rama> (41 bytes) |
| Índice | Lo que entraría en el próximo commit | .git/index (binario) |
| Directorio de trabajo | Los ficheros reales en disco | Tu carpeta |
Cuando git status dice "Changes to be committed", está comparando índice con HEAD. Cuando dice "Changes not staged for commit", compara directorio de trabajo con índice. Con eso claro, reset deja de ser un misterio: cada modo decide hasta cuál de esas tres zonas llega el cambio.
git reset mueve la rama: la idea central
git reset mueve la rama: la idea centralAntes de los modos, la operación básica. Esto es lo que hace git reset <commit> siempre, en los tres modos:
Mueve la rama actual para que apunte a
<commit>. Y comoHEADapunta a la rama,HEADse mueve con ella.
No borra commits. No modifica commits. Reescribe un fichero de 41 bytes (lección 03-01).
flowchart LR
subgraph A["Antes"]
direction LR
a1["e91d4a8"] --> a2["4f8a2e6"] --> a3["b52c9d1"] --> a4["7d3a8f4"]
ra(["GT-241"]) -.-> a4
ha(["HEAD"]) -.-> ra
end
subgraph B["git reset HEAD~2"]
direction LR
b1["e91d4a8"] --> b2["4f8a2e6"] --> b3["b52c9d1<br/>(inalcanzable)"] --> b4["7d3a8f4<br/>(inalcanzable)"]
rb(["GT-241"]) -.-> b2
hb(["HEAD"]) -.-> rb
end
A ==> B
b52c9d1 y 7d3a8f4 siguen existiendo íntegros en .git/objects. Lo que ha cambiado es que GT-241 ya no los alcanza, así que git log no los muestra. Recuperarlos es trivial mientras el reflog los recuerde (lección 09-04).
De aquí salen las dos consecuencias que hay que interiorizar:
1. reset es seguro sobre trabajo local. Aunque te equivoques, los commits siguen ahí.
2. reset es peligroso sobre trabajo publicado. Porque mover la rama hacia atrás y volver a avanzar produce una historia distinta de la que otros tienen. El push será rechazado (non-fast-forward), y forzarlo destruiría el trabajo de los demás. Es la regla de oro de la lección 05-01, y su tratamiento completo es la lección 09-03.
Un tercer matiz que casi nadie conoce y que evita muchos sustos:
Sin argumento de commit, reset usa HEAD, es decir, no mueve nada. Solo actúa sobre el índice. Por eso git reset a secas es completamente inofensivo: vacía el área de preparación y deja tus ficheros intactos.
- Los tres modos:
--soft, --mixed, --hard
--soft, --mixed, --hardLa diferencia entre los tres es hasta dónde propaga el cambio.
| Modo | Mueve HEAD/rama |
Reescribe el índice | Reescribe el directorio de trabajo | ¿Destruye trabajo? |
|---|---|---|---|---|
--soft |
Sí | No | No | No |
--mixed (por defecto) |
Sí | Sí | No | No |
--hard |
Sí | Sí | Sí | Sí: lo no confirmado |
--merge |
Sí | Sí | Parcialmente (conserva cambios locales no conflictivos) | Rara vez |
--keep |
Sí | Sí | Parcialmente (aborta si hay conflicto) | No |
Visualmente, sobre las tres zonas:
flowchart TD
subgraph S["--soft"]
s1["HEAD ⟵ se mueve"]
s2["Índice: intacto"]
s3["Trabajo: intacto"]
end
subgraph M["--mixed (por defecto)"]
m1["HEAD ⟵ se mueve"]
m2["Índice ⟵ se reescribe"]
m3["Trabajo: intacto"]
end
subgraph H["--hard"]
h1["HEAD ⟵ se mueve"]
h2["Índice ⟵ se reescribe"]
h3["Trabajo ⟵ SE SOBRESCRIBE"]
end
--soft: deshacer el commit, conservarlo todo preparado
El commit ha desaparecido de la rama, y todo su contenido está preparado, listo para volver a confirmarse. Los M y A con la letra en la primera columna significan "en el índice".
Es el modo de "quiero rehacer este commit". De hecho, es exactamente lo que hace git commit --amend por dentro.
--mixed: deshacer el commit y despreparar
Ahora la letra está en la segunda columna: los cambios están en el directorio de trabajo, sin preparar. Y estilos.css, que era nuevo, ha vuelto a ser un fichero sin seguimiento.
Es el modo de "quiero rehacer este commit y volver a elegir qué entra". Es el que usaste en la lección 05-02 para dividir un commit en dos.
--hard: deshacer el commit y el contenido
Todo limpio, como si aquel commit nunca hubiera existido. Y aquí está el peligro, que conviene enunciar con precisión:
--hardno destruye los commits (siguen en la base de datos). Destruye los cambios sin confirmar que hubiera en ese momento en el índice y en el directorio de trabajo.
Eso es lo que no tiene vuelta atrás, porque nunca estuvo en .git/objects (lección 09-01, tabla del apartado 1).
Una excepción importante y tranquilizadora: --hard no toca los ficheros sin seguimiento. Si tenías un notas.txt que nunca añadiste, sigue ahí. Lo que se lleva por delante son las modificaciones de ficheros seguidos.
La comparación en un solo experimento
# Punto de partida: un commit, y además un cambio sin confirmar
git log --oneline -2
git status --short| Comando | git log -1 |
app.js (mi cambio sin confirmar) |
Contenido del commit deshecho |
|---|---|---|---|
git reset --soft HEAD~1 |
b52c9d1 |
Conservado | En el índice |
git reset --mixed HEAD~1 |
b52c9d1 |
Conservado (mezclado) | En el directorio de trabajo |
git reset --hard HEAD~1 |
b52c9d1 |
PERDIDO | Descartado |
La fila del --hard es la que hay que memorizar: se lleva dos cosas a la vez, el commit y tu trabajo en curso. Y solo una de las dos se puede recuperar.
--merge y --keep: los modos que casi nadie usa
Existen y en dos situaciones son exactamente lo que quieres:
--keep es un --hard educado: mueve la rama y actualiza los ficheros que difieren, pero aborta si eso pisaría un cambio local. Es la opción sensata cuando quieres retroceder y no estás seguro de tener todo confirmado:
Ese error, aquí, es una buena noticia: te ha salvado.
--merge es parecido pero intenta fusionar los cambios locales en el destino. Se usa sobre todo para abortar una fusión a mano.
Regla práctica: cuando tengas la tentación de escribir --hard y no estés seguro, escribe --keep. Si funciona, no había nada que perder; si falla, acabas de evitar el problema.
- Casos ordenados por lo que quieres deshacer
Este es el recetario. Está ordenado de menor a mayor alcance.
4.1. Descartar cambios de un fichero en el directorio de trabajo
Ana ha estado probando cosas en estilos.css y quiere volver a como estaba.
git restore estilos.css # forma moderna (Git 2.23+)
git checkout -- estilos.css # forma antigua, equivalenteEsto sí destruye, y sin red: las modificaciones no confirmadas no están en ninguna parte. Git ni siquiera pregunta.
# Antes de destruir, mira qué vas a perder
git diff estilos.css
# Todo el directorio de trabajo
git restore .4.2. Sacar algo del índice sin perder el cambio
Bruno ha hecho git add . y ha metido sin querer notas-personales.md.
git restore --staged notas-personales.md # forma moderna
git reset notas-personales.md # forma antigua, equivalenteNo has perdido nada: el fichero sigue modificado en el directorio de trabajo, solo deja de estar preparado.
Fíjate en la asimetría, que es la fuente de confusión número uno:
| Quita del índice | Destruye el cambio | |
|---|---|---|
git restore --staged <f> |
Sí | No |
git restore <f> |
No | Sí |
git restore --staged --worktree <f> |
Sí | Sí |
git restore sin opciones actúa sobre el directorio de trabajo. Con --staged, sobre el índice. Es lo contrario de lo que mucha gente asume, y es la razón de que git restore merezca su propio apartado (el 6).
4.3. Deshacer el último commit conservando los cambios
El caso más frecuente de todos. Carla ha confirmado y se ha dado cuenta de que el commit mezcla dos cosas.
No has perdido nada. El commit ya no está en la rama, pero todo su contenido está preparado. Ahora puede rehacerlo:
Si además quiere volver a elegir qué entra en cada commit, --mixed y git add -p (lección 02-04):
git reset HEAD~1
git add -p app.js # elegir trozo a trozo
git commit -m "GT-241 calcular el contador sobre la lista completa"
git add .
git commit -m "GT-241 estilos del indicador de pendientes"Ese es el flujo completo de "he mezclado dos cosas en un commit", y es el que se usa a diario.
4.4. Solo quiero cambiar el mensaje
No hace falta reset para esto. --amend sustituye el último commit (lección 02-04), y con -m ni siquiera abre el editor:
Recuerda que el hash cambia: si estaba publicado, esto reescribe historial.
4.5. Tirar los últimos N commits
Tres commits fuera de la rama. Antes de ejecutarlo, dos costumbres que cuestan cinco segundos:
# 1. Anota dónde estás
git log --oneline -1
git rev-parse HEAD > /tmp/donde-estaba.txt
# 2. O mejor: pon un marcador. Es gratis y no se olvida
git branch respaldo-GT-241Con git branch respaldo-GT-241, el --hard deja de ser irreversible en ningún sentido: los commits siguen alcanzables desde el respaldo. Es el mismo patrón que en la lección 09-01, apartado 5.2, y el que se generaliza en la 09-04.
Y si ya lo hiciste sin respaldo: tampoco pasa nada, mientras no fueran cambios sin confirmar.
git reflog -5 # busca el hash anterior al reset
git reset --hard b52c9d1 # o mejor: git branch rescate b52c9d14.6. Volver la rama exactamente a como está en el remoto
Es el "descarta todo lo mío y déjame como el servidor". Útil cuando tu copia local se ha enredado sin remedio y sabes que no hay nada tuyo que salvar.
Comprueba antes qué vas a tirar:
git log --oneline origin/main..HEAD # commits míos que no están en el remoto
git status --short # cambios sin confirmarSi la primera lista no está vacía, no ejecutes el --hard sin crear antes una rama de respaldo.
4.7. Deshacer algo que ya está publicado
Aquí reset no es la respuesta. Reescribir una rama publicada obliga a forzar el push y destruye el trabajo de quien tenga la versión anterior.
La respuesta es git revert (lección 05-06): crea un commit nuevo con el cambio inverso, no reescribe nada, no hace falta forzar, y queda constancia.
La única excepción: una rama publicada que es exclusivamente tuya (tu rama de trabajo GT-241, que nadie más usa). Ahí sí puedes reescribir y forzar con --force-with-lease, y eso es la lección 09-03.
git reset <commit> -- <ruta>: la otra forma
git reset <commit> -- <ruta>: la otra formaAquí está la fuente principal de confusión con reset, y merece un apartado propio.
Cuando añades una ruta, git reset hace algo completamente distinto:
git reset <commit> -- <ruta>NO mueve la rama. Copia el estado de<ruta>en<commit>al índice, y deja el directorio de trabajo intacto.
| Forma | ¿Mueve la rama? | ¿Toca el índice? | ¿Toca el directorio de trabajo? |
|---|---|---|---|
git reset <commit> |
Sí | Según el modo | Según el modo |
git reset <commit> -- <ruta> |
No, nunca | Sí, solo esa ruta | No, nunca |
Por qué no puede mover la rama: mover HEAD a otro commit es una operación sobre el repositorio entero. No existe "mover la rama solo para un fichero". Así que cuando das una ruta, Git entiende que quieres la otra operación: rellenar el índice desde un commit.
Consecuencia directa: git reset <commit> -- <ruta> no acepta --hard.
Precisamente porque el modo --hard implica tocar el directorio de trabajo, y esta forma nunca lo hace.
Para qué sirve realmente
Es la forma de preparar el contenido antiguo de un fichero, típicamente para deshacer solo una parte de un commit:
# El commit 4f8a2e6 tocó tres ficheros; quiero deshacer solo lo de estilos.css
git reset 4f8a2e6~1 -- estilos.css
git status --shortEl índice tiene ahora la versión anterior de estilos.css, preparada. Un git commit crea un commit que deshace solo esa parte.
Y si además quieres que el fichero en disco cambie, la forma moderna y mucho más clara es git restore:
El caso "ese fichero no debería estar en el repositorio"
Retomando la lección 08-03:
git rm --cached quita el fichero del índice (deja de tener seguimiento) pero lo conserva en disco. Es la operación que hay que hacer cuando algo entró en el repositorio y luego lo añadiste al .gitignore. Es una prima hermana de git reset -- <ruta>, pero para dejar de seguir el fichero en lugar de restaurar su contenido.
git restore: el comando moderno para el trabajo diario
git restore: el comando moderno para el trabajo diarioGit 2.23 partió el viejo git checkout en dos comandos con nombres honestos: git switch para las ramas (lección 03-02) y git restore para los ficheros. Es la herramienta que deberías usar a diario, dejando reset para lo que de verdad implica mover la rama.
# Descartar cambios del directorio de trabajo
git restore app.js
git restore .
# Quitar del índice (conservando el cambio en disco)
git restore --staged app.js
# Ambas cosas: dejar el fichero como en HEAD, sin rastro del cambio
git restore --staged --worktree app.js
# Traer un fichero desde otro commit o rama
git restore --source=HEAD~3 app.js
git restore --source=origin/main estilos.css
# Interactivo: elegir trozo a trozo qué descartar
git restore -p app.jsLa tabla de equivalencias con lo antiguo, porque vas a ver las dos formas en documentación y en respuestas de foro:
| Objetivo | Moderno | Antiguo |
|---|---|---|
| Descartar cambios en disco | git restore <f> |
git checkout -- <f> |
| Quitar del índice | git restore --staged <f> |
git reset HEAD <f> |
| Traer un fichero de otro commit | git restore --source=<c> <f> |
git checkout <c> -- <f> |
| Cambiar de rama | git switch <rama> |
git checkout <rama> |
| Crear y cambiar | git switch -c <rama> |
git checkout -b <rama> |
git restore -p merece una mención aparte. Es el hermano de git add -p: te enseña cada trozo del diff y te pregunta si quieres descartarlo. Es la forma quirúrgica de deshacer parte de un cambio sin perder el resto, y evita el "voy a descartar todo y rehacerlo".
@@ -12,6 +12,9 @@ function renderizarTareas() {
lista.innerHTML = '';
+ console.log('DEBUG tareas', tareas);
tareas.forEach(tarea => {
Discard this hunk from worktree [y,n,q,a,d,e,?]?Perfecto para quitar los console.log de depuración conservando el trabajo real.
git clean: el que sí destruye
git clean: el que sí destruyeTodo lo anterior tiene red de seguridad en algún grado. Este comando no tiene ninguna, y por eso merece el tono más serio de la lección.
git clean borra ficheros sin seguimiento. Y un fichero sin seguimiento nunca ha pasado por .git/objects: no hay commit, no hay blob, no hay reflog, no hay fsck que valga. Cuando git clean lo borra, está borrado, igual que con rm.
La regla absoluta: -n primero, siempre
-n (o --dry-run) no borra nada: solo lista. Léelo entero. Si en esa lista aparece algo que te importa, no ejecutes el comando real.
Esta costumbre no es opcional. Es la diferencia entre limpiar el proyecto y perder el fichero de configuración local que llevas seis meses manteniendo a mano.
Las opciones
| Opción | Qué hace | Peligro |
|---|---|---|
-n / --dry-run |
Solo lista lo que borraría | Ninguno |
-f / --force |
Borra de verdad (obligatoria: sin ella no hace nada) | Alto |
-d |
Incluye directorios sin seguimiento | Alto |
-x |
Incluye también lo ignorado por .gitignore |
Muy alto |
-X |
Borra solo lo ignorado, conservando lo demás | Medio |
-i / --interactive |
Pregunta fichero a fichero | Bajo |
-e <patrón> |
Excluye lo que coincida | — |
Los usos habituales:
# Lo que casi siempre quieres: ver, y luego borrar ficheros y directorios nuevos
git clean -nd
git clean -fd
# Dejar el proyecto como recién clonado (¡también borra node_modules, .env, builds!)
git clean -ndx
git clean -fdx
# Borrar solo lo ignorado: limpiar artefactos de compilación conservando lo demás
git clean -fdX
# El modo seguro cuando dudas
git clean -id-x es la que hay que mirar dos veces. Borra lo que .gitignore protege, y ahí suele estar precisamente lo que no está en el repositorio pero sí te importa: .env con la configuración local, credenciales de desarrollo, bases de datos de prueba.
El modo interactivo
Would remove the following items:
capturas/ notas-personales.md volcado.sql
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers
4: ask each 5: quit 6: help
What now> 4La opción 4 (ask each) pregunta uno por uno. Cuando hay muchos ficheros y solo algunos sobran, es la forma correcta.
El "reset total" y su coste
La combinación que circula por internet como "dejarlo todo limpio":
Eso deja el directorio exactamente como un clon recién hecho de ese commit. Y se lleva por delante, sin preguntar y sin recuperación posible:
- Todos los cambios sin confirmar de ficheros seguidos.
- Todos los ficheros nuevos que no habías añadido.
- Todo lo ignorado:
.env,node_modules/, configuraciones locales, bases de datos de prueba.
Es un comando legítimo —en CI, en un contenedor, en un repositorio de usar y tirar— y una catástrofe en la máquina de alguien que llevaba dos horas trabajando. Nunca lo pegues sin haber ejecutado antes git clean -ndx y leído la lista.
La única prevención que funciona
git stash -u (lección 05-04) guarda también los ficheros sin seguimiento, y los guarda como objetos de Git. Con eso, lo que ibas a borrar pasa a estar en la base de datos y, por tanto, deja de ser irrecuperable. Cuesta un segundo.
ORIG_HEAD: la red de seguridad inmediata
ORIG_HEAD: la red de seguridad inmediataAntes de una operación que mueve HEAD de forma sustancial —reset, merge, rebase, pull—, Git guarda la posición anterior en una referencia especial:
Así que el "deshacer" inmediato de un reset es:
O, siguiendo el patrón seguro:
Dos advertencias sobre ORIG_HEAD:
- Solo guarda una posición, la de la última operación. Si haces dos
resetseguidos, la primera se pierde. Para eso está el reflog, que guarda todo el historial de movimientos (lección 09-04). - No recupera cambios sin confirmar.
ORIG_HEADes un hash de commit: devuelve la rama a su sitio, no tu trabajo en curso.
Es la red de seguridad de los treinta segundos siguientes. Para todo lo demás, la 09-04.
- La tabla de decisión
La pregunta correcta no es "¿qué comando uso para deshacer?", sino "¿dónde está lo que quiero deshacer?". Con esa respuesta, el comando es inmediato.
flowchart TD
Q1{"¿Está confirmado?"}
Q1 -->|No| Q2{"¿Está preparado<br/>(git add)?"}
Q1 -->|Sí| Q3{"¿Está publicado<br/>en el remoto?"}
Q2 -->|No| R1["git restore <f><br/>(o git clean si no tiene seguimiento)"]
Q2 -->|Sí| R2["git restore --staged <f>"]
Q3 -->|No| Q4{"¿Solo el último<br/>commit?"}
Q3 -->|Sí| R5["git revert (05-06)<br/>o --force-with-lease si la rama es tuya (09-03)"]
Q4 -->|Sí| R3["git commit --amend<br/>o git reset --soft HEAD~1"]
Q4 -->|No| R4["git reset HEAD~N<br/>o git rebase -i (05-02)"]
Y en forma de tabla, que es como se consulta en el momento:
| Quiero deshacer... | Estado | Comando | ¿Recuperable? |
|---|---|---|---|
| Cambios en un fichero editado | Sin confirmar | git restore <f> |
No |
| Parte de los cambios de un fichero | Sin confirmar | git restore -p <f> |
No |
Un git add |
Preparado | git restore --staged <f> |
Sí (no se pierde nada) |
| Un fichero nuevo que sobra | Sin seguimiento | git clean -n y luego -f |
No |
| El último commit, conservando el trabajo | Sin publicar | git reset --soft HEAD~1 |
Sí (reflog) |
| El mensaje del último commit | Sin publicar | git commit --amend |
Sí (reflog) |
| Un fichero olvidado en el último commit | Sin publicar | git add <f> && git commit --amend --no-edit |
Sí (reflog) |
| Los últimos N commits y su contenido | Sin publicar | git reset --hard HEAD~N |
Sí los commits; no lo sin confirmar |
| Un commit del medio de la rama | Sin publicar | git rebase -i con drop (05-02) |
Sí (reflog) |
| Solo un fichero de un commit | Sin publicar | git reset <c>~1 -- <f> y confirmar |
Sí |
| Un commit ya publicado | Publicado | git revert <sha> (05-06) |
Sí (es aditivo) |
| Una fusión ya publicada | Publicado | git revert -m 1 <sha> (05-06) |
Sí |
| Un commit en mi rama personal publicada | Publicado, rama propia | reset + push --force-with-lease (09-03) |
Sí, avisando |
| Todo: volver al estado del remoto | Cualquiera | git fetch && git reset --hard origin/<rama> |
Los commits sí; lo sin confirmar no |
| Todo, incluidos ficheros nuevos | Cualquiera | git reset --hard && git clean -fdx |
No lo no confirmado |
La fila que gobierna todas las demás es la de "¿está publicado?". Si la respuesta es sí y la rama es compartida, reset sale de la lista y la única herramienta correcta es revert.
Errores Comunes y Consejos
Error 1: creer que reset --hard borra commits. No los borra: mueve la rama. Los commits siguen en .git/objects y el reflog los encuentra (09-04). Lo que sí destruye son los cambios sin confirmar.
Error 2: usar --hard cuando bastaba --soft. Si solo querías rehacer el commit, --soft conserva todo preparado. --hard se lleva además tu trabajo en curso.
Error 3: confundir git restore <f> con git restore --staged <f>. El primero destruye el cambio en disco; el segundo solo lo saca del índice y no pierde nada. Son casi opuestos.
Error 4: esperar que git reset <commit> -- <ruta> mueva la rama. No lo hace nunca, y por eso --hard con rutas da error. Es otra operación con el mismo nombre.
Error 5: ejecutar git clean -fdx copiado de internet. Borra .env, configuraciones locales y todo lo ignorado, sin recuperación posible. -n primero, siempre.
Error 6: hacer reset en una rama compartida y forzar el push. Destruye el trabajo de quien tuviera los commits. En lo publicado, revert (05-06).
Error 7: usar git reset --hard origin/main sin comprobar qué hay tuyo por delante. git log --oneline origin/main..HEAD cuesta un segundo y te dice exactamente qué vas a tirar.
Error 8: olvidar que --hard respeta los ficheros sin seguimiento. No es un error grave, pero explica el "he hecho reset --hard y esto sigue aquí": no tenía seguimiento.
Consejo 1: cuando dudes entre --hard y no hacer nada, usa --keep. Hace lo mismo, pero aborta en lugar de pisar cambios locales.
Consejo 2: git branch respaldo-<algo> antes de cualquier reset --hard. Cuesta dos segundos y convierte la operación en trivialmente reversible.
Consejo 3: usa git restore y git switch en lugar de git checkout. Los nombres dicen lo que hacen y evitan errores por confusión.
Consejo 4: git restore -p para quitar los console.log. Quirúrgico, y conserva el resto del trabajo.
Consejo 5: git stash push -u antes de cualquier limpieza. Convierte lo irrecuperable en recuperable por un segundo de esfuerzo.
Consejo 6: recuerda ORIG_HEAD. El deshacer inmediato de un reset, merge, rebase o pull que acabas de lamentar.
Ejercicios
Ejercicio 1: los tres modos, en el mismo punto de partida
- Crea un repositorio con
app.jsy tres commits. En el tercero, añade tambiénestilos.css. - Modifica
app.jssin confirmar y crea un fichero nuevonotas.txtsin añadirlo. - Ejecuta
git reset --soft HEAD~1y anota la salida degit status --shortygit log --oneline -1. - Deshaz con
git reset --hard ORIG_HEAD... y observa qué ha pasado con tu modificación deapp.js. Explícalo. - Repite el experimento completo con
--mixedy con--hard, y rellena una tabla con tres columnas:log -1, cambio sin confirmar enapp.js, ynotas.txt. - Explica por qué
notas.txtsobrevive incluso a--hard.
Ejercicio 2: reset con ruta frente a reset sin ruta
- Sobre un repositorio con cuatro commits, donde el tercero modificó
app.js,estilos.csseindex.html, intentagit reset --hard HEAD~1 -- app.jsy explica el error. - Usa
git reset HEAD~2 -- estilos.cssy comprueba congit status,git diffygit diff --cacheden qué zona ha quedado el cambio. - Confirma. Comprueba que has deshecho solo la parte de
estilos.cssdel tercer commit y que el resto sigue en el proyecto. - Consigue el mismo resultado con
git restore --source=... --staged --worktreey compara el estado del directorio de trabajo en ambos casos.
Ejercicio 3: el límite real de la recuperación
- En un repositorio de pruebas, crea tres situaciones a la vez: un commit sin publicar, un cambio preparado con
git add, y un cambio solo en disco. - Ejecuta
git reset --hard HEAD~1. - Recupera el commit usando el reflog.
- Intenta recuperar el cambio preparado con
git fsck --lost-foundygit cat-file -p. - Intenta recuperar el cambio que estaba solo en disco. Documenta por qué no es posible.
- Repite todo el ejercicio ejecutando antes
git stash push -uy explica qué cambia.
Soluciones
Solución 1:
mkdir /tmp/p9-02 && cd /tmp/p9-02 && git init -q -b main
git config user.name "Ana Ferrer"; git config user.email "[email protected]"
echo "// gestor-tareas" > app.js && git add . && git commit -q -m "chore: inicio"
echo "// listado" >> app.js && git commit -q -am "feat: listado"
echo "// filtro" >> app.js && echo "body{margin:0}" > estilos.css
git add . && git commit -q -m "feat: filtro y estilos"
# 2. Estado sucio antes del reset
echo "// TRABAJO SIN CONFIRMAR" >> app.js
echo "notas privadas" > notas.txt
git status --shortEl commit ha salido de la rama y su contenido está preparado (M y A en la primera columna). Y aquí hay un detalle fino: app.js aparece una sola vez porque el índice contiene tanto lo del commit deshecho como tu modificación sin confirmar aún fuera... en realidad la modificación de app.js sigue en el directorio de trabajo, y el índice tiene la versión del commit deshecho. Compruébalo:
git diff --cached --stat # lo del commit deshecho, preparado
git diff --stat # tu modificación sin confirmar, aún pendienteNada se ha perdido: las dos capas están intactas y separadas.
# 4. Deshacer con ORIG_HEAD
git reset --hard ORIG_HEAD
git log --oneline -1
git status --short
grep -c "TRABAJO SIN CONFIRMAR" app.jsLa rama ha vuelto a su sitio, pero la modificación de app.js ha desaparecido: --hard la ha sobrescrito. ORIG_HEAD devuelve la posición de la rama, no tu trabajo en curso. Es exactamente la advertencia del apartado 8.
| Comando | git log -1 |
Modificación de app.js |
notas.txt |
|---|---|---|---|
git reset --soft HEAD~1 |
b52c9d1 |
Conservada (en disco) | Intacto |
git reset --mixed HEAD~1 |
b52c9d1 |
Conservada (mezclada con lo del commit) | Intacto |
git reset --hard HEAD~1 |
b52c9d1 |
PERDIDA | Intacto |
notas.txt no está en el índice: Git no lo sigue. reset opera sobre HEAD, el índice y los ficheros que Git conoce; un fichero sin seguimiento le es invisible. Para borrarlo hace falta git clean (apartado 7).
Solución 2:
mkdir /tmp/p9-02-b && cd /tmp/p9-02-b && git init -q -b main
echo "v1" > app.js; echo "v1" > estilos.css; echo "v1" > index.html
git add . && git commit -q -m "c1"
echo "v2" > app.js && git commit -q -am "c2"
echo "v3" > app.js; echo "v3" > estilos.css; echo "v3" > index.html
git add . && git commit -q -m "c3: toca los tres ficheros"
echo "v4" > app.js && git commit -q -am "c4"Con una ruta, reset nunca toca el directorio de trabajo ni mueve la rama; --hard implica ambas cosas, así que es contradictorio. Git lo rechaza en lugar de hacer algo a medias.
# 2. La forma correcta
git reset HEAD~2 -- estilos.css # HEAD~2 = c2, es decir, antes de c3
git status --shortgit diff --cached estilos.css # índice vs HEAD: sí hay cambio
git diff estilos.css # trabajo vs índice: también, en sentido inverso
cat estilos.cssClave: el fichero en disco sigue siendo v3. Solo el índice tiene v1. Por eso git diff (trabajo vs índice) muestra el cambio inverso. Y la rama no se ha movido:
# 3. Confirmar solo esa parte
git commit -q -m "revert parcial: devolver estilos.css al estado previo a c3"
cat app.js estilos.css index.htmlapp.js conserva v4, index.html conserva v3 (lo que aportó c3), y solo estilos.css ha vuelto atrás. Se ha deshecho una tercera parte de un commit.
Aunque atención: el fichero en disco seguía siendo v3 al confirmar. El commit dice v1 y el disco dice v3, así que ahora hay una modificación pendiente. Esa asimetría es justo lo que evita restore:
# 4. La forma moderna, que deja disco e índice coherentes
git restore --source=HEAD~3 --staged --worktree estilos.css
git status --short
cat estilos.cssÍndice y directorio de trabajo coinciden. Esa es la diferencia práctica: reset -- <ruta> solo prepara; restore --staged --worktree prepara y escribe en disco. Para el uso diario, restore es más predecible.
Solución 3:
mkdir /tmp/p9-02-c && cd /tmp/p9-02-c && git init -q -b main
echo "base" > app.js && git add . && git commit -q -m "c1"
# 1. Las tres situaciones
echo "COMMIT SIN PUBLICAR" >> app.js && git commit -q -am "c2: trabajo confirmado"
echo "CAMBIO PREPARADO" >> estilos.css && git add estilos.css
echo "CAMBIO SOLO EN DISCO" >> app.js
git status --shortTodo fuera: el commit, el fichero preparado y la modificación en disco.
3f8a1d6 HEAD@{0}: reset: moving to HEAD~1
8f4c2a9 HEAD@{1}: commit: c2: trabajo confirmado
3f8a1d6 HEAD@{2}: commit (initial): c1Recuperado íntegro.
El git add lo salvó. Al preparar el fichero, Git creó el blob y lo escribió en .git/objects. El reset --hard quitó la referencia desde el índice, pero el objeto se quedó.
# 5. El cambio que estaba solo en disco
git fsck --lost-found | wc -l
grep -c "SOLO EN DISCO" $(git rev-list --objects --all | awk '{print $1}' > /dev/null; echo app.js)Solo hay un objeto colgante, el del add. La línea CAMBIO SOLO EN DISCO no existe en ninguna parte de .git. Nunca se calculó su hash, nunca se comprimió, nunca se escribió. Git no puede recuperar algo de lo que jamás tuvo noticia. Las únicas vías reales serían el historial local de tu editor (VS Code, IntelliJ y Vim con undofile lo guardan) o una copia de seguridad del sistema de ficheros.
# 6. Con stash previo, todo cambia
cd /tmp && rm -rf p9-02-d && mkdir p9-02-d && cd p9-02-d && git init -q -b main
echo "base" > app.js && git add . && git commit -q -m "c1"
echo "COMMIT SIN PUBLICAR" >> app.js && git commit -q -am "c2"
echo "CAMBIO PREPARADO" >> estilos.css && git add estilos.css
echo "CAMBIO SOLO EN DISCO" >> app.js
echo "fichero nuevo sin añadir" > borrador.txt
git stash push -u -m "por si acaso"
git reset --hard HEAD~1
git stash listTodo intacto, incluido el fichero sin seguimiento gracias a -u. El stash convirtió las tres capas en commits ocultos dentro de la base de datos, y por tanto en algo recuperable incluso si se hubiera borrado el stash (lección 09-04).
Un segundo de git stash push -u compra la diferencia entre "he perdido dos horas" y "no ha pasado nada".
Conclusión
reset deja de dar miedo en cuanto se entiende que hace dos cosas con el mismo nombre.
git reset <commit>mueve la rama. No borra commits: reescribe un fichero de 41 bytes. Los commits que dejan de ser alcanzables siguen íntegros en la base de datos.- Los tres modos se distinguen por hasta dónde llegan:
--softsolo mueveHEAD;--mixed(por defecto) además reescribe el índice;--hardademás sobrescribe el directorio de trabajo. Solo--harddestruye algo, y lo que destruye son los cambios sin confirmar, no los commits. Los ficheros sin seguimiento no los toca. --keepes el--hardprudente: aborta en lugar de pisar tu trabajo local.git reset <commit> -- <ruta>es otra operación: nunca mueve la rama, nunca toca el disco, solo rellena el índice. Por eso--hardcon rutas es un error.- Para el trabajo diario,
git restore:--stagedpara despreparar (no pierde nada), sin opciones para descartar en disco (destruye),--sourcepara traer de otro commit y-ppara hacerlo trozo a trozo. git cleanes el único comando de este módulo sin red de seguridad, porque lo que nunca se confirmó no está en.git/objects.-nantes que-f, siempre;-xmerece leerse dos veces;git stash push -ulo convierte todo en recuperable.ORIG_HEADdeshace el últimoreset,merge,rebaseopull. Solo guarda una posición.- Y la decisión, en una pregunta: ¿está publicado? Si no,
reset,--amendorebase -i. Si sí y la rama es compartida,revert(lección 05-06), sin excepciones.
Queda pendiente el caso que la tabla de decisión despacha con un "ver 09-03": qué hacer cuando lo que se ha enredado no está dentro de tu repositorio, sino entre tu repositorio y el servidor. El push rechazado, el mensaje de ramas divergentes, el compañero que reescribió el historial publicado y te dejó con trabajo colgando de una base que ya no existe.
Todo eso, y el cierre de la promesa que dejamos abierta en la lección 04-05, en la lección 09-03: Resolviendo Divergencias con el Remoto.
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
