En la lección anterior usamos git push varias veces sin explicarlo. Tocaba: había que poner el proyecto en el servidor para poder estudiar la recepción. Ahora le damos la vuelta al canal y desmenuzamos el comando que envía.

git push es el comando con consecuencias para los demás. Todo lo que has hecho hasta ahora —confirmar, ramificar, fusionar, incluso equivocarte— ocurría dentro de tu .git/ y se podía deshacer sin que nadie se enterara. Un push cambia el repositorio que todo el equipo usa como referencia. Lo que envías, otros lo descargan; y lo que borras del servidor, otros dejan de tener.

Por eso esta lección dedica tanto espacio a los rechazos y a las opciones de forzado. Saber enviar es fácil; saber por qué el servidor te dice que no, y qué es seguro hacer entonces, es lo que distingue a alguien que no destruye el trabajo de sus compañeros.

Ana envía por fin main y sus ramas al servidor del equipo, y Bruno y Carla las reciben.

Contenido

  1. Qué envía exactamente git push
  2. El primer envío: git push -u origin main
  3. Qué hace exactamente el -u
  4. El refspec de envío
  5. Las formas abreviadas y push.default
  6. Cuando el servidor dice que no: non-fast-forward
  7. --force-with-lease frente a --force
  8. Borrar una rama en el servidor
  9. Enviar etiquetas
  10. Ana publica sus ramas; Bruno y Carla las reciben

  1. Qué envía exactamente git push

La definición precisa, que es más estrecha de lo que la gente imagina:

git push hace dos cosas: sube al remoto los objetos que le faltan y le pide que actualice una referencia para que apunte a un commit concreto.

Objetos y un puntero. Nada más.

Lo que NO envía:

  • Tu directorio de trabajo. Los ficheros modificados sin confirmar se quedan en tu disco. Si has editado app.js y no has hecho commit, ese cambio no sale de tu máquina por mucho push que hagas.
  • Tu índice. El área de preparación es tuya y personal.
  • Tu configuración. .git/config no viaja: los remotos, alias y ajustes de cada persona son suyos.
  • Tus otras ramas, salvo que las pidas explícitamente.
  • Tu reflog. El registro de tus movimientos es local.
  • Los ficheros ignorados. Lo que no está en un commit, no existe para el push.

Esa primera es la más importante y la fuente de un desconcierto clásico: "he hecho push y mi compañero no ve el cambio". Casi siempre significa que el cambio no llegó a confirmarse. Solo viaja lo que está dentro de un commit.

Los pasos internos

sequenceDiagram
    participant L as Repositorio local
    participant R as Servidor

    L->>R: ¿Qué referencias tienes y dónde apuntan?
    R-->>L: refs/heads/main → c2a8f1e
    Note over L: Calcula qué objetos tiene<br/>el servidor y cuáles no
    L->>R: Envía el packfile con los objetos que faltan
    Note over R: Guarda los objetos<br/>y comprueba que la actualización<br/>es un fast-forward
    L->>R: Actualiza refs/heads/main → b4d7e93
    R-->>L: Aceptado

Ese penúltimo paso —la comprobación de fast-forward— es el corazón del apartado 6 y la razón de casi todos los rechazos.

Y una consecuencia interesante: el envío es incremental. Git calcula qué objetos le faltan al servidor y manda solo esos, comprimidos en un packfile. Enviar el commit número mil de un proyecto no cuesta más que enviar el segundo.

  1. El primer envío: git push -u origin main

Volvamos al momento en que Ana publicó. Su repositorio tenía el remoto registrado (lección 04-02) y la autenticación resuelta (lección 04-03), pero nunca había enviado nada:

cd ~/proyectos/gestor-tareas
git push -u origin main
Enumerating objects: 34, done.
Counting objects: 100% (34/34), done.
Delta compression using up to 8 threads
Compressing objects: 100% (22/22), done.
Writing objects: 100% (34/34), 4.87 KiB | 4.87 MiB/s, done.
Total 34 (delta 9), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (9/9), done.
To https://git.ejemplo.es/equipo/gestor-tareas.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

Vale la pena leer esa salida entera, porque cuenta todo el proceso:

Línea Qué significa
Enumerating objects: 34 Git ha contado los objetos que hay que enviar
Delta compression using up to 8 threads Está comprimiendo, guardando diferencias entre objetos parecidos
Writing objects: 100% (34/34), 4.87 KiB Subida real: 34 objetos en menos de 5 KB
remote: Resolving deltas Lo que dice el servidor: está reconstruyendo los objetos. Todo lo que empieza por remote: viene de allí
* [new branch] main -> main Se ha creado la rama main en el servidor
branch 'main' set up to track 'origin/main' Efecto del -u: se ha configurado el seguimiento

Fíjate en la eficiencia: toda la historia del proyecto —cuatro commits base, dos fusiones, un squash, un conflicto resuelto— cabe en 4,87 KB. Esa es la compresión delta del modelo de datos que estudiamos en la lección 01-04.

Y una comprobación desde el servidor:

git ls-remote origin
c2a8f1e4b6d9f3a7e2c5b8d1f4a6c9e3b7d2f5a8	HEAD
c2a8f1e4b6d9f3a7e2c5b8d1f4a6c9e3b7d2f5a8	refs/heads/main

El repositorio que estaba vacío ya tiene una rama y una historia.

  1. Qué hace exactamente el -u

-u es la abreviatura de --set-upstream, y aparece en absolutamente todos los tutoriales de Git sin que casi ninguno explique qué hace.

Lo que hace es escribir dos líneas en .git/config:

cat .git/config
[remote "origin"]
	url = https://git.ejemplo.es/equipo/gestor-tareas.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
	remote = origin
	merge = refs/heads/main

Esa sección [branch "main"] es la rama de seguimiento: la asociación entre tu rama local main y la rama main de origin. Y a partir de ese momento cambian varias cosas:

Sin seguimiento Con seguimiento (-u)
git push origin main cada vez git push a secas
git pull origin main cada vez git pull a secas
git status no dice nada del servidor git status dice "2 commits ahead of 'origin/main'"
git branch -vv no muestra emparejamiento Muestra [origin/main] y el desfase

La comprobación inmediata:

git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

Ese "up to date with 'origin/main'" solo aparece si hay seguimiento configurado. Sin él, git status no tendría con qué comparar.

El -u solo hace falta la primera vez que envías una rama. Después, la configuración ya está escrita y git push a secas funciona.

Y recuerda que en la lección 01-06 configuramos esto:

git config --global push.autoSetupRemote true

Con ese ajuste (disponible desde Git 2.37), el -u es innecesario: al enviar una rama nueva, Git configura el seguimiento automáticamente. Ana podría haber escrito simplemente git push y el resultado habría sido idéntico. Lo hemos usado explícitamente para que veas lo que ocurre por debajo.

Todo lo relativo a las ramas de seguimiento —cómo se consultan, cómo se cambian, qué significa exactamente ese "2 commits ahead"— es el tema de la lección 04-06, la que cierra el módulo.

  1. El refspec de envío

En la lección 04-02 desmenuzamos el refspec de descarga. El de envío usa exactamente la misma sintaxis, y verlo cierra el círculo.

La forma completa de un push es:

git push <remoto> <refspec-local>:<refspec-remota>
git push origin main:main

Leído: "envía mi rama main y actualiza con ella la rama main del servidor."

main            :            main
└─┬─┘                        └─┬─┘
ORIGEN                      DESTINO
(aquí, en mi                (allí, en el
 repositorio)                servidor)

Comparado con el refspec de descarga, la lógica es idéntica y solo se invierte la dirección:

Refspec Origen (izquierda) Destino (derecha)
fetch +refs/heads/*:refs/remotes/origin/* El servidor Tu disco
push main:main Tu disco El servidor

En los dos casos, izquierda = de dónde sale, derecha = a dónde va. Una vez lo ves así, deja de ser sintaxis arbitraria.

Casos que solo se pueden expresar con la forma completa

# Enviar mi rama local a una rama del servidor con OTRO nombre
git push origin funcionalidad/orden-alfabetico:experimental/orden

# Enviar un commit concreto (no la punta de la rama) a una rama remota
git push origin 9d1e4b7:refs/heads/revision-parcial

# Enviar HEAD a una rama con nombre distinto
git push origin HEAD:main

# Actualizar una rama del servidor con el contenido de OTRA rama mía
git push origin main:produccion

El tercero, HEAD:main, es útil cuando estás en HEAD desacoplado o en una rama con nombre local distinto y quieres enviar lo que tienes a main.

Nombres cortos y nombres completos

Estas dos líneas son equivalentes:

git push origin main:main
git push origin refs/heads/main:refs/heads/main

Git expande los nombres cortos. Pero hay un caso en que la forma completa es obligatoria: cuando la rama de destino no existe todavía y el nombre es ambiguo. Si en el servidor hubiera una etiqueta llamada revision y quisieras crear una rama con ese nombre, Git no sabría qué quieres:

git push origin main:revision
error: dst refspec revision matches more than one

La solución es ser explícito:

git push origin main:refs/heads/revision

Los dos puntos vacíos: borrar

Si dejas el lado izquierdo vacío, estás enviando nada a esa referencia, lo cual la borra:

git push origin :funcionalidad/vieja

Es la sintaxis antigua de borrado, y la vemos en el apartado 8.

  1. Las formas abreviadas y push.default

En el día a día casi nadie escribe el refspec completo. Estas son las formas cortas y lo que significa cada una:

# 1. Todo explícito
git push origin main:main

# 2. Un solo nombre: se envía a la rama del mismo nombre
git push origin main

# 3. Solo el remoto: depende de push.default
git push origin

# 4. Nada: usa el remoto de seguimiento y push.default
git push

La forma 4 es la que usarás el 95 % de las veces, y su comportamiento depende de un ajuste que ya configuramos en la lección 01-06:

git config --global push.default simple

Los valores posibles y qué hacen:

Valor Comportamiento
simple (Por defecto desde Git 2.0) Envía solo la rama actual, y solo si la remota se llama igual. Si no hay seguimiento, se niega y te dice qué escribir
current Envía la rama actual a una rama del mismo nombre, creándola si hace falta. No exige seguimiento
upstream Envía la rama actual a su rama de seguimiento, aunque se llame distinto
matching (El viejo comportamiento, peligroso) Envía todas las ramas locales que tengan una homónima en el servidor
nothing Se niega a hacer nada sin un refspec explícito

matching merece una advertencia histórica. Era el valor por defecto hasta Git 2.0, y provocaba una sorpresa desagradable: escribías git push pensando en enviar la rama en la que estabas, y Git enviaba de golpe las ocho ramas locales que tenían homónima en el servidor, incluidos experimentos a medias que no querías publicar. Cambiarlo a simple fue una de las mejores decisiones de Git. No lo pongas en matching.

Y una opción que sí es útil cuando de verdad quieres enviar varias:

# Enviar TODAS las ramas locales, explícitamente
git push --all origin

Explícito y consciente, que es como debe ser.

  1. Cuando el servidor dice que no: non-fast-forward

Este es el rechazo más frecuente de Git y el que hay que entender de verdad.

Bruno se pone a trabajar por la mañana, hace dos commits y envía:

git push
To https://git.ejemplo.es/equipo/gestor-tareas.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'https://git.ejemplo.es/equipo/gestor-tareas.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.

Por qué ocurre

El servidor tiene commits que tú no tienes. Mientras Bruno trabajaba, Ana envió los suyos. El grafo está así:

flowchart LR
    C1["c2a8f1e"] --> C2["9e2f4a7"]
    C1 --> C3["7d1f4b8"]
    C2 --> C4["b4d7e93<br/><b>main del servidor</b>"]
    C3 --> C5["3e9c2a5<br/><b>main de Bruno</b>"]

    style C4 fill:#e8f0ff
    style C5 fill:#fff0e8

Los dos partieron de c2a8f1e y cada uno avanzó por su lado. Han divergido, exactamente el concepto de la lección 03-01.

Si Git aceptara el envío de Bruno, la referencia main del servidor pasaría de b4d7e93 a 3e9c2a5. Y como b4d7e93 no es un antepasado de 3e9c2a5, los dos commits de Ana dejarían de ser alcanzables desde ninguna rama: desaparecerían del proyecto sin que nadie se enterase, y cualquiera que clonara después no vería ni rastro de ellos.

Por eso Git rechaza el envío: para no destruir trabajo ajeno. La regla es que una referencia del servidor solo puede avanzar de forma fast-forward, es decir, hacia un descendiente de donde estaba. Es la misma noción de fast-forward del módulo 3, aplicada ahora como medida de protección.

Y aquí conviene relacionar dos cosas. Recuerda de la lección 04-02 que el refspec de descarga lleva un + delante:

fetch = +refs/heads/*:refs/remotes/origin/*

Ese + permite actualizaciones no fast-forward en tus referencias remotas, porque su trabajo es reflejar el servidor sea cual sea su estado. En el envío no hay ningún + por defecto, y es deliberado: hacia dentro, permisivo; hacia fuera, protector.

Los mensajes que verás

Mensaje Situación
! [rejected] main -> main (fetch first) El servidor tiene commits que tú no tienes
! [rejected] main -> main (non-fast-forward) Lo mismo, con la referencia ya descargada
! [rejected] main -> main (stale info) Tu información del servidor está desactualizada (típico con --force-with-lease)

La salida básica

El procedimiento es siempre el mismo, y es exactamente lo que aprendiste en la lección anterior:

# 1. Traer lo que hay en el servidor
git fetch origin

# 2. Ver qué es
git log --oneline main..origin/main
git log --oneline origin/main..main

# 3. Integrar
git merge origin/main

# 4. Volver a enviar
git push

Con pull.ff only configurado, el paso 3 en forma de git pull se detendrá diciendo Not possible to fast-forward, porque hay divergencia real y Git no decide por ti. En ese punto tienes que integrar explícitamente.

Lo que nunca debes hacer es lo que sugiere el instinto y algún que otro consejo malintencionado de internet:

# NO. Esto destruye los commits de Ana.
git push --force

Eso sobrescribe la referencia del servidor con tu versión, y los commits de Ana quedan huérfanos. Ella los recuperará de su propio disco, sí, pero cualquiera que clone en ese intervalo no los verá, y el desconcierto está garantizado.

Este apartado cubre la causa y la salida básica. El tratamiento completo de la divergencia —cómo decidir entre fusionar, reaplicar o replantear; qué hacer si además hay conflictos; cómo salir de los casos enredados— es el tema de la lección 09-03: Resolviendo Divergencias con el Remoto. Aquí quédate con tres ideas:

  1. El rechazo es una protección, no un fallo.
  2. La causa es siempre la misma: el remoto tiene commits que tú no tienes.
  3. La salida por defecto es integrar primero y enviar después.

  1. --force-with-lease frente a --force

Hay situaciones legítimas en las que necesitas sobrescribir una rama del servidor: has reescrito tu propia rama de trabajo (con las técnicas del módulo 5) y el resultado ya no es descendiente de lo que hay publicado. En esos casos, el envío normal será rechazado con toda la razón, y hay que forzar.

Pero hay dos formas de forzar, y la diferencia entre ellas puede ser el trabajo de una tarde de un compañero.

--force: "escribe esto, pase lo que pase"

git push --force origin funcionalidad/orden-alfabetico

Git sobrescribe la referencia del servidor sin comprobar nada. Si alguien había enviado algo mientras tanto, sus commits quedan huérfanos.

--force-with-lease: "escribe esto, si el servidor sigue donde yo creo"

git push --force-with-lease origin funcionalidad/orden-alfabetico

Git compara el valor actual de la referencia en el servidor con tu origin/funcionalidad/orden-alfabetico, es decir, con la última foto que tienes. Si coinciden, sobrescribe. Si no coinciden —porque alguien ha enviado algo desde tu último fetch—, rechaza el envío:

 ! [rejected]        funcionalidad/orden-alfabetico -> funcionalidad/orden-alfabetico (stale info)
error: failed to push some refs

La palabra lease es "arrendamiento": tú tenías reservada la referencia en un estado concreto, y si ha cambiado, el contrato se rompe y no se escribe nada.

flowchart TB
    P["git push --force-with-lease"] --> Q{"¿El servidor está donde<br/>dice mi origin/rama?"}
    Q -->|Sí| A["Sobrescribe:<br/>nadie ha tocado nada<br/>desde mi último fetch"]
    Q -->|No| R["RECHAZA:<br/>alguien ha enviado algo<br/>que yo no he visto"]

    style A fill:#e8f4e8
    style R fill:#f9e8e8

La comparativa

--force (-f) --force-with-lease
Comprueba el estado del servidor No
Si un compañero ha enviado mientras tanto Lo destruye en silencio Rechaza el envío
Protege de reescribir trabajo ajeno No
Necesita un fetch reciente para ser útil (ver el aviso de abajo)
Cuándo usarlo Prácticamente nunca Cuando de verdad hay que forzar

El aviso importante sobre --force-with-lease

La protección se basa en tu referencia remota local. Y eso abre un agujero:

# PELIGRO: esto anula la protección
git fetch
git push --force-with-lease

Si haces fetch justo antes, tu origin/rama se actualiza con lo que el compañero acaba de enviar, la comparación coincide y --force-with-lease acepta encantado… sobrescribiendo precisamente lo que la opción debía protegerte de sobrescribir.

Hay dos formas de hacerlo bien:

Primera: mirar antes de forzar. Un fetch seguido de una inspección deliberada:

git fetch origin
git log --oneline origin/funcionalidad/orden-alfabetico
# ¿Es todo mío? ¿Reconozco todos los commits?
git push --force-with-lease

Segunda, y mejor: decir explícitamente qué valor esperas.

git push --force-with-lease=funcionalidad/orden-alfabetico:9d1e4b7 origin funcionalidad/orden-alfabetico

Aquí no dependes de ninguna foto: le dices a Git "solo escribe si el servidor está exactamente en 9d1e4b7". Es la forma más segura, y la que conviene usar en scripts.

Desde Git 2.30 existe además una opción complementaria:

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

Comprueba, además, que los commits que hay en el servidor están incluidos en lo que vas a enviar, es decir, que realmente los has integrado y no simplemente descargado.

Las reglas de oro del forzado

  1. Nunca fuerces main, develop ni ninguna rama compartida. Si el equipo trabaja sobre ella, reescribirla obliga a todo el mundo a resolver un lío. Y es exactamente lo que impiden las ramas protegidas de las plataformas.
  2. Fuerza solo ramas tuyas, de trabajo, que nadie más esté usando.
  3. Usa siempre --force-with-lease. Convierte --force en una palabra que no escribes.
  4. Avisa al equipo si fuerzas una rama que alguien pueda tener clonada.
  5. Si dudas, no fuerces. Un historial con un commit de fusión de más no ha matado nunca a nadie; un --force sobre main un viernes, casi.

Un alias que ayuda a que la buena costumbre sea la más cómoda:

git config --global alias.pushf "push --force-with-lease"

Y una nota tranquilizadora, porque conviene no vivir con miedo: si alguien fuerza y destruye commits, casi siempre se pueden recuperar. Quien los tenía en su disco los conserva; y en el servidor, el reflog o las herramientas de la plataforma suelen permitir rescatarlos. Es un lío, no una catástrofe. La recuperación se estudia en la lección 09-04.

  1. Borrar una rama en el servidor

Cuando una rama se integra, hay que borrarla en los dos sitios: en tu repositorio (con git branch -d, lección 03-06) y en el servidor.

La forma moderna

git push origin --delete funcionalidad/contador-tareas
To https://git.ejemplo.es/equipo/gestor-tareas.git
 - [deleted]         funcionalidad/contador-tareas

Legible y difícil de confundir. Es la que debes usar.

La forma antigua

git push origin :funcionalidad/contador-tareas

Hace exactamente lo mismo, y ahora entiendes por qué: es el refspec <origen>:<destino> con el origen vacío. Estás enviando "nada" a esa referencia del servidor, lo cual la elimina.

La verás en documentación y en scripts antiguos, así que conviene reconocerla. Pero también conviene saber el riesgo: un espacio de más y estás borrando algo que no querías. Compara:

git push origin :main          # BORRA main del servidor
git push origin main           # Envía main al servidor

Un carácter de diferencia entre publicar y destruir. Por eso --delete es mejor.

Qué hay que hacer después

Borrar una rama en el servidor no borra las referencias que tienen tus compañeros. Cada uno tendrá que limpiarlas, como vimos en la lección anterior:

# En el repositorio de cada persona
git fetch --prune

Y si además tienen la rama local, esa se borra aparte:

git branch -d funcionalidad/contador-tareas

Tres cosas distintas otra vez: la rama del servidor, la referencia remota de cada uno y la rama local de cada uno. La lección siguiente insiste en esa distinción.

Un aviso: borrar una rama en el servidor puede dejar sus commits sin ninguna referencia que los alcance. Si esa rama no estaba fusionada, esos commits quedan a merced del recolector de basura del servidor. Comprueba antes con git log --oneline main..origin/la-rama que no hay nada que perder.

  1. Enviar etiquetas

Un detalle que sorprende a casi todo el mundo la primera vez: git push no envía etiquetas.

git tag v1.0.0
git push
Everything up-to-date

La etiqueta se ha creado en tu repositorio y ahí se queda. El refspec por defecto solo trata ramas (refs/heads/*), no etiquetas (refs/tags/*).

Las formas de enviarlas:

# Una etiqueta concreta
git push origin v1.0.0

# TODAS las etiquetas locales
git push origin --tags

# Solo las etiquetas ANOTADAS que apuntan a commits que se están enviando
git push origin --follow-tags

La diferencia entre las dos últimas importa:

Opción Qué envía
--tags Todas tus etiquetas locales, incluidas las de pruebas y las que apuntan a commits sin enviar
--follow-tags Solo las etiquetas anotadas que apuntan a commits alcanzables desde lo que estás enviando

--follow-tags es casi siempre la opción correcta en el uso diario, y se puede dejar configurada:

git config --global push.followTags true

Y para borrar una etiqueta del servidor, la misma sintaxis de siempre:

git push origin --delete v1.0.0
git push origin :refs/tags/v1.0.0    # forma antigua equivalente

Qué son las etiquetas, la diferencia entre ligeras y anotadas, cómo se usan para marcar versiones y cómo encajan en el flujo de publicación es el tema de la lección 05-05: Etiquetando Confirmaciones. Aquí solo nos interesa el transporte: que no viajan solas y con qué opción se envían.

  1. Ana publica sus ramas; Bruno y Carla las reciben

Cerramos con el escenario completo del equipo.

Ana tiene main ya publicada y dos ramas locales pendientes de compartir: documentacion/actualizar-notas y correccion/foco-tras-borrar.

git branch -vv
  correccion/foco-tras-borrar     8a3f7c1 Corregir el foco tras borrar una tarea
  documentacion/actualizar-notas  7f3c9a2 Actualizar las notas internas con el nuevo flujo
* main                            b4d7e93 [origin/main] Ajustar el estilo del pie de página

Solo main tiene rama de seguimiento (los corchetes). Las otras dos no existen en el servidor todavía.

git push origin documentacion/actualizar-notas
Enumerating objects: 5, done.
Writing objects: 100% (3/3), 412 bytes | 412.00 KiB/s, done.
To https://git.ejemplo.es/equipo/gestor-tareas.git
 * [new branch]      documentacion/actualizar-notas -> documentacion/actualizar-notas
branch 'documentacion/actualizar-notas' set up to track 'origin/documentacion/actualizar-notas'.

Fíjate en la última línea: el seguimiento se ha configurado solo, sin -u. Es push.autoSetupRemote true de la lección 01-06 funcionando.

git switch correccion/foco-tras-borrar
git push
 * [new branch]      correccion/foco-tras-borrar -> correccion/foco-tras-borrar
branch 'correccion/foco-tras-borrar' set up to track 'origin/correccion/foco-tras-borrar'.

Ni siquiera ha hecho falta nombrar el remoto ni la rama. Estado final:

git ls-remote origin
b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7	HEAD
8a3f7c1e4b9d2f6a5c8e3b7d1f4a9c2e6b5d8f3a	refs/heads/correccion/foco-tras-borrar
7f3c9a28e4b1d6f9a2c5e8b3d7f1a4c6e9b2d5f8	refs/heads/documentacion/actualizar-notas
b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7	refs/heads/main

Bruno recibe

cd ~/proyectos/gestor-tareas
git fetch --prune
From https://git.ejemplo.es/equipo/gestor-tareas
 * [new branch]      correccion/foco-tras-borrar    -> origin/correccion/foco-tras-borrar
 * [new branch]      documentacion/actualizar-notas -> origin/documentacion/actualizar-notas
git branch -a
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/correccion/foco-tras-borrar
  remotes/origin/documentacion/actualizar-notas
  remotes/origin/main

Las ramas de Ana están en su disco como referencias remotas, no como ramas locales. Puede examinarlas sin crear nada:

git log --oneline origin/correccion/foco-tras-borrar -3
git diff main origin/correccion/foco-tras-borrar

Carla recibe y trabaja

Carla hace lo mismo y decide continuar la corrección de Ana:

git fetch --prune
git switch correccion/foco-tras-borrar
branch 'correccion/foco-tras-borrar' set up to track 'origin/correccion/foco-tras-borrar'.
Switched to a new branch 'correccion/foco-tras-borrar'

Trabaja, confirma y envía:

echo "// Devolver el foco al campo tras borrar" >> app.js
git commit -am "Devolver el foco al campo tras borrar una tarea"
git push
To [email protected]:equipo/gestor-tareas.git
   8a3f7c1..d5e9b2f  correccion/foco-tras-borrar -> correccion/foco-tras-borrar

Este es el ciclo completo cerrado. Fíjate en la forma de esa última línea, distinta de la del primer envío:

  • * [new branch] X -> X → se ha creado una rama en el servidor.
  • 8a3f7c1..d5e9b2f X -> X → una rama existente ha avanzado de un commit a otro, en fast-forward.

El proyecto que llevaba tres módulos encerrado en un portátil circula ya entre tres máquinas y tres sistemas operativos distintos.

Errores Comunes y Consejos

Error 1: "he hecho push y no ven mi cambio". El 90 % de las veces, el cambio no llegó a confirmarse. git push envía commits, no ficheros modificados. Comprueba con git status y git log --oneline -3.

Error 2: responder a un rechazo con --force. El rechazo non-fast-forward significa que el servidor tiene trabajo que tú no tienes. Forzar lo destruye. La respuesta correcta es fetch + integrar + enviar. Ver 09-03.

Error 3: usar --force en lugar de --force-with-lease. El primero no comprueba nada; el segundo se niega si alguien ha enviado algo desde tu último fetch. Cuesta lo mismo escribir uno que otro; haz un alias.

Error 4: hacer fetch justo antes de --force-with-lease. Anula la protección, porque actualiza la referencia con la que Git compara. O miras deliberadamente qué ha llegado, o usas la forma --force-with-lease=rama:hash.

Error 5: forzar una rama compartida. Reescribir main obliga a todo el equipo a resolver un lío que no ha causado. Fuerza solo ramas tuyas de trabajo.

Error 6: creer que git push envía las etiquetas. No lo hace: el refspec por defecto solo cubre refs/heads/*. Usa --follow-tags o configura push.followTags true.

Error 7: usar git push origin :rama sin fijarse. Un espacio de más entre : y el nombre cambia el significado por completo. Usa --delete, que dice lo que hace.

Error 8: borrar una rama del servidor y esperar que desaparezca del repositorio de los demás. Cada persona tiene que hacer git fetch --prune. Configura fetch.prune true en el equipo.

Consejo 1: mira antes de enviar. git log --oneline origin/main..main te enseña exactamente qué commits van a salir. Diez segundos que evitan publicar un wip o un console.log olvidado.

Consejo 2: haz git fetch antes de empezar, no antes de enviar. Así detectas la divergencia cuando todavía es barata de resolver, en vez de descubrirla con el trabajo terminado.

Consejo 3: alias para el forzado seguro.

git config --global alias.pushf "push --force-with-lease"

Consejo 4: revisa qué configura push.default. El valor simple es el correcto. Si trabajas en un equipo con configuraciones antiguas, comprueba que nadie tiene matching.

Consejo 5: --dry-run para ensayar.

git push --dry-run origin main

Muestra qué haría el envío sin llegar a hacerlo. Útil con refspecs complicados o antes de un borrado.

Ejercicios

Ejercicio 1: anatomía de un envío

Monta un servidor bare y un repositorio de trabajo. Después:

  1. Confirma tres commits y envía con -u.
  2. Muestra el .git/config antes y después, y señala qué líneas ha añadido el -u.
  3. Explica la salida completa del push, línea por línea.
  4. Confirma otro commit y envía sin -u. ¿Qué cambia en la salida y por qué?
  5. Comprueba desde el servidor que las referencias están donde deben, sin clonar.

Ejercicio 2: provocar y resolver un rechazo

Con un servidor y dos clones (Ana y Bruno):

  1. Los dos parten del mismo commit.
  2. Ana confirma y envía.
  3. Bruno confirma (sin hacer fetch) e intenta enviar.
  4. Captura el mensaje de rechazo y explica exactamente por qué se ha producido, dibujando el grafo.
  5. Resuélvelo correctamente sin forzar y demuestra que los commits de los dos siguen en el servidor.
  6. Repite el experimento resolviéndolo con --force y demuestra que el commit de Ana ha desaparecido de la rama.

Ejercicio 3: refspecs de envío

Con un repositorio conectado a un bare, consigue lo siguiente y explica cada comando:

  1. Enviar tu rama local funcionalidad/prueba a una rama del servidor llamada experimental/prueba.
  2. Enviar el estado de un commit anterior a la punta de tu rama a una rama del servidor llamada revision.
  3. Enviar HEAD a main estando en HEAD desacoplado.
  4. Borrar experimental/prueba del servidor de las dos formas posibles.
  5. Comprobar el resultado con git ls-remote tras cada paso.

Soluciones

Solución 1:

mkdir -p /tmp/ej-push && cd /tmp/ej-push
git init --bare servidor.git
git init -b main trabajo
cd trabajo
# 1. Tres commits
echo "<h1>Gestor de tareas</h1>" > index.html
git add . && git commit -m "Estructura inicial del gestor de tareas"
echo "body { font-family: sans-serif; }" > estilos.css
git add . && git commit -m "Añadir estilos base del listado"
echo "console.log('tareas');" > app.js
git add . && git commit -m "Añadir borrado de tareas al listado"

# 2a. Configuración ANTES
git remote add origin /tmp/ej-push/servidor.git
cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "origin"]
	url = /tmp/ej-push/servidor.git
	fetch = +refs/heads/*:refs/remotes/origin/*
git push -u origin main
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 8 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (9/9), 782 bytes | 782.00 KiB/s, done.
Total 9 (delta 1), reused 0 (delta 0), pack-reused 0
To /tmp/ej-push/servidor.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.
# 2b. Configuración DESPUÉS
cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "origin"]
	url = /tmp/ej-push/servidor.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
	remote = origin
	merge = refs/heads/main

Las líneas nuevas son la sección [branch "main"] con remote y merge. Eso es todo lo que hace -u: establecer la rama de seguimiento.

3. La salida, línea por línea:

  • Enumerating objects: 9 — Git ha identificado 9 objetos que enviar: 3 commits + 3 árboles + 3 blobs.
  • Delta compression using up to 8 threads — comprime en paralelo, guardando diferencias entre objetos parecidos.
  • Writing objects: 100% (9/9), 782 bytes — la subida real: 782 bytes.
  • Total 9 (delta 1), reused 0 — de los 9 objetos, 1 se ha guardado como diferencia respecto a otro; ninguno se ha reutilizado porque el servidor estaba vacío.
  • To /tmp/ej-push/servidor.git — el destino.
  • * [new branch] main -> mainse ha creado la rama main en el servidor. El asterisco y [new branch] marcan la creación.
  • branch 'main' set up to track 'origin/main' — el efecto del -u.
# 4. Otro commit, envío sin -u
echo "# Gestor de tareas" > README.md
git add . && git commit -m "Documentar la instalación en el README"
git push
Enumerating objects: 4, done.
Writing objects: 100% (3/3), 298 bytes | 298.00 KiB/s, done.
To /tmp/ej-push/servidor.git
   7d2f9a1..4c8e3b6  main -> main

Dos diferencias, y las dos tienen explicación:

  • Ha bastado git push a secas: el seguimiento ya estaba configurado por el -u anterior, así que Git sabe a qué remoto y a qué rama enviar.
  • La línea de resultado ya no dice [new branch] sino 7d2f9a1..4c8e3b6: la rama ya existía y ha avanzado de un commit a otro. Ese rango con dos puntos indica un fast-forward, exactamente la notación que vimos en el fetch.

Además, solo se han enviado 3 objetos nuevos en lugar de los 9 iniciales: el envío es incremental.

# 5. Comprobar el servidor sin clonar
git ls-remote origin
4c8e3b6f9a2d5c8e1b7f4a3d6c9e2b5f8a1d4c7e	HEAD
4c8e3b6f9a2d5c8e1b7f4a3d6c9e2b5f8a1d4c7e	refs/heads/main
# Otra vía: consultar el bare directamente
git --git-dir=/tmp/ej-push/servidor.git log --oneline main
4c8e3b6 Documentar la instalación en el README
7d2f9a1 Añadir borrado de tareas al listado
9f4c2a8 Añadir estilos base del listado
5a1d8f3 Estructura inicial del gestor de tareas

Solución 2:

mkdir -p /tmp/ej-rechazo && cd /tmp/ej-rechazo
git init --bare servidor.git

git clone servidor.git ana
cd ana
echo "base" > f.txt
git add . && git commit -m "Estructura inicial del gestor de tareas"
git push -u origin main
cd ..

git clone servidor.git bruno
# 2. Ana confirma y envía
cd /tmp/ej-rechazo/ana
echo "contador" >> f.txt
git commit -am "Añadir el contador de tareas pendientes"
git push
git log --oneline -2
9e2f4a7 Añadir el contador de tareas pendientes
5a1d8f3 Estructura inicial del gestor de tareas
# 3. Bruno confirma sin fetch e intenta enviar
cd /tmp/ej-rechazo/bruno
echo "filtro" >> f.txt
git commit -am "Añadir el filtro de tareas pendientes"
git push
To /tmp/ej-rechazo/servidor.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '/tmp/ej-rechazo/servidor.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.

4. Por qué: los dos partieron de 5a1d8f3. Ana creó 9e2f4a7 encima y lo publicó; Bruno creó 7d1f4b8 encima del mismo 5a1d8f3. El grafo:

                  9e2f4a7  ← main del servidor (Ana)
                 /
    5a1d8f3 ────
                 \
                  7d1f4b8  ← main de Bruno

Para que el envío de Bruno fuera válido, 9e2f4a7 tendría que ser antepasado de 7d1f4b8. No lo es: son ramas hermanas. Si el servidor aceptara la actualización, refs/heads/main pasaría a 7d1f4b8 y 9e2f4a7 dejaría de ser alcanzable: el commit de Ana desaparecería del proyecto. Git lo impide.

Se puede comprobar formalmente:

git fetch origin
git merge-base --is-ancestor origin/main main && echo "Sería fast-forward" || echo "NO es fast-forward: por eso rechaza"
NO es fast-forward: por eso rechaza
# 5. Resolución correcta
git log --oneline main..origin/main
9e2f4a7 Añadir el contador de tareas pendientes
git merge origin/main

Si el fichero es el mismo, habrá conflicto (los dos añadieron una línea al final de f.txt). Se resuelve con lo aprendido en 03-05:

# Resolver dejando las dos aportaciones
printf "base\ncontador\nfiltro\n" > f.txt
git add f.txt
git commit -m "Fusionar el contador y el filtro de tareas"
git push
To /tmp/ej-rechazo/servidor.git
   9e2f4a7..c3b8f5d  main -> main
# Los commits de los DOS están en el servidor
git --git-dir=/tmp/ej-rechazo/servidor.git log --oneline --graph main
*   c3b8f5d Fusionar el contador y el filtro de tareas
|\
| * 9e2f4a7 Añadir el contador de tareas pendientes
* | 7d1f4b8 Añadir el filtro de tareas pendientes
|/
* 5a1d8f3 Estructura inicial del gestor de tareas

Nada se ha perdido. Los dos trabajos conviven, unidos por un commit de fusión con dos padres, exactamente como en el módulo 3.

# 6. La versión destructiva, para verlo con tus propios ojos
cd /tmp/ej-rechazo
rm -rf servidor.git ana bruno
git init --bare servidor.git
git clone servidor.git ana
cd ana && echo "base" > f.txt && git add . && git commit -m "Base" && git push -u origin main && cd ..
git clone servidor.git bruno

cd ana
echo "contador" >> f.txt && git commit -am "Añadir el contador de tareas pendientes" && git push
cd ../bruno
echo "filtro" >> f.txt && git commit -am "Añadir el filtro de tareas pendientes"
git push --force
To /tmp/ej-rechazo/servidor.git
 + 9e2f4a7...7d1f4b8 main -> main (forced update)

Ese + y el (forced update) son la marca del desastre:

git --git-dir=/tmp/ej-rechazo/servidor.git log --oneline main
7d1f4b8 Añadir el filtro de tareas pendientes
5a1d8f3 Base

El commit de Ana ya no está en la rama. Cualquiera que clone ahora no lo verá. Sigue existiendo como objeto suelto en el servidor —y desde luego en el repositorio de Ana—, pero ninguna referencia lo alcanza. Y compara con lo que habría pasado usando la opción segura:

git push --force-with-lease
 ! [rejected]        main -> main (stale info)

Rechazado. El servidor no estaba donde la foto de Bruno decía, así que la protección ha actuado. Esta es, en una línea, la razón para no escribir --force nunca más.

Solución 3:

mkdir -p /tmp/ej-refspec && cd /tmp/ej-refspec
git init --bare servidor.git
git init -b main trabajo
cd trabajo
echo "base" > f.txt && git add . && git commit -m "Base"
git remote add origin /tmp/ej-refspec/servidor.git
git push -u origin main

git switch -c funcionalidad/prueba
echo "uno" >> f.txt && git commit -am "Primer paso"
echo "dos" >> f.txt && git commit -am "Segundo paso"
echo "tres" >> f.txt && git commit -am "Tercer paso"
# 1. Rama local -> rama remota con OTRO nombre
git push origin funcionalidad/prueba:experimental/prueba
 * [new branch]      funcionalidad/prueba -> experimental/prueba

El refspec <origen>:<destino> desacopla los dos nombres: a la izquierda lo que envío, a la derecha cómo se llama allí.

git ls-remote origin
5f8b2e1...	HEAD
9c4e2b7...	refs/heads/experimental/prueba
5f8b2e1...	refs/heads/main
# 2. Un commit anterior a la punta, a una rama nueva
git log --oneline -3
9c4e2b7 Tercer paso
2c9d4e6 Segundo paso
8f1a3d5 Primer paso
git push origin 2c9d4e6:refs/heads/revision
 * [new branch]      2c9d4e6 -> revision

Aquí la forma completa refs/heads/revision es necesaria: como la rama de destino no existe todavía, Git no puede deducir si quieres crear una rama o una etiqueta. Con el nombre completo no hay ambigüedad. Y fíjate en que el lado izquierdo es un hash, no un nombre de rama: cualquier expresión que resuelva a un commit vale.

git ls-remote origin | grep revision
2c9d4e6f8a3b1d5c9e2f7a4b8d6c1e3f5a9b2d7c	refs/heads/revision
# 3. HEAD desacoplado -> main
git switch --detach 8f1a3d5
git push origin HEAD:refs/heads/desde-detached
 * [new branch]      HEAD -> desde-detached

HEAD resuelve al commit en el que estás, aunque no haya ninguna rama apuntándolo. Es la forma de publicar trabajo desde un HEAD desacoplado sin tener que crear una rama local antes.

git switch funcionalidad/prueba
# 4a. Borrado, forma moderna
git push origin --delete experimental/prueba
 - [deleted]         experimental/prueba
# 4b. Borrado, forma antigua
git push origin :revision
 - [deleted]         revision

Las dos hacen lo mismo. La segunda es el refspec con el origen vacío: enviar "nada" a esa referencia la elimina. Es exactamente la misma sintaxis, llevada al extremo.

# 5. Comprobación final
git ls-remote origin
5f8b2e1...	HEAD
8f1a3d5...	refs/heads/desde-detached
5f8b2e1...	refs/heads/main

Quedan main y desde-detached; experimental/prueba y revision han desaparecido. Y un detalle importante: las ramas locales de este repositorio no se han visto afectadas por ninguno de los borrados.

git branch
* funcionalidad/prueba
  main

Conclusión

Con esta lección, el trabajo del equipo circula en los dos sentidos. Lo esencial:

  • git push envía objetos y pide actualizar una referencia. No envía tu directorio de trabajo, ni tu índice, ni tu configuración, ni tus otras ramas. Solo viaja lo que está dentro de un commit.
  • git push -u origin <rama>: el -u escribe la sección [branch "…"] en .git/config y establece la rama de seguimiento. A partir de ahí, git push y git pull a secas funcionan y git status puede informarte del desfase. Con push.autoSetupRemote true (lección 01-06) ni siquiera hace falta.
  • El refspec de envío usa la misma sintaxis que el de descarga: origen:destino, izquierda de dónde sale y derecha a dónde va. Permite enviar a una rama con otro nombre, enviar un commit concreto o enviar HEAD.
  • push.default simple envía solo la rama actual. El antiguo matching enviaba todas las homónimas y era una fuente de sorpresas: no lo uses.
  • El rechazo non-fast-forward ocurre porque el remoto tiene commits que tú no tienes. Si Git aceptara el envío, esos commits dejarían de ser alcanzables y desaparecerían del proyecto. Es una protección, no un fallo. La salida básica es fetch → integrar → enviar; el tratamiento completo, en la lección 09-03.
  • --force-with-lease frente a --force: el primero comprueba que el servidor sigue donde tu referencia remota dice y rechaza si alguien ha enviado algo; el segundo sobrescribe sin mirar y puede dejar huérfano el trabajo de otra persona. Usa siempre el primero, y no hagas fetch justo antes (o usa la forma --force-with-lease=rama:hash). Nunca fuerces ramas compartidas.
  • Borrar una rama del servidor: git push origin --delete <rama>, o la forma antigua git push origin :<rama> (refspec con origen vacío). Los demás tendrán que hacer git fetch --prune para que desaparezca de sus referencias.
  • Las etiquetas no viajan solas: --tags envía todas, --follow-tags solo las anotadas alcanzables desde lo enviado, que suele ser lo que quieres. Las etiquetas, en la lección 05-05.

Lo que viene

Ya sabes enviar y recibir. Pero a lo largo de estas lecciones han ido apareciendo mensajes que aún no hemos explicado del todo: "Your branch is ahead of 'origin/main' by 2 commits", "branch 'main' set up to track 'origin/main'", esos corchetes de git branch -vv, y ese git switch que crea una rama local a partir de una remota sin que se lo pidas.

Todo eso apunta al mismo concepto: las ramas de seguimiento. En la lección 04-06: Rastreando Ramas, que cierra el módulo, veremos qué es exactamente el upstream de una rama y dónde vive en .git/config, aclararemos de una vez la diferencia entre main, origin/main y el main del servidor, y desmontaremos cómo calcula Git ese "2 commits por delante". Es la lección que ata todos los cabos del módulo.

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