La lección 06-06 cerró el bloque técnico del curso con una constatación incómoda: Ana, Bruno y Carla dominan la herramienta, pero no han acordado cómo trabajar juntos. Y la primera de esas preguntas sin responder es también la más concreta: cuando alguien termina un cambio, ¿cómo lo propone? ¿Lo envía directamente a main y avisa por el chat? ¿Y qué pasa si esa persona no tiene permiso de escritura en el repositorio?

Ese caso ha dejado de ser hipotético. La empresa ha decidido publicar gestor-tareas como proyecto abierto en git.ejemplo.es/equipo/gestor-tareas, y ya ha aparecido el primer colaborador de fuera: Diego Rueda, desarrollador de una consultora que usa la aplicación en un cliente. Diego ha encontrado un fallo —la lista de tareas no se reordena al marcar una como completada— y quiere arreglarlo. Puede clonar el repositorio, porque es público. Puede crear una rama en su portátil. Lo que no puede es hacer git push: el servidor le responderá con un error de permisos.

Esta lección resuelve exactamente ese problema. Y de paso cierra la promesa pendiente del módulo 4: qué significa realmente el remoto llamado upstream.

Contenido

  1. El problema: proponer un cambio sin permiso de escritura
  2. Qué es un fork y por qué no es un concepto de Git
  3. El triángulo: origin, upstream y tu repositorio local
  4. Montar el triángulo paso a paso
  5. Mantener el fork al día
  6. Qué es una pull request (o merge request)
  7. Anatomía de una buena pull request
  8. Pull requests en borrador
  9. El recorrido completo de Diego, de principio a fin
  10. Descargar la rama de una PR para probarla en local
  11. Forks o ramas en el mismo repositorio: criterios para elegir

  1. El problema: proponer un cambio sin permiso de escritura

Recordemos cómo funciona el envío de cambios (lección 04-05). Cuando Ana ejecuta git push origin funcionalidad/orden-alfabetico, ocurren dos cosas:

  1. Git negocia con el servidor qué objetos faltan y los transfiere.
  2. El servidor actualiza una referencia: crea o mueve refs/heads/funcionalidad/orden-alfabetico.

El paso 2 es una escritura en el repositorio del servidor, y toda plataforma de alojamiento la protege con permisos. Ana, Bruno y Carla los tienen porque son el equipo. Diego no.

Si Diego lo intenta, ve esto:

git push origin correccion/reordenar-al-completar
remote: Permission to equipo/gestor-tareas.git denied to drueda.
fatal: unable to access 'https://git.ejemplo.es/equipo/gestor-tareas.git/':
The requested URL returned error: 403

Aquí es donde mucha gente se atasca, y conviene decirlo claro: Git no tiene ninguna respuesta a este problema. Git es un sistema distribuido; en su diseño original, la forma de proponer un cambio era enviar parches por correo electrónico (lo veremos en la lección 07-02). No existe un comando git propose ni git pull-request.

La respuesta la inventaron las plataformas de alojamiento, y consta de dos piezas complementarias:

Pieza Qué resuelve Dónde vive
Fork Darle a Diego un sitio donde sí pueda escribir Plataforma (GitHub, GitLab, Gitea…)
Pull request Darle un canal formal para proponer su cambio y discutirlo Plataforma

Ninguna de las dos es parte de Git. Ambas se construyen encima de operaciones de Git que ya conoces: clonar, enviar y fusionar.

  1. Qué es un fork y por qué no es un concepto de Git

La definición, sin adornos:

Un fork es una copia del repositorio hecha en el propio servidor, alojada bajo tu cuenta, sobre la que tienes permiso total de escritura.

Es decir: un git clone que ocurre de servidor a servidor, no de servidor a tu portátil. Diego pulsa el botón de crear fork y la plataforma crea git.ejemplo.es/drueda/gestor-tareas con todo el historial, todas las ramas y todas las etiquetas del original.

flowchart LR
    A["git.ejemplo.es/equipo/gestor-tareas<br/>(original — Diego solo puede leer)"]
    B["git.ejemplo.es/drueda/gestor-tareas<br/>(fork — Diego escribe)"]
    C["Portátil de Diego<br/>(clon local)"]
    A -->|"fork (en el servidor)"| B
    B -->|"git clone"| C
    C -->|"git push"| B
    B -.->|"pull request"| A

Fíjate en la flecha de puntos: el fork no devuelve nada automáticamente. Diego puede llenar su fork de commits durante meses y el repositorio original no se enterará. La pull request es el mecanismo explícito que dice "mira lo que tengo, ¿lo quieres?".

