Ha passat el que era previsible. L'Ana fa mesos que extreu peces reutilitzables de gestor-tasques: el botó amb estat de càrrega, el diàleg de confirmació, el camp de text amb validació. Al principi vivien a app.js; després en un components.js a part. I ara hi ha dos projectes més a l'empresa que els volen fer servir.

L'equip ha fet el que tocava: ha creat un repositori propi, components-ui, a git.exemple.cat/equip/components-ui.git, amb el seu propi historial, les seves pròpies versions etiquetades i els seus propis responsables.

I ara sorgeix la pregunta incòmoda: com fa servir gestor-tasques aquesta biblioteca?

Copiar els fitxers i enganxar-los funciona fins al primer apany, moment en què hi ha tres còpies divergents. I no n'hi ha prou amb "tenir la biblioteca en algun lloc": cal que quedi registrat quina versió exacta de components-ui fa servir cada commit de gestor-tasques, perquè en fer git checkout v1.0.0 d'aquí a sis mesos es pugui reconstruir exactament aquella aplicació.

La resposta nativa de Git a aquest problema són els submòduls. Són potents, són la solució correcta per a certs casos, i tenen fama —merescuda— de mossegar qui no entén com funcionen. Aquesta lliçó va d'entendre'ls bé.

Contingut

  1. Què és exactament un submòdul
  2. git submodule add: afegir components-ui
  3. Què es confirma exactament: l'objecte de tipus commit
  4. Clonar un projecte amb submòduls
  5. Actualitzar: el submòdul davant del punter
  6. Treballar dins d'un submòdul i el detached HEAD
  7. Inspecció: status, foreach i diff --submodule
  8. submodule.recurse i altres opcions que treuen dolor
  9. Els problemes reals
  10. Alternatives: submòduls, subtree, paquets i monorepo

  1. Què és exactament un submòdul

La definició completa, i tota la resta se'n dedueix:

Un submòdul és una entrada a l'arbre del repositori pare que apunta a un commit concret d'un altre repositori, més una línia al fitxer .gitmodules que diu d'on clonar aquell altre repositori.

Dues peces, cap més:

  1. El punter: una entrada a l'arbre (lliçó 01-04) el tipus de la qual no és blob ni tree, sinó commit. Desa un hash de 40 caràcters i res més.
  2. .gitmodules: un fitxer de text versionat, a l'arrel del pare, que associa cada ruta amb la seva URL.
flowchart TD
    subgraph P["Repositori gestor-tasques"]
      C1["commit c8f2a1e"]
      T1["tree arrel"]
      B1["blob app.js"]
      B2["blob index.html"]
      B3["blob .gitmodules"]
      SM["commit 7f3a9d2<br/>(entrada 'components-ui')"]
    end
    subgraph S["Repositori components-ui"]
      X1["commit 4b8e1c5"]
      X2["commit 7f3a9d2"]
      X3["commit 9d2f6a8"]
    end
    C1 --> T1
    T1 --> B1
    T1 --> B2
    T1 --> B3
    T1 --> SM
    SM -.->|"apunta a"| X2

El que és crucial: el repositori pare no conté el codi de la biblioteca. Conté un post-it que diu "aquí va components-ui, exactament al commit 7f3a9d2". El codi viu a l'altre repositori, amb el seu propi .git, el seu propi historial i les seves pròpies branques.

D'aquí surten les tres propietats que defineixen l'experiència de treballar amb submòduls:

  • Reproductibilitat exacta. Cada commit de gestor-tasques fixa una versió concreta de la biblioteca. git checkout v1.0.0 + actualitzar submòduls reconstrueix l'aplicació tal com era.
  • Historials independents. Un commit a la biblioteca no apareix a l'historial de l'aplicació. Són dos repositoris diferents amb dos git log diferents.
  • Actualització explícita. El punter no es mou sol. Si la biblioteca avança, el pare continua apuntant al mateix commit fins que algú decideix moure'l i el confirma.

Aquesta última propietat és simultàniament la virtut més gran i la font de queixes més gran.

  1. git submodule add: afegir components-ui

L'Ana ho fa des de l'arrel de gestor-tasques:

cd ~/projectes/gestor-tasques
git submodule add [email protected]:equip/components-ui.git vendor/components-ui
Cloning into '/home/ana/projectes/gestor-tasques/vendor/components-ui'...
remote: Enumerating objects: 214, done.
remote: Total 214 (delta 89), reused 214 (delta 89)
Receiving objects: 100% (214/214), 48.32 KiB | 4.83 MiB/s, done.
Resolving deltas: 100% (89/89), done.

Sintaxi:

git submodule add [-b <branca>] <url> [<ruta>]
  • Si ometes la ruta, es fa servir el nom del repositori (components-ui/).
  • -b <branca> registra una branca de seguiment, útil per a --remote (apartat 5).

El que ha passat, exactament:

git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   .gitmodules
	new file:   vendor/components-ui

Dos fitxers nous, no dos-cents. El directori sencer de la biblioteca apareix com una sola entrada. Aquest és el primer indici visible que no s'està desant el contingut.

El .gitmodules generat:

[submodule "vendor/components-ui"]
	path = vendor/components-ui
	url = [email protected]:equip/components-ui.git

I el commit:

git commit -m "Afegeix components-ui com a submòdul

La biblioteca de components viu ara al seu propi repositori.
Es fixa a la versió v2.1.0 perquè el build sigui reproduïble."
[main 3d8f1a6] Afegeix components-ui com a submòdul
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 vendor/components-ui

Fixa't en l'última línia: create mode 160000. Els modes que coneixíem de la lliçó 01-04 eren 100644 (fitxer normal), 100755 (executable) i 040000 (directori). 160000 és el mode especial d'un submòdul, i aquest número és literalment com Git marca "aquí hi ha un punter a un commit extern".

L'Ana ja el pot fer servir des d'index.html:

<script src="vendor/components-ui/dist/components.js"></script>
// app.js
const dialeg = ComponentsUI.creaDialeg({
  titol: 'Confirmar esborrat',
  missatge: 'Segur que vols esborrar aquesta tasca?',
});

I envia:

git push

  1. Què es confirma exactament: l'objecte de tipus commit

Val la pena veure-ho per dins, perquè entendre-ho aquí evita tota la confusió posterior. Reprenem les eines de la lliçó 01-04:

git cat-file -p HEAD^{tree}
100644 blob 8f2a1c9e...	.gitmodules
100644 blob 4b7d9c3e...	README.md
100644 blob 2e5f8a1c...	app.js
100644 blob 9c4e7b2f...	estils.css
100644 blob 1d6a8f3c...	index.html
040000 tree 7b3d5c9a...	vendor
git cat-file -p HEAD:vendor
160000 commit 7f3a9d2c4e8b1f6a3d5c9e2b7f4a8d1c6e3b5a9f	components-ui

Aquí és. L'arbre vendor conté una entrada de tipus commit amb mode 160000. No és un tree, no és un blob: és una referència a un objecte commit que ni tan sols és a la base de dades d'objectes de gestor-tasques.

git cat-file -t 7f3a9d2
fatal: git cat-file: could not get object info

Efectivament: aquell objecte viu al repositori de components-ui, no aquí. El pare només en desa el nom.

I la mida total que ocupa aquesta referència al repositori pare:

git cat-file -s $(git rev-parse HEAD:vendor)

Un arbre amb una entrada: unes desenes de bytes. El repositori de gestor-tasques no ha crescut gens per incorporar una biblioteca de 200 commits.

D'aquí es dedueix per què el diff d'un canvi de submòdul té aquell aspecte tan estrany:

diff --git a/vendor/components-ui b/vendor/components-ui
index 7f3a9d2..9d2f6a8 160000
--- a/vendor/components-ui
+++ b/vendor/components-ui
@@ -1 +1 @@
-Subproject commit 7f3a9d2c4e8b1f6a3d5c9e2b7f4a8d1c6e3b5a9f
+Subproject commit 9d2f6a8b3e7c1d5f9a2b6c4e8d1f3a7c5b9e2d6f

Una línia que canvia. Tot el contingut de la biblioteca pot haver canviat, però per al pare això és un punter que es mou.

  1. Clonar un projecte amb submòduls

En Bruno clona el projecte per primera vegada:

git clone [email protected]:equip/gestor-tasques.git
cd gestor-tasques
ls vendor/components-ui/
(buit)

El directori existeix però és buit. Aquest és el primer entrebanc clàssic, i li passa a tothom: git clone no baixa els submòduls per defecte.

Hi ha dues maneres d'arreglar-ho.

La correcta: clonar amb --recurse-submodules

git clone --recurse-submodules [email protected]:equip/gestor-tasques.git
Cloning into 'gestor-tasques'...
...
Submodule 'vendor/components-ui' ([email protected]:equip/components-ui.git) registered for path 'vendor/components-ui'
Cloning into '/home/bruno/gestor-tasques/vendor/components-ui'...
Submodule path 'vendor/components-ui': checked out '7f3a9d2c4e8b1f6a3d5c9e2b7f4a8d1c6e3b5a9f'

Clona el pare i, tot seguit, inicialitza i clona cada submòdul al commit fixat.

Si ja has clonat sense l'opció

git submodule init          # llegeix .gitmodules i registra els submòduls a .git/config
git submodule update        # clona i fa checkout del commit fixat

O en una sola ordre:

git submodule update --init
git submodule update --init --recursive     # si els submòduls tenen submòduls

La separació en dos passos té sentit quan l'entens:

Ordre Què fa On escriu
git submodule init Copia la configuració de .gitmodules a .git/config .git/config local
git submodule update Clona (si cal) i fa checkout del commit fixat El directori del submòdul

El pas d'init existeix perquè permet canviar la URL localment abans de clonar (per exemple, per fer servir un mirall intern o HTTPS en lloc d'SSH) sense tocar el .gitmodules que comparteix tot l'equip:

git submodule init
git config submodule.vendor/components-ui.url https://git.exemple.cat/equip/components-ui.git
git submodule update

També se'n pot inicialitzar només algun, en projectes amb molts:

git submodule update --init vendor/components-ui

  1. Actualitzar: el submòdul davant del punter

Aquí hi ha la distinció que més confusió genera, i mereix un encapçalament propi.

