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

  1. Qué significa exactamente "han divergido"
  2. Medir la divergencia
  3. Los cuatro mensajes de la misma situación
  4. Las tres políticas de pull y cuál elegir
  5. Cómo se llega a una divergencia
  6. El push rechazado: diagnóstico y salidas
  7. --force-with-lease y por qué a veces no protege
  8. Cuando el remoto reescribió y tú tienes trabajo encima
  9. Recuperar la rama remota anterior con origin/rama@{1}
  10. Historias no relacionadas: --allow-unrelated-histories
  11. Coordinación: lo que hay que decir y cuándo

  1. 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:

git merge-base main origin/main
git merge-base --all main origin/main     # si hay varios candidatos
4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a

  1. Medir la divergencia

Antes de resolver, mide. Estos comandos no modifican nada.

# Lo primero, SIEMPRE: traer el estado real del servidor
git fetch origin

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

git rev-list --left-right --count main...origin/main
2	3

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:

git rev-list --left-right --count @{u}...HEAD
git status -sb
## main...origin/main [ahead 2, behind 3]

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..main
7d3a8f4 GT-241 estilos del indicador de pendientes
b52c9d1 GT-241 calcular el contador sobre la lista completa
# Los del servidor que yo no tengo
git log --oneline main..origin/main
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 ellos

Si 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'
git div

  1. 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

fatal: Not possible to fast-forward, aborting.

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:

 ! [rejected]        main -> main (fetch first)

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.

  1. Las tres políticas de pull y cuál elegir

git 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 --rebase

O, 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 devuelve

rebase.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 --rebase reescribe 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:

git config --global pull.rebase.autoSquash true

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.

  1. 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.

git log --oneline -1
git push
b52c9d1 GT-247 mostrar el número de tareas pendientes
 ! [rejected]        GT-247 -> GT-247 (non-fast-forward)

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.

git rev-list --left-right --count GT-247...origin/GT-247
1	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.

  1. El push rechazado: diagnóstico y salidas

Este 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}
2	3
# 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
# 4. ¿Y qué tengo yo?
git log --oneline @{u}..HEAD

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 push

Con pull configurado:

git pull --rebase
git push

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:

git rebase --abort      # vuelves exactamente al estado anterior

Salida B: forzar, cuando la rama es tuya

Solo si se cumplen las tres condiciones:

  1. La rama es de funcionalidad y nadie más la usa.
  2. Lo que hay en el servidor son versiones antiguas de tus propios commits.
  3. Has comprobado el punto 3 del diagnóstico y no hay commits de otra persona.
git push --force-with-lease
 + b52c9d1...7d3a8f4 GT-241 -> GT-241 (forced update)

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:

git push origin GT-241:GT-241-v2

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).

  1. --force-with-lease y por qué a veces no protege

Recordando 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).

git push --force-with-lease
 ! [rejected]        GT-241 -> GT-241 (stale info)
error: failed to push some refs

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 fetch justo 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 Bruno

Lo 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:

git push --force-with-lease --force-if-includes

--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

Configúralo como comportamiento por defecto:

git config --global push.useForceIfIncludes true

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:b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b

Verboso, pero no hay forma de equivocarse.

  1. 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.

git fetch origin
git status -sb
## GT-241...origin/GT-241 [ahead 3, behind 7]

Paso 1: entender qué ha pasado

git log --oneline --graph --all -20
* 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 origin/main...7d3a8f4

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

git branch respaldo-GT-241
git branch respaldo-main-viejo 7d3a8f4

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:

git rebase --onto <nueva-base> <base-antigua> <rama>

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 (el main viejo del que colgaba tu rama).
  • Rama: GT-241.
git rebase --onto origin/main 7d3a8f4 GT-241
Successfully rebased and updated refs/heads/GT-241.
git log --oneline --graph -8
* 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

git switch main
git status -sb
## main...origin/main [ahead 3, behind 3]

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:

git reset --hard origin/main

Si 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