Tres consecuencias que conviene tener claras desde el principio:

  • El fork es un repositorio Git normal y corriente. Tiene su URL, se clona igual, acepta push igual. Lo único especial es que la plataforma recuerda de dónde salió, para poder ofrecerte crear pull requests contra el original.
  • El fork no se actualiza solo. Es una foto del momento en que lo creaste. Si el equipo sigue trabajando, tu fork se queda atrás. Mantenerlo al día es responsabilidad tuya (apartado 5).
  • La palabra tiene otro significado histórico. En el mundo del software libre, "hacer un fork" significaba tradicionalmente escindir un proyecto para llevarlo en otra dirección, con otro equipo (LibreOffice de OpenOffice, por ejemplo). Ese sentido sigue existiendo. El fork del que hablamos aquí es puramente técnico y temporal: una copia para poder contribuir.

Nombres en cada plataforma. GitHub, GitLab y Gitea lo llaman fork. Bitbucket también. GitLab, además, mantiene un enlace visible entre el fork y el original que permite sincronizar desde la interfaz.

  1. El triángulo: origin, upstream y tu repositorio local

Ahora Diego tiene tres repositorios en juego, no dos. Y ahí es donde entra la convención que el módulo 4 dejó anunciada.

En la lección 04-01 vimos que un remoto no es más que un nombre corto para una URL, y que origin y upstream no son palabras reservadas de Git: son costumbres. También avisamos de que upstream significa dos cosas distintas. Aquí usamos la primera:

upstream (como nombre de remoto): el remoto que apunta al repositorio original del que hiciste el fork.

El reparto convencional queda así:

Remoto Apunta a Permisos de Diego Se usa para
origin Su fork (drueda/gestor-tareas) Lectura y escritura push de sus ramas de trabajo
upstream El original (equipo/gestor-tareas) Solo lectura fetch para estar al día
flowchart TD
    U["upstream<br/>equipo/gestor-tareas"]
    O["origin<br/>drueda/gestor-tareas"]
    L["Local<br/>~/proyectos/gestor-tareas"]
    U -->|"git fetch upstream"| L
    L -->|"git push origin"| O
    O -->|"pull request"| U
    U -->|"fork inicial"| O

Léelo como un ciclo: el trabajo baja del original, pasa por tu local, sube a tu fork y vuelve al original en forma de propuesta. Ese recorrido triangular es la razón de que a esta configuración se la llame triangular workflow, y Git incluso tiene opciones pensadas para ella (remote.pushDefault, push.default = current), que veremos en el apartado 4.

El error más habitual de quien empieza es hacer git pull esperando traer los cambios del proyecto original, cuando pull habla con origin, que es tu fork, que no se ha movido. Se queda uno mirando la pantalla convencido de que "Git no baja nada". Y tiene razón: no hay nada que bajar de ahí.

  1. Montar el triángulo paso a paso

Diego ya ha creado el fork en la plataforma. Ahora, en su portátil:

# 1. Clonar SU FORK (no el original). origin queda configurado solo.
git clone [email protected]:drueda/gestor-tareas.git
cd gestor-tareas
# 2. Añadir el original como segundo remoto llamado upstream
git remote add upstream [email protected]:equipo/gestor-tareas.git

