Módulo 5: Ramas, integración, remotos y colaboración

Unidad: GIT-06–07
Público: alumnado de DAW, DAM, ASIR y grados de informática.
Itinerario: tronco, bloque de colaboración; tras el ciclo local completo.
Prerrequisitos: módulos 2 a 4; manejar status, add, commit, log, diff y restore.
Objetivo observable: crear una rama de tarea, integrar su trabajo resolviendo un conflicto y sincronizarla con un remoto, distinguiendo en todo momento qué hace Git y qué aporta una forja como GitHub.
Fuente técnica: documentación oficial de git switch, git merge, git fetch y git push.
Duración orientativa: 4 horas.

Idea clave

Una rama es una referencia móvil que apunta a un commit. Crear una rama no duplica el proyecto ni copia archivos: solo añade un nombre nuevo que apunta al commit actual y que avanzará con cada commit que hagas sobre ella.

Un remoto es otro repositorio Git, normalmente en un servidor. Tu repositorio local guarda además unas referencias de seguimiento (origin/main) que recuerdan dónde estaba cada rama del remoto la última vez que hablaste con él.

De ahí salen las tres ideas que este módulo tiene que dejar firmes:

  • merge integra historial dentro de tu repositorio local.
  • fetch descarga sin integrar: mueve origin/main, no mueve main ni toca tus archivos.
  • push publica commits y referencias locales, y puede ser rechazado si el remoto ha avanzado.

Las pull requests no son un comando de Git: son una función de GitHub, GitLab u otra forja.

Parte 1 — Ramas e integración

Crear una rama de tarea

Partimos de un repositorio nuevo con un commit:

mkdir tienda && cd tienda
git init
printf "# Tienda\n\nProyecto de práctica.\n" > README.md
git add README.md && git commit -m "docs: crea README"

Crea una rama y sitúate en ella:

git switch -c feat/catalogo

Salida esperada:

Switched to a new branch 'feat/catalogo'

-c significa create. Sin -c, git switch cambia a una rama que ya existe. Comprueba en cualquier momento dónde estás:

git branch --show-current
git branch
feat/catalogo
* feat/catalogo
  main

El asterisco marca la rama activa. Ahora trabaja con normalidad:

printf "Camiseta\nSudadera\n" > catalogo.txt
git add catalogo.txt && git commit -m "feat: añade catálogo"

Integrar: fast-forward

Vuelve a main e integra:

git switch main
git merge feat/catalogo

Salida esperada, con hashes distintos a los tuyos:

Updating e9827d4..f3a4a50
Fast-forward
 catalogo.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 catalogo.txt

Fast-forward significa que main no había avanzado desde que creaste la rama, así que Git no ha tenido que fusionar nada: simplemente ha adelantado la referencia main hasta el commit de la rama. No se crea ningún commit nuevo y el historial queda en línea recta:

git log --oneline --graph --all
* f3a4a50 feat: añade catálogo
* e9827d4 docs: crea README

Integrar: commit de fusión

A veces interesa que la integración quede registrada aunque el fast-forward sea posible, para que el historial muestre que hubo una rama. Eso es --no-ff:

git switch -c feat/envios
printf "Envío en 24 h\n" > envios.txt
git add envios.txt && git commit -m "feat: añade condiciones de envío"
git switch main
git merge --no-ff feat/envios -m "merge: integra condiciones de envío"
Merge made by the 'ort' strategy.
 envios.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 envios.txt

ort es el nombre de la estrategia de fusión que usa Git por defecto; no hay que configurar nada. Ahora el grafo sí muestra la bifurcación:

git log --oneline --graph --all
*   43aa33b merge: integra condiciones de envío
|\
| * 5025fa5 feat: añade condiciones de envío
|/
* f3a4a50 feat: añade catálogo
* e9827d4 docs: crea README
Fast-forward Merge con commit de fusión
Cuándo ocurre main no ha avanzado desde que se creó la rama main ha avanzado, o se fuerza con --no-ff
Commits nuevos Ninguno Uno, con dos padres
Historial Línea recta Muestra la bifurcación y la integración

Parte 2 — Conflictos

Un conflicto no es un error ni un fallo de Git: es Git avisando de que dos ramas han cambiado las mismas líneas de forma distinta y que la decisión es humana.

Provocar un conflicto controlado

Desde main, crea una rama que cambie una línea del README:

git switch -c feat/horario
printf "# Tienda\n\nAbrimos de 9:00 a 14:00.\n" > README.md
git add README.md && git commit -m "docs: añade horario de mañana"

Vuelve a main y cambia esa misma línea de otra forma:

git switch main
printf "# Tienda\n\nAbrimos de 10:00 a 20:00.\n" > README.md
git add README.md && git commit -m "docs: añade horario continuo"

Intenta integrar:

git merge feat/horario
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

Leer el estado durante el conflicto

Lo primero, siempre, es git status:

git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   README.md

both modified es la marca del conflicto: las dos ramas tocaron el archivo. Fíjate en que Git te dice literalmente las dos salidas posibles, resolver o abortar.

Los marcadores de conflicto

Git ha escrito ambas versiones dentro del archivo:

cat README.md
# Tienda

<<<<<<< HEAD
Abrimos de 10:00 a 20:00.
=======
Abrimos de 9:00 a 14:00.
>>>>>>> feat/horario

Se lee así:

  • Entre <<<<<<< HEAD y ======= está tu versión, la de la rama en la que estás (main).
  • Entre ======= y >>>>>>> feat/horario está la versión de la rama que integras.

Los marcadores son texto normal dentro del archivo. Si los dejas ahí y confirmas, quedan guardados en el historial: ese es el error más habitual del módulo.

Resolver y cerrar el merge

Resolver significa dejar el archivo como debe quedar, sin marcadores. No estás obligado a elegir una de las dos versiones: puedes escribir una tercera.

printf "# Tienda\n\nAbrimos de 9:00 a 20:00, jornada continua.\n" > README.md
git add README.md
git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   README.md

git add sobre un archivo en conflicto significa «doy esta resolución por buena». Cierra el merge:

git commit

Git abre el editor con un mensaje de fusión ya redactado; puedes aceptarlo tal cual. Si prefieres no abrir el editor, git commit --no-edit usa ese mensaje directamente.

[main c054319] Merge branch 'feat/horario'

Comprueba el resultado:

git log --oneline --graph --all
git status --short
*   c054319 Merge branch 'feat/horario'
|\
| * 9649ec1 docs: añade horario de mañana
* | 74b279c docs: añade horario continuo
|/
*   43aa33b merge: integra condiciones de envío

git status --short no imprime nada: el árbol de trabajo está limpio.

Qué ha cambiado en cada zona

Zona Durante el conflicto Tras git add Tras git commit
Directorio de trabajo README.md con marcadores Contenido resuelto Contenido resuelto
Área de preparación Ruta en conflicto, sin resolver Resolución preparada Vacía
Historial local Merge a medias, sin commit Merge a medias, sin commit Commit de fusión con dos padres
Remoto Sin cambios Sin cambios Sin cambios hasta git push

La última fila es la que conviene repetir: nada de esto ha salido de tu equipo.

Cancelar: git merge --abort

Si el conflicto es mayor de lo esperado o no tienes la información para decidir, puedes deshacer el intento:

git merge --abort
git status --short

Git devuelve el repositorio al estado anterior al merge. Es la opción segura cuando dudas, y es preferible a improvisar con comandos destructivos. Empieza un merge siempre con el árbol de trabajo limpio: si tienes cambios sin confirmar, revísalos antes.

Parte 3 — Remotos

Un remoto de práctica

Para practicar no hace falta GitHub. Un repositorio bare (sin directorio de trabajo) sirve de remoto local:

git init --bare ../tienda-remoto.git
git remote add origin ../tienda-remoto.git
git push -u origin main
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

origin es solo un nombre convencional para el remoto principal. -u establece el seguimiento: a partir de ahora main sabe que se compara con origin/main, y eso es lo que permite que git status te informe de si vas por delante o por detrás.

git remote -v
origin	../tienda-remoto.git (fetch)
origin	../tienda-remoto.git (push)

Otra persona publica

Simula a un compañero clonando el remoto en otra carpeta:

cd ..
git clone tienda-remoto.git tienda-compa
cd tienda-compa
printf "Devoluciones en 30 días\n" > devoluciones.txt
git add devoluciones.txt && git commit -m "docs: añade política de devoluciones"
git push origin main
   c054319..7510d6a  main -> main

El remoto ha avanzado. Tu repositorio original todavía no sabe nada.

fetch: descargar sin integrar

Vuelve a tu repositorio y mira el estado antes de nada:

cd ../tienda
git status -sb
## main...origin/main

No informa de ninguna diferencia, porque tu repositorio aún cree que origin/main está donde lo dejaste. Descarga:

git fetch origin
From ../tienda-remoto
   c054319..7510d6a  main       -> origin/main

Lee bien esa flecha: lo que se ha actualizado es origin/main, no main. Compruébalo:

git status -sb
git log --oneline -1 main
git log --oneline -1 origin/main
ls devoluciones.txt
## main...origin/main [behind 1]
c054319 Merge branch 'feat/horario'
7510d6a docs: añade política de devoluciones
ls: devoluciones.txt: No such file or directory

Tres hechos en una sola pantalla:

  1. [behind 1]: vas un commit por detrás del remoto.
  2. main sigue en su commit; origin/main está en otro.
  3. El archivo del compañero no existe en tu directorio de trabajo.

Eso es exactamente lo que significa «descargar sin integrar». Y como ya lo tienes descargado, puedes revisarlo antes de aceptarlo:

git log --oneline main..origin/main
git diff --stat main origin/main
7510d6a docs: añade política de devoluciones
 devoluciones.txt | 1 +
 1 file changed, 1 insertion(+)

Integrar de forma explícita

Cuando has revisado y estás conforme, integras como cualquier otra rama:

git merge origin/main
ls devoluciones.txt
git status -sb
Updating c054319..7510d6a
Fast-forward
 devoluciones.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 devoluciones.txt
devoluciones.txt
## main...origin/main

Ahora sí: el archivo aparece y status vuelve a estar a la par.

Sobre git pull

git pull es fetch seguido de una integración automática, en un solo comando. Funciona, y con el tiempo lo usarás a diario. Pero mientras estés aprendiendo, hacer los dos pasos por separado tiene una ventaja concreta: puedes mirar qué te llega antes de aceptarlo. Con pull la integración ocurre antes de que hayas visto nada, y si aparece un conflicto te encuentras resolviéndolo sin haber elegido el momento.

Cuando push se rechaza

Si el remoto ha avanzado y tú también, push falla:

git push origin main
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '../tienda-remoto.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref.

Esto no es un problema que se arregle con fuerza. El rechazo te está protegiendo de sobrescribir el trabajo de otra persona. La salida correcta es siempre la misma secuencia:

git fetch origin
git status -sb
git log --oneline main..origin/main
git merge origin/main -m "merge: integra el trabajo del remoto"
git push origin main
## main...origin/main [ahead 1, behind 1]
b2a761c docs: añade contacto
Merge made by the 'ort' strategy.
   b2a761c..cbeea59  main -> main

[ahead 1, behind 1] describe la situación exacta: hay un commit tuyo que el remoto no tiene y uno del remoto que tú no tienes. Se llama historial divergente, y se resuelve integrando, no forzando. git push --force sobre una rama compartida borra commits de otras personas del remoto; no se usa en este curso.

Parte 4 — Colaboración en GitHub

Qué pone Git y qué pone la forja

Todo lo anterior ha funcionado sin GitHub. Lo que aporta una forja es el trabajo alrededor del código:

Lo pone Git Lo pone la forja (GitHub)
Ramas, commits, merge, conflictos Alojamiento del repositorio remoto
fetch, push, referencias de seguimiento Pull requests y revisión con comentarios
Todo el historial Protección de ramas y permisos
Integración continua (módulo 11)

No existe git pull-request. Una PR es un objeto de GitHub que envuelve algo que Git ya sabía hacer: comparar dos ramas.

El flujo con pull request

Con el repositorio alojado en GitHub, el ciclo de una tarea es:

git switch main
git fetch origin
git merge origin/main
git switch -c feat/precios
# … trabajar y hacer commits …
git push -u origin feat/precios

A partir del último comando, GitHub ofrece abrir la pull request desde su interfaz. Allí se revisa, se comenta y se aprueba; al aceptarla, GitHub ejecuta en su servidor el mismo merge que habrías hecho tú. Después, en tu equipo:

git switch main
git fetch origin
git merge origin/main
git branch -d feat/precios

git branch -d borra la rama local ya integrada. Si no lo estuviera, Git se niega y avisa: es una red de seguridad, no un obstáculo.

Revisar una contribución antes de aceptarla

Para ver qué aporta una rama sin ruido de otros cambios, se usa el diff de tres puntos:

git diff --stat main...feat/precios
git log --oneline main..feat/precios
 catalogo.txt | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
ce5d415 feat: añade precios

main...feat/precios, con tres puntos, compara la rama contra el punto en que ambas se separaron. Responde a «qué ha hecho esta rama», que es la pregunta de una revisión. Con dos puntos, main..feat/precios en git log lista los commits que tiene la rama y no tiene main.

Errores frecuentes

  • «He trabajado directamente en main sin darme cuenta».
    Comprueba con git branch --show-current antes de empezar. Si ya has hecho commits, aún puedes crear la rama desde donde estás: git switch -c feat/tarea la crea en el commit actual y se lleva el trabajo.

  • «He confirmado el archivo con los marcadores <<<<<<< dentro».
    Los marcadores son texto normal: Git no los borra por ti. Revisa el archivo antes de git add y, si ya has confirmado, corrígelo con un commit nuevo.

  • «Mi push ha sido rechazado, uso --force».
    El rechazo indica que el remoto tiene trabajo que tú no tienes. fetch, revisar, integrar y volver a publicar. En una rama compartida, --force destruye commits ajenos.

  • «He hecho fetch y no ha cambiado nada».
    Ha cambiado origin/main, no tu rama ni tus archivos. Es el comportamiento correcto: compruébalo con git status -sb y git log --oneline main..origin/main.

  • «Creo la pull request desde la terminal».
    No es un comando de Git. Publica la rama con push y abre la PR en GitHub.

  • «Resuelvo el conflicto quedándome siempre con mi versión».
    Lee las dos y decide con criterio; a veces la resolución correcta no es ninguna de las dos. Si no tienes información para decidir, git merge --abort y pregunta.

Ejercicio y criterios

Sobre un repositorio nuevo llamado agenda-taller con un commit inicial:

  1. Crea la rama feat/clientes, añade un archivo clientes.md y confírmalo. Intégrala en main con fast-forward.
  2. Crea feat/horarios y feat/tarifas desde el mismo commit, y haz que ambas modifiquen la misma línea de README.md. Integra la primera y resuelve el conflicto de la segunda escribiendo una tercera redacción.
  3. Crea un remoto bare de práctica y publica main con seguimiento.
  4. Clónalo en otra carpeta, haz allí un commit y publícalo.
  5. En el repositorio original, ejecuta git fetch y demuestra con salidas de comandos que origin/main avanzó y main no. Después integra.
  6. Provoca un push rechazado y resuélvelo sin usar --force.

Criterios de revisión:

  • git log --oneline --graph --all muestra el fast-forward, el commit de fusión y la resolución del conflicto.
  • Ningún archivo del historial contiene marcadores de conflicto (compruébalo con git log -S "<<<<<<<" --oneline).
  • El estado final es limpio y git status -sb no indica ahead ni behind.
  • Se explica, con la salida concreta, qué cambió al hacer fetch y qué cambió al integrar.
  • El push rechazado se resuelve con fetch e integración, y se justifica por qué no se usó fuerza.

Resumen

Al finalizar, el alumnado debe saber:

  • Crear ramas de tarea con git switch -c y saber siempre en cuál está.
  • Distinguir un fast-forward de un commit de fusión, y provocar cada uno.
  • Leer git status durante un conflicto, interpretar los marcadores y resolverlo.
  • Cancelar una integración con git merge --abort cuando no puede decidir con seguridad.
  • Explicar que fetch mueve origin/main y no toca ni main ni el directorio de trabajo.
  • Integrar de forma explícita y entender qué hace git pull por debajo.
  • Resolver un push rechazado sin recurrir a --force.
  • Situar las pull requests como función de GitHub, y revisar una contribución con git diff main...rama.

Siguiente módulo: Módulo 6 · Recuperación segura.

Estado editorial: listo.

Registro de revisión técnica

Revisado el 4 de agosto de 2026 con Git 2.47.1. Toda la secuencia se ha reproducido desde una carpeta vacía, con identidad de práctica y configuración aislada. Se ha comprobado que:

  • git merge feat/catalogo produce un fast-forward y git merge --no-ff feat/envios un commit de fusión con la estrategia ort; el grafo de git log --oneline --graph --all refleja ambos casos.
  • El conflicto en README.md genera CONFLICT (content), git status informa de both modified, el archivo contiene los marcadores <<<<<<< HEAD, ======= y >>>>>>> feat/horario, y tras git add el estado pasa a All conflicts fixed but you are still merging.
  • git merge --abort devuelve el repositorio al estado previo, con el contenido del archivo intacto.
  • Con un segundo clon que publica un commit, git fetch origin actualiza origin/main y deja main en su commit anterior: git status -sb pasa de ## main...origin/main a ## main...origin/main [behind 1] y el archivo del segundo clon no existe en el directorio de trabajo hasta ejecutar git merge origin/main.
  • Con historial divergente, git push devuelve ! [rejected] main -> main (fetch first), git status -sb muestra [ahead 1, behind 1] y la secuencia fetch → revisión → mergepush la resuelve sin fuerza.

Los hashes que aparecen en las salidas son los de esta ejecución y serán distintos en cada repositorio. Las rutas del remoto se han abreviado a ../tienda-remoto.git.

Pendiente al llevar el módulo a GitHub: sustituir el remoto bare de práctica por el repositorio real del curso en el punto 4, una vez creado, y comprobar el flujo de pull request con dos cuentas.