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
- Qué envía exactamente
git push - El primer envío:
git push -u origin main - Qué hace exactamente el
-u - El refspec de envío
- Las formas abreviadas y
push.default - Cuando el servidor dice que no: non-fast-forward
--force-with-leasefrente a--force- Borrar una rama en el servidor
- Enviar etiquetas
- Ana publica sus ramas; Bruno y Carla las reciben
- Qué envía exactamente
git push
git pushLa definición precisa, que es más estrecha de lo que la gente imagina:
git pushhace 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.jsy no has hechocommit, ese cambio no sale de tu máquina por muchopushque hagas. - Tu índice. El área de preparación es tuya y personal.
- Tu configuración.
.git/configno 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.
- El primer envío:
git push -u origin main
git push -u origin mainVolvamos 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:
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:
c2a8f1e4b6d9f3a7e2c5b8d1f4a6c9e3b7d2f5a8 HEAD c2a8f1e4b6d9f3a7e2c5b8d1f4a6c9e3b7d2f5a8 refs/heads/main
El repositorio que estaba vacío ya tiene una rama y una historia.
- Qué hace exactamente el
-u
-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:
[remote "origin"]
url = https://git.ejemplo.es/equipo/gestor-tareas.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/mainEsa 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:
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:
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.
- 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:
Leído: "envía mi rama main y actualiza con ella la rama main del 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:produccionEl 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 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:
La solución es ser explícito:
Los dos puntos vacíos: borrar
Si dejas el lado izquierdo vacío, estás enviando nada a esa referencia, lo cual la borra:
Es la sintaxis antigua de borrado, y la vemos en el apartado 8.
- Las formas abreviadas y
push.default
push.defaultEn 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 pushLa 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:
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:
Explícito y consciente, que es como debe ser.
- 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:
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:
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 pushCon 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:
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:
- El rechazo es una protección, no un fallo.
- La causa es siempre la misma: el remoto tiene commits que tú no tienes.
- La salida por defecto es integrar primero y enviar después.
--force-with-lease frente a --force
--force-with-lease frente a --forceHay 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 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 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 | Sí |
| Si un compañero ha enviado mientras tanto | Lo destruye en silencio | Rechaza el envío |
| Protege de reescribir trabajo ajeno | No | Sí |
Necesita un fetch reciente para ser útil |
— | Sí (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:
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-leaseSegunda, y mejor: decir explícitamente qué valor esperas.
git push --force-with-lease=funcionalidad/orden-alfabetico:9d1e4b7 origin funcionalidad/orden-alfabeticoAquí 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:
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
- Nunca fuerces
main,developni 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. - Fuerza solo ramas tuyas, de trabajo, que nadie más esté usando.
- Usa siempre
--force-with-lease. Convierte--forceen una palabra que no escribes. - Avisa al equipo si fuerzas una rama que alguien pueda tener clonada.
- Si dudas, no fuerces. Un historial con un commit de fusión de más no ha matado nunca a nadie; un
--forcesobremainun viernes, casi.
Un alias que ayuda a que la buena costumbre sea la más cómoda:
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.
- 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
Legible y difícil de confundir. Es la que debes usar.
La forma antigua
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:
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:
Y si además tienen la rama local, esa se borra aparte:
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.
- Enviar etiquetas
Un detalle que sorprende a casi todo el mundo la primera vez: git push no envía etiquetas.
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-tagsLa 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:
Y para borrar una etiqueta del servidor, la misma sintaxis de siempre:
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.
- 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.
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.
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.
* [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:
b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7 HEAD 8a3f7c1e4b9d2f6a5c8e3b7d1f4a9c2e6b5d8f3a refs/heads/correccion/foco-tras-borrar 7f3c9a28e4b1d6f9a2c5e8b3d7f1a4c6e9b2d5f8 refs/heads/documentacion/actualizar-notas b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7 refs/heads/main
Bruno recibe
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
* 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-borrarCarla recibe y trabaja
Carla hace lo mismo y decide continuar la corrección de Ana:
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 pushTo [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.
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.
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:
- Confirma tres commits y envía con
-u. - Muestra el
.git/configantes y después, y señala qué líneas ha añadido el-u. - Explica la salida completa del
push, línea por línea. - Confirma otro commit y envía sin
-u. ¿Qué cambia en la salida y por qué? - 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):
- Los dos parten del mismo commit.
- Ana confirma y envía.
- Bruno confirma (sin hacer
fetch) e intenta enviar. - Captura el mensaje de rechazo y explica exactamente por qué se ha producido, dibujando el grafo.
- Resuélvelo correctamente sin forzar y demuestra que los commits de los dos siguen en el servidor.
- Repite el experimento resolviéndolo con
--forcey 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:
- Enviar tu rama local
funcionalidad/pruebaa una rama del servidor llamadaexperimental/prueba. - Enviar el estado de un commit anterior a la punta de tu rama a una rama del servidor llamada
revision. - Enviar
HEADamainestando en HEAD desacoplado. - Borrar
experimental/pruebadel servidor de las dos formas posibles. - Comprobar el resultado con
git ls-remotetras 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/*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'.
[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/mainLas 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 -> main— se ha creado la ramamainen 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 pushEnumerating 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 pusha secas: el seguimiento ya estaba configurado por el-uanterior, así que Git sabe a qué remoto y a qué rama enviar. - La línea de resultado ya no dice
[new branch]sino7d2f9a1..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 elfetch.
Además, solo se han enviado 3 objetos nuevos en lugar de los 9 iniciales: el envío es incremental.
4c8e3b6f9a2d5c8e1b7f4a3d6c9e2b5f8a1d4c7e HEAD 4c8e3b6f9a2d5c8e1b7f4a3d6c9e2b5f8a1d4c7e refs/heads/main
# Otra vía: consultar el bare directamente
git --git-dir=/tmp/ej-push/servidor.git log --oneline main4c8e3b6 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# 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 pushTo /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:
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"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# 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 --forceEse + y el (forced update) son la marca del desastre:
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:
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/pruebaEl refspec <origen>:<destino> desacopla los dos nombres: a la izquierda lo que envío, a la derecha cómo se llama allí.
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.
# 3. HEAD desacoplado -> main
git switch --detach 8f1a3d5
git push origin HEAD:refs/heads/desde-detachedHEAD 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.
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.
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.
Conclusión
Con esta lección, el trabajo del equipo circula en los dos sentidos. Lo esencial:
git pushenví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-uescribe la sección[branch "…"]en.git/configy establece la rama de seguimiento. A partir de ahí,git pushygit pulla secas funcionan ygit statuspuede informarte del desfase. Conpush.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 enviarHEAD. push.default simpleenvía solo la rama actual. El antiguomatchingenviaba 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-leasefrente 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 hagasfetchjusto 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 antiguagit push origin :<rama>(refspec con origen vacío). Los demás tendrán que hacergit fetch --prunepara que desaparezca de sus referencias. - Las etiquetas no viajan solas:
--tagsenvía todas,--follow-tagssolo 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
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