Un detalle práctico: como Diego nunca va a poder escribir en upstream, conviene usar la URL de solo lectura (https://) para ese remoto, o directamente desactivar el envío:

# Que un push accidental a upstream falle de inmediato, sin llegar al servidor
git remote set-url --push upstream NO_ENVIAR

Comprobamos el resultado:

git remote -v
origin    [email protected]:drueda/gestor-tareas.git (fetch)
origin    [email protected]:drueda/gestor-tareas.git (push)
upstream  [email protected]:equipo/gestor-tareas.git (fetch)
upstream  NO_ENVIAR (push)

Ahora git push upstream muere en local con un error claro en lugar de intentar autenticarse y fallar con un 403 confuso.

Ajustes que ahorran errores

Estas tres opciones convierten el triángulo en algo cómodo:

# Que 'git push' sin argumentos vaya siempre al fork
git config remote.pushDefault origin

# Que envíe la rama actual con su mismo nombre
git config push.default current

# Que 'git pull' rebase en lugar de fusionar (lección 05-01)
git config pull.rebase true

Y una más, muy útil: hacer que la rama main local siga al original, no al fork. Así git status le dice a Diego cuánto se ha alejado del proyecto de verdad, que es lo que le importa:

git branch --set-upstream-to=upstream/main main

Ojo con la coincidencia de palabras: aquí estamos usando los dos sentidos de upstream en la misma línea. --set-upstream-to configura la rama de seguimiento (lección 04-06) y upstream/main es la rama del remoto llamado upstream. Que el nombre coincida es casualidad de la convención, no magia.

  1. Mantener el fork al día

El fork envejece. Mientras Diego prepara su corrección, Ana y Carla siguen integrando cosas en equipo/gestor-tareas. Si Diego construye su rama sobre una base de hace tres semanas, su pull request llegará con conflictos y con código que ya no encaja.

La rutina de puesta al día son tres comandos:

# 1. Traer el estado real del proyecto original
git fetch upstream

# 2. Poner mi main local exactamente donde está el suyo
git switch main
git merge --ff-only upstream/main

# 3. Empujar ese main actualizado a mi fork
git push origin main

Merece la pena detenerse en cada uno.

git fetch upstream solo actualiza las referencias remotas upstream/*. No toca ninguna rama local ni el directorio de trabajo. Es siempre seguro.

git merge --ff-only upstream/main es deliberadamente estricto. Como vimos en la lección 03-03, --ff-only aborta si la fusión no puede resolverse avanzando el puntero. Y eso es exactamente lo que queremos: si falla, significa que hay commits en tu main local que no están en el original, lo cual en un fork casi siempre es un error (trabajaste directamente sobre main en lugar de crear una rama). Mejor enterarse con un error que con una fusión sorpresa.

Si de verdad quieres tirar tu main y adoptar el del original sin preguntas:

git switch main
git reset --hard upstream/main
git push --force-with-lease origin main

Esto es legítimo en tu propio fork, porque tu main no es un historial compartido: nadie más lo consume. Sigue aplicando la regla de oro de la lección 05-06 (no reescribas lo que otros han recibido), pero aquí el "otros" eres tú mismo. Y --force-with-lease en lugar de --force, siempre (lección 04-05).

¿Y la rama de trabajo?

Distinto caso. Si Diego ya tiene commits en correccion/reordenar-al-completar y el original ha avanzado, tiene dos opciones que ya conoce:

git switch correccion/reordenar-al-completar

# Opción A: rebase sobre el nuevo main (historial lineal, hay que reenviar con fuerza)
git rebase upstream/main
git push --force-with-lease origin correccion/reordenar-al-completar

# Opción B: fusionar main dentro de la rama (no reescribe, añade un commit de fusión)
git merge upstream/main
git push origin correccion/reordenar-al-completar

Cuál elegir depende del acuerdo del proyecto, y ese acuerdo es materia de la lección 08-02. Como orientación: mientras la pull request no tenga revisiones publicadas, el rebase es limpio y no molesta a nadie. Una vez hay comentarios sobre commits concretos, un rebase los descoloca y suele preferirse la fusión (o esperar a integrar y dejar que quien fusiona decida).

Sobre el botón "Sync fork". Muchas plataformas ofrecen sincronizar el fork desde la web con un clic. Hace exactamente lo del bloque de arriba, pero solo actualiza el fork en el servidor: tu clon local sigue sin enterarse hasta que hagas git fetch origin o git pull. Es una fuente clásica de confusión.

  1. Qué es una pull request (o merge request)

Definición operativa:

Una pull request es una petición formal de integrar los commits de una rama en otra, acompañada de un hilo de conversación, un diff calculado por la plataforma y un conjunto de comprobaciones automáticas.

Fíjate en lo que no dice. No dice "los cambios de un fork": una PR puede ir perfectamente de una rama a otra dentro del mismo repositorio, y de hecho así es como trabajan la mayoría de equipos internos (apartado 11). El fork es un caso particular, no la definición.

Una pull request tiene siempre estos elementos:

Elemento Qué es
Rama origen (source, compare, head) De dónde vienen los commits: drueda:correccion/reordenar-al-completar
Rama destino (target, base) Dónde se quieren integrar: equipo:main
Título y descripción La explicación para humanos
Diff Calculado desde el ancestro común (lección 07-02)
Conversación Comentarios generales y comentarios anclados a líneas concretas
Comprobaciones Resultados del CI (lección 07-06)
Estado Abierta, borrador, cerrada o integrada

El nombre cambia según la plataforma, pero la cosa es la misma:

Plataforma Nombre
GitHub, Gitea, Bitbucket Pull request (PR)
GitLab Merge request (MR)
Gerrit Change (con un modelo distinto, basado en un commit por cambio)

El nombre de GitHub es el históricamente correcto: "te pido que hagas pull de mi rama". El de GitLab describe mejor lo que realmente pasa al final: una fusión.

Y una precisión importante sobre el ciclo de vida: la PR es un objeto vivo. No es un envío de una sola vez. Si Diego hace git push de nuevos commits a la misma rama de su fork, la PR se actualiza sola: aparecen los commits nuevos, se recalcula el diff y se vuelven a lanzar las comprobaciones. No hay que crear una PR nueva por cada corrección.

  1. Anatomía de una buena pull request

Una PR es, ante todo, una petición de tiempo ajeno. Alguien va a dejar lo que estaba haciendo para leer tu trabajo. Todo lo que sigue va de respetar ese tiempo.

La rama

Nombre significativo, siguiendo la convención del proyecto (lección 03-06). En gestor-tareas: funcionalidad/, correccion/, documentacion/, hotfix/. correccion/reordenar-al-completar dice qué hay dentro antes de abrir nada; parche2 o arreglo-diego no dicen nada.

El tamaño

Este es el factor que más impacto tiene en la calidad de la revisión, y lo cuantificaremos con datos en la lección 07-02. De momento, la regla: una PR debe hacer una cosa. Si al escribir la descripción necesitas la palabra "y" tres veces, son tres PRs.

Un caso concreto: Diego encuentra el fallo de reordenación, pero de paso ve que estilos.css tiene indentación inconsistente y que README.md tiene un enlace roto. La tentación de arreglarlo todo en el mismo cambio es enorme. No lo hagas. El revisor tendrá que separar mentalmente el arreglo real de sesenta líneas de reformateo, y el arreglo real es lo único que importa.

Los commits

Que se puedan leer uno a uno. Aquí es donde se paga el rebase interactivo de la lección 05-02: antes de abrir la PR, mira tu propio historial y limpia.

git log --oneline upstream/main..HEAD
9f2a1c8 arreglado
7e3d0b4 ahora si
c5a8f21 pruebas
1d4e9a7 wip

Eso no se le enseña a nadie. Con un git rebase -i upstream/main y unos cuantos fixup, se convierte en:

a7c2e91 Reordenar la lista al marcar una tarea como completada

Un commit, un cambio, un mensaje que explica el porqué. Las reglas concretas de redacción de esos mensajes son el contenido de la lección 08-01; aquí basta con la idea de que el historial que propones forma parte de la propuesta.

La descripción

Una buena descripción responde a cuatro preguntas:

  1. Qué problema resuelve (y enlace al ticket: GT-214, la convención que ya validan los hooks del módulo 6).
  2. Qué has hecho y, si hay varias opciones, por qué esta.
  3. Cómo se comprueba: pasos concretos para reproducir el fallo y ver que ya no ocurre.
  4. Qué queda fuera, si has decidido conscientemente no abordar algo.

Ejemplo real de la PR de Diego:

## Problema (GT-214)

Al marcar una tarea como completada, la tarea se queda en su posición
original de la lista en lugar de moverse al final. Al recargar la página
sí aparece bien colocada, porque el orden se recalcula al leer de
localStorage.

## Solución

`marcarCompletada()` en `app.js` actualizaba el estado y repintaba solo
el elemento afectado. Ahora llama a `renderizarLista()`, que aplica el
mismo criterio de ordenación que la carga inicial.

## Cómo comprobarlo

1. Abrir `index.html` con tres tareas pendientes.
2. Marcar la primera como completada.
3. Debe desplazarse al final de la lista, sin recargar.

## Fuera de alcance

`estilos.css` tiene indentación inconsistente en la zona que he tocado.
No lo he arreglado aquí para no mezclar; lo propongo en otra PR.

Muchos proyectos automatizan esto con una plantilla de pull request: un fichero versionado (por ejemplo .github/pull_request_template.md o .gitlab/merge_request_templates/) cuyo contenido aparece precargado en la caja de descripción. Es un fichero de texto en el repositorio, nada más.

  1. Pull requests en borrador

Hay un momento incómodo: quieres enseñar el trabajo antes de terminarlo. Para pedir opinión sobre el enfoque, para que el CI lo pruebe, para que nadie duplique tu esfuerzo. Pero si abres una PR normal, alguien la revisará a fondo y perderá el tiempo, o peor, la integrará.

Para eso están las pull requests en borrador (draft en GitHub, merge request marcada como Draft en GitLab, que históricamente se indicaba con el prefijo WIP: en el título).

Una PR en borrador:

  • Es visible y tiene su hilo de conversación y su diff.
  • Lanza las comprobaciones automáticas igual que una normal (según configuración).
  • No se puede integrar hasta que se marque como lista.
  • Normalmente no pide revisores de forma automática.

Es la herramienta correcta para tres situaciones: pedir validación temprana del enfoque antes de invertir tres días, dejar constancia pública de que estás trabajando en algo, y usar el CI del proyecto para probar en entornos que no tienes en local (Carla, en Windows 11, la usa para comprobar que su cambio también funciona en Linux).

  1. El recorrido completo de Diego, de principio a fin

Todo junto, en orden. Diego parte de cero.

Paso 1: fork. En la plataforma, sobre equipo/gestor-tareas. Resultado: drueda/gestor-tareas.

Paso 2: clonar el fork y montar el triángulo.

git clone [email protected]:drueda/gestor-tareas.git
cd gestor-tareas
git remote add upstream https://git.ejemplo.es/equipo/gestor-tareas.git
git remote set-url --push upstream NO_ENVIAR
git config remote.pushDefault origin

Paso 3: partir del estado más reciente del original. No de lo que había cuando hizo el fork.

git fetch upstream
git switch -c correccion/reordenar-al-completar upstream/main

Este switch -c ... upstream/main es el gesto clave: crea la rama sobre la punta del proyecto real, no sobre su fork desactualizado.

Paso 4: trabajar. Editar app.js, confirmar, editar, confirmar. Sin preocuparse por la limpieza todavía.

git add app.js
git commit -m "Repintar la lista completa al marcar completada"
# ... más iteraciones

Paso 5: limpiar antes de enseñar.

git fetch upstream
git rebase -i upstream/main

Aplasta los tanteos, deja los commits que cuentan una historia, y de paso comprueba que sigue funcionando sobre la base actual.

Paso 6: enviar al fork.

git push -u origin correccion/reordenar-al-completar
remote: Crea una pull request para 'correccion/reordenar-al-completar' en:
remote:   https://git.ejemplo.es/equipo/gestor-tareas/compare/main...drueda:correccion/reordenar-al-completar
To [email protected]:drueda/gestor-tareas.git
 * [new branch]      correccion/reordenar-al-completar -> correccion/reordenar-al-completar

Ese mensaje remote: no lo inventa Git: lo emite el servidor mediante un hook post-receive (los que 06-01 dejó pendientes y que retomaremos en 07-06).

Paso 7: abrir la pull request. En la plataforma, con rama origen drueda:correccion/reordenar-al-completar y destino equipo:main, con la descripción del apartado 7.

Paso 8: la revisión. Ana revisa y pide un cambio: que la función no repinte la lista entera si no ha cambiado el orden, por rendimiento con muchas tareas.

Paso 9: responder a la revisión. Diego no abre una PR nueva. Trabaja en la misma rama:

git switch correccion/reordenar-al-completar
# editar app.js
git commit -am "Repintar solo si el orden ha cambiado"
git push origin correccion/reordenar-al-completar

La PR se actualiza sola. Ana ve el commit nuevo y puede revisar solo eso, sin releerlo todo.

Paso 10: integración. Ana aprueba y pulsa el botón. La plataforma ejecuta la fusión en el servidor —con el método que el proyecto haya decidido: fusión normal, squash o rebase, tema de la lección 07-04— y cierra la PR.

Paso 11: limpieza.

git switch main
git fetch upstream
git merge --ff-only upstream/main            # ya contiene su cambio
git push origin main                          # actualizar el fork
git branch -d correccion/reordenar-al-completar
git push origin --delete correccion/reordenar-al-completar

Y con git fetch --prune (lección 04-04), las referencias remotas obsoletas desaparecen.

  1. Descargar la rama de una PR para probarla en local

Ana no quiere revisar el cambio de Diego solo leyendo un diff en el navegador. Quiere ejecutarlo. Pero la rama está en el fork de Diego, un repositorio que ella no tiene configurado.

Hay tres formas de traérsela, de menos a más elegante.

Forma 1: añadir el fork como remoto

Funciona siempre, pero acumula remotos si revisas a mucha gente:

git remote add drueda https://git.ejemplo.es/drueda/gestor-tareas.git
git fetch drueda
git switch -c revision-diego drueda/correccion/reordenar-al-completar

Forma 2: la referencia especial de la PR

Aquí está el truco bueno. Las plataformas publican cada pull request como una referencia dentro del repositorio original, aunque la rama viva en un fork. En GitHub:

# La PR número 42, traída a una rama local llamada revision/pr-42
git fetch origin pull/42/head:revision/pr-42
git switch revision/pr-42

Léelo con lo que sabes de refspecs (lección 04-02): pull/42/head es la referencia en el servidor, revision/pr-42 es el nombre local. Los dos puntos separan origen y destino, exactamente igual que en cualquier otro refspec.

En GitLab, la ruta cambia de nombre pero el mecanismo es idéntico:

# GitLab: merge request número 42
git fetch origin merge-requests/42/head:revision/mr-42

Y en Gitea/Forgejo:

git fetch origin pull/42/head:revision/pr-42

GitHub publica además una segunda referencia muy útil:

Referencia Qué contiene
pull/42/head La punta de la rama tal como la envió su autor
pull/42/merge El resultado de fusionarla con la rama destino, precalculado por el servidor

pull/42/merge es lo que en realidad prueba el CI cuando comprueba una pull request, y es el origen de una sorpresa clásica que desarrollaremos en la lección 07-06: las pruebas no se ejecutan sobre tu rama, sino sobre tu rama fusionada con main.

Forma 3: configurar el refspec de una vez

Si revisas PRs a diario, añade esto a la configuración del remoto para que un simple git fetch traiga todas:

git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'
git fetch origin
git switch -c revision-42 origin/pr/42

En .git/config queda así:

[remote "origin"]
    url = [email protected]:equipo/gestor-tareas.git
    fetch = +refs/heads/*:refs/remotes/origin/*
    fetch = +refs/pull/*/head:refs/remotes/origin/pr/*

El + inicial permite actualizaciones no fast-forward, necesario porque el autor de una PR puede haber reescrito su rama con un rebase.

Cuidado en repositorios muy activos. En un proyecto con diez mil pull requests históricas, esa segunda línea hace que cada fetch traiga miles de referencias. Úsala en repositorios de tamaño razonable, o restringe el patrón.

Y un apunte práctico: las herramientas de línea de comandos de las plataformas (gh pr checkout 42 en GitHub, glab mr checkout 42 en GitLab) hacen exactamente esto por ti, incluyendo configurar el seguimiento para que puedas enviar correcciones si tienes permiso. Son cómodas, pero saber qué hay debajo es lo que te salva cuando no funcionan.

Combinado con lo del módulo 6, la revisión sale gratis en tiempo de contexto:

# Revisar sin abandonar lo que estabas haciendo (lección 06-06)
git fetch origin pull/42/head:revision/pr-42
git worktree add ../revision-42 revision/pr-42

  1. Forks o ramas en el mismo repositorio: criterios para elegir

Diego usa un fork porque no tiene otra opción. Ana, Bruno y Carla no están en esa situación: tienen permiso de escritura, y para ellos el fork sería un rodeo innecesario. Trabajan con ramas en el mismo repositorio y abren pull requests de rama a rama.

La comparación completa:

Aspecto Forks Ramas en el mismo repositorio
Permisos necesarios Solo lectura en el original Escritura en el original
Configuración Dos remotos, hay que mantener el fork al día Un remoto, nada que sincronizar
Visibilidad del trabajo Baja: las ramas están en repositorios ajenos Alta: git branch -r las muestra todas
Descargar la rama de otro Requiere refspec de PR o añadir remoto git fetch && git switch <rama>
Colaborar en la rama de otro Solo si el autor lo permite explícitamente Directo, si hay permiso
Aislamiento Total: nadie ensucia el repositorio principal Menor: las ramas abandonadas se acumulan
CI con secretos Restringido: las PRs desde forks no reciben credenciales Acceso completo
Adecuado para Proyectos abiertos, colaboradores externos, contratas Equipos con confianza mutua

El punto de los secretos en CI merece explicación, porque es una restricción de seguridad importante y sorprende a mucha gente. Si el CI del proyecto tuviera acceso a credenciales de despliegue al ejecutar el código de cualquier PR, cualquier persona de internet podría robarlas simplemente abriendo una PR cuyo código imprima las variables de entorno. Por eso las plataformas ejecutan las PRs procedentes de forks en un modo restringido, sin secretos, y a menudo exigiendo que un miembro del equipo apruebe la ejecución. Lo retomaremos en la lección 07-06.

Criterio práctico

La regla se reduce a una pregunta: ¿confías en esta persona para escribir en el repositorio?

  • → ramas en el mismo repositorio. Menos fricción, más visibilidad.
  • No, o todavía no → fork.

Muchos proyectos abiertos aplican la regla escalonada: los colaboradores de fuera usan forks, y cuando alguien demuestra constancia se le da permiso de escritura y pasa a trabajar con ramas. Diego, si sigue contribuyendo a gestor-tareas, acabará en el segundo grupo.

Un matiz sobre los permisos. "Permiso de escritura" no significa "puede romper main". Con las ramas protegidas de la lección 07-06, alguien puede crear ramas y abrir PRs pero seguir teniendo prohibido enviar directamente a main. Los dos mecanismos son independientes y se combinan.

Errores Comunes y Consejos

Error 1: clonar el original en lugar del fork. Diego clona equipo/gestor-tareas, trabaja, y al enviar recibe un 403. Se arregla sin perder nada: git remote rename origin upstream y git remote add origin <url-del-fork>.

Error 2: creer que el fork se actualiza solo. Es una foto del momento de creación. Si no ejecutas git fetch upstream periódicamente, trabajas sobre una base antigua y tu PR llegará con conflictos.

Error 3: git pull esperando los cambios del proyecto original. pull habla con origin, que es tu fork, que no se mueve solo. Lo que quieres es git fetch upstream.

Error 4: sincronizar el fork en la web y creer que el local está al día. El botón "Sync fork" actualiza el servidor. Tu portátil sigue igual hasta que hagas git fetch origin.

Error 5: trabajar directamente sobre main en el fork. Convierte cada puesta al día en un conflicto y te impide tener dos propuestas simultáneas. Rama nueva siempre, y creada sobre upstream/main.

Error 6: abrir una PR nueva por cada corrección de la revisión. La PR se actualiza sola al enviar a la misma rama. Cerrar y abrir otra tira a la basura toda la conversación previa.

Error 7: meter reformateos o arreglos ajenos en la misma PR. Convierte un diff de diez líneas revisables en uno de doscientas ilegibles. Una PR aparte, siempre.

Error 8: enviar sin limpiar el historial. Cuatro commits llamados wip, arreglado, ahora si y pruebas son una falta de respeto al revisor. git rebase -i upstream/main antes de enviar.

Error 9: borrar la rama del fork antes de que la PR se integre. La PR se queda sin contenido y la plataforma la cierra sola. Borrar después.

Consejo 1: desactiva el push a upstream. git remote set-url --push upstream NO_ENVIAR. Un error tipográfico menos.

Consejo 2: crea las ramas con git switch -c <rama> upstream/main. Explicitar la base evita el 90% de los conflictos de una PR.

Consejo 3: lee el CONTRIBUTING.md antes de abrir nada. Muchos proyectos documentan ahí el formato de mensajes, la rama destino correcta y si aceptan o no ciertos cambios. Ignorarlo es la vía rápida a que te cierren la PR.

Consejo 4: abre en borrador si aún no está listo. Es más honesto que un título con NO FUSIONAR en mayúsculas.

Consejo 5: aprende git fetch origin pull/<n>/head:<rama> de memoria. Es el comando que convierte la revisión en algo real y no en lectura de diffs en un navegador.

Consejo 6: combina la referencia de PR con git worktree. Revisar sin perder el contexto de tu propio trabajo (lección 06-06).

Ejercicios

Ejercicio 1: simular el triángulo en local

Sin plataforma ni internet, reproduce la topología completa con repositorios bare (lección 04-01):

  1. Crea /tmp/servidor/original.git como repositorio bare y publica en él un main con index.html, app.js y README.md.
  2. Simula el fork: crea /tmp/servidor/fork.git como clon bare del original.
  3. Clona el fork en /tmp/diego, añade upstream apuntando al original y desactiva su envío.
  4. Comprueba con git remote -v que el triángulo está bien montado.
  5. Desde otro clon /tmp/ana, añade un commit al original para que el fork quede desactualizado.
  6. Desde /tmp/diego, pon el main local al día y actualiza el fork.

Ejercicio 2: el ciclo completo de una contribución

Continuando con el escenario anterior:

  1. En /tmp/diego, crea correccion/enlace-roto sobre upstream/main y haz tres commits desordenados sobre README.md (uno bueno y dos de tanteo).
  2. Límpialos con rebase interactivo hasta dejar un solo commit con un mensaje decente.
  3. Envíalo al fork.
  4. Desde /tmp/ana, añade el fork como remoto, trae la rama de Diego, revísala con git log y git diff, y fusiónala en main del original.
  5. Vuelve a /tmp/diego, actualiza main, actualiza el fork y borra la rama en local y en el fork.

Ejercicio 3: refspecs de pull request

  1. En /tmp/servidor/original.git, crea a mano una referencia que imite la de una PR: toma el commit de la rama de Diego y guárdalo como refs/pull/1/head (pista: git update-ref, ejecutado dentro del repositorio bare).
  2. Desde /tmp/ana, trae esa referencia a una rama local llamada revision/pr-1 con un solo git fetch.
  3. Configura el refspec permanente en .git/config de /tmp/ana para que todas las refs/pull/*/head lleguen como origin/pr/*.
  4. Comprueba con git branch -r que aparece origin/pr/1.
  5. Crea un worktree en /tmp/revision-1 apuntando a esa referencia, sin abandonar tu rama actual.

Soluciones

Solución 1:

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

# Publicar contenido inicial desde un clon temporal
git clone /tmp/servidor/original.git /tmp/inicial
cd /tmp/inicial
git switch -c main 2>/dev/null || git switch main
echo "<h1>Gestor de Tareas</h1>" > index.html
echo "// app.js" > app.js
printf '# gestor-tareas\n\nVer docs en http://enlace-roto.ejemplo.es\n' > README.md
git add . && git commit -q -m "Estructura inicial de la aplicacion"
git push -u origin main
# El "fork": un clon bare de servidor a servidor
cd /tmp/servidor
git clone --bare /tmp/servidor/original.git fork.git
# El clon de Diego, con el triángulo montado
git clone /tmp/servidor/fork.git /tmp/diego
cd /tmp/diego
git remote add upstream /tmp/servidor/original.git
git remote set-url --push upstream NO_ENVIAR
git remote -v
origin    /tmp/servidor/fork.git (fetch)
origin    /tmp/servidor/fork.git (push)
upstream  /tmp/servidor/original.git (fetch)
upstream  NO_ENVIAR (push)
# Ana avanza el original
git clone /tmp/servidor/original.git /tmp/ana
cd /tmp/ana
echo "body { margin: 0; }" > estilos.css
git add . && git commit -q -m "Anadir hoja de estilos base"
git push origin main
# Diego se pone al día
cd /tmp/diego
git fetch upstream
git switch main
git merge --ff-only upstream/main
git push origin main
git log --oneline -2

Solución 2:

cd /tmp/diego
git fetch upstream
git switch -c correccion/enlace-roto upstream/main

sed -i 's|http://enlace-roto.ejemplo.es|https://docs.ejemplo.es/gestor-tareas|' README.md
git commit -qam "arreglo enlace"
echo "" >> README.md && git commit -qam "wip"
echo "## Documentacion" >> README.md && git commit -qam "ahora si"
git log --oneline upstream/main..HEAD
# Limpieza: aplastar los tres en uno
GIT_SEQUENCE_EDITOR="sed -i '2,3s/^pick/fixup/'" git rebase -i upstream/main
git commit --amend -q -m "Corregir el enlace roto de la documentacion en README

El enlace apuntaba a un dominio que se retiro. Ahora apunta al portal
de documentacion actual. Refs GT-231."
git log --oneline upstream/main..HEAD
b3f1a7d Corregir el enlace roto de la documentacion en README
git push -u origin correccion/enlace-roto
# Ana revisa e integra
cd /tmp/ana
git remote add drueda /tmp/servidor/fork.git
git fetch drueda
git log --oneline main..drueda/correccion/enlace-roto
git diff main...drueda/correccion/enlace-roto
git merge --no-ff drueda/correccion/enlace-roto -m "Fusionar correccion/enlace-roto de drueda"
git push origin main
# Diego limpia
cd /tmp/diego
git switch main
git fetch upstream
git merge --ff-only upstream/main
git push origin main
git branch -d correccion/enlace-roto
git push origin --delete correccion/enlace-roto

Solución 3:

# 1. Crear la referencia de PR a mano en el bare
COMMIT=$(git --git-dir=/tmp/servidor/fork.git rev-parse correccion/enlace-roto 2>/dev/null \
         || git -C /tmp/diego rev-parse origin/main)
git --git-dir=/tmp/servidor/original.git update-ref refs/pull/1/head "$COMMIT"
git --git-dir=/tmp/servidor/original.git show-ref | grep pull
b3f1a7d1c4e8f9a2b7d0c3e5f8a1b4d7e0c3f6a9 refs/pull/1/head

Nota: si el commit no existe en original.git porque no llegó a fusionarse, tráelo antes con git --git-dir=/tmp/servidor/original.git fetch /tmp/servidor/fork.git 'refs/heads/*:refs/remotes/fork/*'.

# 2. Traerla a una rama local
cd /tmp/ana
git fetch origin pull/1/head:revision/pr-1
git switch revision/pr-1
git log --oneline -1
# 3. Refspec permanente
git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'
git fetch origin
# 4. Comprobar
git branch -r
  origin/HEAD -> origin/main
  origin/main
  origin/pr/1
# 5. Worktree para revisar sin cambiar de contexto
git switch main
git worktree add /tmp/revision-1 origin/pr/1
git worktree list
/tmp/ana          a1b2c3d [main]
/tmp/revision-1   b3f1a7d (detached HEAD)

Conclusión

Esta lección ha respondido a la primera pregunta que dejó abierta el módulo 6: cómo propone un cambio quien no puede escribir en el repositorio. Lo esencial:

  • Ni el fork ni la pull request son conceptos de Git. Son invenciones de las plataformas construidas sobre operaciones que ya conocías: clonar, enviar y fusionar.
  • Un fork es un clon del repositorio hecho en el servidor, bajo tu cuenta, donde sí tienes permiso de escritura. No se actualiza solo.
  • El triángulo origin (tu fork) / upstream (el original) / local es la configuración estándar del colaborador externo, y cierra la convención upstream que anunciamos en el módulo 4. Recuerda que la palabra tiene otro sentido —la rama de seguimiento de la lección 04-06— y que no son lo mismo.
  • Mantener el fork al día es git fetch upstream + git merge --ff-only upstream/main + git push origin main. Crear las ramas con git switch -c <rama> upstream/main evita la mayoría de los conflictos.
  • Una pull request es una petición de integrar una rama en otra, más el hilo de conversación. Es un objeto vivo: enviar más commits a la rama la actualiza, no hace falta abrir otra.
  • Una buena PR: rama con nombre significativo, una sola cosa, tamaño acotado, commits legibles tras un rebase interactivo, y una descripción que diga qué problema resuelve y cómo comprobarlo.
  • Las PRs en borrador sirven para enseñar trabajo sin terminar sin que nadie pierda el tiempo revisándolo ni lo integre por error.
  • Las plataformas publican cada PR como una referencia del repositorio original: git fetch origin pull/<n>/head:<rama> en GitHub y Gitea, merge-requests/<n>/head en GitLab. Es la forma de probar de verdad lo que vas a revisar. GitHub publica además pull/<n>/merge, el resultado precalculado de la fusión, que será importante en la lección 07-06.
  • Forks para quien no tiene permiso (proyectos abiertos, colaboradores externos, restricción de secretos en CI); ramas en el mismo repositorio para equipos con confianza mutua. Ana, Bruno y Carla usan ramas; Diego usa fork.

Ya tenemos el canal por el que llega una propuesta. Falta lo que ocurre dentro de ese canal: qué mira exactamente Ana cuando revisa el trabajo de Diego, con qué comandos lo examina sin depender del navegador, cómo comenta sin desmoralizar a nadie y cuándo aprueba. Ese es el contenido de la lección 07-02: Revisiones de Código con Git, donde descubriremos, entre otras cosas, por qué el diff correcto para revisar lleva tres puntos y no dos.

Dominando Git: De Principiante a Avanzado

Módulo 1: Introducción a Git

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

Módulo 6: Herramientas y Técnicas de Git

Módulo 7: Estrategias de Colaboración y Flujo de Trabajo

Módulo 8: Mejores Prácticas y Consejos de Git

Módulo 9: Solución de Problemas y Depuración

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados