A lo largo de este módulo han ido apareciendo mensajes que hemos ido dejando para después:
Your branch is ahead of 'origin/main' by 2 commits. branch 'main' set up to track 'origin/main'. main b4d7e93 [origin/main: behind 3] Ajustar el estilo del pie de página
Y también comportamientos que Git ha adivinado por su cuenta: git push a secas sabiendo a dónde ir, git pull sin argumentos, y ese git switch documentacion/actualizar-notas que creó una rama local que no existía.
Todo eso apunta al mismo concepto, y es el que cierra el módulo: las ramas de seguimiento. Un mecanismo sencillo —dos líneas de configuración por rama— del que dependen la mitad de las comodidades de Git y la mayoría de sus mensajes de estado.
De paso vamos a aclarar de una vez por todas la confusión que arrastramos desde la lección 04-01: main, origin/main y el main del servidor son tres cosas distintas que se llaman parecido y que la gente mezcla constantemente. Cuando termines esta lección, esa distinción será automática, y con ella el módulo entero encajará.
Contenido
- Qué es una rama de seguimiento
- Dónde vive:
branch.<rama>.remoteybranch.<rama>.merge - Las tres cosas que se llaman
main - De dónde sale "ahead of 'origin/main' by 2 commits"
git branch -vv: el estado de todas de un vistazo- Establecer, cambiar y quitar el upstream
- Crear una rama local a partir de una remota: el DWIM de Git
- El caso ambiguo de varios remotos
@{upstream}y@{push}: referirse al seguimiento- Cierre del módulo
- Qué es una rama de seguimiento
La definición:
Una rama de seguimiento (tracking branch) es una rama local que tiene registrada una asociación con una referencia remota concreta. A esa referencia asociada se la llama su upstream ("aguas arriba").
Dicho en llano: tu rama main "sabe" que su pareja en el servidor es origin/main. Y con ese dato, Git puede:
- Enviar sin argumentos:
git pushsabe a qué remoto y a qué rama. - Recibir sin argumentos:
git pulltambién. - Informarte del desfase: "vas 2 commits por delante y 3 por detrás".
- Ofrecer atajos:
@{upstream}para referirte a la pareja sin escribir su nombre.
Sin esa asociación, todo funciona igual, pero hay que escribirlo todo:
# Sin seguimiento
git push origin funcionalidad/orden-alfabetico
git pull origin funcionalidad/orden-alfabetico
git log funcionalidad/orden-alfabetico..origin/funcionalidad/orden-alfabetico
# Con seguimiento
git push
git pull
git log @{u}..Un aviso terminológico importante, porque la documentación de Git no siempre es consistente. Hay dos usos de "rama de seguimiento" que conviene no mezclar:
| Expresión | A qué se refiere |
|---|---|
| Rama de seguimiento (tracking branch) | Una rama local que tiene un upstream configurado. Por ejemplo, tu main |
| Rama de seguimiento remota (remote-tracking branch) | La referencia remota en sí: origin/main. No es una rama tuya |
Y recuerda además el otro sentido de la palabra upstream, el de la lección 04-01: el nombre convencional del remoto que apunta al proyecto original del que hiciste un fork (módulo 7). Tres usos de dos palabras. Aquí, siempre que digamos upstream, nos referimos a la referencia remota asociada a una rama local.
- Dónde vive:
branch.<rama>.remote y branch.<rama>.merge
branch.<rama>.remote y branch.<rama>.mergeComo todo en Git, esto no es magia: son dos líneas en un fichero de texto.
[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
[branch "correccion/foco-tras-borrar"]
remote = origin
merge = refs/heads/correccion/foco-tras-borrarCada rama con seguimiento tiene su propia sección:
| Clave | Valor | Qué significa |
|---|---|---|
branch.main.remote |
origin |
Con qué remoto habla esta rama |
branch.main.merge |
refs/heads/main |
Qué rama de ese remoto es su pareja |
Un detalle que despista al leerlo por primera vez: merge = refs/heads/main es el nombre de la rama en el servidor, no refs/remotes/origin/main. Tiene su lógica: la configuración dice "mi pareja es la rama main del repositorio remoto"; ya se encarga el refspec de saber que esa rama se guarda localmente en refs/remotes/origin/main.
Se consultan como cualquier otra configuración:
Y también existe una clave opcional que quizá encuentres:
Si vale true, git pull en esa rama usará --rebase en lugar de fusionar (lección 05-01).
Interioriza esto: el seguimiento es configuración local de tu repositorio. No viaja con el push, no la ve nadie más y cada persona del equipo tiene la suya. Bruno puede tener su main siguiendo a origin/main mientras Carla la tiene siguiendo a espejo/main, y ninguno de los dos se entera.
- Las tres cosas que se llaman
main
mainEste es el apartado clave de la lección, y merece toda tu atención. Estas tres cosas se llaman parecido y son distintas:
main |
origin/main |
main del servidor |
|
|---|---|---|---|
| Qué es | Tu rama local | Tu foto del servidor | La rama real del repositorio remoto |
| Dónde está | .git/refs/heads/main, en tu disco |
.git/refs/remotes/origin/main, en tu disco |
En otra máquina |
| Quién la mueve | Tú, al confirmar o fusionar | Git, al hacer fetch/pull/push |
Cualquiera del equipo, al enviar |
| Puedes confirmar en ella | Sí | No | No directamente |
| Puede quedarse anticuada | Es tuya: está donde la dejaste | Sí, constantemente | Es la verdad del momento |
| Se ve con | git branch |
git branch -r |
git ls-remote origin |
flowchart TB
subgraph TU["TU DISCO"]
M["<b>main</b><br/>refs/heads/main<br/>La mueves tú al confirmar"]
OM["<b>origin/main</b><br/>refs/remotes/origin/main<br/>Foto del servidor.<br/>La mueve fetch/pull/push"]
end
subgraph SRV["OTRA MÁQUINA"]
SM["<b>main</b><br/>refs/heads/main<br/>La mueve quien envíe"]
end
M -.->|"seguimiento configurado:<br/>branch.main.remote = origin<br/>branch.main.merge = refs/heads/main"| OM
OM <-->|"fetch: actualiza la foto<br/>push: actualiza el servidor"| SM
style M fill:#fff0e8
style OM fill:#e8f0ff
style SM fill:#e8f4e8
La regla mnemotécnica
Cuando algo de remotos te desconcierte, pregúntate: ¿de cuál de las tres estoy hablando?
Ejemplos de aplicación inmediata:
- "Mi compañero dice que ya está subido pero yo no lo veo." → Estáis hablando del
maindel servidor; lo que tú miras es tuorigin/main, que está anticuado. Solución:git fetch. - "He hecho
fetchy mis ficheros no han cambiado." → Elfetchactualizaorigin/main; tus ficheros dependen demain. Correcto. - "He hecho
git reset --hard origin/mainy he perdido mis commits." → Has movido tumaina donde está tu foto del servidor. Los commits siguen en el reflog (lección 09-04), pero la rama ya no los alcanza. - "
git pushdice everything up-to-date pero mi cambio no está." → Tumainestá donde tuorigin/main; el cambio nunca llegó a confirmarse.
Un experimento que fija la idea mejor que cualquier explicación:
# 1. Confirmar algo: se mueve SOLO main
git commit --allow-empty -m "Prueba"
git rev-parse main origin/maind5e9b2f8a3c1e6b4d7f2a9c5e8b1d4f6a3c7e9b2 ← main ha avanzado b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7 ← origin/main sigue igual
Un push con éxito actualiza también tu origin/main, y es lógico: Git acaba de hablar con el servidor y sabe con certeza dónde ha quedado su referencia.
- De dónde sale "ahead of 'origin/main' by 2 commits"
On branch main Your branch is ahead of 'origin/main' by 2 commits. (use "git push" to publish your local commits) nothing to commit, working tree clean
Ese mensaje no sale de ningún sitio mágico: Git compara tu rama con su upstream y cuenta commits a cada lado. Solo puede hacerlo porque el seguimiento está configurado; sin él, git status no diría nada del servidor.
El comando que hace el cálculo
Desglosemos, porque cada pieza importa:
main...origin/main— tres puntos, no dos. Es la diferencia simétrica: todos los commits alcanzables desde una de las dos referencias pero no desde ambas. Es decir, lo exclusivo de cada lado.--left-right— marca cada commit según de qué lado viene:<para el izquierdo (main),>para el derecho (origin/main).--count— en lugar de listar los commits, cuenta cuántos hay de cada lado.
El resultado se lee así:
2 0 │ └── commits que tiene origin/main y no main → "behind" (por detrás) └────── commits que tiene main y no origin/main → "ahead" (por delante)
Dos puntos frente a tres puntos, que es de lo que más se confunde:
| Sintaxis | Significado | Uso típico |
|---|---|---|
main..origin/main |
Lo que tiene origin/main y no main |
"¿Qué me falta por traer?" |
origin/main..main |
Lo que tiene main y no origin/main |
"¿Qué me falta por enviar?" |
main...origin/main |
Lo exclusivo de cada lado | "¿Cuánto hemos divergido?" |
Sin --count, se ve commit a commit:
<d5e9b2f Devolver el foco al campo tras borrar una tarea <8a3f7c1 Corregir el foco tras borrar una tarea >b4d7e93 Ajustar el estilo del pie de página
Dos míos (<) y uno suyo (>): hemos divergido, y git status lo dirá así:
Los cuatro estados posibles
rev-list devuelve |
Situación | Qué dice git status |
Qué hacer |
|---|---|---|---|
0 0 |
Sincronizados | "up to date with 'origin/main'" | Nada |
N 0 |
Por delante | "ahead of 'origin/main' by N commits" | git push |
0 N |
Por detrás | "behind 'origin/main' by N commits, and can be fast-forwarded" | git pull |
N M |
Divergidos | "have diverged" | Integrar (ver 09-03) |
flowchart TB
B["Base común"]
B --> A1["A"] --> A2["B<br/><b>main</b>"]
B --> C1["C"] --> C2["D<br/><b>origin/main</b>"]
N["main...origin/main → 2 y 2<br/>HAN DIVERGIDO"]
style A2 fill:#fff0e8
style C2 fill:#e8f0ff
Un aviso fundamental sobre estos números
El desfase se calcula contra tu
origin/main, que es una foto. No contra el servidor.
Si llevas dos días sin hacer fetch, ese "por delante 2 commits" puede ser completamente falso: el servidor puede haber avanzado quince veces. git status nunca consulta la red.
Por eso el primer comando del día es git fetch: sin él, todos estos números describen un pasado.
Y por eso existe esta opción, que hace las dos cosas de golpe:
Hay una opción de git status que promete hacerlo sola:
Pero --ahead-behind solo controla si se calcula o no el desfase; no trae nada del servidor. La única forma de actualizar la foto sigue siendo fetch.
git branch -vv: el estado de todas de un vistazo
git branch -vv: el estado de todas de un vistazoEn la lección 03-06 mencionamos -vv y dijimos que cobraría sentido con remotos. Ha llegado el momento.
correccion/foco-tras-borrar d5e9b2f Devolver el foco al campo tras borrar una tarea documentacion/actualizar-notas 7f3c9a2 Actualizar las notas internas con el nuevo flujo * main b4d7e93 Ajustar el estilo del pie de página experimento/local 3a7e9c2 Probar una idea sin publicar
Con la doble v:
correccion/foco-tras-borrar d5e9b2f [origin/correccion/foco-tras-borrar] Devolver el foco al campo tras borrar una tarea documentacion/actualizar-notas 7f3c9a2 [origin/documentacion/actualizar-notas: ahead 1] Actualizar las notas internas con el nuevo flujo * main b4d7e93 [origin/main: behind 3] Ajustar el estilo del pie de página experimento/local 3a7e9c2 Probar una idea sin publicar
Los corchetes son la información nueva, y cada línea cuenta algo distinto:
| Rama | Corchetes | Lectura |
|---|---|---|
correccion/foco-tras-borrar |
[origin/correccion/…] |
Tiene upstream y está sincronizada |
documentacion/actualizar-notas |
[origin/…: ahead 1] |
Un commit local sin enviar |
main |
[origin/main: behind 3] |
Tres commits en el servidor sin traer |
experimento/local |
(nada) | No tiene upstream: es puramente local |
Ese último caso es el que hay que saber reconocer. Una rama sin corchetes no existe en ningún servidor: si pierdes el disco, pierdes ese trabajo. Es perfectamente legítimo tener ramas así (experimentos, pruebas), pero conviene saber cuáles son.
Y también puede aparecer este:
gone significa que la rama tenía upstream pero ese upstream ya no existe: alguien borró la rama en el servidor y tú has hecho fetch --prune. Es la señal habitual de "esta rama ya se integró y se limpió; puedes borrar la tuya".
Un comando muy útil para la limpieza periódica:
funcionalidad/antigua 9d1e4b7 [origin/funcionalidad/antigua: gone] Marcar tareas como completadas funcionalidad/exportar-csv e9a2c5f [origin/funcionalidad/exportar-csv: gone] Ahora sí
Estas son las candidatas naturales a git branch -d, y complementa perfectamente el --merged de la lección 03-06 (que fallaba con las ramas integradas mediante squash: si la plataforma la borró tras integrarla, aquí aparecerá aunque --merged no la detecte).
Otras formas de consultar el seguimiento:
# Con formato a medida
git branch --format='%(refname:short) → %(upstream:short) [%(upstream:track)]'correccion/foco-tras-borrar → origin/correccion/foco-tras-borrar [] documentacion/actualizar-notas → origin/documentacion/actualizar-notas [ahead 1] main → origin/main [behind 3] experimento/local → []
Los marcadores %(upstream:short) y %(upstream:track) son los mismos de git for-each-ref que usamos en la lección 03-06 para el listado bonito de ramas.
- Establecer, cambiar y quitar el upstream
Hay cuatro caminos para configurar el seguimiento, y conviene conocerlos todos porque cada uno aparece en un momento distinto.
git push -u (el más habitual)
git push -u (el más habitual)Envía y configura el seguimiento en un solo paso. Es el camino natural cuando publicas una rama nueva, como vimos en la lección 04-05.
- Automáticamente, con
push.autoSetupRemote
push.autoSetupRemoteYa lo tenemos configurado desde la lección 01-06. Con él, un git push a secas sobre una rama sin upstream la crea en el servidor y configura el seguimiento, sin -u:
git switch -c funcionalidad/orden-alfabetico
git commit -am "Mostrar las tareas ordenadas alfabéticamente"
git push* [new branch] funcionalidad/orden-alfabetico -> funcionalidad/orden-alfabetico branch 'funcionalidad/orden-alfabetico' set up to track 'origin/funcionalidad/orden-alfabetico'.
Sin ese ajuste, ese git push habría fallado con el clásico:
fatal: The current branch funcionalidad/orden-alfabetico has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin funcionalidad/orden-alfabeticoUn mensaje que has visto mil veces si has usado Git sin esa configuración.
git branch --set-upstream-to (a posteriori)
git branch --set-upstream-to (a posteriori)Para una rama que ya existe en los dos sitios pero no está emparejada:
Hay una forma corta con -u (que aquí significa lo mismo que en push):
Sin nombre de rama, se aplica a la actual. Es el comando que necesitas cuando has creado el repositorio con init y has añadido el remoto después, o cuando la configuración se ha perdido por algún motivo.
Se puede apuntar a una rama con otro nombre, que es útil en casos de migración:
Ahora tu main local sigue a origin/desarrollo. Es raro, pero perfectamente válido: la asociación no exige que los nombres coincidan.
- Al crear la rama
# Explícito
git switch -c mi-copia --track origin/main
# O directamente desde la referencia remota (el DWIM del apartado 7)
git switch mainQuitar el seguimiento
Fíjate en lo que ha desaparecido: la línea del desfase. Sin upstream, Git no tiene con qué comparar y git status se queda mudo respecto al servidor. Es la mejor demostración de que ese mensaje depende enteramente de esta configuración.
Volvemos a ponerlo:
Resumen
| Comando | Cuándo |
|---|---|
git push -u origin <rama> |
Al publicar una rama por primera vez |
git push (con push.autoSetupRemote) |
Igual, pero automático |
git branch -u origin/<rama> |
La rama ya existe en ambos lados, sin emparejar |
git switch -c <local> --track origin/<rama> |
Al crear una local desde una remota, con nombre distinto |
git switch <rama> (DWIM) |
Al crear una local desde una remota, con el mismo nombre |
git branch --unset-upstream |
Para desvincularla |
- Crear una rama local a partir de una remota: el DWIM de Git
Carla acaba de clonar y quiere trabajar en la rama de documentación de Ana. Solo tiene una rama local:
origin/HEAD -> origin/main origin/correccion/foco-tras-borrar origin/documentacion/actualizar-notas origin/main
Las referencias remotas no son ramas suyas, como sabemos desde la lección 04-01: son de solo lectura y no puede confirmar en ellas. Necesita una rama local.
El camino explícito sería este:
Largo y redundante. Por eso Git tiene un atajo:
branch 'documentacion/actualizar-notas' set up to track 'origin/documentacion/actualizar-notas'. Switched to a new branch 'documentacion/actualizar-notas'
Esto es el DWIM de Git (Do What I Mean, "haz lo que quiero decir"). El razonamiento interno es:
- ¿Existe una rama local llamada
documentacion/actualizar-notas? No. - ¿Existe exactamente una referencia remota con ese nombre? Sí,
origin/documentacion/actualizar-notas. - Entonces la persona quiere trabajar en esa rama: creo la local a partir de ella y configuro el seguimiento.
Y el resultado es exactamente el mismo que la forma larga:
* documentacion/actualizar-notas 7f3c9a2 [origin/documentacion/actualizar-notas] Actualizar las notas internas con el nuevo flujo main b4d7e93 [origin/main] Ajustar el estilo del pie de página
Lo mismo funciona con checkout por compatibilidad, aunque como sabemos desde la lección 03-02 es preferible switch:
Los límites del DWIM
No funciona si el nombre local ya existe:
Si ya tienes main local, esto simplemente cambia a tu rama existente. No crea nada ni reconfigura nada.
No funciona si no has hecho fetch:
Si un compañero acaba de crear esa rama y tú no has traído las referencias, Git no la conoce. Solución: git fetch primero. Es un error muy común al incorporarse a un trabajo en curso.
No funciona con varios remotos ambiguos: es el apartado siguiente.
Desactivarlo
Hay una configuración para exigir siempre la forma explícita:
Con ella, git switch <rama> solo funciona con ramas locales existentes. La mayoría de la gente prefiere el DWIM activado, pero conviene saber que existe si el comportamiento automático te incomoda.
- El caso ambiguo de varios remotos
Cuando hay más de un remoto con una rama del mismo nombre, el DWIM no puede adivinar y se rinde.
Supongamos que Bruno tiene dos remotos, como en la lección 04-02:
origin https://git.ejemplo.es/equipo/gestor-tareas.git (fetch) origin https://git.ejemplo.es/equipo/gestor-tareas.git (push) personal [email protected]:bruno/gestor-tareas.git (fetch) personal [email protected]:bruno/gestor-tareas.git (push)
origin/main origin/funcionalidad/orden-alfabetico personal/main personal/funcionalidad/orden-alfabetico
Y ahora:
Git se niega, y hace bien: elegir por ti significaría configurar un seguimiento hacia un repositorio que quizá no es el que quieres, con el riesgo de acabar enviando trabajo al sitio equivocado.
Las tres formas de resolverlo
1. Ser explícito con la referencia remota (la más clara):
O con --track, que además deja constancia de la intención:
2. Usar la sintaxis de desambiguación:
Con --track y sin -c, Git crea una rama local con el mismo nombre corto que la remota.
3. Configurar un remoto preferente:
A partir de ahí, ante la ambigüedad Git elegirá siempre origin sin preguntar:
branch 'funcionalidad/orden-alfabetico' set up to track 'origin/funcionalidad/orden-alfabetico'. Switched to a new branch 'funcionalidad/orden-alfabetico'
Es la solución recomendada si trabajas habitualmente con varios remotos, por ejemplo en el flujo de forks del módulo 7, donde tener origin y upstream a la vez es lo normal.
Nombres locales distintos
Con varios remotos, a veces conviene desacoplar los nombres para no perderse:
git switch -c orden-equipo origin/funcionalidad/orden-alfabetico
git switch -c orden-personal personal/funcionalidad/orden-alfabetico
git branch -vvorden-equipo a1e5c93 [origin/funcionalidad/orden-alfabetico] Mostrar las tareas ordenadas alfabéticamente * orden-personal 4f8c2a1 [personal/funcionalidad/orden-alfabetico] Probar otra forma de ordenar
Dos ramas locales con nombres claros, cada una siguiendo a un remoto distinto. Recuerda que el seguimiento no exige que los nombres coincidan.
@{upstream} y @{push}: referirse al seguimiento
@{upstream} y @{push}: referirse al seguimientoGit ofrece una sintaxis para referirse al upstream de una rama sin escribir su nombre. Es un ahorro pequeño pero muy cómodo, y aparece constantemente en alias y scripts.
@{upstream} (abreviado @{u}) resuelve al upstream de la rama actual:
Y se puede aplicar a otra rama:
Los usos más prácticos, y todos independientes del nombre de la rama en la que estés:
# ¿Qué tengo sin enviar?
git log --oneline @{u}..
# ¿Qué me falta por traer?
git log --oneline ..@{u}
# ¿Cuánto hemos divergido?
git rev-list --left-right --count @{u}...
# ¿Qué cambia respecto al servidor?
git diff @{u}Fíjate en la elegancia de @{u}..: al omitir el lado derecho, Git usa HEAD. Y ..@{u} omite el izquierdo, con el mismo efecto. Son los rangos del módulo 2 aprovechando los valores por defecto.
@{push}, el hermano menos conocido
Existe una segunda referencia, @{push}, que apunta a dónde iría un git push. Normalmente coincide con @{upstream}, pero no siempre: si trabajas con remote.pushDefault configurado o con refspecs de envío distintos de los de descarga, pueden diferir.
Es la referencia correcta para responder a "¿qué commits saldrían si hiciera push ahora?":
En el flujo de forks del módulo 7 —donde lees de upstream y escribes en origin— esa distinción se vuelve muy útil.
Y un par de alias que valen su peso en oro:
git config --global alias.sinenviar "log --oneline @{u}.."
git config --global alias.pendiente "log --oneline ..@{u}"
git config --global alias.desfase "rev-list --left-right --count @{u}..."d5e9b2f Devolver el foco al campo tras borrar una tarea 8a3f7c1 Corregir el foco tras borrar una tarea
Los alias, a fondo, en la lección 06-04.
- Cierre del módulo
Recapitulemos lo que hemos construido en estas seis lecciones, porque el módulo 4 es un salto conceptual y conviene verlo entero.
Empezamos deshaciendo el mito: un remoto no es un servidor mágico, es un nombre corto para una URL guardado en .git/config. Vimos que en un sistema distribuido no hay servidor central técnicamente: todos los repositorios son iguales y el "oficial" lo es por acuerdo del equipo. Entendimos por fin qué es un repositorio bare —el contenido de .git/ sin directorio de trabajo— y por qué es la forma correcta de montar un repositorio que recibe envíos. Y descubrimos que las referencias remotas viven en .git/refs/remotes/, en tu disco, y que origin/main es una foto con fecha, no una ventana en directo.
Después, Ana publicó el proyecto. Registramos el remoto con git remote add —una operación puramente local que ni siquiera valida la URL—, aprendimos a inspeccionarlo, renombrarlo y cambiarle la dirección, y desmenuzamos el refspec +refs/heads/*:refs/remotes/origin/*, esa línea que casi nadie sabe leer y que explica de dónde sale la barra de origin/main.
Resolvimos la autenticación: por qué leer un repositorio público no requiere credenciales y escribir siempre sí, qué es un token personal de acceso y por qué sustituyó a la contraseña, cómo se genera y se usa un par de claves SSH, y cómo cada sistema operativo guarda las credenciales —con Git Credential Manager abriéndole el camino a Carla desde Windows 11.
Atacamos la confusión más extendida de Git: fetch frente a pull. El primero descarga y actualiza referencias remotas sin tocar un solo fichero, y es absolutamente seguro; el segundo añade una integración que sí modifica tu trabajo y puede dar conflictos. Aprendimos a mirar en el hueco entre ambos, a limpiar referencias fantasma con --prune, y Carla clonó por fin un repositorio con toda la historia del módulo 3 dentro.
Le dimos la vuelta al canal con git push: qué envía exactamente, qué hace el -u, cómo se escribe un refspec de envío, por qué el servidor rechaza los envíos non-fast-forward —para no destruir trabajo ajeno— y por qué --force-with-lease es casi siempre la respuesta correcta cuando de verdad hay que forzar.
Y hemos terminado atando los cabos: las ramas de seguimiento, dos líneas de configuración por rama de las que dependen git push sin argumentos, git pull sin argumentos, los mensajes de git status y el DWIM de git switch.
La idea que sostiene todo el módulo, y que conviene llevarse: los remotos no han cambiado nada de lo que ya sabías. Fusionar el trabajo de Bruno es el mismo git merge del módulo 3. Los conflictos se resuelven igual. Las ramas siguen siendo ficheros de 41 bytes. Lo único que se ha añadido es el transporte: cómo consiguen los objetos viajar de un .git/ a otro. Una vez han llegado, todo funciona como ya sabías.
El problema que queda abierto
El equipo ya colabora. Los tres envían, reciben e integran, y el proyecto avanza. Pero si Ana mira el historial de main con ojos críticos, empieza a ver cosas que no le gustan:
* 4f8c2a1 Merge branch 'main' of https://git.ejemplo.es/equipo/gestor-tareas |\ | * d5e9b2f Devolver el foco al campo tras borrar una tarea * | 8a3f7c1 Corregir el foco tras borrar una tarea |/ * e7b3f2a Merge branch 'main' of https://git.ejemplo.es/equipo/gestor-tareas |\ | * b4d7e93 Ajustar el estilo del pie de página * | 3e9c2a5 arreglo |/ * 9e2f4a7 Añadir el contador de tareas pendientes * c2a8f1e Fusionar el mensaje de lista vacía
Dos problemas distintos conviviendo:
- Commits de fusión que no cuentan nada. Esos
Merge branch 'main' of https://…no son decisiones de diseño: son el rastro de dos personas que hicieronpulla la vez. Ensucian el grafo y no aportan información. - Commits que no deberían existir tal cual.
arreglono describe nada. Y en las ramas de trabajo haywip,wip 2,ahora síy algúnconsole.logolvidado que se envió sin querer.
Falta además todo un repertorio de operaciones que el trabajo en equipo hace imprescindibles: llevarse un commit concreto de una rama a otra sin arrastrar el resto; guardar el trabajo a medias para atender una urgencia y recuperarlo después; marcar versiones con etiquetas que se publiquen en el servidor; y deshacer un commit ya enviado sin reescribir el historial que los demás ya tienen.
En el módulo 5: Operaciones Avanzadas de Git entramos en ese terreno. Veremos git rebase —que reescribe commits para producir un historial lineal, y del que hemos hablado tres veces sin desarrollarlo—, el rebase interactivo para reordenar, unir y reescribir commits antes de publicarlos, git cherry-pick para trasplantar commits sueltos, git stash para apartar trabajo temporalmente, las etiquetas para marcar versiones, y git revert para deshacer sin borrar.
Es un módulo de precisión, y llega en el momento adecuado: ahora que el historial es compartido, reescribirlo tiene consecuencias para otras personas. Todo lo que has aprendido aquí sobre push, sobre el rechazo non-fast-forward y sobre --force-with-lease va a ser exactamente lo que te permita distinguir qué se puede reescribir y qué no.
Errores Comunes y Consejos
Error 1: confundir main, origin/main y el main del servidor. Es el error que engloba a casi todos los demás. Ante cualquier duda, pregúntate de cuál de los tres estás hablando. Los dos primeros están en tu disco.
Error 2: fiarse del "ahead/behind" sin haber hecho fetch. git status nunca consulta la red: compara contra tu foto. Si llevas dos días sin fetch, esos números describen un pasado.
Error 3: confundir .. con .... Dos puntos es "lo que tiene B y no A"; tres puntos es "lo exclusivo de cada lado". Para el desfase se usa ... con --left-right --count.
Error 4: fatal: The current branch X has no upstream branch. La rama no tiene seguimiento. Se arregla con git push -u origin X, o de una vez para siempre con push.autoSetupRemote true.
Error 5: que el DWIM falle porque no has hecho fetch. Si un compañero acaba de crear la rama y tú no has traído las referencias, git switch <rama> dará invalid reference. Un git fetch lo resuelve.
Error 6: ignorar las ramas marcadas como gone. Significan que su rama en el servidor ha desaparecido, normalmente porque se integró. Son las candidatas más claras a borrar en local.
Error 7: no darse cuenta de que una rama no tiene upstream. Sin corchetes en git branch -vv, esa rama solo existe en tu disco. Legítimo para un experimento, peligroso para trabajo que importa.
Error 8: creer que el seguimiento se comparte con el equipo. Es configuración local de tu .git/config. Cada persona tiene la suya y nadie ve la de nadie.
Consejo 1: git fetch && git status como rutina de inicio. Dos comandos que te dicen la verdad en lugar de una foto vieja.
Consejo 2: usa git branch -vv para la revisión semanal. Ves de un golpe qué está sin enviar, qué está sin traer, qué es solo local y qué se puede borrar.
Consejo 3: activa push.autoSetupRemote true. Elimina el error de "no upstream branch" para siempre. Ya lo tienes desde la lección 01-06.
Consejo 4: aprende @{u}. git log @{u}.. para ver lo que no has enviado y git log ..@{u} para lo que no has traído funcionan en cualquier rama sin cambiar una letra. Guárdalos como alias.
Consejo 5: con varios remotos, configura checkout.defaultRemote origin. Evita el error de ambigüedad y te ahorra escribir la referencia completa cada vez.
Ejercicios
Ejercicio 1: las tres cosas
Monta un servidor bare y dos clones. Después, y con comandos, demuestra cada afirmación:
- Al confirmar en local, solo se mueve
main. - Al hacer
push, se muevenmain,origin/mainy la rama del servidor. - Cuando el otro clon envía algo, el
origin/maindel primero no cambia hasta que hacefetch. git statuscalcula el desfase sin consultar la red: provoca una situación en la que el mensaje sea objetivamente falso respecto al servidor y explica por qué.- Reproduce el cálculo de
git statusa mano congit rev-list.
Ejercicio 2: gestionar el seguimiento
En un repositorio conectado a un bare:
- Crea una rama local sin upstream y demuestra con dos comandos distintos que no lo tiene.
- Comprueba qué dice
git pushen esa rama si desactivaspush.autoSetupRemote. - Configúrale el upstream de las tres formas posibles (deshaciendo entre una y otra) y verifica el resultado en
.git/configcada vez. - Quítale el upstream y observa qué desaparece de
git status. - Crea una rama local que siga a una remota con otro nombre y demuestra que funciona.
Ejercicio 3: DWIM y ambigüedad
- Monta dos servidores bare y un repositorio de trabajo con ambos registrados.
- Crea en los dos una rama con el mismo nombre, con contenido distinto.
- Intenta usar el DWIM y captura el error.
- Resuélvelo de las tres formas explicadas en la lección.
- Demuestra con
git branch -vvque en cada caso la rama local sigue al remoto correcto. - Comprueba con
git logque el contenido es realmente distinto según el remoto elegido.
Soluciones
Solución 1:
mkdir -p /tmp/ej-track && cd /tmp/ej-track
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 brunoecho "contador" >> f.txt
git commit -am "Añadir el contador de tareas pendientes"
git rev-parse main origin/main9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9 ← main ha avanzado 5a1d8f39c2e7b4a6f1d8c3e9b2a5f7d4c6e8b1a3 ← origin/main NO
Y en el servidor tampoco ha cambiado nada:
# 2. Push mueve las tres
git push
git rev-parse main origin/main
git --git-dir=/tmp/ej-track/servidor.git rev-parse main9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9 9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9 9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9
Las tres alineadas. El push actualiza tu origin/main porque Git acaba de hablar con el servidor y sabe con certeza dónde ha quedado.
# 3. Lo que envía Bruno no llega solo a Ana
cd /tmp/ej-track/bruno
git pull
echo "filtro" >> f.txt
git commit -am "Añadir el filtro de tareas pendientes"
git push
# En el repositorio de Ana, SIN hacer fetch:
cd /tmp/ej-track/ana
git rev-parse origin/main
git --git-dir=/tmp/ej-track/servidor.git rev-parse main9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9 ← la foto de Ana, anticuada c3b8f5d2a9e4f7b1c6d3a8e5b2f9c4d7a1e6b3f8 ← la realidad del servidor
El mensaje es objetivamente falso. El servidor tiene un commit que Ana no tiene, así que su rama está por detrás. Pero git status compara contra origin/main, que es una foto tomada antes del envío de Bruno, y nunca consulta la red. No está mintiendo: está diciendo la verdad sobre una información caducada.
On branch main Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded. (use "git pull" to update your local branch)
Ahora sí. La única forma de que esos números sean ciertos es hacer fetch antes.
Cero commits exclusivos de main (nada por enviar), uno exclusivo de origin/main (por traer). Es exactamente "behind by 1 commit". Y con detalle:
El > indica que viene del lado derecho, origin/main.
Solución 2:
mkdir -p /tmp/ej-up && cd /tmp/ej-up
git init --bare servidor.git
git clone servidor.git trabajo
cd trabajo
echo "base" > f.txt && git add . && git commit -m "Base"
git push -u origin main# 1. Rama local sin upstream
git switch -c funcionalidad/orden-alfabetico
echo "orden" >> f.txt
git commit -am "Mostrar las tareas ordenadas alfabéticamente"
# Prueba a: sin corchetes en branch -vv
git branch -vv* funcionalidad/orden-alfabetico a1e5c93 Mostrar las tareas ordenadas alfabéticamente main 5f8b2e1 [origin/main] Base
Solo aparece main. La rama nueva no tiene ninguna entrada.
fatal: The current branch funcionalidad/orden-alfabetico has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin funcionalidad/orden-alfabeticoGit no adivina: exige que digas a dónde va la rama la primera vez.
# 3a. Forma 1: push -u
git push -u origin funcionalidad/orden-alfabetico
git config --get-regexp '^branch\.funcionalidad'branch.funcionalidad/orden-alfabetico.remote origin branch.funcionalidad/orden-alfabetico.merge refs/heads/funcionalidad/orden-alfabetico
# Deshacer para probar la siguiente
git branch --unset-upstream
git config --get-regexp '^branch\.funcionalidad'# 3b. Forma 2: branch --set-upstream-to
git branch --set-upstream-to=origin/funcionalidad/orden-alfabeticogit branch --unset-upstream
# 3c. Forma 3: automática con autoSetupRemote
git config --local push.autoSetupRemote true
git commit --allow-empty -m "Otro commit"
git pushLas tres producen exactamente la misma configuración. Cambia el momento y la comodidad, no el resultado.
On branch funcionalidad/orden-alfabetico Your branch is up to date with 'origin/funcionalidad/orden-alfabetico'. nothing to commit, working tree clean
Ha desaparecido la línea del desfase. Sin upstream, Git no tiene con qué comparar. Es la prueba de que ese mensaje depende enteramente de esta configuración.
# 5. Rama local con nombre distinto del upstream
git switch main
git switch -c orden-local --track origin/funcionalidad/orden-alfabeticobranch 'orden-local' set up to track 'origin/funcionalidad/orden-alfabetico'. Switched to a new branch 'orden-local'
funcionalidad/orden-alfabetico a7c2e94 Otro commit main 5f8b2e1 [origin/main] Base * orden-local a7c2e94 [origin/funcionalidad/orden-alfabetico] Otro commit
Fíjate en la última línea: orden-local -> funcionalidad/orden-alfabetico. La rama local se llama de una forma y la del servidor de otra, y el envío funciona sin problema. El seguimiento no exige que los nombres coincidan.
Solución 3:
mkdir -p /tmp/ej-dwim && cd /tmp/ej-dwim
# 1. Dos servidores y un repositorio de trabajo
git init --bare equipo.git
git init --bare personal.git
git init -b main trabajo
cd trabajo
echo "base" > f.txt && git add . && git commit -m "Base"
git remote add origin /tmp/ej-dwim/equipo.git
git remote add personal /tmp/ej-dwim/personal.git
git push origin main
git push personal main# 2. La misma rama, con contenido distinto, en cada servidor
git switch -c funcionalidad/orden-alfabetico
echo "orden del equipo" >> f.txt
git commit -am "Mostrar las tareas ordenadas alfabéticamente"
git push origin funcionalidad/orden-alfabetico
git reset --hard HEAD~1
echo "otra forma de ordenar" >> f.txt
git commit -am "Probar otra forma de ordenar"
git push personal funcionalidad/orden-alfabetico
# Volver a un estado limpio y borrar la rama local
git switch main
git branch -D funcionalidad/orden-alfabetico
git fetch --all
git branch -rorigin/funcionalidad/orden-alfabetico origin/main personal/funcionalidad/orden-alfabetico personal/main
Git no puede adivinar cuál quieres, y elegir mal significaría acabar enviando trabajo al repositorio equivocado.
# 4a. Forma 1: referencia remota explícita
git switch -c orden-equipo origin/funcionalidad/orden-alfabeticobranch 'orden-equipo' set up to track 'origin/funcionalidad/orden-alfabetico'. Switched to a new branch 'orden-equipo'
# 4b. Forma 2: --track sin -c (toma el nombre corto de la remota)
git switch main
git switch --track personal/funcionalidad/orden-alfabeticobranch 'funcionalidad/orden-alfabetico' set up to track 'personal/funcionalidad/orden-alfabetico'. Switched to a new branch 'funcionalidad/orden-alfabetico'
# 4c. Forma 3: remoto preferente
git switch main
git branch -D funcionalidad/orden-alfabetico
git config checkout.defaultRemote origin
git switch funcionalidad/orden-alfabeticobranch 'funcionalidad/orden-alfabetico' set up to track 'origin/funcionalidad/orden-alfabetico'. Switched to a new branch 'funcionalidad/orden-alfabetico'
Ya no hay ambigüedad: ante la duda, Git elige origin.
* funcionalidad/orden-alfabetico a1e5c93 [origin/funcionalidad/orden-alfabetico] Mostrar las tareas ordenadas alfabéticamente main 5f8b2e1 Base orden-equipo a1e5c93 [origin/funcionalidad/orden-alfabetico] Mostrar las tareas ordenadas alfabéticamente
# 6. El contenido es realmente distinto
git log --oneline -1 origin/funcionalidad/orden-alfabetico
git log --oneline -1 personal/funcionalidad/orden-alfabeticodiff --git a/f.txt b/f.txt
index 8c3d5a1..2e7b9f4 100644
--- a/f.txt
+++ b/f.txt
@@ -1,2 +1,2 @@
base
-orden del equipo
+otra forma de ordenarDos commits distintos con el mismo nombre de rama en dos servidores distintos. Aquí se ve por qué Git prefiere fallar antes que adivinar: elegir mal habría significado trabajar sobre una base equivocada y, peor aún, enviar el resultado al sitio equivocado.
Conclusión
Con esta lección cerramos el módulo 4. Lo aprendido aquí:
- Una rama de seguimiento es una rama local asociada a una referencia remota, su upstream. Esa asociación es lo que permite
git pushygit pullsin argumentos, los mensajes de desfase degit statusy el atajo@{u}. - Vive en dos líneas de
.git/config:branch.<rama>.remote(con qué remoto) ybranch.<rama>.merge(qué rama de ese remoto). Es configuración local: no viaja con elpushy cada persona tiene la suya. main,origin/mainy elmaindel servidor son tres cosas distintas. Las dos primeras están en tu disco: la mueves tú al confirmar, y la segunda la mueve Git al hablar con el servidor. La tercera está en otra máquina y la mueve cualquiera del equipo. Casi todos los desconciertos con remotos se resuelven preguntándose de cuál de las tres se está hablando.- El "ahead/behind" se calcula con
git rev-list --left-right --count main...origin/main, usando la diferencia simétrica de tres puntos. Y se calcula contra tu foto, no contra el servidor: sin unfetchprevio, esos números pueden ser falsos. git branch -vvmuestra el upstream y el desfase de todas las ramas entre corchetes. Sin corchetes = sin upstream, la rama es puramente local. Congone= su rama del servidor ya no existe, normalmente porque se integró: candidata a borrar.- El upstream se establece con
git push -u, automáticamente conpush.autoSetupRemote true, a posteriori congit branch -u origin/<rama>, o al crear la rama con--track. Se quita con--unset-upstream, y entoncesgit statusdeja de informar del desfase. - El DWIM de
git switch <rama>crea una rama local a partir de una remota del mismo nombre y configura el seguimiento, siempre que no exista ya en local y haya exactamente una referencia remota que coincida. Con varios remotos falla: se resuelve siendo explícito, con--track, o configurandocheckout.defaultRemote. @{upstream}(o@{u}) se refiere al upstream de la rama actual sin nombrarlo:git log @{u}..(lo que no has enviado) ygit log ..@{u}(lo que no has traído) funcionan en cualquier rama.
Lo que viene
El proyecto ha salido del portátil de Ana y circula entre tres máquinas y tres sistemas operativos. El equipo colabora de verdad: los tres envían, reciben e integran, y gestor-tareas avanza.
Pero el historial empieza a acusarlo. Se llena de commits de fusión que no cuentan nada, de mensajes como arreglo y wip 2, y de trabajo a medias que se publicó sin querer. Y aparecen necesidades nuevas que el trabajo en solitario no planteaba: llevarse un commit concreto de una rama a otra, apartar el trabajo sin terminar para atender una urgencia, marcar una versión publicada o deshacer algo que ya está en el servidor.
En el módulo 5: Operaciones Avanzadas de Git aprenderemos a manipular el historial con precisión: git rebase para reescribir commits y obtener una historia lineal, el rebase interactivo para reordenarlos, unirlos y corregirlos antes de publicarlos, git cherry-pick para trasplantar commits sueltos, git stash para apartar trabajo temporalmente, las etiquetas para marcar versiones, y git revert para deshacer sin borrar.
Y todo lo que has aprendido en este módulo será justamente lo que te permita usarlas con criterio. Porque a partir de ahora el historial es compartido, y reescribir algo que otra persona ya tiene descargado no es una operación local: es una decisión que afecta a todo el equipo.
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
