Con la autenticación resuelta, el canal está abierto. Ana ha subido gestor-tareas al servidor del equipo —el mecanismo exacto del envío lo desmenuzaremos en la lección siguiente— y por primera vez en el curso hay objetos Git en una máquina distinta de la suya.
Ahora toca el otro sentido del viaje: recibir. Y aquí nos topamos con la confusión más extendida de todo Git, la que produce más sustos y más preguntas en foros: la diferencia entre git fetch y git pull.
La confusión es comprensible, porque los nombres no ayudan: fetch es "buscar" y pull es "tirar de algo". Suenan a sinónimos. No lo son en absoluto:
git fetchdescarga y actualiza tus referencias remotas. No toca ni un solo fichero de tu directorio de trabajo. Es una operación completamente inofensiva.git pullhace unfetchy, acto seguido, integra lo descargado en tu rama actual. Sí modifica tus ficheros y puede provocar conflictos.
Entender bien esa diferencia cambia por completo la relación que tienes con los remotos: pasas de "ejecuto pull y a ver qué pasa" a "miro qué hay, decido y luego integro". En esta lección la desmenuzamos, y Carla clona por fin el proyecto y se pone al día.
Contenido
- El punto de partida: el proyecto ya está en el servidor
git fetch: qué hace exactamente- El antes y el después de un
fetch FETCH_HEAD: la referencia que casi nadie conoce- Inspeccionar lo traído antes de integrarlo
- Integrar a mano:
mergesobreorigin/main git pull=fetch+ integración- Los modos de
git pull - Limpiar referencias obsoletas:
--prune - Carla se incorpora y se pone al día
- Cuando las historias divergen
- El punto de partida: el proyecto ya está en el servidor
Situación actual del equipo:
flowchart TB
S["git.ejemplo.es<br/>gestor-tareas.git (bare)<br/>main → c2a8f1e"]
A["Ana · Ubuntu<br/>main → c2a8f1e<br/>origin/main → c2a8f1e"]
B["Bruno · macOS<br/>main → c2a8f1e<br/>origin/main → c2a8f1e"]
C["Carla · Windows 11<br/>(nada todavía)"]
A --> S
S --> B
S -.->|"clone pendiente"| C
Ana ha publicado. Bruno ya ha actualizado su clon. Carla todavía no tiene nada.
Para reproducirlo en tu máquina, montamos el escenario completo con lo aprendido en las lecciones anteriores:
mkdir -p /tmp/equipo && cd /tmp/equipo
# El servidor
git init --bare gestor-tareas.git
# El repositorio de Ana
git init -b main ana
cd ana
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"
echo "# Gestor de tareas" > README.md
git add . && git commit -m "Documentar la instalación en el README"
git remote add origin /tmp/equipo/gestor-tareas.git
git push -u origin main
cd ..
# El clon de Bruno
git clone /tmp/equipo/gestor-tareas.git brunoCuatro commits en el servidor y dos repositorios sincronizados. A partir de aquí trabajamos.
git fetch: qué hace exactamente
git fetch: qué hace exactamenteBruno se pone a trabajar por la mañana. Ana lleva desde ayer añadiendo cosas y ha enviado dos commits nuevos. Bruno no lo sabe: recuerda de la lección 04-01 que su origin/main es una foto con fecha de ayer y que nadie le va a avisar.
Simulemos el trabajo de Ana:
cd /tmp/equipo/ana
echo "console.log('contador');" >> app.js
git commit -am "Añadir el contador de tareas pendientes"
echo "footer { color: gray; }" >> estilos.css
git commit -am "Ajustar el estilo del pie de página"
git pushY ahora Bruno:
remote: Enumerating objects: 8, done. remote: Counting objects: 100% (8/8), done. remote: Compressing objects: 100% (4/4), done. remote: Total 6 (delta 2), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (6/6), 612 bytes | 612.00 KiB/s, done. From /tmp/equipo/gestor-tareas.git c2a8f1e..b4d7e93 main -> origin/main
La línea que importa es la última, y conviene leerla con cuidado:
c2a8f1e..b4d7e93 main -> origin/main
└───┬────────┘ └─┬─┘ └────┬────┘
de dónde a dónde la rama la referencia
ha avanzado en el local que se
servidor ha actualizadoLo que se ha actualizado es origin/main, no main. Esa flecha lo dice literalmente: la rama main del servidor se ha guardado en tu referencia origin/main. Es el refspec de la lección 04-02 en acción.
Las tres cosas que hace un fetch
- Se conecta al remoto y le pregunta qué referencias tiene.
- Descarga los objetos que a ti te faltan (commits, árboles, blobs) y los guarda en
.git/objects/. - Actualiza las referencias remotas en
.git/refs/remotes/origin/según el refspec configurado.
Las tres cosas que NO hace
- No toca tu directorio de trabajo. Ni un fichero. Ni uno.
- No mueve ninguna de tus ramas locales. Tu
mainsigue exactamente donde estaba. - No puede provocar conflictos. No hay nada que combinar: solo se añaden objetos y se mueven punteros de referencias que no son tuyas.
La demostración es inmediata:
On branch main Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded. (use "git pull" to update your local branch) nothing to commit, working tree clean
"nothing to commit, working tree clean": el fetch no ha cambiado nada en el disco de trabajo. Lo único nuevo es que ahora Git sabe que hay dos commits esperando, porque puede comparar main con origin/main. De dónde sale exactamente ese mensaje y cómo se calcula es el tema de la lección 04-06.
Dos referencias, dos commits distintos. Este es el estado normal después de un fetch, y no tiene nada de anómalo.
La consecuencia práctica más importante de toda la lección:
git fetches absolutamente seguro. No puede romper nada, no puede perder trabajo, no puede provocar un conflicto y no puede dejarte a medias de nada. Puedes ejecutarlo cuando quieras, incluso con cambios sin confirmar en tu directorio de trabajo. La única consecuencia es que estarás mejor informado.
Variantes útiles
# Traer de todos los remotos configurados
git fetch --all
# Solo una rama concreta
git fetch origin main
# Traer también las etiquetas (lección 05-05)
git fetch --tags
# Ver qué se traería, sin traer nada
git fetch --dry-run
# Salida detallada de todas las referencias, no solo las que cambian
git fetch --verboseY uno que no descarga nada en absoluto y es utilísimo para diagnosticar:
b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7 HEAD b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7 refs/heads/main 7f3c9a28e4b1d6f9a2c5e8b3d7f1a4c6e9b2d5f8 refs/heads/documentacion/actualizar-notas
Pregunta al servidor qué referencias tiene y a qué commits apuntan, sin descargar un solo objeto. Es la forma más barata de comprobar si hay novedades o si el remoto responde.
- El antes y el después de un
fetch
fetchVisto en el grafo, que es donde mejor se entiende:
flowchart TB
subgraph ANTES["ANTES del fetch — repositorio de Bruno"]
direction LR
A1["1a4c8d6"] --> A2["8b6d3c2"] --> A3["4e7f2a9"] --> A4["c2a8f1e<br/><b>main</b><br/><b>origin/main</b>"]
end
subgraph DESPUES["DESPUÉS del fetch — repositorio de Bruno"]
direction LR
B1["1a4c8d6"] --> B2["8b6d3c2"] --> B3["4e7f2a9"] --> B4["c2a8f1e<br/><b>main</b>"] --> B5["9e2f4a7"] --> B6["b4d7e93<br/><b>origin/main</b>"]
end
ANTES --> DESPUES
Fíjate en los dos detalles que resumen toda la lección:
- Han aparecido dos commits nuevos (
9e2f4a7yb4d7e93) en la base de datos de objetos de Bruno. Están ahí, completos, consultables sin conexión. mainno se ha movido. Sigue enc2a8f1e, exactamente donde estaba. Soloorigin/mainha avanzado.
Y como los objetos ya están en su disco, Bruno puede examinarlos con todo lo que aprendió en el módulo 2, sin red y sin haber integrado nada:
Esto es una de las grandes ventajas del modelo distribuido: descargar y decidir son dos actos separados. Puedes traerte el trabajo de todo el equipo en el tren, con conexión intermitente, y estudiarlo tranquilamente después.
FETCH_HEAD: la referencia que casi nadie conoce
FETCH_HEAD: la referencia que casi nadie conoceCada git fetch escribe un fichero en la raíz de .git/:
b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7 branch 'main' of /tmp/equipo/gestor-tareas.git 7f3c9a28e4b1d6f9a2c5e8b3d7f1a4c6e9b2d5f8 not-for-merge branch 'documentacion/actualizar-notas' of /tmp/equipo/gestor-tareas.git
FETCH_HEAD registra qué se trajo en la última operación de descarga. Tiene tres columnas:
| Columna | Contenido |
|---|---|
| 1 | El hash del commit descargado |
| 2 | Vacía si esa rama es candidata a integrarse; not-for-merge si no |
| 3 | De qué rama y de qué URL viene |
Ese not-for-merge es la clave: marca las ramas que se han traído por el refspec pero que no son la que integrarías. Solo la rama de seguimiento de tu rama actual queda sin la marca.
Se puede usar como cualquier referencia:
Para qué sirve en la práctica
Primero: es el mecanismo interno de git pull. Cuando ejecutas git pull, Git hace un fetch y luego un git merge FETCH_HEAD. Saberlo desmitifica por completo el comando: no hay magia, hay dos pasos encadenados.
Segundo: permite traer de una URL sin registrar ningún remoto. Este caso ocurre más de lo que parece, por ejemplo al revisar el trabajo de alguien externo:
# Traer una rama de un repositorio con el que no quiero relación permanente
git fetch https://git.ejemplo.es/carla/gestor-tareas.git funcionalidad/prototipo
# No se ha creado ninguna referencia remota, pero el commit está aquí:
git log --oneline FETCH_HEAD -3
git switch -c revision-prototipo FETCH_HEADEs efímero: cada fetch sobrescribe FETCH_HEAD. Si vas a necesitar ese commit más adelante, crea una rama o una referencia con nombre; si no, el recolector de basura acabará llevándoselo.
- Inspeccionar lo traído antes de integrarlo
Aquí está el hábito profesional que distingue a quien entiende Git de quien lo padece: entre el fetch y la integración hay un hueco, y ese hueco es para mirar.
Bruno ya tiene los commits de Ana en su disco. Antes de mezclarlos con su trabajo, los estudia.
¿Qué me falta a mí?
Recuerda del módulo 2 la semántica del rango a..b: "commits alcanzables desde b pero no desde a". Es decir: lo que tiene el servidor y yo no.
¿Qué tengo yo que ellos no tengan?
Vacío: Bruno no ha confirmado nada desde su última sincronización. Si hubiera commits aquí y en el rango anterior, las historias habrían divergido; ese caso lo tratamos en el apartado 11.
¿Qué ficheros cambian y cuánto?
Un vistazo rápido a la superficie del cambio. Y si quieres el detalle:
diff --git a/app.js b/app.js
index 3f8a2c1..7d4e9b6 100644
--- a/app.js
+++ b/app.js
@@ -1 +1,2 @@
console.log('tareas');
+console.log('contador');El unified diff de la lección 02-05, sin ninguna diferencia: origin/main es una referencia como cualquier otra.
El grafo completo
* b4d7e93 (origin/main) Ajustar el estilo del pie de página * 9e2f4a7 Añadir el contador de tareas pendientes * c2a8f1e (HEAD -> main) Documentar la instalación en el README * 4e7f2a9 Añadir borrado de tareas al listado * 8b6d3c2 Añadir estilos base del listado * 1a4c8d6 Estructura inicial del gestor de tareas
La opción --all incluye todas las referencias, también las remotas. Es la vista que hay que tener a mano en cuanto trabajas con un remoto.
¿Quién ha hecho qué?
b4d7e93 Ana Ferrer: Ajustar el estilo del pie de página 9e2f4a7 Ana Ferrer: Añadir el contador de tareas pendientes
El resumen en una tabla
| Pregunta | Comando |
|---|---|
| ¿Qué hay nuevo en el servidor? | git log --oneline main..origin/main |
| ¿Qué tengo yo sin enviar? | git log --oneline origin/main..main |
| ¿Cuántos commits a cada lado? | git rev-list --left-right --count main...origin/main |
| ¿Qué ficheros cambian? | git diff --stat main origin/main |
| ¿Qué cambia exactamente? | git diff main origin/main |
| ¿Cómo queda el grafo? | git log --oneline --graph --all |
| ¿Quién lo ha hecho? | git log --format='%h %an: %s' main..origin/main |
Este es el flujo recomendado: fetch → mirar → decidir → integrar. Cuesta veinte segundos y evita el desconcierto de un pull que de repente ha modificado quince ficheros que no esperabas.
- Integrar a mano:
merge sobre origin/main
merge sobre origin/mainBruno ya ha visto lo que hay y le parece bien. Ahora sí, integra:
Updating c2a8f1e..b4d7e93 Fast-forward app.js | 1 + estilos.css | 1 + 2 files changed, 2 insertions(+)
Y esto es exactamente el git merge del módulo 3. Nada nuevo. Como Bruno no tenía commits propios, main estaba por detrás sin haber divergido, y Git ha aplicado un fast-forward: mover el puntero hacia delante. El mismo caso que estudiamos con las ramas locales.
Ahora las tres referencias coinciden: main, origin/main y la rama del servidor.
Este es el punto en el que conviene detenerse un momento, porque resume la promesa del final del módulo 3:
Integrar el trabajo de otra persona es exactamente el mismo
git mergeque has usado con tus propias ramas. Fast-forward si no has divergido, fusión a tres bandas si sí, con la posibilidad de conflictos que ya sabes resolver. Los remotos no cambian nada de eso: solo cambian de dónde vienen los commits.
git pull = fetch + integración
git pull = fetch + integraciónY con eso, git pull deja de tener misterio:
git pulles un atajo degit fetchseguido de una integración de la rama de seguimiento en tu rama actual.
Es literalmente así. Estos dos bloques hacen lo mismo:
flowchart TB
P["git pull"] --> F["1 · git fetch<br/>Descarga objetos<br/>Actualiza origin/main"]
F --> M["2 · Integración<br/>merge (o rebase)<br/>de origin/main en main"]
M --> R["Directorio de trabajo<br/><b>modificado</b>"]
style F fill:#e8f4e8
style M fill:#f9e8e8
Las dos mitades tienen naturalezas opuestas, y por eso conviene verlas separadas:
Paso 1: fetch |
Paso 2: integración | |
|---|---|---|
| Toca la red | Sí | No |
| Toca el directorio de trabajo | No | Sí |
| Puede dar conflictos | No | Sí |
| Se puede deshacer fácilmente | No hace falta | Sí (merge --abort, reset) |
| Es seguro sin mirar | Sí | No |
Cuándo usar cada uno
Usa git fetch cuando:
- Quieres saber qué hay de nuevo sin comprometerte a nada.
- Tienes cambios sin confirmar y no quieres que nada se mueva.
- Estás en mitad de algo delicado.
- Quieres revisar el trabajo de otra persona antes de mezclarlo.
- Vas a quedarte sin conexión y quieres tenerlo todo descargado.
Usa git pull cuando:
- Empiezas la jornada y solo quieres ponerte al día.
- Estás seguro de que no has divergido.
- Trabajas en una rama que solo tocas tú.
Recomendación honesta: si estás empezando, acostúmbrate a git fetch + mirar + integrar. Es un paso más y quince segundos, pero te da un modelo mental correcto y evita el 90 % de los sustos. Cuando ya entiendas perfectamente qué va a pasar, git pull es un atajo legítimo.
- Los modos de
git pull
git pullLa parte de fetch de pull siempre es igual. Lo que cambia es cómo integra, y ahí hay tres comportamientos posibles. Este es el motivo por el que en la lección 01-06 configuramos:
--ff-only: solo si es un avance limpio
Integra únicamente si se puede resolver con un fast-forward, es decir, si tu rama no tiene ningún commit propio. Si has divergido, se detiene sin hacer nada:
Esto no es un error: es la configuración funcionando. Git te está diciendo "aquí hay una decisión que tomar y no la voy a tomar yo". Es exactamente lo que queríamos al fijar pull.ff only en la configuración inicial: nunca crear commits de fusión por sorpresa.
Es el modo más conservador y el mejor mientras aprendes.
--no-rebase: fusionar siempre
Integra con git merge. Si puede hacer fast-forward, lo hace; si has divergido, crea un commit de fusión con dos padres.
Este era el comportamiento por defecto histórico de Git, y es la razón por la que tantos repositorios están llenos de commits titulados Merge branch 'main' of https://…. Cada uno de ellos es alguien que ejecutó git pull habiendo divergido, sin enterarse de que estaba creando un commit.
Ese ruido en el historial es, precisamente, uno de los problemas que abordará el módulo 5.
--rebase: reaplicar tus commits encima
En lugar de fusionar, reaplica tus commits locales encima de lo que ha traído del servidor, produciendo un historial lineal sin commits de fusión.
Es una técnica potente y muy usada, pero reescribe tus commits locales —les cambia el hash— y tiene reglas importantes sobre cuándo se puede y cuándo no. Merece una lección entera, y la tiene: lección 05-01: Rebase. Hasta entonces, quédate solo con que la opción existe y con lo que hace en una frase.
Comparativa
| Modo | Si no has divergido | Si has divergido | Historial |
|---|---|---|---|
--ff-only |
Fast-forward | Se detiene | Lineal |
--no-rebase (merge) |
Fast-forward | Commit de fusión | Con bifurcaciones |
--rebase |
Fast-forward | Reaplica tus commits | Lineal |
Y la configuración correspondiente, que ya conoces de la lección 01-06:
# Lo que tenemos configurado en el curso
git config --global pull.ff only
# Alternativas (no las apliques ahora)
git config --global pull.rebase true # siempre rebase
git config --global pull.rebase false # siempre mergeTambién se puede afinar por rama:
Un detalle: pull con cambios sin confirmar
Si tienes modificaciones sin confirmar y la integración necesita tocar esos mismos ficheros, Git se niega:
error: Your local changes to the following files would be overwritten by merge: app.js Please commit your changes or stash them before you merge. Aborting.
Y hace bien: está protegiendo trabajo que solo existe en tu disco. Las opciones son confirmar, o guardar los cambios temporalmente con git stash (que es el tema de la lección 05-04).
Nótese que un git fetch en esa misma situación habría funcionado sin problema, porque no toca el directorio de trabajo. Una razón más para preferirlo.
- Limpiar referencias obsoletas:
--prune
--pruneCon el tiempo aparece un problema menor pero molesto. El equipo integra la rama funcionalidad/contador-tareas y la borra del servidor. Pero en el repositorio de Bruno, la referencia origin/funcionalidad/contador-tareas sigue ahí para siempre.
¿Por qué? Porque el refspec por defecto solo dice qué traer, no qué borrar. Un fetch normal nunca elimina referencias remotas.
origin/HEAD -> origin/main origin/main origin/funcionalidad/contador-tareas ← ya no existe en el servidor origin/funcionalidad/filtro-pendientes ← ya no existe en el servidor origin/documentacion/actualizar-notas
Dos referencias fantasma. Y git remote show origin las delata:
Remote branches:
main tracked
documentacion/actualizar-notas tracked
funcionalidad/contador-tareas stale (use 'git remote prune' to remove)
funcionalidad/filtro-pendientes stale (use 'git remote prune' to remove)Ese stale es exactamente lo que vimos en la lección 04-02: "tú tienes esta referencia, pero en el servidor ya no está."
Las dos formas de limpiar
From /tmp/equipo/gestor-tareas.git - [deleted] (none) -> origin/funcionalidad/contador-tareas - [deleted] (none) -> origin/funcionalidad/filtro-pendientes
Pruning origin URL: /tmp/equipo/gestor-tareas.git * [pruned] origin/funcionalidad/contador-tareas * [pruned] origin/funcionalidad/filtro-pendientes
Y para ver qué se borraría sin borrar nada:
Hazlo automático
Como esto hay que hacerlo siempre y no tiene ningún inconveniente, lo razonable es configurarlo de una vez:
A partir de ahora, todos tus fetch y pull limpian las referencias muertas. Es uno de los ajustes que más agradece un repositorio de equipo con rotación de ramas.
Y si además quieres limpiar etiquetas borradas en el servidor:
Con esta última hay que ir con más cuidado, porque las etiquetas suelen ser permanentes y borrar una localmente puede no ser lo que quieres. Las etiquetas son el tema de la lección 05-05.
Muy importante: prune borra referencias remotas, nunca ramas locales ni commits. Si tienes una rama local funcionalidad/contador-tareas, sigue ahí intacta después del prune. Solo desaparece la nota que decía "el servidor tenía esta rama".
- Carla se incorpora y se pone al día
Ha llegado el momento que anunciamos al cerrar el módulo 3.
Carla, desde su Windows 11, con Git instalado (lección 01-02), configurado (lección 01-06) y su clave SSH generada, cargada y subida (lección 04-03), abre Git Bash y escribe el comando que Bruno ejecutó en la lección 02-02:
cd ~/proyectos
git clone [email protected]:equipo/gestor-tareas.gitCloning into 'gestor-tareas'... remote: Enumerating objects: 47, done. remote: Counting objects: 100% (47/47), done. remote: Compressing objects: 100% (28/28), done. remote: Total 47 (delta 15), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (47/47), 8.42 KiB | 8.42 MiB/s, done. Resolving deltas: 100% (15/15), done.
Pero esta vez no es el mismo clon que hizo Bruno. Aquel era un repositorio de dos commits recién nacido. Este tiene meses de trabajo dentro:
* b4d7e93 (HEAD -> main, origin/main, origin/HEAD) Ajustar el estilo del pie de página * 9e2f4a7 Añadir el contador de tareas pendientes * c2a8f1e Fusionar el mensaje de lista vacía |\ | * 6d3f8b2 Mostrar un mensaje cuando el listado está vacío * | 3b9e7d1 Añadir exportación del listado de tareas a CSV |/ * f7a3e92 Fusionar el filtro de tareas pendientes |\ | * b2e6d3f Corregir el foco del campo tras añadir una tarea * | 8d4e6b2 Fusionar el contador de tareas pendientes |\ \ | |/ | * 9d1e4b7 Marcar tareas como completadas al hacer clic * | c5d9b1e Documentar la instalación en el README |/
Ahí está toda la historia del módulo 3, con sus fusiones a tres bandas, su squash y su conflicto resuelto, descargada íntegramente en el portátil de Carla. Puede consultar cualquier commit de cualquier momento sin conexión, porque —como sabe desde la lección 04-01— su repositorio es completo e independiente.
Y lo que dijimos en la lección 02-02 sobre lo que deja configurado el clon:
origin [email protected]:equipo/gestor-tareas.git (fetch) origin [email protected]:equipo/gestor-tareas.git (push)
* main remotes/origin/HEAD -> origin/main remotes/origin/documentacion/actualizar-notas remotes/origin/main
Fíjate en un detalle que confunde a mucha gente al incorporarse a un proyecto: Carla solo tiene una rama local (main), aunque el servidor tenga varias. Las demás existen en su repositorio como referencias remotas, no como ramas propias. Para trabajar en una:
branch 'documentacion/actualizar-notas' set up to track 'origin/documentacion/actualizar-notas'. Switched to a new branch 'documentacion/actualizar-notas'
Git ha visto que no existe esa rama local pero sí origin/documentacion/actualizar-notas, ha deducido lo que quería y ha creado la rama local con su seguimiento configurado. Ese comportamiento se llama DWIM y es uno de los temas de la lección 04-06.
Su primera jornada de trabajo
Carla ya está operativa. Su rutina diaria será esta, y es la de cualquier persona que trabaja con un repositorio compartido:
# 1. Al empezar el día: ver qué ha pasado mientras dormía
git fetch --prune
git log --oneline main..origin/main# 3. Abrir una rama para lo suyo
git switch -c funcionalidad/orden-por-fecha
# 4. Trabajar, confirmar… y enviar (lección 04-05)Tres comandos por la mañana. Ese es todo el ritual.
- Cuando las historias divergen
Falta un caso, y hay que nombrarlo aunque no lo desarrollemos aquí.
Supón que Carla ha confirmado dos commits en main mientras Ana enviaba otros tres. Ahora las dos historias han divergido: cada una tiene commits que la otra no tiene. Es el escenario que estudiamos en la lección 03-01 con ramas locales, pero ahora una de las dos "ramas" está en otra máquina.
On branch main Your branch and 'origin/main' have diverged, and have 2 and 3 different commits each, respectively. (use "git pull" if you want to integrate the remote branch with yours)
Con pull.ff only configurado:
Insistimos: esto no es un fallo, es la protección funcionando. Git se niega a decidir por ti entre fusionar y reescribir, y hace bien: son decisiones con consecuencias distintas sobre el historial del equipo.
Las salidas existen y son varias —fusionar explícitamente, reaplicar tus commits con rebase, o replantear el trabajo—, y cada una tiene sus implicaciones y sus contraindicaciones. Todo ello, con los criterios para elegir y los procedimientos completos, es el tema de la lección 09-03: Resolviendo Divergencias con el Remoto.
Aquí basta con que sepas reconocer el mensaje, que entiendas por qué aparece (las dos partes han avanzado por separado desde un ancestro común) y que tengas claro que nada se ha roto ni se ha perdido: simplemente hay una decisión pendiente.
Lo mismo ocurrirá en el sentido contrario, cuando intentes enviar y el servidor rechace tu push. Es exactamente la misma situación vista desde el otro lado, y la veremos en la lección siguiente.
Errores Comunes y Consejos
Error 1: creer que git fetch no ha hecho nada porque "los ficheros siguen igual". Ha hecho justo lo que debía: descargar objetos y actualizar origin/main. Compruébalo con git log --oneline main..origin/main y verás todo lo que ha traído.
Error 2: ejecutar git pull a ciegas y sorprenderse del resultado. Si has divergido y tienes pull en modo merge, acabas de crear un commit de fusión que a lo mejor no querías. fetch + mirar + integrar cuesta quince segundos y elimina las sorpresas.
Error 3: interpretar fatal: Not possible to fast-forward como un error grave. Es pull.ff only haciendo su trabajo: hay divergencia y Git no decide por ti. Ver 09-03.
Error 4: no usar --prune nunca. Al cabo de un año tienes cuarenta referencias origin/… de ramas que se borraron hace meses, el autocompletado es inservible y git branch -r no se puede leer. Configura fetch.prune true y olvídate.
Error 5: pensar que origin/main se actualiza sola. Es un fichero de tu disco. Solo cambia con fetch, pull, clone o un push con éxito. Si un compañero te dice "ya está subido" y tú no lo ves, tu primer comando es git fetch.
Error 6: hacer pull con cambios sin confirmar y bloquearse con el error. El mensaje es claro: confirma o guarda temporalmente. Y recuerda que un fetch en esa misma situación habría funcionado sin problema.
Error 7: confundir git remote prune con git prune. Son comandos distintos: el primero limpia referencias remotas obsoletas; el segundo es una operación de mantenimiento de la base de datos de objetos que elimina objetos inalcanzables. No los mezcles.
Consejo 1: convierte git fetch --prune en tu primer comando del día. Es gratis, es seguro y te sitúa. Después decides qué hacer.
Consejo 2: guarda un alias para ver el grafo con las referencias remotas.
Con git gr ves de un golpe dónde estás tú, dónde está el servidor y qué ramas hay por medio. Los alias se tratan a fondo en la lección 06-04.
Consejo 3: usa git ls-remote para comprobar el servidor sin descargar nada. Es instantáneo y perfecto para responder a "¿está ya subido?" o "¿este remoto responde?".
Consejo 4: si vas a viajar o a quedarte sin conexión, haz git fetch --all --tags antes. Te llevas todo el trabajo del equipo en el disco y puedes revisarlo, comparar ramas y consultar el historial sin red.
Consejo 5: git fetch es seguro. Ejecútalo sin miedo. No hay ninguna situación en la que un fetch estropee algo. Interiorizarlo es lo que permite trabajar con remotos con tranquilidad.
Ejercicios
Ejercicio 1: demostrar que fetch no toca nada
Monta un servidor bare y dos clones que simulen a Ana y a Carla. Después:
- Ana confirma y envía dos commits.
- Carla, antes de hacer nada, anota el hash de su
mainy de suorigin/main, y guarda una copia del contenido de un fichero. - Carla ejecuta
git fetch. - Demuestra con comandos que: (a)
origin/mainha cambiado, (b)mainno, (c) el fichero del disco es idéntico y (d) los commits nuevos ya están en su base de datos de objetos. - Ahora integra y comprueba qué tipo de integración ha hecho Git y por qué.
Ejercicio 2: el hueco entre fetch e integración
Partiendo del ejercicio anterior, y sin integrar, responde con un comando a cada pregunta:
- ¿Cuántos commits me faltan?
- ¿Quién los ha escrito y cuándo?
- ¿Qué ficheros modifican y con cuántas líneas cada uno?
- ¿Qué cambia exactamente en
app.js? - ¿Tengo yo algún commit que el servidor no tenga?
- ¿Cómo queda el grafo con todas las referencias?
Ejercicio 3: referencias fantasma
- Crea en el servidor tres ramas además de
main. - Clona y comprueba que aparecen las tres como referencias remotas.
- Borra dos de ellas en el servidor.
- Haz un
git fetchnormal y comprueba que las referencias fantasma siguen ahí. - Explica por qué el
fetchno las ha eliminado. - Límpialas de las dos formas posibles y configura Git para que no vuelva a pasar.
Soluciones
Solución 1:
mkdir -p /tmp/ej-fetch && cd /tmp/ej-fetch
git init --bare servidor.git
git clone servidor.git ana
cd ana
echo "<h1>Gestor de tareas</h1>" > index.html
echo "console.log('tareas');" > app.js
git add . && git commit -m "Estructura inicial del gestor de tareas"
git push -u origin main
cd ..
git clone servidor.git carla# 1. Ana trabaja y envía
cd /tmp/ej-fetch/ana
echo "console.log('contador');" >> app.js
git commit -am "Añadir el contador de tareas pendientes"
echo "footer { color: gray; }" > estilos.css
git add . && git commit -m "Ajustar el estilo del pie de página"
git push# 2. Carla anota su estado ANTES
cd /tmp/ej-fetch/carla
git rev-parse main origin/main
md5sum app.js5a1d8f39c2e7b4a6f1d8c3e9b2a5f7d4c6e8b1a3 5a1d8f39c2e7b4a6f1d8c3e9b2a5f7d4c6e8b1a3 7c1e9a4b8d2f6c3a5e7b9d1f4a8c2e6b app.js
Exactamente el mismo hash de antes del fetch.
Misma suma de comprobación y git status --short sin ninguna línea: el directorio de trabajo está intacto.
# 4d. …pero los commits nuevos YA están en su repositorio
git cat-file -t e4b7c92
git log --oneline main..origin/main
git show --stat e4b7c92 | head -8commit e4b7c923f8a1d6b4e2c7f9a3d5b8c1e6f4a2d7b9 Author: Ana Ferrer <[email protected]> Date: Sat Aug 1 10:14:22 2026 +0200 Ajustar el estilo del pie de página estilos.css | 1 +
Esta es la demostración clave: Carla puede examinar el contenido completo de commits que no ha integrado, sin conexión, porque los objetos están en su .git/objects/. Lo que no ha cambiado es su rama ni sus ficheros.
Updating 5a1d8f3..e4b7c92 Fast-forward app.js | 1 + estilos.css | 1 + 2 files changed, 2 insertions(+)
Fast-forward, y la razón está en el grafo: Carla no había confirmado nada, así que su main era un antepasado directo de origin/main. No había divergencia y no hacía falta ningún commit de fusión: bastaba con mover el puntero. Se puede comprobar formalmente:
Solución 2:
Volviendo al estado justo después del fetch y antes del merge:
e4b7c92 · Ana Ferrer · hace 4 minutos · Ajustar el estilo del pie de página 9f2a5c8 · Ana Ferrer · hace 5 minutos · Añadir el contador de tareas pendientes
diff --git a/app.js b/app.js
index 3f8a2c1..7d4e9b6 100644
--- a/app.js
+++ b/app.js
@@ -1 +1,2 @@
console.log('tareas');
+console.log('contador');El -- separa las referencias de las rutas, para que Git no dude si app.js es un fichero o una rama.
Nada. No he divergido, así que la integración será limpia. La versión compacta de la misma pregunta:
Cero commits míos exclusivos, dos suyos. (Esta sintaxis se explica a fondo en la lección 04-06.)
* e4b7c92 (origin/main, origin/HEAD) Ajustar el estilo del pie de página * 9f2a5c8 Añadir el contador de tareas pendientes * 5a1d8f3 (HEAD -> main) Estructura inicial del gestor de tareas
Una sola línea recta con main por detrás: la imagen visual de "puedo hacer fast-forward".
Solución 3:
mkdir -p /tmp/ej-prune && cd /tmp/ej-prune
git init --bare servidor.git
git clone servidor.git trabajo
cd trabajo
echo "inicio" > f.txt
git add . && git commit -m "Estructura inicial del gestor de tareas"
git push -u origin main
# 1. Tres ramas más en el servidor
for r in funcionalidad/contador funcionalidad/filtro documentacion/notas; do
git switch -c "$r" > /dev/null 2>&1
git commit --allow-empty -m "Trabajo en $r" > /dev/null
git push -u origin "$r" > /dev/null 2>&1
done
git switch main
cd ..origin/HEAD -> origin/main origin/documentacion/notas origin/funcionalidad/contador origin/funcionalidad/filtro origin/main
# 3. Borrar dos EN EL SERVIDOR
cd /tmp/ej-prune/trabajo
git push origin --delete funcionalidad/contador
git push origin --delete funcionalidad/filtroorigin/HEAD -> origin/main origin/documentacion/notas origin/funcionalidad/contador origin/funcionalidad/filtro origin/main
Las dos fantasma siguen ahí. Y git remote show lo confirma:
Remote branches:
documentacion/notas tracked
main tracked
refs/remotes/origin/funcionalidad/contador stale (use 'git remote prune' to remove)
refs/remotes/origin/funcionalidad/filtro stale (use 'git remote prune' to remove)5. Por qué el fetch no las ha eliminado: porque el refspec por defecto, +refs/heads/*:refs/remotes/origin/*, solo describe qué traer. Es una instrucción de copia, no de sincronización: dice "las ramas que existan allí, guárdalas aquí", y no dice nada sobre qué hacer con las que ya no existen. Borrar referencias es un comportamiento adicional que hay que pedir explícitamente, y Git es conservador por defecto: no elimina cosas sin que se lo mandes.
From /tmp/ej-prune/servidor - [deleted] (none) -> origin/funcionalidad/contador - [deleted] (none) -> origin/funcionalidad/filtro
# 6b. Segunda forma (comprobándolo primero en seco)
git remote prune --dry-run origin
git remote prune origin# 6c. Que no vuelva a pasar
git config --global fetch.prune true
git config --global --get fetch.pruneA partir de ahora, todos los fetch y pull limpian solos. Y una comprobación tranquilizadora final:
# Ni los objetos: el commit de la rama borrada sigue accesible por su hash
git cat-file -t 4c9e2a7 2>/dev/null || echo "(ya recolectado)"prune borra referencias remotas, nada más.
Conclusión
Esta lección ha deshecho la confusión más extendida de Git. Lo esencial:
git fetchdescarga objetos y actualiza tus referencias remotas. No toca el directorio de trabajo, no mueve tus ramas y no puede provocar conflictos. Es una operación completamente segura que puedes ejecutar en cualquier momento, incluso con cambios sin confirmar.git pullesgit fetch+ una integración en tu rama actual. La primera mitad es inofensiva; la segunda modifica tus ficheros y puede dar conflictos. Verlas por separado es lo que quita el misterio al comando.- La salida
c2a8f1e..b4d7e93 main -> origin/mainsignifica: la ramamaindel servidor se ha guardado en tu referencia localorigin/main. Tumainno se ha movido. FETCH_HEADguarda qué se trajo en la última descarga, con la marcanot-for-mergeen las ramas que no son candidatas a integrarse. Es el mecanismo interno depully permite traer de una URL sin registrar remoto.- Entre el
fetchy la integración hay un hueco, y es para mirar:git log main..origin/main,git diff --stat main origin/main,git log --oneline --graph --all. Veinte segundos que eliminan las sorpresas. - Integrar el trabajo remoto es el mismo
git mergedel módulo 3: fast-forward si no has divergido, fusión a tres bandas si sí. Los remotos solo cambian de dónde vienen los commits. - Los modos de
pull:--ff-only(el nuestro, porpull.ff only) se detiene ante la divergencia en lugar de decidir por ti;--no-rebasefusiona y crea commits de fusión;--rebasereaplica tus commits y se estudia en la lección 05-01. git fetch --pruneygit remote pruneeliminan las referencias de ramas ya borradas en el servidor, que unfetchnormal nunca limpia. Configurafetch.prune truey olvídate. Solo borra referencias remotas: nunca ramas locales ni commits.- Carla ya está dentro: ha clonado un repositorio con historia completa, tiene
originconfigurado, una rama local y referencias remotas para las demás. - Si las historias divergen,
pull --ff-onlyse detiene. No es un fallo: es una decisión pendiente. El tratamiento completo, en la lección 09-03.
Lo que viene
Ya sabes recibir. Falta la otra mitad, y es la que tiene consecuencias para todo el equipo: enviar.
En la lección 04-05: Enviando Cambios veremos qué manda exactamente git push (objetos y la actualización de una referencia), qué hace ese -u que aparece en todos los tutoriales, cómo se escribe un refspec de envío y por qué se relaciona directamente con lo que estudiamos en 04-02. Y sobre todo: por qué un push puede ser rechazado, qué significa exactamente non-fast-forward, y por qué --force-with-lease es casi siempre la respuesta correcta cuando --force puede destruir el trabajo de un compañero sin dejar rastro.
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