Hi ha dues actualitzacions diferents i fan coses oposades:

Ordre Què fa Direcció
git submodule update Posa el submòdul al commit que diu el pare El pare mana
git submodule update --remote Porta l'últim del remot del submòdul i mou el punter El submòdul mana

git submodule update: obeir el pare

És el cas normal, i el que s'executa després d'un git pull:

git pull
git submodule update --init --recursive

L'Ana ha pujat un commit que mou el punter de la biblioteca. En Bruno fa git pull del pare, i submodule update deixa la seva còpia de components-ui exactament al commit que l'Ana va fixar. Sincronització, no actualització.

git submodule update --remote: portar el que és nou

git submodule update --remote vendor/components-ui
Submodule path 'vendor/components-ui': checked out '9d2f6a8b3e7c1d5f9a2b6c4e8d1f3a7c5b9e2d6f'

Git ha fet fetch al submòdul i ha fet checkout de la punta de la seva branca de seguiment. Quina branca? Per aquest ordre:

  1. La indicada a .gitmodules amb branch = <branca> (o git submodule add -b).
  2. Si no n'hi ha cap, HEAD del remot, que sol ser main.
[submodule "vendor/components-ui"]
	path = vendor/components-ui
	url = [email protected]:equip/components-ui.git
	branch = estable

I ara el que és important: això encara no ha canviat res a l'historial del pare. És un canvi sense confirmar:

git status
On branch main
Changes not staged for commit:
	modified:   vendor/components-ui (new commits)

(new commits) significa: "el submòdul és en un commit diferent del que jo tinc registrat". Perquè l'actualització sigui real i arribi a la resta de l'equip, cal confirmar-la al pare:

git add vendor/components-ui
git commit -m "Actualitza components-ui a v2.2.0

