Git submodule: come aggiungere, aggiornare e rimuovere i sottomoduli
Un Git submodule è un altro repository annidato nel vostro e fissato su un commit preciso. Si aggiunge con git submodule add <url> <percorso>, si ottiene al momento del clone con git clone --recurse-submodules, si aggiorna con git submodule update --remote e si rimuove con git submodule deinit e git rm.
Git submodule (sottomodulo) è un repository Git separato annidato in una cartella del vostro progetto. La sua caratteristica principale: il repository padre non conserva i file del sottomodulo, ma un riferimento a un commit preciso al suo interno. Per questo chiunque cloni il progetto avrà il sottomodulo nella stessa identica versione, finché non siete voi a cambiarla esplicitamente.
I sottomoduli si usano quando bisogna includere una libreria di terzi senza package manager, condividere codice comune tra più progetti, spostare un tema grafico o un firmware in un repository separato con una storia propria.
Come sono fatti i git submodules
Un sottomodulo è composto da tre parti, e capire come sono collegate elimina gran parte dei problemi:
- Il file
.gitmodulesnella radice del progetto: viene committato e descrive quale percorso viene preso da quale URL. - La voce gitlink nell'albero del commit: un elemento speciale con modalità
160000che, al posto del contenuto, contiene l'hash del commit del sottomodulo. - Il repository del sottomodulo stesso: i suoi dati sono conservati in
.git/modules/<nome>del padre, mentre nella cartella del sottomodulo c'è un file.gitche vi rimanda.
cat .gitmodules
[submodule "libs/ui-kit"]
path = libs/ui-kit
url = https://github.com/example/ui-kit.git
git ls-tree HEAD libs/
160000 commit 8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c libs/ui-kit
Notate bene: il padre registra proprio un commit, non un branch. È questa la differenza fondamentale tra un submodule git e una dipendenza del tipo «ultima versione di main».
Come aggiungere un sottomodulo
git submodule add https://github.com/example/ui-kit.git libs/ui-kit
Cloning into '/home/dev/shop/libs/ui-kit'...
done.
git status
new file: .gitmodules
new file: libs/ui-kit
git commit -m "Aggiungi ui-kit come sottomodulo"
Il comando clona il repository, crea o integra .gitmodules e aggiunge il gitlink all'index. Resta da fare il commit. Se non indicate il percorso, la cartella prenderà il nome del repository.
Perché il sottomodulo segua un branch specifico durante gli aggiornamenti, indicatelo subito: git submodule add -b develop <url> <percorso>. Il branch verrà scritto in .gitmodules come branch = develop.
URL relativo
Se il sottomodulo si trova sullo stesso server, potete indicare un percorso relativo: git submodule add ../ui-kit.git libs/ui-kit. Viene risolto rispetto all'URL del repository remoto del padre, quindi funziona sia via SSH sia via HTTPS, a seconda di come è stato clonato il progetto.
Come clonare un progetto con i sottomoduli
La lamentela più frequente: «ho clonato, ma la cartella del sottomodulo è vuota». Per impostazione predefinita git clone non scarica i sottomoduli. Il modo corretto è questo:
git clone --recurse-submodules https://github.com/example/shop.git
Se il progetto è già stato clonato senza sottomoduli:
git submodule update --init --recursive
--init registra i sottomoduli di .gitmodules nel .git/config locale, update li scarica e li porta sul commit fissato, --recursive fa lo stesso per i sottomoduli dentro i sottomoduli.
Come aggiornare un sottomodulo
Qui è importante distinguere due compiti: «ottenere la versione committata dai colleghi» e «passare a una nuova versione della libreria».
Ottenere la versione fissata nel progetto
Dopo un git pull nel padre il gitlink può essere cambiato, ma i file del sottomodulo non si aggiornano da soli: git status mostrerà modified: libs/ui-kit (new commits). Sincronizzate:
git pull
git submodule update --init --recursive
Per non farlo a mano, attivate l'automatismo: git config submodule.recurse true. Così pull, checkout e switch aggiorneranno i sottomoduli da soli. L'alternativa per una singola chiamata è git pull --recurse-submodules.
Passare a una nuova versione della libreria
# Opzione 1: prendere l'ultimo commit del branch seguito
git submodule update --remote libs/ui-kit
# Opzione 2: scegliere la versione a mano
cd libs/ui-kit
git fetch
git checkout v3.2.0
cd ../..
# In entrambi i casi: registrare la nuova versione nel padre
git add libs/ui-kit
git commit -m "Aggiorna ui-kit alla v3.2.0"
--remote prende il branch dal parametro branch in .gitmodules e, se manca, il branch a cui punta l'HEAD del repository remoto del sottomodulo (di solito main). Senza l'ultimo commit nel padre i colleghi non riceveranno la nuova versione: aggiornare un sottomodulo significa sempre modificare il gitlink, e questa modifica va committata.
Per vedere cosa è cambiato nei sottomoduli è comodo fare così:
git diff --submodule=log
git config diff.submodule log # rendere questo output il formato predefinito
git submodule status
8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c libs/ui-kit (v3.1.0)
+1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b libs/charts (v1.4.0-3-g1a2b3c4)
Il prefisso in git submodule status: - indica un sottomodulo non inizializzato, + che il suo commit attuale differisce da quello fissato nel padre, U un conflitto di merge.
Come modificare il codice di un sottomodulo
Dopo git submodule update il sottomodulo è in stato detached HEAD: su un commit preciso, non su un branch. Se iniziate a committare così, rischiate di perdere i commit al prossimo update. L'ordine di lavoro:
cd libs/ui-kit
git switch main # posizionarsi su un branch
# ... modifiche ...
git commit -am "Correggi i margini del pulsante"
git push # prima il push del sottomodulo!
cd ../..
git add libs/ui-kit
git commit -m "ui-kit: corretti i margini del pulsante"
git push
L'ordine dei push è fondamentale. Se inviate il padre prima del sottomodulo, sul server il progetto farà riferimento a un commit che nessuno tranne voi può scaricare, e ai colleghi git submodule update fallirà con l'errore fatal: remote error: upload-pack: not our ref o did not contain. Per proteggervi c'è git push --recurse-submodules=check (rifiuta il push se il sottomodulo non è stato inviato) oppure =on-demand (invia per primi i sottomoduli da solo).
Come rimuovere un sottomodulo
Un tempo la rimozione richiedeva di modificare a mano diversi file. Nelle versioni moderne di Git bastano tre passaggi:
git submodule deinit -f libs/ui-kit # togliere da .git/config e svuotare la cartella
git rm -f libs/ui-kit # togliere il gitlink e la voce da .gitmodules
rm -rf .git/modules/libs/ui-kit # eliminare i dati salvati del sottomodulo
git commit -m "Rimuovi il sottomodulo ui-kit"
L'ultimo passaggio non è indispensabile per la correttezza, ma senza di esso, se riaggiungete un sottomodulo nello stesso percorso, Git potrebbe lamentarsi che la cartella locale esiste già (A git directory for 'libs/ui-kit' is found locally).
Se è cambiato l'URL del sottomodulo
Il repository si è spostato: correggete .gitmodules e sincronizzate la configurazione locale:
git submodule set-url libs/ui-kit https://gitlab.com/example/ui-kit.git
git submodule sync --recursive
git commit -am "Nuovo indirizzo di ui-kit"
Anche i colleghi, dopo il pull, devono eseguire git submodule sync: l'URL di .gitmodules viene copiato nel loro .git/config solo con init o sync.
Comandi utili con più sottomoduli
| Compito | Comando |
|---|---|
| Elenco e stato | git submodule status --recursive |
| Eseguire un comando in ciascuno | git submodule foreach 'git switch main && git pull' |
| Velocizzare il download | git submodule update --init --jobs 4 |
| Scaricare senza storia | git submodule update --init --depth 1 |
| Riepilogo sintetico in status | git config status.submoduleSummary true |
Problemi tipici con i sottomoduli
- Cartella vuota dopo il clone: non è stato eseguito l'init. Soluzione:
git submodule update --init --recursive. - «modified: (new commits)» subito dopo il pull: il padre punta a un altro commit, ma i file sono vecchi. Soluzione:
git submodule update. - Sottomodulo riportato indietro per errore: avete eseguito
git add .quando il sottomodulo era su un commit vecchio e avete committato il ritorno alla versione precedente. Controllate sempregit diff --submoduleprima del commit. - Conflitto sul gitlink durante un merge: in due branch il sottomodulo è stato aggiornato a commit diversi. Entrate nel sottomodulo, scegliete il commit giusto (spesso il più recente, che include entrambe le modifiche), poi
git add percorsoe completate il merge. - Sottomodulo privato in CI: la build non riesce a scaricare il sottomodulo senza accesso. Bisogna fornire alla CI una chiave o un token per il repository del sottomodulo; gli URL relativi semplificano la configurazione.
Sottomoduli nel team: le regole condivise
La maggior parte dei problemi con i sottomoduli non nasce da Git, ma dal fatto che i membri del team agiscono in modo diverso. Alcune regole che conviene fissare nel README del progetto:
- Clonare solo con
--recurse-submodulese attivaresubmodule.recurse trueper tutti. - Aggiornare il sottomodulo con un commit separato e un messaggio chiaro: quale versione e perché.
- Non modificare il codice del sottomodulo nel progetto padre senza un commit e un push separati nel sottomodulo stesso.
- In CI eseguire esplicitamente
git submodule update --init --recursivee verificare che la build non dipenda da modifiche locali.
Per verificare in CI che tutti i commit fissati dei sottomoduli siano davvero disponibili basta un clone pulito: se qualcuno ha dimenticato di inviare il sottomodulo, la build fallirà al passo di update, e non a un collega una settimana dopo.
git clone --recurse-submodules --jobs 4 "$REPO_URL" build
cd build
git submodule status --recursive
Submodule o alternative
I sottomoduli offrono un controllo preciso della versione e storie separate, ma aggiungono passaggi manuali per ogni membro del team. Se il codice è una normale libreria, è più semplice un package manager (npm, pip, Composer, Go modules). Se conta che il codice esterno stia semplicemente dentro il progetto e si cloni senza comandi aggiuntivi, valutate git subtree: copia la storia nel repository principale e i colleghi non devono sapere nulla. Per molti servizi collegati i team scelgono spesso un monorepo. Il sottomodulo è giustificato quando il codice annidato ha una vita propria, rilasci propri e un team proprio.
In sintesi
Un Git submodule è un riferimento a un commit preciso di un altro repository. Si aggiunge con git submodule add, si ottiene con --recurse-submodules o update --init, si aggiorna a una nuova versione con update --remote più un commit nel padre, si rimuove con deinit, git rm e la pulizia di .git/modules. Ricordate il detached HEAD dentro il sottomodulo e l'ordine dei push. Gli strumenti avanzati di Git si affrontano nella pratica nel corso su Git.
Domande frequenti
Perché il sottomodulo è sempre in detached HEAD?
Il padre conserva l'hash di un commit, non un branch, e git submodule update porta il sottomodulo proprio su quel commit. Per modificarlo passate a un branch a mano o usate git submodule update --remote --merge.
Si può fissare un sottomodulo su un tag?
Non direttamente, ma potete passare al tag nel sottomodulo con git checkout e committare nel padre il commit risultante; in git submodule status il tag sarà visibile tra parentesi.
GitHub include i sottomoduli nell'archivio ZIP?
No, gli archivi dei sorgenti di GitHub non includono il contenuto dei sottomoduli, le cartelle saranno vuote. Usate git clone --recurse-submodules.
Che differenza c'è tra git submodule e git subtree?
Submodule conserva un riferimento a un commit del repository esterno e richiede comandi separati per ottenere il codice, mentre subtree copia file e storia direttamente nel repository principale.
Si può spostare un sottomodulo in un'altra cartella?
Sì, con il comando git mv old/path new/path: Git aggiornerà da solo .gitmodules e i riferimenti interni, resta solo da fare il commit.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Git da zero a sviluppatore sicuro».
Vai al corso