git switch GT-241
git range-diff respaldo-GT-241...GT-241
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:

git branch -D respaldo-GT-241 respaldo-main-viejo

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:

git push --force-with-lease --force-if-includes

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)

  1. Recuperar la rama remota anterior con origin/rama@{1}

Un detalle que salva situaciones y que casi nadie conoce: las ramas de seguimiento remotas también tienen reflog.

git reflog show origin/main
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-forward

Dos cosas valiosísimas en esa salida:

  1. forced-update es la prueba de que alguien reescribió el historial publicado. Si dudabas de qué pasó, ahí está la evidencia con su fecha.
  2. origin/main@{1} es dónde estaba el main del 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/main

Esto 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:main

Y también con fechas:

git log --oneline origin/main@{yesterday}
git log --oneline origin/main@{2.days.ago}

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.

  1. Historias no relacionadas: --allow-unrelated-histories

git pull origin main
fatal: refusing to merge unrelated histories

Qué 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
git merge-base main origin/main
echo $?           # 1: no hay ancestro común

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 -20

Si el remoto solo tiene el commit inicial de la plataforma:

git pull origin main --allow-unrelated-histories

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 servidor

Y 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.

  1. 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:

  1. Avisa antes, no después. Un mensaje de treinta segundos en el canal del equipo: "Voy a forzar main a las 16:00 para quitar el volcado de la base de datos del historial. No hagáis pull hasta que avise."
  2. 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.
    
  3. Hazlo en un momento de poca actividad. Nunca un viernes por la tarde, nunca durante una entrega.
  4. Confirma cuando termine. El silencio genera pull a ciegas.

Cuando ya ha pasado y no avisaste:

  1. 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.
  2. Ofrece el procedimiento del apartado 8, no un "arregladlo".
  3. 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 request

Una 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-push

Errores 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

  1. Monta un remoto local con git init --bare y clónalo dos veces (simulando a Ana y a Bruno).
  2. Desde el clon de Bruno, haz dos commits y publícalos.
  3. Desde el de Ana, sin hacer fetch, haz dos commits distintos.
  4. Ejecuta git fetch y mide la divergencia con git status -sb, git rev-list --left-right --count y git log --left-right --oneline.
  5. Intenta git push y transcribe el mensaje exacto.
  6. Resuélvelo con pull --rebase y comprueba con git log --graph que el historial ha quedado lineal.
  7. Repite todo el ejercicio con pull --no-rebase y compara los dos grafos resultantes.

Ejercicio 2: --force-with-lease y su agujero

  1. Con el mismo montaje, haz que Ana publique un commit en GT-241.
  2. Ana ejecuta git commit --amend sobre ese commit (sin publicar todavía).
  3. Mientras tanto, Bruno publica un commit nuevo en GT-241.
  4. Ana ejecuta git push --force-with-lease. Anota el resultado y explícalo.
  5. Ana ejecuta git fetch y vuelve a intentarlo. Anota el resultado y explica por qué ha cambiado.
  6. Comprueba que el commit de Bruno ha desaparecido del servidor y recupéralo usando origin/GT-241@{1} desde el clon de Bruno.
  7. Repite el paso 5 con --force-if-includes y comprueba que ahora sí protege.

Ejercicio 3: trasplante tras una reescritura ajena

  1. Monta un remoto con tres commits en main y clónalo dos veces.
  2. Desde el clon de Ana, crea GT-241 sobre main con tres commits propios (sin publicarla).
  3. Desde el clon de Diego, haz git rebase -i sobre main para modificar el mensaje del segundo commit, y fuerza el push.
  4. Ana hace fetch y diagnostica la situación con git log --graph --all y git range-diff.
  5. Ana trasplanta GT-241 con git rebase --onto y alinea su main.
  6. Verifica con range-diff que sus tres commits son idénticos en contenido.
  7. Comprueba en git reflog show origin/main que queda registrado el forced-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}
## main...origin/main [ahead 2, behind 2]
2	2
< 7d3a8f4 GT-241 estilos del indicador
< b52c9d1 GT-241 base del filtro
> 3e7f1a8 GT-244 script de arranque
> 9c4e7b2 GT-238 corregir el foco
# 5. El rechazo
git push
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.
# 6. Rebase
git pull -q --rebase
git log --oneline --graph -5
git push -q && echo "publicado"
* 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
publicado

Lineal, sin bifurcaciones. Sus dos commits tienen hashes nuevos (b52c9d18f2d6a1): 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
6e2b9c7
# 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 -q
# 4. Ana fuerza con lease, SIN fetch previo
cd /tmp/p9-03/ana
git push --force-with-lease
To /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.

# 5. Ana hace fetch "para actualizarse" y reintenta
git fetch -q
git push --force-with-lease
To /tmp/p9-03/servidor.git
 + b52c9d1...6e2b9c7 GT-241 -> GT-241 (forced update)

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ó.

# 6. Recuperarlo desde el clon de Bruno
cd /tmp/p9-03/bruno
git reflog show origin/GT-241
6e2b9c7 refs/remotes/origin/GT-241@{0}: fetch: forced-update
b52c9d1 refs/remotes/origin/GT-241@{1}: push

En realidad Bruno ni siquiera necesita el reflog: tiene el commit en su rama local. Su GT-241 local sigue intacto:

git log --oneline -2
git branch rescate-bruno GT-241
2f8c6e1 GT-241 validar el filtro vacio
b52c9d1 GT-241 primera version

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-includes
To /tmp/p9-03/servidor.git
 ! [rejected]        GT-241 -> GT-241 (stale info)

Ahora 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ó.

git config --global push.useForceIfIncludes true    # para no tener que acordarse

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 main
GT-241
7d3a8f4

Anota 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
2f8c6e1 chore: commit 3
7a4d9b3 chore: commit 2 (mensaje corregido)
5c1e8f2 chore: commit 1
# 4. Ana diagnostica
cd /tmp/p9-03b/ana
git fetch -q
git log --oneline --graph --all -10
* 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.

git range-diff origin/main...main
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 3

Solo 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
git switch -q main
git reset -q --hard origin/main
git switch -q GT-241
# 6. Verificación
git range-diff respaldo-GT-241...GT-241
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.

git diff respaldo-GT-241 GT-241 -- filtro.js
(sin salida)
git branch -D respaldo-GT-241
# 7. La evidencia en el reflog remoto
git reflog show origin/main
2f8c6e1 refs/remotes/origin/main@{0}: fetch: forced-update
7d3a8f4 refs/remotes/origin/main@{1}: clone: from /tmp/p9-03b/servidor.git

forced-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 -sb y con git rev-list --left-right --count main...origin/main, y se entiende mirando los dos lados con git log --left-right. Todo diagnóstico empieza por git fetch.
  • Los cuatro mensajes son la misma situación: el "have diverged" de status, el "You have divergent branches" de pull, el "Not possible to fast-forward" de pull.ff only y el non-fast-forward del push.
  • Tres políticas de pull: merge para ramas realmente compartidas, rebase para las tuyas, ff-only cuando prefieres decidir a mano. La elección es por rama, no por costumbre.
  • El push rechazado 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-lease no protege si has hecho fetch justo antes, porque el arrendamiento se actualiza con lo que aún no has visto. --force-if-includes lo arregla; push.useForceIfIncludes true lo deja puesto para siempre.
  • Cuando otro reescribe el historial publicado y tú tienes trabajo encima, el procedimiento es: respaldar con git branch, trasplantar con git rebase --onto <nueva-base> <base-vieja> <rama>, alinear main con reset --hard origin/main, y verificar con git range-diff que 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, y forced-update es la evidencia de que ocurrió. Con eso, cualquier clon del equipo puede restaurar lo destruido.
  • refusing to merge unrelated histories no 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 main en 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

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

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

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

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

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

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados