Error rejected non-fast-forward en git push: cómo solucionarlo
El error rejected non-fast-forward significa que el remoto tiene commits que no tienes. Explico por qué pasa y cómo arreglarlo con git pull, sin forzar nada.
Estás a punto de cerrar la tarea. Haces git push y, en vez del main -> main de siempre, el terminal te escupe un ! [rejected] con la coletilla (non-fast-forward). El push falla, no entiendes por qué, y la primera respuesta de Google te dice que uses --force. No lo hagas todavía. El error rejected non-fast-forward tiene una causa muy concreta y una solución de dos comandos que no destruye el trabajo de nadie. Aquí te explico qué está pasando por debajo y cómo salir del paso sin liarla.
¿Qué significa “rejected non-fast-forward” exactamente?
Significa que la rama remota tiene commits que tu rama local no tiene, y Git se niega a sobrescribirlos con tu push. Este es el bloque completo que imprime, tal cual:
$ git push origin main
To github.com:equipo/proyecto.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:equipo/proyecto.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.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Léelo de arriba abajo, porque cada línea dice algo. La primera, ! [rejected] main -> main (non-fast-forward), es el veredicto: Git ni siquiera intentó enviar la rama. La segunda, error: failed to push some refs, confirma que el push entero quedó en nada. Las líneas hint: son las importantes. the tip of your current branch is behind its remote counterpart es el diagnóstico exacto: la punta de tu rama va por detrás de la del remoto. Y Integrate the remote changes (e.g. 'git pull ...') es la receta que el propio Git te está dando.
Aquí no ha fallado ni el permiso ni la conexión: es Git protegiéndote.
¿Por qué Git rechaza tu push?
Git rechaza el push porque no puede hacer un fast-forward: tu commit no es descendiente del commit que ahora mismo tiene el remoto. Para entenderlo hay que pensar en la rama como un puntero que apunta a un commit, y en el historial como una cadena donde cada commit conoce a su padre.
Imagina que tú y un compañero partís del mismo commit, a1b2c3d. Tú haces tu commit encima, 9f8e7d6, con tu rama apuntando ahí. Mientras tanto, tu compañero también parte de a1b2c3d, hace el suyo, 4c5d6e7, y lo sube antes que tú. Ahora el remoto apunta a 4c5d6e7 y tú apuntas a 9f8e7d6. Los dos salisteis de a1b2c3d, pero por caminos distintos. La historia se ha bifurcado.
Un fast-forward es el caso fácil: si el commit del remoto fuese un ancestro del tuyo, Git solo tendría que deslizar el puntero hacia delante en línea recta, sin perder nada por el camino. Pero aquí no lo es. Para que tu push entrara, Git tendría que tirar el commit 4c5d6e7 de tu compañero, y eso destruiría historia. Por eso corta y te manda integrar primero.
Entendido el porqué, ahora toca decidir qué escribo para arreglarlo.
La solución: git pull y luego git push
Haz git pull para traer e integrar los commits del remoto, resuelve cualquier conflicto si aparece, y después vuelve a hacer git push [2]. Con eso resuelves el rechazo en la inmensa mayoría de los casos.
# Trae los commits del remoto y los integra con los tuyos
git pull origin main
# Ahora tu rama ya contiene el commit del compañero + el tuyo
git push origin main
Lo que hace pull es dos cosas en un solo comando: primero un fetch (baja los commits nuevos del remoto a tu repo local) y luego los une con tu trabajo. Si tus cambios y los suyos tocan archivos distintos, la unión es automática y no tienes que hacer nada más. Si tocan las mismas líneas, Git para y te marca el conflicto para que lo resuelvas a mano antes de continuar. Si quieres el detalle de qué separa a fetch de pull, lo tienes en la diferencia entre git pull y git fetch.
La parte que a casi todo el mundo le sorprende la primera vez es cómo une esos dos historiales bifurcados. Ahí es donde entra la decisión entre merge y rebase.
Antes de seguir, prueba tú a resolver un push rechazado paso a paso. En este ejercicio interactivo tienes el remoto por delante de tu rama y decides qué comando lanzar:
Cargando ejercicio...
¿Merge o rebase al integrar los cambios?
git pull sin más hace un merge: crea un commit de fusión que junta las dos líneas de historia. git pull --rebase hace un rebase: coge tus commits locales, los aparta un momento, avanza tu rama hasta el estado del remoto, y reaplica tus commits encima uno a uno. El resultado se ve distinto.
# Merge (comportamiento por defecto): crea un commit de fusión
git pull origin main
# Rebase puntual: reaplica tus commits encima de los remotos
git pull --rebase origin main
| Aspecto | Merge (git pull) | Rebase (git pull --rebase) |
|---|---|---|
| Historial | Conserva las dos líneas y las une en un punto | Lineal, como si hubieras partido del último commit remoto |
| Commit extra de fusión | Sí | No |
| Reescribe tus hashes | No, tus commits quedan intactos | Sí, se recrean con hashes nuevos porque cambia su punto de partida |
| Cuándo preferirlo | Cuando quieres un registro fiel de cuándo se integró cada rama | En tu rama de trabajo, para dejar un historial limpio antes de abrir el pull request |
Si no quieres decidirlo cada vez, puedes fijar el comportamiento por defecto de pull en tu configuración:
# Que pull haga siempre merge
git config pull.rebase false
# Que pull haga siempre rebase
git config pull.rebase true
Un detalle sobre rebase que muerde a mucha gente: nunca reescribas commits que ya habías subido y compartido. El rebase les cambia el hash, y si otro ya tenía esos commits con el hash antiguo, cuando vuestros repos se sincronicen aparecerán duplicados y conflictos raros. Rebasa tu trabajo local antes de publicarlo, pero nunca rebases commits que ya están en el remoto y otros usan.
Forzar el push: por qué —force destruye
git push --force sí sube tu rama, pero lo hace tirando por la borda todo lo que el remoto tenía y tú no. En el ejemplo de antes, forzar el push haría que el remoto pasara de 4c5d6e7 a 9f8e7d6 y el commit de tu compañero desaparecería del historial de la rama como si nunca hubiera existido. La documentación oficial lo dice sin rodeos: un update non-fast-forward pierde historia [1]. En una rama que solo usas tú, ese daño te lo haces a ti mismo. En una rama compartida, se lo haces a todo el equipo, y muchas veces sin que nadie se entere hasta que a alguien le faltan sus cambios.
--force desactiva todas las comprobaciones de seguridad que Git pone precisamente para evitar esto. Por eso, cuando de verdad necesitas rehacer una rama (por ejemplo, después de un rebase legítimo de tu propia feature), existe una opción intermedia.
—force-with-lease: forzar con red de seguridad
git push --force-with-lease fuerza el push solo si el remoto sigue exactamente como estaba la última vez que hiciste fetch. Si desde entonces alguien ha subido algo, la referencia remota ya no coincide con lo que tú esperabas, y Git aborta el push en vez de pisar ese trabajo [1].
# Fuerza el push, pero solo si nadie ha tocado el remoto desde tu último fetch
git push --force-with-lease origin mi-feature
--force | --force-with-lease | |
|---|---|---|
| Comprueba el estado actual del remoto | No | Sí, compara con lo que viste en tu último fetch |
| Si un compañero subió algo entretanto | Lo borra sin avisar | Aborta y te avisa |
| Cuándo tiene sentido | Casi nunca en trabajo compartido | Rehacer tu propia rama de feature tras un rebase |
Un matiz importante para no confiarte: --force-with-lease es más seguro que --force, pero no es “la única forma segura de forzar”, y tampoco es infalible. Su red de seguridad se basa en tu último fetch. Si haces un git fetch justo antes de forzar sin mirar qué ha llegado, la comprobación pasa porque para Git “ya conocías” ese estado, y puedes acabar pisando cambios igualmente. Sigue siendo una herramienta para cuando sabes exactamente qué estás reescribiendo y por qué.
Errores comunes al resolver el rechazo
git push —force en una rama compartida
Este es el clásico, y el más caro. Encuentras --force en un hilo de Stack Overflow, lo pegas, el push pasa, y te quedas tranquilo. Dos días después un compañero pregunta dónde está su commit del martes. Ya no está. Si la rama la toca más de una persona, --force no es una opción rápida, es un incidente esperando a ocurrir.
Usar —force-with-lease sin entender qué protege
--force-with-lease protege el estado del remoto respecto a tu último fetch, no respecto al estado real de ahora mismo. Si automatizas un git fetch && git push --force-with-lease, has anulado la protección: acabas de decirle a Git que aceptas el estado más reciente sin mirarlo. Haz el fetch, revisa qué ha entrado, y solo entonces decide.
Hacer pull con cambios sin commitear
Si intentas git pull con modificaciones sin guardar que chocan con lo que baja, Git aborta para no perderlas. Commitea o guarda con git stash antes de integrar, y te ahorras el enredo.
Repetir el push a ver si esta vez cuela
Volver a lanzar git push sin haber integrado nada da exactamente el mismo rejected non-fast-forward. El remoto no ha cambiado y tu rama tampoco. El error no se cansa; hay que hacer el pull.
Cómo evitar que te vuelva a pasar
La forma más fiable de no volver a ver este error es reducir la ventana en la que tu rama y la remota pueden divergir. Cuatro hábitos que funcionan:
- Haz
git pullantes de empezar a trabajar cada día, no cuando ya tienes tres commits encima y toca subirlos. - Sube tu trabajo a menudo. Cuanto más frecuente es el push, menos commits acumula el remoto por detrás de ti.
- Trabaja en ramas de feature en vez de todos sobre
main. Aíslan tu historia y hacen que las integraciones sean explícitas cuando abres el pull request. - Si tú y otra persona vais a tocar el mismo archivo, coordinaos antes. Un mensaje de treinta segundos evita media hora de resolver conflictos.
Ninguno de estos hábitos elimina el error del todo, porque colaborar significa que a veces alguien sube antes que tú. Pero lo convierten en un pull rápido en vez de una tarde peleándote con conflictos. Para ver cómo encaja esto en un flujo de equipo completo, tienes la guía de trabajo en equipo con Git y GitHub. Y si te aparece otro motivo de rechazo distinto al non-fast-forward, los otros motivos por los que Git rechaza un push cubren el resto de casos.
Resolver un push rechazado sin romper nada es un reflejo que se coge practicando, no leyendo. En el curso Git de cero a profesional practicas este y otros escenarios de colaboración con repos reales, integrando divergencias y forzando con criterio, hasta que el pull antes del push te sale solo.
Un concepto nuevo cada semana
Fuentes
- Documentación oficial de git push (git-scm.com). Sección “Note about fast-forwards” (un update non-fast-forward pierde historia) y descripción de
--forcey--force-with-lease. - Dealing with non-fast-forward errors (GitHub Docs). Explicación del rechazo por non-fast-forward y la recomendación de integrar antes de volver a hacer push.
Preguntas Frecuentes
¿git pull va a sobrescribir mis cambios locales?
No. git pull integra los commits del remoto con los tuyos, no los reemplaza. Si tus cambios y los que bajan no chocan, la unión es automática y conservas ambos. Si tocan las mismas líneas, Git para, te marca el conflicto y no continúa hasta que decides tú qué versión gana. Lo único que puede frenarlo es tener cambios sin commitear que entren en conflicto; en ese caso haz commit o git stash primero.
¿Cuál es la diferencia entre —force y —force-with-lease?
--force sube tu rama pisando lo que haya en el remoto, sin comprobar nada. --force-with-lease primero comprueba que el remoto siga como estaba en tu último fetch y solo entonces fuerza; si alguien ha subido algo entretanto, aborta. Por eso --force-with-lease es la opción menos peligrosa cuando tienes que reescribir tu propia rama de feature, aunque ninguna de las dos debería tocar una rama compartida a la ligera.
¿Por qué me sale rejected non-fast-forward si soy el único en el repo?
Porque el remoto tiene un commit que tu copia local no tiene, aunque lo hayas puesto tú. Suele pasar cuando editaste un archivo directamente desde la web de GitHub, cuando hiciste commit --amend o un rebase que ya habías subido, o cuando trabajas desde otra máquina y subiste algo desde allí. Haz git pull para traer ese commit y el error desaparece.
¿Merge o rebase, cuál elijo al hacer pull?
Si dudas, quédate con merge, que es el comportamiento por defecto y no reescribe nada. Usa git pull --rebase en tu rama de trabajo cuando quieras un historial lineal y limpio antes de abrir el pull request. La regla que no debes saltarte: no hagas rebase de commits que ya habías compartido con otros, porque les cambia el hash y genera duplicados cuando vuestros repos se sincronicen.
Ya hice —force y borré commits, ¿los recupero?
Casi siempre sí, si actúas pronto. Git guarda un registro local de dónde ha estado cada rama en el reflog, y los commits “borrados” siguen ahí un tiempo aunque ninguna rama los apunte.
# Busca el hash del commit que perdiste
git reflog
# Recupéralo creando una rama que lo apunte
git branch recupera-esto 9f8e7d6
El reflog es local, así que revísalo en la máquina donde tenías esos commits. Si el que forzó fue otro y tú nunca los tuviste en tu repo, pídele que los recupere desde el suyo.