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
- Què és exactament un submòdul
git submodule add: afegircomponents-ui- Què es confirma exactament: l'objecte de tipus commit
- Clonar un projecte amb submòduls
- Actualitzar: el submòdul davant del punter
- Treballar dins d'un submòdul i el detached HEAD
- Inspecció:
status,foreachidiff --submodule submodule.recursei altres opcions que treuen dolor- Els problemes reals
- Alternatives: submòduls, subtree, paquets i monorepo
- 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
.gitmodulesque diu d'on clonar aquell altre repositori.
Dues peces, cap més:
- El punter: una entrada a l'arbre (lliçó 01-04) el tipus de la qual no és
blobnitree, sinócommit. Desa un hash de 40 caràcters i res més. .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-tasquesfixa 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 logdiferents. - 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.
git submodule add: afegir components-ui
git submodule add: afegir components-uiL'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-uiCloning 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:
- 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:
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.gitI 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:
// app.js
const dialeg = ComponentsUI.creaDialeg({
titol: 'Confirmar esborrat',
missatge: 'Segur que vols esborrar aquesta tasca?',
});I envia:
- 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:
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
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.
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:
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 9d2f6a8b3e7c1d5f9a2b6c4e8d1f3a7c5b9e2d6fUna línia que canvia. Tot el contingut de la biblioteca pot haver canviat, però per al pare això és un punter que es mou.
- 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/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.gitCloning 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 fixatO en una sola ordre:
git submodule update --init
git submodule update --init --recursive # si els submòduls tenen submòdulsLa 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 updateTambé se'n pot inicialitzar només algun, en projectes amb molts:
- 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:
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 ha fet fetch al submòdul i ha fet checkout de la punta de la seva branca de seguiment. Quina branca? Per aquest ordre:
- La indicada a
.gitmodulesambbranch = <branca>(ogit submodule add -b). - Si no n'hi ha cap,
HEADdel remot, que sol sermain.
[submodule "vendor/components-ui"]
path = vendor/components-ui
url = [email protected]:equip/components-ui.git
branch = estableI ara el que és important: això encara no ha canviat res a l'historial del pare. És un canvi sense confirmar:
(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 pushsequenceDiagram
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à.
- 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:
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 pushEl 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:
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 abanscheck és l'opció recomanada: t'avisa i t'obliga a decidir, en lloc de publicar coses pel seu compte.
- Inspecció:
status, foreach i diff --submodule
status, foreach i diff --submodulegit submodule status
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 |
Té conflictes de fusió sense resoldre |
Exemples de les situacions problemàtiques:
→ En Bruno acaba de clonar sense --recurse-submodules.
→ 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 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:
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:
Amb status.submoduleSummary, git status també resumeix els commits pendents en lloc de dir només (new commits).
submodule.recurse i altres opcions que treuen dolor
submodule.recurse i altres opcions que treuen dolorLa 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:
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.recurseno afectagit clone, que continua necessitant el seu--recurse-submodulesexplí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 4I 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).
- 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"
fiProblema 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à.
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 dirtyL'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:
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 commitSovint 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.
- 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? | Sí (--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) | Sí | No | Sí |
| 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):
- Crea un repositori
bibliotecaamb tres commits i una etiquetav1.0. - Crea un repositori
aplicacioamb dos commits. - Afegeix
bibliotecacom a submòdul d'aplicacioavendor/bibliotecai confirma-ho. - Inspecciona l'arbre amb
git cat-file -p HEAD^{tree}i localitza l'entrada de mode160000. - 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:
- Afegeix dos commits nous a
biblioteca. - A
aplicacio, comprova quegit statusno diu res (el punter continua igual). - Executa
git submodule update --remotei observa què canvia agit status. - Confirma el punter nou amb un missatge que expliqui el perquè.
- Comprova el diff del punter amb
--submodule=short,--submodule=logi--submodule=diff.
Exercici 3: simular els problemes i resoldre'ls
- Clona
aplicaciosense--recurse-submodulesi comprova el directori buit i la sortida degit submodule status. - Arregla-ho amb
git submodule update --init. - 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.
- Recupera aquell commit orfe posant-lo en una branca.
- 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"L'opció
-c protocol.file.allow=alwayscal des de Git 2.38 per fer servir submòduls amb rutes locals; amb URLshttps://ogit@no és necessària.
100644 blob 3f8a1c9e... .gitmodules 100644 blob 7d2e5b4c... app.js 100644 blob 9a4f1c6b... index.html 040000 tree 2c8d5f9a... vendor
Aquí hi ha el mode 160000 i el tipus commit.
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 --shortEl pare continua apuntant al commit fixat: els commits nous de la biblioteca li són indiferents. Aquesta és la propietat, no una fallada.
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
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.
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 statusPrefix 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/El - inicial és el diagnòstic: no inicialitzat.
git -c protocol.file.allow=always submodule update --init
ls vendor/biblioteca/
git submodule statusecho "export function menu() {}" > menu.js
git add . && git commit -q -m "Afegeix el component menu"
git log --oneline -1
cd ../..
git submodule statusEl + avisa: el submòdul és en un commit diferent del registrat. I aquell commit no és a cap branca:
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)"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
160000i tipuscommit, 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, ogit submodule update --init --recursivedesprés. És l'error més freqüent. - Hi ha dues actualitzacions oposades:
submodule updateposa el submòdul on diu el pare;submodule update --remoteporta 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 statusamb els seus prefixos (,-,+,U),foreachidiff --submodule=logsón les eines d'inspecció.submodule.recurse true,diff.submodule logistatus.submoduleSummary trueeliminen 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
- Què és Git?
- Instal·lant Git
- Terminologia Bàsica de Git
- El Model de Dades de Git
- Configurant Git
- Configuració Inicial
Mòdul 2: Operacions Bàsiques de Git
- Creant un Repositori
- Clonant un Repositori
- Flux de Treball Bàsic de Git
- Preparant i Confirmant Canvis
- Inspeccionant Canvis amb git diff
- Visualitzant l'Historial de Confirmacions
Mòdul 3: Branques i Fusió
- Entenent les Branques
- Creant i Canviant Branques
- Fusionant Branques
- Estratègies de Fusió
- Resolent Conflictes de Fusió
- Gestió de Branques
Mòdul 4: Treballant amb Repositoris Remots
- Entenent els Repositoris Remots
- Afegint un Repositori Remot
- Autenticació amb Repositoris Remots
- Obtenint i Baixant Canvis
- Enviant Canvis
- Rastrejant Branques
Mòdul 5: Operacions Avançades de Git
- Rebase
- Rebase Interactiu
- Cherry-Picking de Confirmacions
- Desant Canvis Temporals
- Etiquetant Confirmacions
- Revertint Confirmacions
Mòdul 6: Eines i Tècniques de Git
- Usant Git Hooks
- Git Bisect
- Git Blame
- Git Log i Àlies
- Submòduls de Git
- Múltiples Còpies de Treball amb git worktree
Mòdul 7: Estratègies de Col·laboració i Flux de Treball
- Forks i Pull Requests
- Revisions de Codi amb Git
- Flux de Treball Git Flow
- GitHub Flow
- Trunk Based Development
- Integració Contínua amb Git
Mòdul 8: Bones Pràctiques i Consells de Git
- Escrivint Bons Missatges de Confirmació
- Mantenint un Historial Net
- Ignorant Fitxers amb .gitignore
- Atributs de Fitxer amb .gitattributes
- Bones Pràctiques de Seguretat
- Consells de Rendiment
Mòdul 9: Resolució de Problemes i Depuració
- Problemes Habituals de Git
- Desfent Canvis
- Resolent Divergències amb el Remot
- Recuperant Confirmacions Perdudes
- Tractant amb Repositoris Corruptes
- Tècniques Avançades de Depuració
