Al cerrar el módulo 9 dejamos a Ana, Bruno, Carla y Diego dominando Git sobre gestor-tareas: saben construir el historial, reescribirlo con criterio, colaborar con un proceso y salir de los líos. Pero gestor-tareas son cuatro ficheros y cuatro personas. El mundo real es más grande y más raro.
Esta lección no enseña comandos nuevos. Enseña algo que a estas alturas vale más: criterio. Vamos a mirar cómo usan Git cuatro tipos de proyecto muy distintos —desde el más grande y más antiguo hasta uno exactamente igual que el vuestro— y a extraer de cada uno qué problema resuelve su flujo, qué precio paga por él y qué hace falta para que funcione.
La conclusión que buscamos ya te la adelanto, porque es la única que hay que llevarse: no existe "el flujo correcto de Git". Existe el flujo que encaja con el tamaño del equipo, con la confianza entre sus miembros, con el ritmo de publicación y con lo que cuesta equivocarse. Copiar el flujo del kernel de Linux en un equipo de cuatro personas es tan absurdo como coordinar el kernel por pull requests de una plataforma web.
Todo lo que se describe aquí es público y bien documentado: los flujos de trabajo del kernel están escritos en su propia documentación, los proyectos de código abierto publican sus guías de contribución, y las prácticas de monorepo empresarial se han descrito en artículos y charlas técnicas durante años. Cuando dé una magnitud, la daré como orden de magnitud aproximado, nunca como dato exacto: lo que importa es la escala, no la cifra.
Contenido
- Cómo leer un estudio de caso
- Caso 1: el kernel de Linux, el flujo por correo electrónico
- La jerarquía de mantenedores y el "árbol de árboles"
- Por qué el kernel no usa pull requests de plataforma
- Qué exige el modelo del kernel a quien contribuye
- Caso 2: un proyecto de código abierto de tamaño medio
- La infraestructura social de un proyecto abierto
- Caso 3: una empresa con monorepo
- Caso 4: un equipo pequeño de producto (el nuestro)
- Los cuatro modelos, comparados
- La lección transversal: el flujo se elige, no se hereda
- Cómo leer un estudio de caso
Antes de mirar ningún proyecto conviene fijar las preguntas. Todo flujo de trabajo con Git, por complejo que parezca, responde a cinco preguntas:
| Pregunta | Qué determina |
|---|---|
| ¿Cómo propone un cambio alguien de fuera del núcleo? | El mecanismo de propuesta: correo, pull request, rama directa |
| ¿Quién decide que un cambio entra? | La estructura de autoridad: un mantenedor, dos revisores, un propietario de directorio |
| ¿Sobre qué rama se trabaja y durante cuánto tiempo? | El modelo de ramas y su vida media |
| ¿Qué aspecto tiene el historial resultante? | Si es lineal, si tiene merges, si cada commit compila |
| ¿Qué hace falta para que eso funcione? | La infraestructura y la disciplina necesarias |
La quinta es la que casi siempre se ignora, y la que explica por qué un flujo copiado de otro sitio fracasa. Un flujo no es solo una secuencia de comandos: es una secuencia de comandos más las herramientas, las normas y los hábitos que la sostienen.
Vamos a responder esas cinco preguntas para cada caso.
- Caso 1: el kernel de Linux, el flujo por correo electrónico
El kernel de Linux es el proyecto para el que se escribió Git, en 2005, cuando el equipo perdió el acceso a la herramienta que usaba hasta entonces. Es también el proyecto que sigue usando el flujo más antiguo y, para casi todo el mundo, el más sorprendente: los cambios se proponen por correo electrónico, en listas públicas, en forma de parches de texto.
Las magnitudes, como órdenes de magnitud: decenas de millones de líneas de código, decenas de miles de ficheros, más de veinte años de historial en Git (y más de treinta de proyecto), y del orden de miles de personas contribuyendo cada año. Ningún equipo pequeño se parece a esto, y por eso el caso es instructivo: muestra qué pasa cuando llevas la colaboración con Git a su límite.
El ciclo de un parche
flowchart TD
A["Desarrollador<br/>hace commits locales"] --> B["git format-patch<br/>genera ficheros .patch"]
B --> C["git send-email<br/>a la lista del subsistema"]
C --> D["Revisión pública<br/>en la lista de correo"]
D -->|"cambios pedidos"| E["Nueva versión: v2, v3…"]
E --> C
D -->|"aceptado"| F["Mantenedor:<br/>git am aplica el parche"]
F --> G["Árbol del subsistema"]
G --> H["git request-pull<br/>al mantenedor superior"]
H --> I["Árbol principal (Linus)"]
Fíjate en un detalle importante: el parche viaja como texto, no como una referencia a un repositorio. Quien lo recibe no necesita añadir un remoto, ni tener acceso al repositorio de quien lo envía, ni siquiera saber si ese repositorio existe. Recibe un correo, y ese correo contiene el cambio entero.
git format-patch: convertir commits en correos
Cada fichero es un correo completo: cabeceras, asunto tomado de la primera línea del mensaje del commit, cuerpo del mensaje, y el diff al final. Si aplicamos esto a nuestro proyecto para verlo por dentro:
From 8a1f6c3d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c Mon Sep 17 00:00:00 2001 From: Ana Ferrer <[email protected]> Date: Fri, 31 Jul 2026 13:11:58 +0200 Subject: [PATCH 1/2] GT-142 mostrar el contador de pendientes Se añade un contador en la cabecera con el número de tareas no completadas, que se recalcula al marcar o desmarcar una tarea. Signed-off-by: Ana Ferrer <[email protected]> --- app.js | 12 ++++++++++++ index.html | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-)
Ese From 8a1f6c3d... de la primera línea no es una cabecera de correo real: es el hash del commit original, que Git guarda ahí para que quien lo aplique pueda saber de dónde salió. La fecha Mon Sep 17 00:00:00 2001 es un valor centinela histórico que Git escribe siempre; no significa nada.
Opciones que se usan de verdad:
# Numerar y añadir una carta de presentación (0000-cover-letter.patch)
git format-patch main --cover-letter --numbered
# Marcar que es la segunda versión de la serie tras la revisión
git format-patch main -v2
# Solo los tres últimos commits
git format-patch -3
# Con el nombre de la rama en el asunto, útil para series largas
git format-patch main --subject-prefix="PATCH gestor-tareas"La carta de presentación (--cover-letter) es el correo 0/N de la serie: explica el conjunto, no cada parche. En el kernel es donde se argumenta el porqué del cambio completo, y donde se resume qué ha cambiado respecto a la versión anterior de la serie.
git send-email: enviarlos
git send-email [email protected] \
[email protected] \
0001-*.patch 0002-*.patchgit send-email los envía como un hilo de correo: la carta de presentación es el mensaje raíz y los parches cuelgan de ella como respuestas. Requiere configurar un servidor SMTP:
git config --global sendemail.smtpServer smtp.ejemplo.es
git config --global sendemail.smtpUser [email protected]
git config --global sendemail.smtpEncryption tls
git config --global sendemail.smtpServerPort 587Es el comando que más gente ha instalado y nunca ha llegado a usar. Pero conviene conocerlo: cuando alguien te diga "mándame el parche", esto es lo que quiere decir.
git am: aplicar el parche recibido
Al otro lado, el mantenedor guarda el correo y lo aplica:
# Aplicar una serie de parches en orden
git am 0001-*.patch 0002-*.patch
# Aplicar directamente desde un buzón de correo
git am /ruta/al/buzon.mbox
# Añadir mi propia firma al aplicarlo (práctica estándar en el kernel)
git am --signoff 0001-*.patcham significa apply mailbox. Crea un commit por parche conservando el autor original y poniendo al mantenedor como committer. Aquí se ve por primera vez con sentido práctico la distinción autor/committer que apareció en el modelo de datos (lección 01-04): el kernel la usa constantemente, porque quien escribe el código y quien lo integra casi nunca son la misma persona.
Si un parche no aplica limpiamente:
# Ver dónde ha fallado
git am --show-current-patch=diff
# Intentar una aplicación con más margen usando la información de blobs
git am -3
# Arreglar a mano, marcar como resuelto y continuar
git add .
git am --continue
# O abandonar la serie entera y volver al estado previo
git am --abortgit am -3 (fusión a tres bandas) es el que salva la mayoría de los casos: usa los hashes de blob que el parche lleva incrustados para reconstruir el contexto y hacer una fusión real en lugar de una aplicación textual. Es exactamente el mecanismo de tres bandas de la lección 03-03, aplicado a un parche.
git request-pull: pedir que integren tu árbol
Cuando el mantenedor de un subsistema ya tiene los parches en su repositorio y quiere que el nivel superior los integre, no manda parches: manda una petición de extracción por correo, generada por Git:
The following changes since commit 3f2a1b9c...:
Linux 6.10 (2026-06-14 18:02:11 -0700)
are available in the Git repository at:
https://git.ejemplo.es/subsistema-red.git para-6.11
for you to fetch changes up to 9d4e7f2a...:
net: anadir prueba de regresion (2026-07-28 11:40:03 +0200)
----------------------------------------------------------------
Ana Ferrer (2):
net: corregir fuga de memoria en el driver
net: anadir prueba de regresion
drivers/net/ejemplo.c | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)Esto es el pull request original, el de verdad: un correo que dice "en este repositorio, en esta rama, desde este commit base, hay estos cambios; tráetelos". Las plataformas web que usamos a diario no inventaron el concepto: le pusieron una interfaz encima. Volveremos sobre request-pull en la lección 10-02, como puente entre los dos mundos.
- La jerarquía de mantenedores y el "árbol de árboles"
El kernel no tiene un repositorio: tiene cientos. Cada subsistema —red, sistemas de ficheros, gráficos, una arquitectura de procesador concreta— tiene su propio repositorio público y su propio mantenedor.
flowchart BT
D1["Colaborador"] --> S1["Árbol de un driver"]
D2["Colaborador"] --> S1
D3["Colaborador"] --> S2["Árbol de otro driver"]
S1 --> M1["Árbol del subsistema<br/>(red)"]
S2 --> M1
S3["Árbol del subsistema<br/>(sistemas de ficheros)"] --> T["Árbol principal"]
M1 --> T
S4["Árbol del subsistema<br/>(gráficos)"] --> T
T --> R["Publicación<br/>vX.Y"]
El flujo de la información es de abajo hacia arriba, por confianza. Cada nivel confía en el de abajo porque conoce a esas personas y ha revisado su trabajo durante años, y responde ante el de arriba por lo que integra. El árbol principal no revisa cada parche: revisa peticiones de extracción de mantenedores en los que confía.
Esto tiene tres consecuencias técnicas muy visibles en el historial:
- El historial principal está lleno de commits de fusión entre árboles de subsistema. No es desorden: cada merge documenta "en esta fecha integré el trabajo de este subsistema". El mensaje de esos merges suele ser el texto del
request-pullcorrespondiente. - Los parches individuales llegan ya limpios, porque se han discutido y reescrito en la lista antes de entrar en ningún árbol. La limpieza no se consigue reescribiendo después: se consigue no dejando entrar lo sucio.
- El ritmo es de ventana. Hay periodos en los que el árbol principal acepta cambios nuevos y periodos en los que solo acepta correcciones, cerrando con una publicación. Ese ritmo hace previsible el trabajo de todos los niveles inferiores.
Signed-off-by y el certificado de origen
Cada commit del kernel lleva una o varias líneas al final:
Signed-off-by: Ana Ferrer <[email protected]> Signed-off-by: Bruno Salas <[email protected]>
No es una firma criptográfica (eso es git commit -S, lección 08-05). Es una declaración legal: quien la pone certifica que tiene derecho a aportar ese código bajo la licencia del proyecto. Se llama Developer Certificate of Origin, es un texto público y corto, y se añade con:
Cada nivel de la jerarquía añade la suya al integrar, de modo que el commit acaba llevando la cadena completa de custodia: quién lo escribió y por qué manos ha pasado hasta el árbol principal. Es trazabilidad social escrita en el propio objeto commit.
- Por qué el kernel no usa pull requests de plataforma
Es la pregunta obvia. La respuesta no es "por costumbre" ni "por resistencia al cambio", aunque la inercia pese. Hay razones estructurales:
| Razón | Explicación |
|---|---|
| Independencia de proveedor | Depender de una plataforma concreta significa que su política, su disponibilidad y su precio condicionan el proyecto. El correo lo sirve cualquiera. |
| La revisión es el producto | En una lista, la discusión técnica queda archivada, es citable, se puede responder en línea sobre el propio código y la lee gente que no estaba siguiendo ese cambio. Es una revisión pública por defecto, no privada por defecto. |
| Escala de participantes | Miles de personas revisando en paralelo cientos de series simultáneas funciona mejor con filtros de correo que con notificaciones de una interfaz web. |
| Nadie necesita permisos | Para proponer un cambio no hace falta una cuenta, ni un fork, ni que nadie te dé acceso a nada. Basta con saber la dirección de la lista. |
| El parche es autocontenido | Se puede aplicar, guardar, reenviar, citar y archivar sin depender de que ningún servidor siga existiendo dentro de quince años. |
Y también hay costes, que conviene decir con la misma claridad:
- La barrera de entrada es alta. Configurar el correo saliente para que no destroce los parches es un rito de paso famoso por lo doloroso.
- No hay estado centralizado de "en qué punto está esta propuesta". Existen herramientas externas de seguimiento de parches para paliarlo, pero no es lo mismo que un panel.
- La integración continua es más difícil de atar a una propuesta concreta, porque la propuesta es un correo, no una rama de un servidor.
En los últimos años han aparecido herramientas que automatizan las partes tediosas (enviar series, recuperarlas desde los archivos públicos, gestionar versiones sucesivas) sin abandonar el correo. La tendencia no es sustituir el flujo, sino hacerlo menos áspero.
- Qué exige el modelo del kernel a quien contribuye
Este es el punto de la lección donde el caso deja de ser una curiosidad y empieza a ser útil para ti, aunque nunca envíes un parche al kernel.
Exigencia 1: cada commit debe ser correcto por sí solo. No "el conjunto funciona": cada commit compila y pasa las pruebas, porque git bisect (lección 06-02) tiene que poder detenerse en cualquiera de ellos. Esto obliga a partir el trabajo en pasos coherentes, y es lo que convierte una serie de parches en algo revisable.
Exigencia 2: el mensaje se lee solo. Un mantenedor recibe el parche en un correo, sin acceso a tu ticket, sin tu contexto y sin poder preguntarte en el pasillo. El mensaje es la documentación del cambio. Todo lo que vimos en la lección 08-01 —el porqué antes que el qué, el imperativo, el cuerpo que explica la alternativa descartada— aquí no es una buena práctica: es un requisito de funcionamiento.
Exigencia 3: una serie es una narración. Los parches se ordenan de forma que cada uno prepare el siguiente: primero la refactorización que no cambia el comportamiento, luego el cambio de comportamiento, luego las pruebas. Es exactamente el uso del rebase interactivo de la lección 05-02, con un propósito editorial.
Exigencia 4: aceptas reescribir. La versión 5 de una serie es normal. Se rehace con rebase -i, se vuelve a enviar con -v5, y en la carta de presentación se explica qué cambió respecto a v4. Para eso sirve git range-diff (lección 07-02), que compara dos versiones de una misma serie.
Aplicado a nuestro proyecto, así se prepararía Ana para enviar una serie en este estilo:
# 1. Limpiar la serie: reordenar, fusionar los "arreglar typo", reescribir mensajes
git rebase -i main
# 2. Comprobar que cada commit compila, uno por uno
git rebase main --exec "npm test"
# 3. Generar la v2 y comparar con la v1 que ya envié
git format-patch main -v2 --cover-letter
git range-diff main v1-final mainEl --exec del segundo paso es una joya poco conocida: ejecuta ese comando después de aplicar cada commit del rebase, y se detiene en el primero que falle. Es la forma mecánica de garantizar la exigencia 1.
- Caso 2: un proyecto de código abierto de tamaño medio
Bajemos una escala. Hablamos de un proyecto con cientos de colaboradores, decenas de ellos habituales, un puñado de mantenedores, y una plataforma de alojamiento (git.ejemplo.es en nuestro universo ficticio). Una biblioteca popular, un marco de trabajo, una herramienta de línea de comandos conocida.
Este es exactamente el modelo de Diego, el colaborador externo de la lección 07-01, multiplicado por doscientos.
El flujo
flowchart LR
A["Fork personal"] --> B["Rama de tema"]
B --> C["Pull request<br/>al repositorio principal"]
C --> D["CI automática"]
C --> E["Revisión humana"]
D --> F{"¿Todo verde?"}
E --> F
F -->|sí| G["Squash y merge<br/>a main"]
F -->|no| B
Nada de esto es nuevo para ti: es el módulo 7 entero. Lo que cambia con la escala no es la mecánica, sino cuánto trabajo hace la infraestructura para que los mantenedores no se ahoguen.
La aritmética del mantenedor
El dato que explica todo lo demás: en un proyecto así, la revisión es el recurso escaso. Hay cientos de personas capaces de escribir una propuesta y quizá cinco con derecho y tiempo para integrarla. Cada minuto que un mantenedor gasta explicando cómo se ejecutan las pruebas, pidiendo que se rebasee una rama o cerrando propuestas duplicadas es un minuto que no dedica a revisar código.
De ahí que todo el diseño del proceso persiga un objetivo: que la propuesta llegue ya en condiciones.
- La infraestructura social de un proyecto abierto
Estas son las piezas concretas, y merecen atención porque son directamente aplicables a cualquier repositorio, incluido el vuestro.
CONTRIBUTING.md
Un fichero en la raíz del repositorio que las plataformas muestran automáticamente a quien va a abrir una propuesta. Contiene lo que un mantenedor está cansado de repetir:
# Cómo contribuir a gestor-tareas
## Antes de empezar
- Abre una incidencia antes de escribir código para un cambio grande.
- Busca si ya existe una propuesta abierta sobre lo mismo.
## Preparar el entorno
git clone https://git.ejemplo.es/gestor-tareas.git
npm install
npm test
## Convenciones
- Mensajes de commit: Conventional Commits (feat:, fix:, docs:…).
- Una propuesta, un tema. Si tu rama toca dos cosas, parte en dos.
- Rebasea sobre `main` antes de pedir revisión; no fusiones `main` en tu rama.
## Qué esperar
- La CI debe estar en verde antes de que revisemos.
- Un mantenedor responderá en unos días laborables.
- Integramos con squash: tu rama se convierte en un commit en `main`.Fíjate en la última línea. Decir de antemano qué política de integración se aplica (merge, squash o rebase, lección 08-02) evita la discusión más recurrente en las propuestas de gente nueva.
Plantillas de propuesta y de incidencia
Ficheros dentro de un directorio de configuración de la plataforma que precargan el formulario. La plantilla de propuesta suele ser una lista de comprobación:
## Qué cambia
<!-- Descripción breve del cambio y del problema que resuelve -->
## Ticket relacionado
Cierra GT-
## Comprobaciones
- [ ] He ejecutado `npm test` en local y pasa
- [ ] He añadido pruebas para el comportamiento nuevo
- [ ] He actualizado el README si el cambio afecta al uso
- [ ] Mis commits siguen Conventional CommitsSu efecto real no es documental, es psicológico: quien abre la propuesta ve las casillas vacías y se le ocurre revisarlas antes de que se lo pidan.
Etiquetas
Un sistema de etiquetas bien pensado convierte una lista inabarcable de incidencias en un mapa navegable:
| Familia de etiquetas | Ejemplos | Para qué sirve |
|---|---|---|
| Tipo | error, mejora, documentación |
Clasificar de un vistazo |
| Estado | necesita-revisión, esperando-respuesta, bloqueado |
Saber de quién es el turno |
| Dificultad | primera-contribución, ayuda-buscada |
Canalizar a la gente nueva |
| Área | interfaz, almacenamiento, ci |
Enrutar al mantenedor adecuado |
La etiqueta primera-contribución (o su equivalente habitual en inglés) es la más rentable de todas: es cómo un proyecto convierte a lectores en colaboradores.
Automatización de lo repetitivo
En un proyecto de esta escala, los robots hacen el trabajo que en gestor-tareas hace una persona:
name: Comprobaciones de propuesta
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
validar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Comprobar el formato de los mensajes de commit
run: |
git log --format=%s origin/main..HEAD | while read -r asunto; do
echo "$asunto" | grep -qE '^(feat|fix|docs|refactor|test|chore)(\(.+\))?: .+' \
|| { echo "Mensaje no conforme: $asunto"; exit 1; }
done
- name: Verificar que la rama está al día con main
run: |
git merge-base --is-ancestor origin/main HEAD \
|| echo "::warning::Rebasea sobre main antes de pedir revisión"
- name: Pruebas
run: npm ci && npm testEse git merge-base --is-ancestor es un comando de fontanería (lección 09-06) que responde con un código de salida: ¿es origin/main un antepasado de HEAD? Si lo es, la rama está al día.
Qué historial produce este modelo
Casi siempre lineal en main, con un commit por propuesta integrada, mensaje normalizado y una referencia al número de la propuesta. Es el resultado de la política de squash, y tiene una virtud enorme a esta escala: git log --oneline en main se lee como un registro de cambios del proyecto, no como el diario de doscientas personas.
El precio, ya discutido en la lección 08-02, es que se pierde el detalle interno de cada propuesta. En un proyecto abierto se acepta de buen grado, porque ese detalle sigue existiendo en la propia propuesta, archivada en la plataforma.
- Caso 3: una empresa con monorepo
Tercer modelo, radicalmente distinto: una sola empresa, un solo repositorio, muchos productos dentro. La aplicación web, el servicio de datos, la aplicación móvil, las bibliotecas compartidas, las definiciones de infraestructura: todo en el mismo árbol de directorios y en el mismo historial.
Varias empresas grandes han descrito públicamente este modelo durante años. Las magnitudes, siempre como orden de magnitud: cientos de miles a millones de ficheros, millones de commits, y miles de personas trabajando sobre el mismo repositorio. A esa escala, Git de serie no basta, y ese es el tema de la lección 10-04. Aquí nos interesa el porqué, no el cómo.
Por qué alguien elige esto
Razón 1: el cambio atómico entre proyectos. Si la biblioteca compartida cambia de interfaz y hay treinta consumidores, en un monorepo eso es un solo commit que actualiza la biblioteca y los treinta a la vez. Nunca existe un instante en el que el repositorio esté incoherente. Con repositorios separados, ese mismo cambio son treinta y una propuestas coordinadas, versiones intermedias de compatibilidad y semanas de trabajo.
Este es exactamente el problema que dejamos abierto en la lección 06-05 con componentes-ui: cuando cambia el submódulo, hay que actualizar el puntero en el repositorio padre, y durante un rato las dos cosas no cuadran. En un monorepo ese problema no existe porque no hay dos repositorios.
Razón 2: la refactorización global es posible. Renombrar una función usada en toda la empresa es una búsqueda y sustitución más un commit. Con repositorios separados, es un proyecto.
Razón 3: una sola versión de todo. No hay "qué versión de la biblioteca usa el servicio de facturación". Usa la que está en main, igual que todos.
Razón 4: visibilidad y reutilización. Cualquiera puede leer cualquier código, encontrar quién lo mantiene y proponer un cambio.
El precio
| Coste | Cómo se paga |
|---|---|
| Git de serie se ahoga | Clones parciales, sparse-checkout, servidores especializados, cachés de compilación (10-04) |
| Todo el mundo ve todo | Puede ser inaceptable si hay código con requisitos de confidencialidad |
| La CI no puede ejecutarlo todo | Hace falta CI selectiva: calcular qué se ve afectado por el cambio |
| Herramientas propias | Sistema de compilación con grafo de dependencias, herramientas de propiedad de código, automatización de migraciones |
| Nadie puede clonar y empezar | La incorporación de gente nueva requiere formación en el entorno |
Propiedad del código por directorios
La pieza organizativa clave. Un fichero de texto en el repositorio declara quién debe aprobar los cambios de cada zona del árbol:
# CODEOWNERS # Regla por defecto: el equipo de plataforma * @plataforma # Cada equipo es dueño de su directorio /servicios/facturacion/ @equipo-facturacion /servicios/tareas/ @equipo-tareas /bibliotecas/componentes-ui/ @equipo-diseno @equipo-tareas # Lo crítico exige aprobación adicional /infra/produccion/ @plataforma @seguridad /bibliotecas/autenticacion/ @seguridad
La plataforma lee ese fichero, calcula qué directorios toca cada propuesta y asigna automáticamente los revisores obligatorios. Combinado con las ramas protegidas de la lección 07-06, el resultado es que la autonomía se recupera sin renunciar al repositorio único: cada equipo manda en lo suyo, pero tocar la biblioteca de autenticación requiere el visto bueno de seguridad.
Es la respuesta a la objeción más frecuente contra el monorepo. "Si está todo junto, ¿cualquiera puede cambiar cualquier cosa?" Técnicamente sí; en la práctica, no sin la aprobación de quien es dueño de esa zona.
Integración continua selectiva
Si el repositorio tiene mil proyectos, ejecutar las pruebas de los mil en cada propuesta es imposible. La solución es calcular el conjunto afectado:
name: CI selectiva
on: [pull_request]
jobs:
detectar:
runs-on: ubuntu-latest
outputs:
afectados: ${{ steps.calc.outputs.lista }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: calc
name: Qué proyectos toca esta propuesta
run: |
# Ficheros modificados respecto al punto de divergencia con main
CAMBIOS=$(git diff --name-only origin/main...HEAD)
# Primer nivel de directorio = proyecto
PROYECTOS=$(echo "$CAMBIOS" | cut -d/ -f1-2 | sort -u | tr '\n' ' ')
echo "lista=$PROYECTOS" >> "$GITHUB_OUTPUT"
probar:
needs: detectar
if: needs.detectar.outputs.afectados != ''
runs-on: ubuntu-latest
steps:
- run: echo "Probando solo ${{ needs.detectar.outputs.afectados }}"Ese origin/main...HEAD con tres puntos es el mismo de la revisión de código de la lección 07-02: los cambios de la rama desde que se separó, no las diferencias con el estado actual de main. Es un detalle que aquí importa mucho: con dos puntos, cualquier avance de main haría creer a la tubería que la propuesta toca proyectos que no ha tocado.
En la práctica, un sistema serio no usa directorios sino el grafo de dependencias del sistema de compilación: si cambia componentes-ui, se prueban también todos los proyectos que dependen de él. Pero el principio es el mismo, y arranca siempre en un git diff --name-only.
- Caso 4: un equipo pequeño de producto (el nuestro)
Cuarto caso: cuatro personas, un producto, una publicación cada pocas semanas. Es decir, gestor-tareas. Es también, con diferencia, el caso más común: la inmensa mayoría de los equipos del mundo se parecen mucho más a este que a los tres anteriores.
Recapitulando lo que este equipo ha ido construyendo a lo largo del curso, sin repetir el detalle:
- Un repositorio en
git.ejemplo.es, máscomponentes-uicomo submódulo (06-05). - Ramas de tema cortas con nombre de ticket (
GT-231-filtro-ocultas), integradas enmainmediante propuesta y revisión (07-01, 07-02), con un flujo cercano a GitHub Flow (07-04). - Integración continua en cada propuesta y rama protegida en
main(07-06). - Conventional Commits y una política de integración acordada (08-01, 08-02).
- Etiquetas anotadas con SemVer para publicar (05-05).
- Diego, colaborador externo, trabaja por fork y propuesta (07-01).
Lo interesante de este caso es lo que NO tiene, y no necesita:
| No tiene | Por qué no le hace falta |
|---|---|
| Jerarquía de mantenedores | Cuatro personas se hablan entre sí; la jerarquía sería teatro |
| Flujo por correo | Todos tienen acceso de escritura al mismo servidor |
CODEOWNERS |
Cualquiera puede revisar cualquier fichero, y es sano que así sea |
| CI selectiva | Las pruebas del proyecto entero tardan dos minutos |
Clones parciales o sparse-checkout |
El repositorio pesa menos que una fotografía |
| Git LFS | No hay binarios grandes… todavía (10-03) |
| Ramas de publicación de larga vida | Solo hay una versión en producción a la vez |
Cada una de esas ausencias es una decisión de diseño, no una carencia. Añadir cualquiera de ellas "porque los proyectos serios lo hacen" empeoraría el flujo: más pasos, más ceremonia, más sitios donde equivocarse, y cero problemas resueltos.
Este es el sesgo profesional más caro que existe en esta materia: confundir la complejidad del proceso con la madurez del equipo.
- Los cuatro modelos, comparados
| Kernel de Linux | Proyecto abierto medio | Monorepo de empresa | Equipo de producto | |
|---|---|---|---|---|
| Tamaño | Miles de personas, cientos de subsistemas | Cientos de colaboradores, pocos mantenedores | Cientos o miles de personas, un repositorio | 3–15 personas |
| Mecanismo de propuesta | Parche por correo (format-patch/send-email/am) |
Fork + pull request en plataforma | Rama en el repositorio central + propuesta | Rama en el repositorio central + propuesta |
| Quién decide | Jerarquía de mantenedores por confianza | Mantenedores del proyecto | Propietarios del directorio (CODEOWNERS) |
Cualquier compañero del equipo |
| Flujo de ramas | Árboles independientes que se fusionan hacia arriba; ventanas de integración | Ramas de tema cortas sobre main |
Ramas de tema muy cortas; main siempre desplegable |
Ramas de tema cortas sobre main |
| Historial resultante | Muchos merges entre árboles; cada commit válido por sí solo | Lineal, un commit por propuesta (squash) | Lineal y de altísimo volumen | Lineal o casi, legible a ojo |
| Reescritura | Constante y esperada (v2, v3…) antes de integrar |
Frecuente antes de integrar | Poca: se integra rápido y pequeño | Ocasional, para limpiar la rama |
| Publicación | Ventanas y ciclo de candidatas | Etiquetas SemVer | Continua, sin versión global | Etiquetas SemVer |
| Qué hace falta | Disciplina extrema en commits y mensajes; cultura de revisión pública; tolerancia a la reescritura | CONTRIBUTING.md, plantillas, etiquetas, CI fiable, mantenedores con tiempo |
Herramientas propias, CI selectiva, escalado de Git, propiedad del código | Confianza mutua, CI básica, convenciones acordadas |
| Falla cuando | Alguien manda un parche sin contexto o sin partir | Los mantenedores se saturan o la CI es lenta e inestable | Las herramientas no siguen el ritmo del repositorio | Se añade ceremonia que nadie necesita |
Léela en vertical, no en horizontal: cada columna es un sistema coherente, donde cada casilla sostiene a las demás. La disciplina extrema del kernel es posible porque hay una jerarquía de confianza que la exige; la jerarquía es sostenible porque los parches llegan impecables. Sacar una casilla de su columna y meterla en otra suele producir un desastre.
- La lección transversal: el flujo se elige, no se hereda
Si tuviera que reducir esta lección a una frase sería esta: el flujo de trabajo es una respuesta a unas restricciones concretas. Cambia las restricciones y la respuesta correcta cambia.
Las restricciones que de verdad importan son cuatro:
1. ¿Cuánta confianza hay entre quienes contribuyen? Con confianza alta (un equipo interno), el acceso de escritura directo y la revisión ligera funcionan. Con confianza baja o desconocida (cualquiera de internet), hace falta un mecanismo donde la propuesta llegue sin permisos y la aprobación sea explícita: fork y propuesta, o parche por correo.
2. ¿Cuánto cuesta un error en producción? Si desplegar de nuevo cuesta treinta segundos, puedes permitirte integrar rápido y corregir después (07-05). Si el error implica retirar firmware de dispositivos, necesitas ventanas, candidatas y ramas de estabilización (07-03).
3. ¿Cuántas versiones tienes vivas a la vez? Una sola versión en producción permite un main único. Mantener tres versiones simultáneas con correcciones de seguridad obliga a ramas de larga vida y a cherry-pick sistemático (05-03).
4. ¿Cuánto pesa el repositorio y cuánta gente lo toca? Es lo que decide si necesitas las técnicas de la lección 10-04 o si Git de serie te sobra durante los próximos diez años.
El anti-patrón
El error clásico tiene un guion siempre igual:
Alguien lee un artículo sobre cómo trabaja una empresa enorme. Propone adoptarlo. Se instalan cinco herramientas, se definen siete tipos de rama y una política de aprobación de tres niveles. Dos meses después, en un equipo de cinco personas, integrar un cambio de dos líneas tarda tres días y nadie recuerda por qué.
El proceso de una organización de cinco mil personas es la cicatriz de problemas que esa organización tuvo. Si no tienes esos problemas, lo que estás copiando no es una solución: es una cicatriz ajena.
La receta sana
- Empieza con el flujo más simple que funcione. Para casi todo el mundo: ramas de tema cortas, propuestas con revisión,
mainprotegido, CI que pasa en minutos. - Añade proceso solo cuando duela algo concreto. Y que la conversación empiece por el dolor, no por la herramienta: "se nos rompió producción tres veces este mes" es un motivo; "deberíamos usar Git Flow" no lo es.
- Escribe las decisiones. En
CONTRIBUTING.mdo en elREADME.md. Un flujo que solo vive en la cabeza de dos personas no sobrevive a la tercera contratación. - Revísalo de vez en cuando. Un proceso adecuado para cuatro personas puede ser insuficiente para doce y absurdo cuando vuelvan a ser cuatro.
Errores Comunes y Consejos
Error 1: copiar el flujo de un proyecto que no se parece al tuyo. Es el error central de esta lección. Antes de adoptar nada, pregunta qué problema resolvía en su contexto original y si tú lo tienes.
Error 2: creer que format-patch/am es cosa de museo. Es la forma más portátil que existe de mover un cambio entre dos repositorios sin conectarlos. Sirve para enviar una corrección a un compañero sin red compartida, para mover un commit entre dos repositorios que no se conocen, o para archivar un cambio como texto. Lo retomamos en la lección 10-02.
Error 3: confundir git request-pull con la pull request de una plataforma. El primero genera un texto que tú envías por el medio que quieras; la segunda es un objeto gestionado por un servidor, con estado, comentarios y comprobaciones. Conceptualmente son lo mismo; operativamente, no.
Error 4: pensar que el monorepo es "todo junto sin orden". Un monorepo que funciona tiene más estructura que un conjunto de repositorios separados: propiedad por directorios, grafo de dependencias explícito y CI selectiva. Lo que elimina no es el orden, sino la coordinación entre repositorios.
Error 5: usar el prestigio de un proyecto como argumento técnico. "El kernel lo hace así" no es una razón. La razón es por qué lo hace así, y si eso aplica a tu caso.
Consejo 1: aprende a leer el historial de un proyecto ajeno. Es la forma más rápida de entender cómo trabaja un equipo antes de contribuir:
git clone --filter=blob:none https://git.ejemplo.es/proyecto.git
cd proyecto
# ¿Hay merges o el historial es lineal?
git log --oneline --graph -40
# ¿Qué proporción de commits son merges?
git rev-list --count --merges HEAD
git rev-list --count HEAD
# ¿Qué convención de mensajes usan?
git log --format=%s -40
# ¿Quién integra de verdad? (committer, no autor)
git log --format='%cn' -300 | sort | uniq -c | sort -rn | head
# ¿Usan Signed-off-by?
git log --format=%b -60 | grep -c 'Signed-off-by'Ese --filter=blob:none del clon es un clon parcial: trae el grafo completo pero no el contenido de los ficheros. Para inspeccionar un historial es perfecto y muchísimo más rápido. Se explica en la lección 10-04.
Consejo 2: antes de contribuir a un proyecto, lee su CONTRIBUTING.md entero. Suena obvio y casi nadie lo hace. Es la diferencia entre una propuesta que se integra y una que se queda seis meses esperando.
Consejo 3: si tu equipo no tiene el flujo escrito, escríbelo tú. Media página en el README.md: qué ramas hay, cómo se nombran, cómo se integra, cómo se publica. Es la aportación con mejor relación esfuerzo/beneficio que puedes hacer.
Ejercicios
Ejercicio 1: mover un cambio sin remoto compartido
Ana y Bruno están en un tren sin conexión, compartiendo una red local. Ana tiene tres commits en la rama GT-244-orden-por-prioridad que Bruno necesita en su repositorio. No hay servidor accesible.
Describe dos formas de conseguirlo con lo aprendido en el curso, y explica cuándo preferirías cada una.
Ejercicio 2: diagnosticar un flujo por su historial
Has clonado un proyecto desconocido y obtienes estos datos:
$ git rev-list --count HEAD 18452 $ git rev-list --count --merges HEAD 6127 $ git log --format=%s -8 Merge tag 'red-para-6.11' de git.ejemplo.es/subsistema-red Merge tag 'ficheros-para-6.11' de git.ejemplo.es/subsistema-ficheros net: corregir fuga de memoria en el driver net: anadir prueba de regresion Merge tag 'graficos-para-6.11' de git.ejemplo.es/subsistema-graficos docs: actualizar la guia de contribucion Merge tag 'usb-para-6.11' de git.ejemplo.es/subsistema-usb Merge tag 'audio-para-6.11' de git.ejemplo.es/subsistema-audio $ git log --format='%an|%cn' -6 Ana Ferrer|Bruno Salas Carla Vidal|Bruno Salas Diego Rueda|Ana Ferrer Ana Ferrer|Bruno Salas
¿A cuál de los cuatro modelos corresponde? Justifica con tres indicios distintos.
Ejercicio 3: elegir flujo para tres equipos
Para cada situación, decide el modelo de trabajo y justifica en tres o cuatro líneas. Di también qué no añadirías.
A. Cinco personas, aplicación web interna, despliegue varias veces al día, todos en la misma oficina, el repositorio pesa 40 MB.
B. Una biblioteca de código abierto con veinte colaboradores habituales y unos doscientos ocasionales al año. Dos mantenedores con poco tiempo. Se usa en producción en muchos sitios, así que una versión rota es un problema serio.
C. Empresa de 400 personas, doce productos que comparten cuatro bibliotecas internas. Hoy tienen dieciséis repositorios y se pasan la vida coordinando versiones entre ellos. Cada cambio de una biblioteca compartida tarda semanas en llegar a todos los consumidores.
Soluciones
Solución 1
Forma A: git bundle (lección 09-05).
# Ana: empaqueta los tres commits en un fichero
git bundle create /tmp/GT-244.bundle main..GT-244-orden-por-prioridad
# (copia el fichero a la máquina de Bruno por la red local o un USB)
# Bruno: comprueba qué necesita el paquete y qué contiene
git bundle verify /tmp/GT-244.bundle
git bundle list-heads /tmp/GT-244.bundle
# Bruno: lo trae como si fuera un remoto
git fetch /tmp/GT-244.bundle GT-244-orden-por-prioridad:GT-244-orden-por-prioridadForma B: format-patch + am (esta lección).
# Ana
git format-patch main -o /tmp/parches-gt244/
# Bruno
git checkout -b GT-244-orden-por-prioridad main
git am /tmp/parches-gt244/*.patchCuándo cada una:
bundle |
format-patch/am |
|
|---|---|---|
| Qué transporta | Objetos Git reales, con sus hashes | El contenido de los cambios, como texto |
| Hashes resultantes | Idénticos a los de Ana | Distintos: cambia el committer y la fecha |
| Legible a ojo | No, es binario | Sí, es texto revisable |
| Aplicable sobre otra base | No: exige tener el commit base | Sí, y con -3 incluso con divergencias |
| Volumen | Eficiente para muchos commits | Un fichero por commit |
Elige bundle cuando quieras la historia exacta (mismos hashes, misma firma, mismos padres). Elige format-patch cuando quieras el cambio para revisarlo, discutirlo o aplicarlo sobre una base distinta. Y hay una tercera vía si las dos máquinas se ven en la red: añadir el repositorio del compañero como remoto por ruta de sistema de ficheros o por SSH y hacer un fetch normal (lección 04-02) —Git es distribuido, y cualquier clon puede ser el remoto de otro.
Solución 2
Es el caso 1: el modelo del kernel, o cualquier proyecto organizado como árbol de árboles. Tres indicios independientes:
-
La proporción de merges es del 33 % (6.127 de 18.452). Un historial con un tercio de commits de fusión no sale de un flujo de ramas de tema con squash, que produce historial casi lineal. Sale de un modelo donde se integran repetidamente árboles completos.
-
Los mensajes de merge dicen
Merge tag '...-para-6.11' de git.ejemplo.es/subsistema-.... Están integrando etiquetas de repositorios distintos, cada uno de un subsistema, todos preparados para la misma versión. Es literalmente el resultado de una serie degit request-pullseguidos degit mergede árboles independientes. -
Autor y committer difieren sistemáticamente, y los committers son un conjunto pequeño y repetido (Bruno, Ana) mientras los autores son variados (Carla, Diego). Eso es la huella dactilar de
git am: hay personas que escriben parches y personas distintas que los aplican. En un flujo de pull requests de plataforma con squash, el committer suele ser el propio servidor o el mismo autor.
Un cuarto indicio de refuerzo: los mensajes de los commits normales llevan prefijo de subsistema (net:, docs:), la convención habitual cuando el árbol se organiza por áreas.
Solución 3
A. Equipo pequeño de producto (caso 4).
Ramas de tema cortas sobre main, propuesta con una revisión, CI que ejecute las pruebas, main protegido, integración con squash o rebase, despliegue desde main. Es GitHub Flow (07-04) o incluso Trunk Based (07-05) si tienen buena cobertura de pruebas y usan interruptores de funcionalidad.
No añadiría: Git Flow con develop y ramas de publicación (no hay múltiples versiones vivas), CODEOWNERS (cinco personas que se sientan juntas), CI selectiva, ni ninguna técnica de escalado. 40 MB no es un problema de rendimiento: es un repositorio pequeño.
B. Proyecto abierto medio (caso 2).
Fork y propuesta, CONTRIBUTING.md explícito, plantillas de propuesta e incidencia, etiquetas para triaje y para captar gente nueva, CI obligatoria y rápida, integración con squash, publicaciones con etiqueta anotada y SemVer.
Como "una versión rota es un problema serio", añadiría una rama de mantenimiento por versión mayor viva (1.x, 2.x) a la que se llevan las correcciones con cherry-pick (05-03), y publicaría candidatas antes de una versión mayor. Ese es el único punto donde tomo prestada una pieza de Git Flow, y por un motivo concreto.
No añadiría: un flujo por correo (la barrera de entrada hundiría las contribuciones ocasionales, que aquí son valiosas), ni develop (con main estable y ramas de mantenimiento basta), ni jerarquía de mantenedores (con dos personas no hay jerarquía que valga).
C. Candidato claro a monorepo (caso 3).
El síntoma que describen —semanas para propagar un cambio de una biblioteca compartida a doce consumidores— es exactamente el problema que el monorepo elimina de raíz: un commit atómico actualiza la biblioteca y todos sus consumidores a la vez.
Antes de decidir, comprobaría tres cosas:
- Que no hay código con requisitos de confidencialidad que impidan la visibilidad total.
- Que están dispuestos a invertir en herramientas: CI selectiva y un sistema de compilación con grafo de dependencias. Sin eso, el monorepo se convierte en una tubería de dos horas por cambio.
- Que el volumen resultante es manejable, o que aceptan las técnicas de la lección 10-04.
Como paso intermedio y reversible, empezaría fusionando solo las cuatro bibliotecas compartidas en un repositorio único con CODEOWNERS por directorio, midiendo el efecto, y consolidando los productos después si la experiencia es buena.
No añadiría de entrada: submódulos (06-05) para atar las bibliotecas a los productos. Resolverían la trazabilidad pero no el problema real, que es la coordinación: seguirían necesitando doce actualizaciones de puntero, una por producto.
Conclusión
Cuatro proyectos, cuatro respuestas distintas a las mismas cinco preguntas.
- El kernel de Linux trabaja con parches por correo:
git format-patchconvierte commits en correos,git send-emaillos manda a listas públicas,git amlos aplica conservando el autor original, ygit request-pullpide a un nivel superior que integre un árbol entero. Sobre eso se levanta una jerarquía de mantenedores basada en confianza, conSigned-off-bycomo cadena de custodia. El precio de admisión es alto: cada commit debe valer por sí solo y cada mensaje debe leerse sin contexto. - Un proyecto abierto medio usa fork y propuesta, y dedica todo su diseño a proteger el recurso escaso, que es el tiempo de los mantenedores:
CONTRIBUTING.md, plantillas, etiquetas y automatización de lo repetitivo. El historial resultante es lineal y legible. - Una empresa con monorepo elige el repositorio único por el cambio atómico entre proyectos y la refactorización global, y paga ese beneficio con herramientas propias, propiedad del código por directorios y CI selectiva calculada a partir de
git diff --name-only origin/main...HEAD. - Un equipo pequeño de producto —el nuestro— funciona con ramas cortas, propuestas y
mainprotegido, y su mayor virtud es todo lo que no tiene.
Y la lección transversal, que es la única que hay que memorizar:
El flujo de trabajo se elige a partir de cuatro restricciones —confianza entre quienes contribuyen, coste de un error en producción, número de versiones vivas y tamaño del repositorio— y no a partir del prestigio de quien lo usa.
El proceso de una organización enorme es la cicatriz de los problemas que esa organización tuvo. Si no tienes esos problemas, copiarlo es adoptar el coste sin el beneficio.
Lo que viene
En los cuatro casos ha aparecido algo que no es Git: listas de correo, plataformas de propuestas, sistemas de compilación, herramientas de seguimiento de parches, tuberías de integración. Git casi nunca se usa solo. Vive rodeado de editores que muestran diferencias en color, de clientes gráficos, de sistemas de tickets que quieren saber qué commit cerró la incidencia GT-231, de linters que se ejecutan antes de cada confirmación y de plataformas que convierten un historial en una conversación.
Esa capa de integración cambia por completo la experiencia diaria y, mal entendida, es también donde más gente pierde el control de lo que está haciendo: el botón "Sincronizar" de un editor esconde al menos dos comandos, y no siempre los que uno espera.
En la lección 10-02: Integrando Git con Otras Herramientas veremos qué aporta de verdad cada pieza del ecosistema, qué conviene seguir haciendo en la terminal y por qué el consejo de fondo es siempre el mismo: automatiza lo repetitivo, pero entiende siempre qué comando hay debajo.
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
