La lección anterior cerró el deshacer local: reset, restore, clean, y una tabla de decisión que terminaba con una fila remitiendo aquí. Esta lección se ocupa de todo lo que ocurre entre tu repositorio y el servidor, y cierra la promesa que quedó abierta en la lección 04-05, donde vimos el rechazo non-fast-forward y solo dimos la salida básica.
El problema tiene un nombre y una definición precisa: divergencia. Y tiene una fama de complicado que no merece, porque en cuanto se dibuja el grafo la solución suele ser evidente. Lo que confunde es que el mismo estado se presenta con cuatro mensajes distintos según cómo lo descubras: un status que dice "have diverged", un pull que se niega a decidir, un push rechazado, o un compañero avisando de que le han desaparecido commits.
Vamos a ver que las cuatro son la misma situación, que hay exactamente tres formas de salir de ella, y que la elección entre las tres depende de una sola pregunta: ¿de quién es esta rama?
Y al final, el caso serio: qué hacer cuando el que reescribió el historial publicado fue otro, y tú tienes dos días de trabajo apoyados en una base que ya no existe.
Contenido
- Qué significa exactamente "han divergido"
- Medir la divergencia
- Los cuatro mensajes de la misma situación
- Las tres políticas de
pully cuál elegir - Cómo se llega a una divergencia
- El
pushrechazado: diagnóstico y salidas --force-with-leasey por qué a veces no protege- Cuando el remoto reescribió y tú tienes trabajo encima
- Recuperar la rama remota anterior con
origin/rama@{1} - Historias no relacionadas:
--allow-unrelated-histories - Coordinación: lo que hay que decir y cuándo
- Qué significa exactamente "han divergido"
Dos ramas han divergido cuando cada una tiene commits que la otra no tiene. Formalmente: existe un ancestro común, y desde él salen dos caminos distintos.
flowchart LR
A["e91d4a8"] --> B["4f8a2e6<br/>(ancestro común)"]
B --> C["b52c9d1"] --> D["7d3a8f4"]
B --> E["9c4e7b2"] --> F["3e7f1a8"] --> G["8a1f6c3"]
L(["main (local)"]) -.-> D
R(["origin/main"]) -.-> G
Tu main tiene dos commits que el servidor no tiene. El servidor tiene tres que tú no tienes. El ancestro común es 4f8a2e6.
Compáralo con los dos casos que no son divergencia:
| Situación | Grafo | Qué pasa |
|---|---|---|
| Al día | Local y remoto en el mismo commit | Nada que hacer |
| Por detrás | El remoto avanzó, tú no | git pull hace fast-forward: sin problema |
| Por delante | Tú avanzaste, el remoto no | git push hace fast-forward: sin problema |
| Divergidos | Ambos avanzaron desde el ancestro | Hay que decidir |
La palabra clave es decidir. Una divergencia no es un error: es una situación en la que Git no puede saber qué quieres y se niega a inventárselo. Todo lo que sigue son formas de tomar esa decisión.
Recuperando la lección 03-01: el ancestro común es el que Git calcula con git merge-base, y es la base de la fusión a tres bandas. Puedes verlo directamente:
- Medir la divergencia
Antes de resolver, mide. Estos comandos no modifican nada.
fetch actualiza origin/main sin tocar tu main (lección 04-04). Sin esto, estás razonando sobre una foto vieja del servidor.
El recuento
Los tres puntos son fundamentales: A...B es la diferencia simétrica, los commits que están en una rama pero no en la otra. --left-right los separa por lado.
| Salida | Significado |
|---|---|
0 0 |
Idénticas |
0 N |
Estás por detrás N commits. pull es fast-forward |
N 0 |
Estás por delante N commits. push es fast-forward |
N M |
Divergidas. Hay que decidir |
Con la rama de seguimiento configurada (lección 04-06), basta:
Ver qué hay en cada lado
Esto es lo que de verdad permite decidir:
# Mis commits que el servidor no tiene
git log --oneline main ^origin/main
# equivalente y más cómodo:
git log --oneline origin/main..main7d3a8f4 GT-241 estilos del indicador de pendientes b52c9d1 GT-241 calcular el contador sobre la lista completa
8a1f6c3 GT-238 corregir el foco tras borrar 3e7f1a8 GT-244 documentar la instalación en el README 9c4e7b2 GT-244 añadir el script de arranque
# Los dos lados a la vez, con marca de lado
git log --oneline --left-right --graph main...origin/main< 7d3a8f4 GT-241 estilos del indicador de pendientes < b52c9d1 GT-241 calcular el contador sobre la lista completa > 8a1f6c3 GT-238 corregir el foco tras borrar > 3e7f1a8 GT-244 documentar la instalación en el README > 9c4e7b2 GT-244 añadir el script de arranque
< es el lado izquierdo (tu main), > el derecho (origin/main).
Y la pregunta que decide si va a haber conflictos:
# ¿Tocan los mismos ficheros?
git diff --stat origin/main...main # lo que aporto yo
git diff --stat main...origin/main # lo que aportan ellosSi los conjuntos de ficheros no se solapan, la integración será limpia.
Un alias que merece la pena
git config --global alias.div '!f() {
git fetch -q ${1:-origin};
echo "--- estado ---"; git status -sb | head -1;
echo "--- míos (no están en el remoto) ---"; git log --oneline @{u}..HEAD;
echo "--- suyos (no los tengo) ---"; git log --oneline HEAD..@{u};
}; f'
- Los cuatro mensajes de la misma situación
Según cómo llegues a la divergencia, Git te la cuenta de forma distinta. Reconocerlas todas es media lección.
A. En git status
On branch main Your branch and 'origin/main' have diverged, and have 2 and 3 different commits each, respectively. (use "git pull" if you want to integrate the remote branch with yours)
Es el diagnóstico puro. Los números coinciden con rev-list --left-right --count.
B. En git pull sin política configurada
Desde Git 2.27, pull se niega a elegir por ti:
hint: You have divergent branches and need to specify how to reconcile them. hint: You can do so by running one of the following commands sometime before hint: your next pull: hint: hint: git config pull.rebase false # merge hint: git config pull.rebase true # rebase hint: git config pull.ff only # fast-forward only hint: fatal: Need to specify how to reconcile divergent branches.
Esto es bueno. Antes, pull hacía una fusión silenciosa y llenaba el historial de commits "Merge branch 'main' of git.ejemplo.es..." que nadie había pedido. Ahora te obliga a tener una política, y eso es el apartado 4.
C. En git pull con pull.ff only
Es la misma información con menos palabras: hay divergencia y tu política dice "no decidas por mí". No es un fallo (lección 04-04, error 3).
D. En git push
! [rejected] main -> main (non-fast-forward) error: failed to push some refs to 'git.ejemplo.es:equipo/gestor-tareas.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: 'git pull ...') before pushing again.
O, en su variante más alarmante:
Las dos dicen lo mismo: el servidor tiene commits que tú no tienes, y aceptar tu envío los dejaría inalcanzables. Es una protección del servidor, no un fallo tuyo. Es el apartado 6.
- Las tres políticas de
pull y cuál elegir
pull y cuál elegirgit pull es fetch + integración (lección 04-04). La política decide qué integración.
| Política | Configuración | Qué hace | Resultado |
|---|---|---|---|
| Merge | pull.rebase false |
git merge origin/main |
Commit de fusión; historial con bifurcaciones |
| Rebase | pull.rebase true |
git rebase origin/main |
Tus commits se reaplican encima; historial lineal |
| Solo fast-forward | pull.ff only |
Aborta si hay divergencia | Ninguno: decides tú, a mano |
Visualmente, sobre el mismo punto de partida:
flowchart TD
subgraph MERGE["pull.rebase false (merge)"]
m1["4f8a2e6"] --> m2["b52c9d1 (mío)"] --> m3["7d3a8f4 (mío)"] --> mm["Merge"]
m1 --> m4["9c4e7b2"] --> m5["3e7f1a8"] --> m6["8a1f6c3"] --> mm
end
subgraph REBASE["pull.rebase true"]
r1["4f8a2e6"] --> r4["9c4e7b2"] --> r5["3e7f1a8"] --> r6["8a1f6c3"] --> r2["b52c9d1' (mío, nuevo hash)"] --> r3["7d3a8f4' (mío, nuevo hash)"]
end
Cuál elegir, según el flujo del módulo 7
| Contexto | Política recomendada | Por qué |
|---|---|---|
Tu rama de funcionalidad personal (GT-241) |
rebase |
Es tuya, nadie más la tiene; el historial queda limpio para la revisión |
main en GitHub Flow / Trunk Based (07-04, 07-05) |
rebase o ff-only |
En main nunca deberías tener commits locales; si los tienes, es un aviso |
| Rama compartida de verdad (dos personas trabajando a la vez) | merge |
Rebasar una rama que otro tiene es reescribir historial publicado |
develop en Git Flow (07-03) |
merge |
Es una rama de larga vida y compartida |
| Cuando no estás seguro | ff-only |
Te obliga a mirar antes de decidir. La opción más didáctica |
La configuración recomendada para la mayoría de la gente:
# Por defecto: no decidas por mí
git config --global pull.ff only
# Y en las ramas de funcionalidad, rebase explícito cuando toque
git pull --rebaseO, si tu equipo trabaja con ramas cortas y quiere historial lineal (que es lo que hace Ana en gestor-tareas):
git config --global pull.rebase true
git config --global rebase.autoStash true # aparta los cambios sin confirmar y los devuelverebase.autoStash evita el "cannot pull with rebase: You have unstaged changes", que es la queja más frecuente contra pull --rebase.
La regla de oro sigue vigente (lección 05-01):
pull --rebasereescribe tus commits locales, que aún no has publicado. Eso es legítimo. Lo que nunca hay que hacer es rebasar commits que ya están en el servidor y otros pueden tener.
Y un ajuste que evita un error clásico:
Si tenías commits fixup! pendientes (lección 05-02), no se aplastarán por sorpresa durante un pull. Compruébalo en tu configuración: si lo tienes activado sin saberlo, un pull --rebase puede reorganizarte commits.
- Cómo se llega a una divergencia
Entender la causa importa, porque determina cuál de las salidas es correcta.
Causa 1: la normal y sana
Ana confirmó en local mientras Bruno publicaba lo suyo. Nadie ha hecho nada mal. Es el funcionamiento normal de un sistema distribuido con varias personas.
Salida: integrar (merge o rebase, según la política) y enviar. Sin drama.
Causa 2: un --amend después de publicar
Carla publicó GT-247, se dio cuenta de una errata en el mensaje y ejecutó git commit --amend.
Qué ha pasado. --amend no modifica el commit: crea uno nuevo con distinto hash y mueve la rama (lección 09-01, apartado 5.3). El commit original sigue en el servidor. Local y remoto tienen ahora un commit cada uno que el otro no tiene: divergencia de 1 y 1.
Salida: si la rama es solo suya, push --force-with-lease. Si no, hay que integrar, y el resultado es feo (el commit aparecería dos veces).
Causa 3: un reset seguido de trabajo nuevo
Bruno hizo git reset --hard HEAD~2 sobre una rama publicada para "quitar dos commits", y después trabajó dos horas más.
Local: A → B → X → Y (X, Y son el trabajo nuevo) Remoto: A → B → C → D (C, D son los commits que quitó)
Divergencia de 2 y 2. Y aquí hay una decisión de fondo: ¿los commits C y D deben desaparecer del proyecto, o no?
Salida: si deben desaparecer y la rama es suya, --force-with-lease. Si la rama es compartida, la respuesta correcta era git revert (lección 05-06) y no reset; ahora toca integrar y revertir.
Causa 4: alguien reescribió el historial publicado
Diego hizo un rebase -i sobre main para "limpiar el historial" y forzó el push. Todos los que tenían main descargado se encuentran con una divergencia que no han provocado.
Local: A → B → C → D (lo que tenías, correcto) Remoto: A → B' → C' → D' (los mismos cambios, otros hashes)
Es el caso serio, y tiene apartado propio: el 8.
Causa 5: git pull sobre una rama con la que trabajan dos personas
Ana y Carla trabajan a la vez en GT-241. Ambas hacen pull --rebase. Cada rebase reescribe los commits de la otra. El resultado es una espiral de commits duplicados que se resuelve mal.
Salida: en ramas realmente compartidas, pull.rebase false. La política tiene que ser por rama, no por costumbre.
- El
push rechazado: diagnóstico y salidas
push rechazado: diagnóstico y salidasEste es el cierre de lo prometido en la lección 04-05.
Diagnóstico completo, en cuatro comandos
# 1. La foto real del servidor
git fetch origin
# 2. ¿Cuánto he divergido?
git rev-list --left-right --count HEAD...@{u}# 3. ¿Qué hay en el servidor que yo no tenga? ¿Es de otra persona?
git log --oneline --format='%h %an %s' HEAD..@{u}8a1f6c3 Bruno Salas GT-238 corregir el foco tras borrar 3e7f1a8 Bruno Salas GT-244 documentar la instalación 9c4e7b2 Carla Vidal GT-244 añadir el script de arranque
Con esas cuatro respuestas ya sabes qué salida corresponde.
El árbol de decisión
flowchart TD
Q1{"¿Hay commits en el remoto<br/>que yo no tenga?"}
Q1 -->|No| R0["No es divergencia:<br/>simplemente haz push"]
Q1 -->|Sí| Q2{"¿Son de otra persona,<br/>o commits legítimos míos<br/>que quiero conservar?"}
Q2 -->|Sí| R1["INTEGRAR:<br/>pull --rebase (rama propia)<br/>o pull --no-rebase (compartida)"]
Q2 -->|No: son versiones viejas<br/>de mis propios commits| Q3{"¿Esta rama la usa<br/>alguien más?"}
Q3 -->|Sí| R2["INTEGRAR igualmente,<br/>o coordinar antes de forzar"]
Q3 -->|No, es solo mía| R3["push --force-with-lease"]
Salida A: integrar y reenviar (el caso normal)
git fetch origin
git rebase origin/main # o: git merge origin/main
# ...resolver conflictos si los hay (lección 03-05)...
git pushCon pull configurado:
Si aparecen conflictos durante el rebase, la mecánica es la de la lección 05-01, incluida la inversión de ours/theirs. Y si te agobias:
Salida B: forzar, cuando la rama es tuya
Solo si se cumplen las tres condiciones:
- La rama es de funcionalidad y nadie más la usa.
- Lo que hay en el servidor son versiones antiguas de tus propios commits.
- Has comprobado el punto 3 del diagnóstico y no hay commits de otra persona.
El + delante indica que ha sido un envío forzado.
Nunca --force a secas. La diferencia es el apartado 7.
Salida C: la que casi nadie considera
A veces la respuesta correcta es no forzar y no integrar, sino publicar en otro sitio:
Especialmente útil si la rama tiene una pull request abierta con comentarios de revisión (lección 07-02): forzar puede dejar los comentarios huérfanos. Publicar una rama nueva conserva todo y permite comparar con git range-diff (lección 07-02).
--force-with-lease y por qué a veces no protege
--force-with-lease y por qué a veces no protegeRecordando la lección 04-05: --force-with-lease solo fuerza si el servidor está donde tú crees que está. Compara la punta remota real con tu copia local de la rama de seguimiento (refs/remotes/origin/GT-241).
Ese stale info significa: "el servidor no está donde tu origin/GT-241 dice; alguien ha publicado algo desde tu último fetch". Es exactamente la protección funcionando: te ha evitado destruir el trabajo de otro.
El agujero: el fetch que anula la protección
Y aquí está el detalle que casi nadie conoce, y que hay que entender bien:
Si haces
git fetchjusto antes del--force-with-lease, la protección desaparece.
Porque fetch actualiza origin/GT-241 con lo que hay en el servidor ahora mismo, incluidos los commits nuevos de tu compañero. Tu "arrendamiento" (lease) pasa a coincidir con la realidad, la comprobación pasa, y fuerzas encima de trabajo que nunca has visto.
# Secuencia peligrosa
git fetch # ahora origin/GT-241 incluye el commit de Bruno
git push --force-with-lease # la comprobación pasa... y destruye el commit de BrunoLo peor es que es una secuencia razonable: "voy a actualizar antes de forzar" suena a buena práctica. Y muchos IDE hacen fetch automáticamente en segundo plano, así que puede pasarte sin que tú escribas el comando.
La solución: --force-if-includes
Git 2.30 añadió la opción que arregla el agujero:
--force-if-includes comprueba, además, que los commits remotos que vas a sobrescribir están incorporados en tu historial local, mirando tu reflog. Es decir: exige que hayas visto e integrado lo que estás a punto de reemplazar, no solo que lo hayas descargado.
| Opción | Qué comprueba | Protege del compañero |
|---|---|---|
--force |
Nada | No |
--force-with-lease |
Que origin/rama local == servidor |
Sí, salvo que hayas hecho fetch |
--force-with-lease --force-if-includes |
Además, que hayas integrado lo remoto | Sí |
Configúralo como comportamiento por defecto:
Con eso, cada --force-with-lease que ejecutes lleva la comprobación extra automáticamente. Es una línea de configuración que elimina una clase entera de accidentes.
Y la variante explícita, cuando quieres ser absolutamente preciso:
# Fuerza solo si la punta remota es EXACTAMENTE este hash
git push --force-with-lease=GT-241:b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9bVerboso, pero no hay forma de equivocarse.
- Cuando el remoto reescribió y tú tienes trabajo encima
Este es el caso serio, y el que produce más pánico. Merece el procedimiento completo.
La situación. Diego hizo un rebase -i sobre main para limpiar el historial y forzó el push. Ana tenía main descargado y, encima, tres commits de GT-241 que aún no había publicado.
Lo primero: no has perdido nada. Tus commits siguen en tu repositorio, íntegros. Y los commits antiguos de main también los tienes tú en local, aunque el servidor ya no los tenga. De hecho, tu clon es ahora la única copia de la versión anterior, y eso te da una posición sólida.
Paso 1: entender qué ha pasado
* 9c4e7b2 (HEAD -> GT-241) GT-241 estilos del indicador * 3e7f1a8 GT-241 calcular el contador * 8a1f6c3 GT-241 base del filtro * 7d3a8f4 GT-238 corregir el foco tras borrar <- mi main viejo * b52c9d1 GT-244 documentar la instalación * 4f8a2e6 chore: configuración de eslint | * 2f8c6e1 (origin/main) GT-238 corregir el foco tras borrar <- el main nuevo | * 7a4d9b3 GT-244 documentar la instalación | * 5c1e8f2 chore: configuración de eslint |/ * 1e6f2c8 chore: commit inicial
Los mensajes se repiten en las dos ramas, con hashes distintos. Ese es el patrón inequívoco de un historial reescrito. Confírmalo:
git range-diff (lección 07-02) compara dos series de commits y dice si el contenido es el mismo:
1: 5c1e8f2 = 1: 4f8a2e6 chore: configuración de eslint
2: 7a4d9b3 = 2: b52c9d1 GT-244 documentar la instalación
3: 2f8c6e1 ! 3: 7d3a8f4 GT-238 corregir el foco tras borrar
@@ app.js
- campo.focus();
+ campoNuevaTarea.focus();Los dos primeros son idénticos (=); el tercero cambió (!) y te enseña exactamente qué. Esta es la información que necesitas antes de decidir nada.
Paso 2: el respaldo
Dos segundos. A partir de aquí, nada de lo que hagas es irreversible.
Paso 3: trasplantar tus commits con rebase --onto
El comando que resuelve exactamente esto es el --onto de la lección 05-01. Su forma es:
que se lee: "coge los commits de <rama> que estén después de <base-antigua> y reaplícalos sobre <nueva-base>".
En nuestro caso:
- Nueva base:
origin/main(el historial nuevo). - Base antigua:
7d3a8f4(elmainviejo del que colgaba tu rama). - Rama:
GT-241.
* 4b8e2c9 (HEAD -> GT-241) GT-241 estilos del indicador * 8f2d6a1 GT-241 calcular el contador * 6e2b9c7 GT-241 base del filtro * 2f8c6e1 (origin/main) GT-238 corregir el foco tras borrar * 7a4d9b3 GT-244 documentar la instalación * 5c1e8f2 chore: configuración de eslint * 1e6f2c8 chore: commit inicial
Tus tres commits, con hashes nuevos, sobre la base nueva. Ni un commit duplicado.
Cómo encontrar la base antigua si no la tienes anotada:
# El reflog de tu propia rama dice de dónde salió
git reflog show GT-241 | tail -5
# O el ancestro común entre tu rama y el main viejo del reflog remoto
git merge-base GT-241 origin/main@{1}Paso 4: poner el main local en su sitio
Tu main local todavía tiene la versión vieja. Como no tenías nada propio en main (todo tu trabajo estaba en GT-241), simplemente aliniéalo:
Si sí tenías commits propios en main, no hagas eso: sácalos primero a una rama (git branch mis-commits-main) y trasplántalos con el mismo --onto.
Paso 5: verificar antes de enviar
1: 8a1f6c3 = 1: 6e2b9c7 GT-241 base del filtro 2: 3e7f1a8 = 2: 8f2d6a1 GT-241 calcular el contador 3: 9c4e7b2 = 3: 4b8e2c9 GT-241 estilos del indicador
Tres =: el contenido de tus commits es idéntico al de antes del trasplante. Nada se ha perdido ni alterado. Ahora puedes borrar el respaldo con tranquilidad:
Y ejecuta las pruebas antes de publicar: el rebase ha reaplicado tu código sobre una base distinta, y aunque no haya habido conflictos, puede haber incompatibilidades semánticas.
El caso en que tu trabajo SÍ estaba publicado
Si tus commits de GT-241 ya estaban en el servidor, después del rebase tu rama diverge de origin/GT-241. Como GT-241 es tuya:
El resumen del procedimiento
| Paso | Comando | Para qué |
|---|---|---|
| 1 | git fetch origin |
La foto real |
| 2 | git log --graph --all + git range-diff |
Entender qué reescribieron |
| 3 | git branch respaldo-<rama> |
Red de seguridad |
| 4 | git rebase --onto origin/main <base-vieja> <rama> |
Trasplantar |
| 5 | git switch main && git reset --hard origin/main |
Alinear main |
| 6 | git range-diff respaldo-<rama>...<rama> |
Verificar |
| 7 | Ejecutar las pruebas | Verificar de verdad |
| 8 | git push --force-with-lease --force-if-includes |
Publicar (si aplica) |
- Recuperar la rama remota anterior con
origin/rama@{1}
origin/rama@{1}Un detalle que salva situaciones y que casi nadie conoce: las ramas de seguimiento remotas también tienen reflog.
2f8c6e1 refs/remotes/origin/main@{0}: fetch: forced-update
7d3a8f4 refs/remotes/origin/main@{1}: fetch: fast-forward
b52c9d1 refs/remotes/origin/main@{2}: fetch: fast-forwardDos cosas valiosísimas en esa salida:
forced-updatees la prueba de que alguien reescribió el historial publicado. Si dudabas de qué pasó, ahí está la evidencia con su fecha.origin/main@{1}es dónde estaba elmaindel servidor antes de la reescritura.
# Ver el historial anterior
git log --oneline origin/main@{1} -10
# Crear una rama sobre él para poder trabajar
git branch main-antes-del-rebase origin/main@{1}
# Comparar las dos versiones
git range-diff origin/main@{1}...origin/mainEsto es lo que permite reconstruir lo que el servidor tenía antes, y es la razón por la que, cuando alguien fuerza y destruye trabajo, cualquier compañero que tuviera la rama descargada puede devolverlo:
# Devolver el main del servidor a como estaba (con permisos y coordinación)
git push --force-with-lease origin main-antes-del-rebase:mainY también con fechas:
Ese es exactamente el mecanismo del reflog, cuyo tratamiento completo —incluida la sintaxis @{n} frente a @{tiempo} y los límites de caducidad— es la lección 09-04.
- Historias no relacionadas:
--allow-unrelated-histories
--allow-unrelated-historiesQué significa. Las dos ramas no tienen ningún ancestro común. No es una divergencia: es que son dos árboles genealógicos completamente independientes, cada uno con su propio commit raíz.
flowchart LR
subgraph H1["Historia local"]
a1["1e6f2c8 (raíz)"] --> a2["4f8a2e6"] --> a3["b52c9d1"]
end
subgraph H2["Historia remota"]
b1["9a3c7d1 (raíz)"] --> b2["2f8c6e1"]
end
Cómo se llega ahí
| Causa | Situación típica |
|---|---|
| Creaste el repositorio local antes de clonar | git init + commits, y luego añadiste un origin que ya tenía un README inicial |
| El servidor inicializó el repositorio | La plataforma creó el repo con README.md y .gitignore, tú tenías el proyecto en local |
| Alguien reescribió el historial completo | git filter-repo (08-05) sin --force, o una reescritura desde la raíz |
push --force de un repositorio equivocado |
Se publicó otro proyecto encima. Esto es un incidente, no un caso normal |
| Fusionar dos proyectos a propósito | Absorber un repositorio dentro de otro |
La salida
Antes de nada, comprueba que es el caso benigno:
# Las dos raíces
git log --oneline --max-parents=0 main
git log --oneline --max-parents=0 origin/main
# ¿Qué hay en el remoto? Si son 47 commits de otro proyecto, PARA.
git log --oneline origin/main | head -20Si el remoto solo tiene el commit inicial de la plataforma:
Probablemente conflicte en README.md: resuélvelo con normalidad (lección 03-05) y confirma.
Si el remoto tiene el proyecto de verdad y tu historia local es la sobrante:
git branch mi-historia-local # respaldo
git fetch origin
git reset --hard origin/main # adoptar la del servidorY si alguien ha publicado otro proyecto encima del vuestro, esto es un incidente: no fusiones nada, avisa al equipo, y usa origin/main@{1} del apartado 9 (o el clon de cualquier compañero) para restaurar. La lección 09-05 trata el uso de otros clones como copia de seguridad.
Nunca ejecutes
--allow-unrelated-histories"porque Git se queja". Esa negativa es una comprobación de cordura muy útil: Git te está diciendo que esas dos cosas no tienen nada que ver. Averigua por qué antes de saltártela.
- Coordinación: lo que hay que decir y cuándo
La parte no técnica, que es la que más tiempo ahorra.
Antes de reescribir algo compartido:
- Avisa antes, no después. Un mensaje de treinta segundos en el canal del equipo: "Voy a forzar
maina las 16:00 para quitar el volcado de la base de datos del historial. No hagáispullhasta que avise." - Di qué tienen que hacer los demás. Aunque sea obvio para ti, no lo es para quien está a otra cosa:
Después del aviso, cada uno: git fetch origin git switch main git reset --hard origin/main Si tenéis ramas con commits propios, avisadme antes de tocar nada y hacemos el rebase --onto juntos. - Hazlo en un momento de poca actividad. Nunca un viernes por la tarde, nunca durante una entrega.
- Confirma cuando termine. El silencio genera
pulla ciegas.
Cuando ya ha pasado y no avisaste:
- Avisa igualmente, cuanto antes. El coste de decir "he forzado
main, no hagáis pull" a los cinco minutos es cero. A las cinco horas es una tarde de trabajo para tres personas. - Ofrece el procedimiento del apartado 8, no un "arregladlo".
- Si has destruido trabajo ajeno, el apartado 9 lo recupera desde cualquier clon del equipo. No está perdido.
Prevención estructural (lección 07-06):
# En el servidor: ramas protegidas
# - main: prohibido el push forzado, prohibido el push directo
# - solo se integra vía pull requestUna rama protegida convierte todo este apartado en innecesario. Si main no acepta --force, el problema no puede ocurrir. Es la medida más rentable de la lección.
Y en local, una protección personal:
# Un hook pre-push que impide forzar sobre main (lección 06-01)
cat > .git/hooks/pre-push <<'EOF'
#!/usr/bin/env bash
# Bloquea el push forzado sobre ramas protegidas
protegidas="main develop"
while read -r ref_local sha_local ref_remota sha_remoto; do
rama="${ref_remota#refs/heads/}"
for p in $protegidas; do
if [ "$rama" = "$p" ] && [ "$sha_remoto" != "0000000000000000000000000000000000000000" ]; then
if ! git merge-base --is-ancestor "$sha_remoto" "$sha_local"; then
echo "BLOQUEADO: esto sería un push no-fast-forward sobre '$rama'." >&2
echo "Si de verdad hace falta, usa --no-verify y avisa al equipo." >&2
exit 1
fi
fi
done
done
exit 0
EOF
chmod +x .git/hooks/pre-pushErrores Comunes y Consejos
Error 1: responder a un push rechazado con --force. El rechazo significa que el servidor tiene commits que tú no tienes. Forzar los destruye. Diagnostica primero: git log --oneline HEAD..@{u} dice de quién son.
Error 2: razonar sobre origin/main sin haber hecho fetch. origin/main es una foto local que puede tener horas. Todo diagnóstico empieza por git fetch.
Error 3: git fetch justo antes de --force-with-lease. Anula la protección. Usa --force-if-includes, o configúralo con push.useForceIfIncludes true.
Error 4: usar pull --rebase en una rama que otra persona también tiene. Reescribe commits publicados y genera duplicados en cadena. La política debe ser por rama, no por costumbre.
Error 5: hacer --allow-unrelated-histories sin mirar qué hay al otro lado. Puedes acabar fusionando otro proyecto entero en el tuyo. Mira git log origin/main primero.
Error 6: git reset --hard origin/main sin comprobar qué tienes por delante. git log --oneline @{u}..HEAD cuesta un segundo y te dice exactamente qué vas a tirar.
Error 7: rehacer el trabajo a mano tras una reescritura ajena. git rebase --onto lo trasplanta en un comando y range-diff demuestra que no se ha alterado nada.
Error 8: forzar una rama con una pull request abierta llena de comentarios. Puede dejar la revisión huérfana. Considera publicar una rama nueva.
Error 9: no avisar. El coste técnico de una reescritura compartida es pequeño; el coste social de no anunciarla, enorme.
Consejo 1: git status -sb como reflejo. Una línea que dice [ahead N, behind M] y resuelve la mitad de las dudas.
Consejo 2: pull.ff only como valor global. Te obliga a mirar antes de integrar, y evita commits de fusión que nadie pidió.
Consejo 3: push.useForceIfIncludes true. Una línea de configuración que elimina el agujero del --force-with-lease.
Consejo 4: git range-diff antes y después de cualquier reescritura. Es la prueba objetiva de que el contenido no ha cambiado.
Consejo 5: recuerda origin/rama@{1}. El historial anterior del servidor sigue en tu reflog remoto, y con él se restaura lo que otro destruyó.
Consejo 6: protege main en el servidor. Es la única solución que hace imposible el problema, en lugar de tratarlo.
Ejercicios
Ejercicio 1: provocar y medir una divergencia
- Monta un remoto local con
git init --barey clónalo dos veces (simulando a Ana y a Bruno). - Desde el clon de Bruno, haz dos commits y publícalos.
- Desde el de Ana, sin hacer
fetch, haz dos commits distintos. - Ejecuta
git fetchy mide la divergencia congit status -sb,git rev-list --left-right --countygit log --left-right --oneline. - Intenta
git pushy transcribe el mensaje exacto. - Resuélvelo con
pull --rebasey comprueba congit log --graphque el historial ha quedado lineal. - Repite todo el ejercicio con
pull --no-rebasey compara los dos grafos resultantes.
Ejercicio 2: --force-with-lease y su agujero
- Con el mismo montaje, haz que Ana publique un commit en
GT-241. - Ana ejecuta
git commit --amendsobre ese commit (sin publicar todavía). - Mientras tanto, Bruno publica un commit nuevo en
GT-241. - Ana ejecuta
git push --force-with-lease. Anota el resultado y explícalo. - Ana ejecuta
git fetchy vuelve a intentarlo. Anota el resultado y explica por qué ha cambiado. - Comprueba que el commit de Bruno ha desaparecido del servidor y recupéralo usando
origin/GT-241@{1}desde el clon de Bruno. - Repite el paso 5 con
--force-if-includesy comprueba que ahora sí protege.
Ejercicio 3: trasplante tras una reescritura ajena
- Monta un remoto con tres commits en
mainy clónalo dos veces. - Desde el clon de Ana, crea
GT-241sobremaincon tres commits propios (sin publicarla). - Desde el clon de Diego, haz
git rebase -isobremainpara modificar el mensaje del segundo commit, y fuerza elpush. - Ana hace
fetchy diagnostica la situación congit log --graph --allygit range-diff. - Ana trasplanta
GT-241congit rebase --ontoy alinea sumain. - Verifica con
range-diffque sus tres commits son idénticos en contenido. - Comprueba en
git reflog show origin/mainque queda registrado elforced-update.
Soluciones
Solución 1:
rm -rf /tmp/p9-03 && mkdir /tmp/p9-03 && cd /tmp/p9-03
git init -q --bare servidor.git
git clone -q servidor.git ana && cd ana
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"
git push -q -u origin main
cd ..
git clone -q servidor.git bruno && cd bruno
git config user.name "Bruno Salas"; git config user.email "[email protected]"
cd ..# 2. Bruno publica
cd /tmp/p9-03/bruno
echo "// GT-238" >> app.js && git commit -q -am "GT-238 corregir el foco"
echo "// GT-244" >> app.js && git commit -q -am "GT-244 script de arranque"
git push -q# 3. Ana trabaja sin saberlo
cd /tmp/p9-03/ana
echo "// GT-241 a" >> estilos.css && git add . && git commit -q -m "GT-241 base del filtro"
echo "// GT-241 b" >> estilos.css && git commit -q -am "GT-241 estilos del indicador"# 4. Medir
git fetch -q
git status -sb | head -1
git rev-list --left-right --count HEAD...@{u}
git log --oneline --left-right HEAD...@{u}< 7d3a8f4 GT-241 estilos del indicador < b52c9d1 GT-241 base del filtro > 3e7f1a8 GT-244 script de arranque > 9c4e7b2 GT-238 corregir el foco
To /tmp/p9-03/servidor.git ! [rejected] main -> main (non-fast-forward) error: failed to push some refs to '/tmp/p9-03/servidor.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart.
* 4b8e2c9 (HEAD -> main) GT-241 estilos del indicador * 8f2d6a1 GT-241 base del filtro * 3e7f1a8 (origin/main) GT-244 script de arranque * 9c4e7b2 GT-238 corregir el foco * 1e6f2c8 chore: inicio
Lineal, sin bifurcaciones. Sus dos commits tienen hashes nuevos (b52c9d1 → 8f2d6a1): el rebase los ha recreado sobre la base nueva.
# 7. La alternativa con merge (sobre una copia)
cd /tmp/p9-03/ana
git switch -q -c prueba-merge 8f2d6a1
git reset -q --hard b52c9d1 # volvemos al estado previo al rebase...
# (más simple: rehacer el escenario. Lo importante es el grafo resultante:)* c7e2a91 Merge branch 'main' of /tmp/p9-03/servidor |\ | * 3e7f1a8 GT-244 script de arranque | * 9c4e7b2 GT-238 corregir el foco * | 7d3a8f4 GT-241 estilos del indicador * | b52c9d1 GT-241 base del filtro |/ * 1e6f2c8 chore: inicio
pull --rebase |
pull --no-rebase |
|
|---|---|---|
| Commits resultantes | 4 | 5 (un commit de fusión extra) |
| Hashes de Ana | Cambian | Se conservan |
| Forma del grafo | Lineal | Bifurcada |
| Bueno para | Ramas propias, historial legible | Ramas realmente compartidas |
Solución 2:
cd /tmp/p9-03/ana && git switch -q -c GT-241 origin/main
echo "// v1" > filtro.js && git add . && git commit -q -m "GT-241 primera version"
git push -q -u origin GT-241
# 2. Ana enmienda
git commit -q --amend -m "GT-241 primera version del filtro de pendientes"
git rev-parse --short HEAD# 3. Bruno publica encima (sin que Ana lo sepa)
cd /tmp/p9-03/bruno
git fetch -q && git switch -q -c GT-241 origin/GT-241
echo "// aportacion de Bruno" >> filtro.js && git commit -q -am "GT-241 validar el filtro vacio"
git push -qTo /tmp/p9-03/servidor.git ! [rejected] GT-241 -> GT-241 (stale info) error: failed to push some refs
La protección ha funcionado. El origin/GT-241 de Ana apunta a su commit original; el servidor está en el commit de Bruno. Como no coinciden, Git se niega.
Ha pasado. Y ha destruido el commit de Bruno. El fetch actualizó origin/GT-241 al commit de Bruno; el "arrendamiento" pasó a coincidir con la realidad; la comprobación se satisfizo. Ana nunca vio el trabajo de Bruno y aun así lo sobrescribió.
6e2b9c7 refs/remotes/origin/GT-241@{0}: fetch: forced-update
b52c9d1 refs/remotes/origin/GT-241@{1}: pushEn realidad Bruno ni siquiera necesita el reflog: tiene el commit en su rama local. Su GT-241 local sigue intacto:
Y para devolverlo al servidor haría falta integrarlo con la versión enmendada de Ana —un cherry-pick del commit de Bruno sobre la rama nueva (lección 05-03)—, coordinándolo entre los dos.
# 7. Con --force-if-includes
cd /tmp/p9-03/ana
# (rehecho el escenario: Bruno vuelve a publicar algo que Ana no tiene)
git fetch -q
git push --force-with-lease --force-if-includesAhora sí protege, incluso después del fetch. La comprobación adicional exige que los commits remotos que se van a sobrescribir estén incorporados en el historial local de Ana, no solo descargados. Y no lo están: nunca los integró.
Solución 3:
rm -rf /tmp/p9-03b && mkdir /tmp/p9-03b && cd /tmp/p9-03b
git init -q --bare servidor.git
git clone -q servidor.git ana && cd ana
git config user.name "Ana Ferrer"; git config user.email "[email protected]"
for i in 1 2 3; do echo "linea $i" >> app.js; git add .; git commit -q -m "chore: commit $i"; done
git push -q -u origin main
cd .. && git clone -q servidor.git diego && cd diego
git config user.name "Diego Rueda"; git config user.email "[email protected]"
cd ..# 2. Ana crea GT-241
cd /tmp/p9-03b/ana
git switch -q -c GT-241
echo "// filtro 1" > filtro.js && git add . && git commit -q -m "GT-241 base del filtro"
echo "// filtro 2" >> filtro.js && git commit -q -am "GT-241 calcular el contador"
echo "// filtro 3" >> filtro.js && git commit -q -am "GT-241 estilos del indicador"
git branch --show-current
git rev-parse --short mainAnota ese 7d3a8f4: es la base antigua.
# 3. Diego reescribe main y fuerza
cd /tmp/p9-03b/diego
GIT_SEQUENCE_EDITOR="sed -i '2s/^pick/reword/'" \
GIT_EDITOR="sed -i '1s/.*/chore: commit 2 (mensaje corregido)/'" \
git rebase -i HEAD~2
git push -q --force
git log --oneline -3* 9c4e7b2 (HEAD -> GT-241) GT-241 estilos del indicador * 3e7f1a8 GT-241 calcular el contador * 8a1f6c3 GT-241 base del filtro * 7d3a8f4 (main) chore: commit 3 * b52c9d1 chore: commit 2 | * 2f8c6e1 (origin/main) chore: commit 3 | * 7a4d9b3 chore: commit 2 (mensaje corregido) |/ * 5c1e8f2 chore: commit 1
Dos líneas paralelas con los mismos mensajes: historial reescrito.
1: 7a4d9b3 ! 1: b52c9d1 chore: commit 2 (mensaje corregido)
@@ Metadata
## Commit message ##
- chore: commit 2 (mensaje corregido)
+ chore: commit 2
2: 2f8c6e1 = 2: 7d3a8f4 chore: commit 3Solo cambió un mensaje; el contenido es idéntico. Diagnóstico completo en un comando.
# 5. Trasplante
git branch respaldo-GT-241
git rebase --onto origin/main 7d3a8f4 GT-241
git log --oneline --graph -7* 4b8e2c9 (HEAD -> GT-241) GT-241 estilos del indicador * 8f2d6a1 GT-241 calcular el contador * 6e2b9c7 GT-241 base del filtro * 2f8c6e1 (origin/main) chore: commit 3 * 7a4d9b3 chore: commit 2 (mensaje corregido) * 5c1e8f2 chore: commit 1
1: 8a1f6c3 = 1: 6e2b9c7 GT-241 base del filtro 2: 3e7f1a8 = 2: 8f2d6a1 GT-241 calcular el contador 3: 9c4e7b2 = 3: 4b8e2c9 GT-241 estilos del indicador
Tres =. Contenido idéntico, base nueva, cero commits duplicados.
2f8c6e1 refs/remotes/origin/main@{0}: fetch: forced-update
7d3a8f4 refs/remotes/origin/main@{1}: clone: from /tmp/p9-03b/servidor.gitforced-update con su fecha, y origin/main@{1} guardando el historial anterior del servidor. Si hubiera hecho falta restaurarlo, ahí estaba.
Conclusión
Las divergencias dejan de asustar en cuanto se ven como lo que son: una situación en la que Git no puede decidir por ti.
- Divergir significa que cada rama tiene commits que la otra no tiene. Se mide con
git status -sby congit rev-list --left-right --count main...origin/main, y se entiende mirando los dos lados congit log --left-right. Todo diagnóstico empieza porgit fetch. - Los cuatro mensajes son la misma situación: el "have diverged" de
status, el "You have divergent branches" depull, el "Not possible to fast-forward" depull.ff onlyy el non-fast-forward delpush. - Tres políticas de
pull:mergepara ramas realmente compartidas,rebasepara las tuyas,ff-onlycuando prefieres decidir a mano. La elección es por rama, no por costumbre. - El
pushrechazado se responde diagnosticando, no forzando:git log --oneline HEAD..@{u}dice de quién son los commits del servidor. Si son de otro, se integra. Si son versiones viejas de los tuyos y la rama es solo tuya, se fuerza con--force-with-lease. --force-with-leaseno protege si has hechofetchjusto antes, porque el arrendamiento se actualiza con lo que aún no has visto.--force-if-includeslo arregla;push.useForceIfIncludes truelo deja puesto para siempre.- Cuando otro reescribe el historial publicado y tú tienes trabajo encima, el procedimiento es: respaldar con
git branch, trasplantar congit rebase --onto <nueva-base> <base-vieja> <rama>, alinearmainconreset --hard origin/main, y verificar congit range-diffque el contenido no ha cambiado. - Las ramas remotas también tienen reflog.
origin/main@{1}guarda dónde estaba el servidor antes de la reescritura, yforced-updatees la evidencia de que ocurrió. Con eso, cualquier clon del equipo puede restaurar lo destruido. refusing to merge unrelated historiesno es divergencia: son dos árboles sin ancestro común. Mira qué hay al otro lado antes de usar--allow-unrelated-histories.- Y lo que más ahorra: avisar antes, y proteger
mainen el servidor para que el problema no pueda ocurrir.
Queda una idea que ha aparecido en casi todos los apartados sin desarrollarse: el reflog. Ha sido la fuente del ORIG_HEAD de la lección anterior, del origin/main@{1} de esta, y de la promesa que llevamos abierta desde la lección 03-06 ("-D es recuperable") y la 05-01 ("un rebase desastroso se puede deshacer").
Toca desarrollarlo entero: qué es exactamente, dónde vive, cuánto dura, cómo se lee, y el recetario completo de recuperación —el reset --hard que se llevó tres días, la rama borrada con -D, el rebase que salió mal, el stash eliminado— más lo que hacer cuando ni siquiera el reflog basta.
Continúa en la lección 09-04: Recuperando Confirmaciones Perdidas.
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