Inclou la correcció del focus al diàleg de confirmació
(components-ui#48) que necessitem per a l'esborrat múltiple."
git push
sequenceDiagram
    participant B as components-ui (remot)
    participant L as vendor/components-ui (local)
    participant P as gestor-tasques (pare)
    L->>B: git submodule update --remote → fetch + checkout
    Note over L: el submòdul avança a 9d2f6a8
    Note over P: git status → "modified: (new commits)"
    P->>P: git add vendor/components-ui + commit
    Note over P: ara el punter registrat és 9d2f6a8

La regla que resumeix tot l'apartat: actualitzar el submòdul és un canvi al teu disc; actualitzar el punter és un commit al pare. Si no ho confirmes, ningú més no veu l'actualització, i el següent git submodule update te la desfà.

  1. Treballar dins d'un submòdul i el detached HEAD

La Carla necessita arreglar el diàleg de confirmació, que és a la biblioteca. Entra al directori del submòdul:

cd vendor/components-ui
git status
HEAD detached at 7f3a9d2
nothing to commit, working tree clean

Detached HEAD, l'estat de la lliçó 03-02. I té tota la lògica del món: el pare no diu "fes servir la branca main de la biblioteca", diu "fes servir el commit 7f3a9d2". Git fa exactament això, i un checkout d'un commit solt deixa HEAD desacoblat.

Conseqüència perillosa: si la Carla edita i confirma aquí sense més, crea un commit que no pertany a cap branca. Tan bon punt algú faci git submodule update, aquell commit queda orfe i només es recupera pel reflog (lliçó 09-04).

El procediment correcte té quatre passos:

# 1. Posar-se en una branca de debò, dins del submòdul
cd vendor/components-ui
git switch main
git pull

# 2. Treballar com a qualsevol repositori
git switch -c correccio/focus-dialeg
# ... editar dialeg.js ...
git commit -am "Retorna el focus al disparador en tancar el diàleg"

# 3. PUBLICAR el canvi al remot DEL SUBMÒDUL
git push -u origin correccio/focus-dialeg
# (després de revisar-lo i integrar-lo a main del submòdul)

# 4. Tornar al pare i confirmar el punter nou
cd ../..
git add vendor/components-ui
git commit -m "Actualitza components-ui: correcció del focus al diàleg"
git push

El pas 3 és el que s'oblida, i ometre'l és la fallada més greu de tots els submòduls. Si la Carla confirma el punter al pare sense haver publicat el commit del submòdul, el resultat és:

# En Bruno, a la seva màquina
git pull
git submodule update
fatal: remote error: upload-pack: not our ref 5c9e2b7f4a8d1c6e3b5a9f7f3a9d2c4e8b1f6a3d
Fetched in submodule path 'vendor/components-ui', but it did not contain
5c9e2b7f4a8d1c6e3b5a9f7f3a9d2c4e8b1f6a3d. Direct fetching of that commit failed.

El pare apunta a un commit que no existeix enlloc llevat del disc de la Carla. El projecte està trencat per a tot l'equip fins que ella el publiqui.

Git té una xarxa de seguretat per a això, i cal activar-la:

git push --recurse-submodules=check      # avorta el push si hi ha commits sense publicar
git push --recurse-submodules=on-demand  # els publica automàticament abans
# Que sigui el comportament per defecte del repositori
git config push.recurseSubmodules check

check és l'opció recomanada: t'avisa i t'obliga a decidir, en lloc de publicar coses pel seu compte.

  1. Inspecció: status, foreach i diff --submodule

git submodule status

git submodule status
 7f3a9d2c4e8b1f6a3d5c9e2b7f4a8d1c6e3b5a9f vendor/components-ui (v2.1.0)

El primer caràcter és un indicador d'estat, i cal saber llegir-lo:

Prefix Significat
(espai) El submòdul és al commit correcte
- No inicialitzat: falta git submodule update --init
+ És en un altre commit diferent del registrat
U conflictes de fusió sense resoldre

Exemples de les situacions problemàtiques:

-7f3a9d2c... vendor/components-ui

→ En Bruno acaba de clonar sense --recurse-submodules.

+9d2f6a8b... vendor/components-ui (v2.2.0)

→ Algú ha mogut el submòdul i no ho ha confirmat al pare.

Amb --recursive baixa pels submòduls imbricats.

git submodule foreach

Executa una ordre a cada submòdul:

git submodule foreach 'git status --short'
git submodule foreach 'git fetch'
git submodule foreach --recursive 'git switch main && git pull'

Dins de l'ordre, Git defineix variables útils:

Variable Contingut
$name El nom del submòdul a .gitmodules
$path La ruta relativa des del pare
$sha1 El commit que el pare té registrat
$toplevel La ruta absoluta del repositori pare
git submodule foreach 'echo "$name es troba a $(git describe --tags --always)"'
Entering 'vendor/components-ui'
vendor/components-ui es troba a v2.1.0

git diff --submodule

Per defecte, un canvi de submòdul es veu com aquella línia de Subproject commit. --submodule=log mostra els commits que hi ha de diferència, que és infinitament més informatiu:

git diff --submodule=log
Submodule vendor/components-ui 7f3a9d2..9d2f6a8:
  > Retorna el focus al disparador en tancar el dialeg
  > Afegeix la variant compacta del boto
  > Corregeix el contrast del text deshabilitat

Tres commits de diferència, amb els seus assumptes. Els modes disponibles:

Mode Sortida
--submodule=short La línia Subproject commit (per defecte)
--submodule=log La llista de commits entre tots dos punts
--submodule=diff El diff complet dels canvis dins del submòdul

I com que és el que es vol gairebé sempre, convé fixar-ho:

git config --global diff.submodule log
git config --global status.submoduleSummary true

Amb status.submoduleSummary, git status també resumeix els commits pendents en lloc de dir només (new commits).

  1. submodule.recurse i altres opcions que treuen dolor

La major part del patiment amb submòduls ve d'oblidar el --recurse-submodules en alguna ordre. Aquesta opció l'activa per defecte per a gairebé totes:

git config --global submodule.recurse true

A partir d'aquí, git pull, git switch, git checkout, git reset i altres actualitzen els submòduls automàticament. És la primera configuració que hauria de posar qualsevol que treballi amb submòduls.

Nota: submodule.recurse no afecta git clone, que continua necessitant el seu --recurse-submodules explícit.

El joc complet de configuració recomanada:

# Recórrer submòduls automàticament a pull, switch, checkout, reset...
git config --global submodule.recurse true

# Veure commits en lloc de hashos en comparar
git config --global diff.submodule log

# Resum de submòduls a git status
git config --global status.submoduleSummary true

# Avisar si enviaré un punter a commits sense publicar
git config --global push.recurseSubmodules check

# Accelerar el fetch de diversos submòduls en paral·lel
git config --global submodule.fetchJobs 4

I un grapat d'ordres que resolen situacions concretes:

# Ha canviat la URL a .gitmodules i cal propagar-la a .git/config
git submodule sync --recursive

# Descartar TOTS els canvis locals dels submòduls i tornar al punter
git submodule update --init --recursive --force

# Eliminar un submòdul del tot
git submodule deinit -f vendor/components-ui
git rm vendor/components-ui
rm -rf .git/modules/vendor/components-ui
git commit -m "Elimina el submòdul components-ui"

Aquest últim mereix explicació: deinit el desregistra, git rm treu l'entrada de l'arbre i la línia de .gitmodules, i el rm -rf de .git/modules/ esborra el repositori intern que Git conserva (des de la versió 1.7.8, el .git del submòdul no és dins del seu directori, sinó a .git/modules/<nom> del pare, i al directori del submòdul hi queda un fitxer .git que hi apunta — la mateixa tècnica que veurem amb worktree a la lliçó 06-06).

  1. Els problemes reals

Tot l'anterior funciona. Aquests són els entrebancs que es donen a la pràctica i la seva solució.

Problema 1: "el directori és buit"

Símptoma. Algú clona i vendor/components-ui/ no té res. L'aplicació no arrenca.

Causa. git clone sense --recurse-submodules.

Solució. git submodule update --init --recursive. I prevenció: documentar-ho al README.md, i —millor encara— un hook post-checkout de la lliçó 06-01 que avisi:

#!/usr/bin/env bash
# .git/hooks/post-checkout
if [ -f .gitmodules ] && git submodule status | grep -q '^-'; then
  echo "⚠  Hi ha submòduls sense inicialitzar. Executa:"
  echo "   git submodule update --init --recursive"
fi

Problema 2: el punter apunta a un commit que no existeix

Ja vist a l'apartat 6: algú va confirmar el punter sense publicar el commit del submòdul.

Solució. Que aquella persona faci git push al submòdul. Prevenció: push.recurseSubmodules check.

Problema 3: canvis sense confirmar dins del submòdul

Símptoma. git status al pare diu modified: vendor/components-ui (modified content) i no hi ha manera que desaparegui.

Causa. Algú ha editat fitxers dins del submòdul sense confirmar-los allà.

git submodule status
+7f3a9d2c... vendor/components-ui (v2.1.0-3-g5c9e2b7)

Solució, segons el que vulguis:

# A) Els canvis són bons: confirma'ls DINS del submòdul i publica
cd vendor/components-ui && git switch main && git commit -am "..." && git push

# B) Els canvis sobren: descarta'ls
git submodule update --force

# C) Només vull que el git status del pare els ignori (amb compte!)
git config submodule.vendor/components-ui.ignore dirty

L'opció ignore accepta none (per defecte), untracked (ignora fitxers sense seguiment), dirty (ignora també les modificacions) i all (ignora fins i tot un punter diferent). all és perillós: amaga exactament la informació que necessites veure.

Problema 4: canviar de branca al pare

Símptoma. La Carla passa d'una branca que té el submòdul a una altra que no el té (o que el té en una altra versió), i apareixen fitxers estranys o el directori es queda amb contingut antic.

Causa. El checkout del pare mou el punter, però el contingut del directori del submòdul no s'actualitza sol llevat que ho demanis.

Solució. submodule.recurse true, o recordar-se de git checkout --recurse-submodules <branca>.

I un cas especialment molest: en tornar a una branca anterior a la incorporació del submòdul, el directori es queda allà amb contingut i sense seguiment. No és una fallada: és que Git no esborra directoris que contenen un repositori per no destruir feina.

Problema 5: conflictes de fusió al punter

Dues branques actualitzen el submòdul a commits diferents:

CONFLICT (submodule): Merge conflict in vendor/components-ui

No hi ha marcadors de conflicte per editar: el conflicte és quin hash ha de guanyar. La resolució consisteix a decidir el commit correcte:

# Veure els dos candidats
git diff --submodule=log

# Triar un dels costats
cd vendor/components-ui
git log --oneline --all -10
git checkout <el-commit-correcte>     # normalment el que inclou tots dos canvis
cd ../..
git add vendor/components-ui
git commit

Sovint la resposta correcta no és cap dels dos, sinó un commit posterior del submòdul que conté els canvis de totes dues branques. Fusiona'ls primer dins del submòdul, i després apunta-hi.

Problema 6: la fricció quotidiana

I el problema del qual menys es parla: els submòduls afegeixen un pas a tot. Cada pull pot necessitar un submodule update; cada canvi a la biblioteca són dos commits i dos push; cada persona nova s'equivoca en clonar la primera vegada. Res d'això no és greu per separat, però s'acumula.

Per això la pregunta important no és "com es fan servir els submòduls?", sinó "és aquesta l'eina adequada per al meu cas?". Anem-hi.

  1. Alternatives: submòduls, subtree, paquets i monorepo

Hi ha quatre maneres raonables de compondre un projecte a partir de diverses peces.

Submòduls git subtree Gestor de paquets Monorepo
Què desa el pare Un punter a un commit El codi real, fusionat al seu historial Una versió declarada (package.json) Tot, és un sol repositori
Mida del repositori pare Mínima Creix amb la biblioteca Mínima Gran
Qui clona necessita passos extra? (--recurse-submodules) No Sí (npm install) No
Historial de la biblioteca Separat i íntegre Barrejat (o aixafat) Invisible Unificat
Actualitzar submodule update --remote + commit git subtree pull Canviar la versió + instal·lar No aplica: és el mateix commit
Contribuir a la biblioteca Natural: és un repositori normal Maldestre: git subtree push Requereix clonar a part Trivial
Reproductibilitat exacta Total (un hash) Total Bona (amb fitxer de blocatge) Total
Corba d'aprenentatge Alta Mitjana Baixa Baixa
Canvis atòmics aplicació+biblioteca No (dos commits) No
Eines externes Cap Cap Sí (npm, pip, Maven...) Sol caldre

Un paràgraf per a cadascuna:

git subtree fa el contrari que els submòduls: copia el contingut de la biblioteca dins del pare i el manté sincronitzat amb fusions. Qui clona no ha de fer res d'especial, perquè el codi hi és. A canvi, el repositori creix, l'historial es barreja i contribuir de tornada a la biblioteca és incòmode. És una bona opció quan consumeixes una dependència i rarament la modifiques, i quan la fricció del --recurse-submodules és inacceptable (per exemple, si el projecte el clona gent aliena a l'equip). Les ordres bàsiques són git subtree add, pull i push; no ho desenvolupem aquí perquè mereixeria la seva pròpia lliçó.

Un gestor de paquets (npm, pip, Maven, Cargo...) és, a la majoria dels projectes moderns, la resposta correcta. Si components-ui es pot publicar com a paquet —encara que sigui en un registre privat de l'empresa—, l'equip declara "@equip/components-ui": "^2.1.0" al seu package.json, el fitxer de blocatge garanteix la reproductibilitat, i tota la maquinària de versions semàntiques (lliçó 05-05) treballa a favor seu. Que Git pugui fer això no vol dir que ho hagi de fer. Abans de triar submòduls, pregunta't sempre si un paquet resoldria el problema.

El monorepo —un únic repositori amb l'aplicació i la biblioteca a dins— elimina el problema d'arrel: un commit pot canviar totes dues alhora, i sempre són coherents. És el que fan moltes empreses grans. El preu és un repositori que creix molt i necessita eines pròpies per a permisos, compilacions parcials i rendiment. Aquell escenari, amb les seves tècniques —clons parcials, sparse-checkout, clons superficials—, és el tema de la lliçó 10-04: Escalant Git per a Projectes Grans.

Els submòduls guanyen quan es compleixen alhora diverses d'aquestes condicions:

  • La dependència és codi font que compiles al costat del teu, no un artefacte publicable.
  • Necessites fixar un commit exacte, no un rang de versions.
  • Modifiques la biblioteca amb una certa freqüència i vols que sigui un repositori de primera classe.
  • No hi ha (o no vols muntar) un registre de paquets intern.
  • L'equip és petit i es pot formar en la mecànica.

Casos reals típics: temes i connectors d'un CMS, biblioteques C/C++ compilades des del codi font, configuracions compartides entre projectes, microprogramari amb components de tercers.

I una menció que cal fer aquí perquè de vegades es confon: si el teu problema no és codi compartit sinó fitxers grans i binaris —imatges d'alta resolució, vídeos, models, executables— la resposta no és cap de les quatre, sinó Git LFS, que substitueix aquests fitxers per punters de text i desa el contingut en un magatzem a part. És el tema de la lliçó 10-03: Git LFS per a Fitxers Grans.

Per a gestor-tasques, l'equip acaba prenent aquesta decisió: submòdul de moment, perquè components-ui és en ple desenvolupament, es modifica cada setmana i no compensa publicar un paquet a cada canvi. Quan la biblioteca s'estabilitzi i altres equips comencin a consumir-la, passaran a publicar-la al registre npm intern. És una decisió assenyada: la manera de compondre un projecte pot canviar amb la seva maduresa.

Errors Habituals i Consells

Error 1: clonar sense --recurse-submodules. L'error número u. Directori buit i aplicació trencada. Solució: git submodule update --init --recursive.

Error 2: confirmar el punter sense publicar el commit del submòdul. Trenca el projecte per a tots els altres. Prevenció: git config push.recurseSubmodules check.

Error 3: treballar al submòdul sense sortir del detached HEAD. Els commits queden orfes. Fes git switch <branca> abans de tocar res.

Error 4: confondre submodule update amb submodule update --remote. El primer obeeix el pare; el segon porta el que és nou del remot. Són oposats.

Error 5: oblidar confirmar al pare després d'actualitzar. Si no hi ha commit al pare, l'actualització només existeix al teu disc.

Error 6: fer servir submodule.<n>.ignore = all. Amaga precisament la informació que necessites veure. Com a molt, dirty.

Error 7: intentar esborrar un submòdul amb rm -rf. Deixa restes a .git/config, .gitmodules i .git/modules/. Fes servir la seqüència deinit + git rm.

Error 8: triar submòduls per defecte. Gairebé sempre hi ha una alternativa més simple. Justifica la tria.

Consell 1: git config --global submodule.recurse true, sempre. És la configuració que més dolor evita.

Consell 2: diff.submodule log i status.submoduleSummary true. Converteixen hashos il·legibles en llistes de commits.

Consell 3: apunta a commits estables, preferiblement etiquetats. Un submòdul apuntant a la punta de main d'un altre equip és una font inesgotable de sorpreses. git describe --tags dins del submòdul et diu on ets.

Consell 4: documenta la mecànica al README.md. Cinc línies amb el clone --recurse-submodules i el submodule update --init estalvien una hora a cada persona nova.

Consell 5: un hook post-checkout que avisi de submòduls sense inicialitzar. És l'ús perfecte del que hem après a la lliçó 06-01.

Consell 6: en actualitzar el punter, explica per què al missatge. "Actualitza components-ui" no diu res; "Actualitza components-ui a v2.2.0 per la correcció del focus" converteix aquell commit en informació útil.

Exercicis

Exercici 1: crear i explorar un submòdul

Treballant en local (sense servidor remot):

  1. Crea un repositori biblioteca amb tres commits i una etiqueta v1.0.
  2. Crea un repositori aplicacio amb dos commits.
  3. Afegeix biblioteca com a submòdul d'aplicacio a vendor/biblioteca i confirma-ho.
  4. Inspecciona l'arbre amb git cat-file -p HEAD^{tree} i localitza l'entrada de mode 160000.
  5. Comprova que l'objecte commit al qual apunta no existeix a la base d'objectes del pare.

Exercici 2: el cicle complet d'actualització

Sobre l'exercici anterior:

  1. Afegeix dos commits nous a biblioteca.
  2. A aplicacio, comprova que git status no diu res (el punter continua igual).
  3. Executa git submodule update --remote i observa què canvia a git status.
  4. Confirma el punter nou amb un missatge que expliqui el perquè.
  5. Comprova el diff del punter amb --submodule=short, --submodule=log i --submodule=diff.

Exercici 3: simular els problemes i resoldre'ls

  1. Clona aplicacio sense --recurse-submodules i comprova el directori buit i la sortida de git submodule status.
  2. Arregla-ho amb git submodule update --init.
  3. Al clon, entra al submòdul i comprova que ets en detached HEAD. Fes un commit allà sense canviar de branca i observa l'avís de Git.
  4. Recupera aquell commit orfe posant-lo en una branca.
  5. Elimina el submòdul del pare del tot (deinit, git rm, .git/modules/) i verifica que no queden restes.

Solucions

Solució 1:

mkdir -p /tmp/practica-sub && cd /tmp/practica-sub

# La biblioteca
git init -q -b main biblioteca
cd biblioteca
echo "export function boto() {}" > boto.js
git add . && git commit -q -m "Afegeix el component boto"
echo "export function dialeg() {}" > dialeg.js
git add . && git commit -q -m "Afegeix el component dialeg"
echo "export function camp() {}" > camp.js
git add . && git commit -q -m "Afegeix el component camp de text"
git tag v1.0
cd ..

# L'aplicació
git init -q -b main aplicacio
cd aplicacio
echo "<html><body></body></html>" > index.html
git add . && git commit -q -m "Afegeix l'esquelet HTML"
echo "console.info('arrencada');" > app.js
git add . && git commit -q -m "Afegeix l'arrencada de l'aplicacio"
git -c protocol.file.allow=always submodule add ../biblioteca vendor/biblioteca
git status --short
A  .gitmodules
A  vendor/biblioteca

L'opció -c protocol.file.allow=always cal des de Git 2.38 per fer servir submòduls amb rutes locals; amb URLs https:// o git@ no és necessària.

git commit -q -m "Afegeix biblioteca com a submodul a v1.0"
git cat-file -p HEAD^{tree}
100644 blob 3f8a1c9e...	.gitmodules
100644 blob 7d2e5b4c...	app.js
100644 blob 9a4f1c6b...	index.html
040000 tree 2c8d5f9a...	vendor
git cat-file -p HEAD:vendor
160000 commit 8b3d6f2a9c4e7b1d5f8a2c6e9b4d7f1a3c5e8b2d	biblioteca

Aquí hi ha el mode 160000 i el tipus commit.

git cat-file -t 8b3d6f2 2>&1 | head -1
fatal: git cat-file: could not get object info

Confirmat: l'objecte no és a la base de dades del pare. Només se'n desa el nom.

Solució 2:

cd /tmp/practica-sub/biblioteca
echo "export function taula() {}" > taula.js
git add . && git commit -q -m "Afegeix el component taula"
echo "// corregit el focus" >> dialeg.js
git commit -qam "Retorna el focus al disparador en tancar el dialeg"
git tag v1.1

cd /tmp/practica-sub/aplicacio
git status --short
(sense sortida)

El pare continua apuntant al commit fixat: els commits nous de la biblioteca li són indiferents. Aquesta és la propietat, no una fallada.

git submodule update --remote
git status
Submodule path 'vendor/biblioteca': checked out 'd5a9c2f...'
On branch main
Changes not staged for commit:
	modified:   vendor/biblioteca (new commits)
git diff --submodule=short
diff --git a/vendor/biblioteca b/vendor/biblioteca
index 8b3d6f2..d5a9c2f 160000
--- a/vendor/biblioteca
+++ b/vendor/biblioteca
@@ -1 +1 @@
-Subproject commit 8b3d6f2a9c4e7b1d5f8a2c6e9b4d7f1a3c5e8b2d
+Subproject commit d5a9c2f7b1e4a8d3c6f9b2e5a8d1c4f7b3e6a9d2
git diff --submodule=log
Submodule vendor/biblioteca 8b3d6f2..d5a9c2f:
  > Retorna el focus al disparador en tancar el dialeg
  > Afegeix el component taula

Molt més útil: dos commits, amb els seus assumptes.

git diff --submodule=diff | head -20

Mostra el contingut real dels canvis dins del submòdul.

git add vendor/biblioteca
git commit -q -m "Actualitza biblioteca a v1.1

Inclou la correccio del focus al dialeg, necessaria per al
flux d'esborrat amb confirmacio."
git submodule status
 d5a9c2f7b1e4a8d3c6f9b2e5a8d1c4f7b3e6a9d2 vendor/biblioteca (v1.1)

Prefix espai: tot en ordre.

Solució 3:

cd /tmp/practica-sub
git -c protocol.file.allow=always clone -q aplicacio clon-app
cd clon-app
ls vendor/biblioteca/
(buit)
git submodule status
-d5a9c2f7b1e4a8d3c6f9b2e5a8d1c4f7b3e6a9d2 vendor/biblioteca

El - inicial és el diagnòstic: no inicialitzat.

git -c protocol.file.allow=always submodule update --init
ls vendor/biblioteca/
git submodule status
boto.js  camp.js  dialeg.js  taula.js
 d5a9c2f7b1e4a8d3c6f9b2e5a8d1c4f7b3e6a9d2 vendor/biblioteca (v1.1)
# 3. El detached HEAD
cd vendor/biblioteca
git status | head -2
HEAD detached at d5a9c2f
echo "export function menu() {}" > menu.js
git add . && git commit -q -m "Afegeix el component menu"
git log --oneline -1
cd ../..
git submodule status
a7c3e9f Afegeix el component menu
+a7c3e9f1d5b8c2e6a9f4d7b1c3e8a5f2d6b9c4e7 vendor/biblioteca (v1.1-1-ga7c3e9f)

El + avisa: el submòdul és en un commit diferent del registrat. I aquell commit no és a cap branca:

cd vendor/biblioteca
git branch --contains HEAD
(sense sortida: no pertany a cap branca)
# 4. Rescatar-lo
git switch -c correccio/menu
git branch --contains HEAD
* correccio/menu

Ja és fora de perill. Sense aquest pas, un git submodule update l'hauria deixat orfe.

# 5. Eliminar el submòdul del tot
cd /tmp/practica-sub/clon-app
git submodule deinit -f vendor/biblioteca
git rm -q vendor/biblioteca
rm -rf .git/modules/vendor/biblioteca
git commit -q -m "Elimina el submodul biblioteca"

cat .gitmodules 2>/dev/null || echo "(.gitmodules ja no existeix)"
git config --get-regexp '^submodule\.' || echo "(sense configuracio de submoduls)"
ls .git/modules 2>/dev/null || echo "(sense repositoris interns)"
(.gitmodules ja no existeix)
(sense configuracio de submoduls)
(sense repositoris interns)

Els tres llocs on viu un submòdul, nets. Un rm -rf a seques hauria deixat restes als tres.

Conclusió

Els submòduls són la resposta nativa de Git a compondre un projecte amb diversos repositoris, i tot el seu comportament es dedueix d'una sola idea. L'essencial:

  • Un submòdul és un punter a un commit concret d'un altre repositori, desat com una entrada d'arbre de mode 160000 i tipus commit, més una línia a .gitmodules. El pare no conté el codi de la biblioteca.
  • Això dona reproductibilitat exacta (cada commit del pare fixa una versió de la dependència), historials independents i actualització explícita: el punter no es mou sol.
  • git submodule add <url> <ruta> l'incorpora; el commit resultant toca dos fitxers, no dos-cents.
  • Clonar requereix --recurse-submodules, o git submodule update --init --recursive després. És l'error més freqüent.
  • Hi ha dues actualitzacions oposades: submodule update posa el submòdul on diu el pare; submodule update --remote porta el que és nou del remot i cal confirmar el punter al pare perquè existeixi de debò.
  • Dins del submòdul s'és en detached HEAD: canvia a una branca abans de treballar, i publica el commit del submòdul abans de confirmar el punter (push.recurseSubmodules check).
  • git submodule status amb els seus prefixos ( , -, +, U), foreach i diff --submodule=log són les eines d'inspecció. submodule.recurse true, diff.submodule log i status.submoduleSummary true eliminen la major part de la fricció.
  • Els problemes reals són sempre els mateixos: directoris buits, punters a commits sense publicar, canvis sense confirmar dins del submòdul, canvis de branca al pare i conflictes de punter. Tots tenen solució coneguda i tots tenen prevenció.
  • I sobretot: no són l'opció per defecte. Compara'ls amb git subtree, amb un gestor de paquets (sovint la millor resposta) i amb el monorepo (lliçó 10-04), i tria amb criteri. Per a binaris grans, l'eina és Git LFS (lliçó 10-03).

gestor-tasques ja es compon de dos repositoris, i l'equip té la traçabilitat que necessitava. Però queda un problema pendent des del final del mòdul 5, i és el de la Carla.

La Carla treballa a funcionalitat/esborrat-multiple, una branca llarga i a mitges. Cada vegada que arriba un avís urgent —una fallada en producció, una revisió de codi per atendre, un dubte sobre una altra branca— ha de fer git stash, canviar de branca, resoldre, tornar i git stash pop. Deu vegades al dia. I amb el submòdul acabat d'afegir la cosa empitjora, perquè cada canvi de branca arrossega també la seva actualització.

La seva primera idea és clonar el repositori dues vegades. Funcionaria, però duplica l'espai, duplica els fetch i deixa dos repositoris amb historials que cal mantenir sincronitzats a mà. Hi ha una solució molt millor, integrada a Git i sorprenentment poc coneguda: diversos directoris de treball compartint una única base de dades d'objectes. És la lliçó 06-06: Múltiples Còpies de Treball amb git worktree.

Dominant Git: De Principiant a Avançat

Mòdul 1: Introducció a Git

Mòdul 2: Operacions Bàsiques de Git

Mòdul 3: Branques i Fusió

Mòdul 4: Treballant amb Repositoris Remots

Mòdul 5: Operacions Avançades de Git

Mòdul 6: Eines i Tècniques de Git

Mòdul 7: Estratègies de Col·laboració i Flux de Treball

Mòdul 8: Bones Pràctiques i Consells de Git

Mòdul 9: Resolució de Problemes i Depuració

Mòdul 10: Git al Món Real

© Copyright 2026. Tots els drets reservats