Articles

Mastering sottomoduli Git

Sottomoduli, passo dopo passo

Ora esploreremo ogni fase dell’utilizzo dei sottomoduli in un progetto collaborativo, assicurandoci di evidenziare comportamenti predefiniti, trappole e miglioramenti disponibili.

Per facilitare il tuo seguito, ho messo insieme alcuni repository di esempio con i loro “telecomandi” (in realtà solo directory)., È possibile decomprimere l’archivio dove vuoi, poi apri una shell (o Git Bash, se sei su Windows) in git-sub directory crea:

Scarica l’esempio pct

troverete tre cartelle in là:

  • principale funge da contenitore repo, locale al primo collaboratore,
  • plugin agisce come centrale di manutenzione repo per il modulo, e
  • telecomandi contiene il filesystem telecomandi per le due precedenti operazioni pronti contro termine.

Nei comandi di esempio riportati di seguito, il prompt visualizza sempre il repository in cui ci troviamo.,

Aggiunta di un sottomodulo

Iniziamo aggiungendo il nostro plugin come sottomodulo all’interno del nostro contenitore (che è in main). Il plugin stesso ha una struttura semplice:

.
├── README.md
├── lib
│ └── index.js
└── plugin-config.json

Quindi andiamo in main e usiamo il comando git submodule add. Prende l’URL del telecomando e una sottodirectory in cui “istanziare” il sottomodulo.

Perché usiamo percorsi invece di URL qui per i nostri telecomandi, abbiamo colpito uno strano, anche se ben noto, intoppo: percorsi relativi per telecomandi sono interpretati rispetto al nostro telecomando principale, no alla directory principale del nostro repository., Questo è super strano, non descritto da nessuna parte, ma l’ho visto accadere ogni volta. Quindi, invece di dire ../ telecomandi / plugin, diciamo solo ../plugin.

main (master u=) $ git submodule add ../plugin vendor/plugins/demo
Cloning into 'vendor/plugins/demo'…
done.
main (master + u=) $

Questo ha aggiunto alcune impostazioni nella nostra configurazione locale:

main (master + u=) $ cat .git/config


url = ../remotes/plugin

E questo ha anche messo in scena due file:

Eh?! Cos’e ‘ questo?file gitmodules? Diamo un’occhiata:

main (master + u=) $ cat .gitmodules

path = vendor/plugins/demo
url = ../plugin

Questo assomiglia furiosamente alla nostra configurazione locale why Quindi perché la duplicazione? Beh, proprio perche ‘la nostra configurazione locale e’ local locale., I nostri collaboratori non lo vedranno (che è perfettamente normale), quindi hanno bisogno di un meccanismo per ottenere le definizioni di tutti i sottomoduli che devono impostare nei propri repository. Ecco cosa .gitmodules è per; sarà letto più tardi dal comando git submodule init, come vedremo tra un attimo.

Mentre siamo sullo stato, nota quanto sia minimalista quando si tratta del nostro sottomodulo: va solo con un nuovo file eccessivamente generico invece di dirci di più su cosa sta succedendo al suo interno., Il nostro sottomodulo è stato effettivamente iniettato nella sottodirectory:


└── vendor
└── plugins
└── demo
├── .git
├── README.md
├── lib
│ └── index.js
└── plugin-config.json

Lo stato, come log e diff, è limitato al repository attivo (in questo momento, il contenitore), non ai sottomoduli, che sono repository nidificati. Questo è spesso problematico (è super facile perdere una regressione quando limitato a questa vista), quindi ti consiglio di impostare uno stato consapevole del sottomodulo una volta per tutte:

git config --global status.submoduleSummary true

E ora:

Aaaah, questo è di gran lunga migliore., Lo stato estende le sue informazioni di base per aggiungere che il sottomodulo presente in vendor/plugins/demo ha ottenuto 3 commit di notizie (come lo abbiamo appena creato, significa che il ramo remoto aveva solo tre commit), l’ultimo è un’aggiunta (nota la parentesi angolare di destra >) con una prima riga di messaggio di commit che

Per portare davvero a casa che abbiamo a che fare con due repository separati qui, entriamo nella directory del sottomodulo:

Il repository attivo è cambiato, perché un nuovo .,git prende il sopravvento: nella directory corrente (demo, la directory del sottomodulo), a .git esiste davvero, anche un singolo file, non una directory. Diamo un’occhiata all’interno:

demo (master u=) $ cat .git
gitdir: ../../../.git/modules/vendor/plugins/demo

Ancora una volta, poiché Git 1.7.8, Git non lascia le directory repo all’interno della directory di lavoro del contenitore, ma le centralizza in quella del contenitore .git directory (all’interno .git / modules) e utilizza un riferimento gitdir nei sottomoduli.,

La logica alla base di questo è semplice: consente al repository del contenitore di avere rami senza sottomodulo, senza dover scartare il repository del sottomodulo dalla directory di lavoro e ripristinarlo in seguito.

Naturalmente, quando si aggiunge il sottomodulo, è possibile scegliere di utilizzare un ramo specifico, o anche un commit specifico, utilizzando l’opzione-b CLI (come al solito, il valore predefinito è master). Nota che non siamo, in questo momento, su una testa distaccata, a differenza di ciò che accadrà in seguito: questo perché Git ha controllato master, non uno specifico SHA1. Avremmo dovuto specificare un SHA1 a-b per ottenere una testa staccata dal get-go.,

Così, al contenitore di repo, e cerchiamo di finalizzare il submodule dell’oltre e spingere per il telecomando:

demo (master u=) $ cd -
main (master + u=) $ git commit -m "Ajout submodule plugin demo"
main (master u+1) $ git push

Afferrare una repo che utilizza sotto-moduli

al fine di illustrare le questioni con la collaborazione su un repo che utilizza sotto-moduli, ti split personalità e di agire come il nostro collega, che cloni del contenitore telecomando per iniziare a lavorare con noi. Lo cloneremo in una directory di colleghi, in modo da poter dire immediatamente quale berretto di personalità abbiamo in un dato momento.,

La prima cosa da notare è che il nostro sottomodulo manca dalla directory di lavoro; solo la sua directory di base è qui:

vendor
└── plugins
└── demo

Come è successo? Ciò è semplicemente dovuto al fatto che, finora, il nostro nuovo repository (collega) non è ancora a conoscenza del nostro sottomodulo: le informazioni per esso non sono da nessuna parte nella sua configurazione locale (controlla il suo .git / config se non mi credi). Dovremo compilarlo, in base a cosa .gitmodules ha da dire, che è esattamente ciò che fa git submodule init:

Our .git / config è ora a conoscenza del nostro sottomodulo., Tuttavia, non l’abbiamo ancora recuperato dal suo telecomando, per non parlare di averlo presente nella nostra directory di lavoro. Eppure, il nostro stato si presenta come pulito!

Vedi, dobbiamo afferrare manualmente i commit rilevanti. Non è qualcosa che ha fatto il nostro clone iniziale, dobbiamo farlo ad ogni tiro. Torneremo a che in un minuto, in quanto questo è un clone comportamento può effettivamente automatizzare, se correttamente chiamato.,

In pratica, quando si ha a che fare con i repository sottomoduli, di solito raggruppiamo i due comandi (init e update) in uno:

colleague (master u=) $ git submodule update --init

È ancora un peccato che Git abbia fatto tutto da solo. Immagina, su progetti di FILO interdentale più grandi, quando i sottomoduli hanno i loro sottomoduli, e così via e così via become Questo diventerebbe rapidamente un incubo.

Si dà il caso che Git fornisca un’opzione CLI per clone per git automaticamente submodule update — init ricorsivamente subito dopo la clonazione: l’opzione piuttosto giustamente chiamata ricorsiva.,

Quindi proviamo di nuovo il tutto:

Ora è meglio! Nota che ora siamo su una testa distaccata all’interno del sottomodulo (come lo saremo d’ora in poi):

git-subs $ cd colleague/vendor/plugins/demo
demo ((master)) $

Vedi il doppio set di parentesi nel mio prompt, invece di un singolo set?, Se la tua richiesta non è configurato come il mio, per visualizzare testa staccata come viene descritto (con Git built-in e comandi di script, è necessario definire il GIT_PS1_DESCRIBE_STYLE=ramo variabile di ambiente), potrete invece vedere qualcosa di simile a questo:

demo ((fe64799...)) $

In ogni caso, lo status di conferma dove siamo:

demo ((master)) $ git status
HEAD detached at fe64799
nothing to commit, working directory clean

come Ottenere un aggiornamento dal submodule remoto

OK, ora che abbiamo la nostra repo (principale) e il nostro “collega” (collega) tutti insieme a collaborare, facciamo un passo nei panni di una terza persona: l’unico che mantiene il plugin., Qui, passiamo ad esso:

Ora, aggiungiamo due pseudo-commit e pubblichiamo questi sul telecomando:

Infine, mettiamo di nuovo il nostro cap “primo sviluppatore”:

plugin (master u=) $ cd ../main
main (master u=) $

Supponiamo che ora vogliamo ottenere questi due commit all’interno del nostro sottomodulo. Per raggiungere questo obiettivo, abbiamo bisogno di aggiornare il suo repository locale, iniziando spostandoci nella sua directory di lavoro in modo che diventi il nostro repository attivo.

In una nota a margine, non consiglierei di usare pull per questo tipo di aggiornamento., Per ottenere correttamente gli aggiornamenti nella directory di lavoro, questo comando richiede che tu sia sul ramo attivo corretto, che di solito non lo sei (sei su una testa distaccata la maggior parte del tempo). Dovresti iniziare con un checkout di quel ramo. Ma ancora più importante, il ramo remoto potrebbe benissimo essersi spostato più avanti dal commit che si desidera impostare, e un pull inietterebbe commit che potresti non volere nella tua base di codice locale.,

Pertanto, consiglio di dividere manualmente il processo: prima git fetch per ottenere tutti i nuovi dati dal telecomando nella cache locale, quindi accedere per verificare ciò che si ha e checkout sullo SHA1 desiderato. Oltre al controllo a grana più fine, questo approccio ha il vantaggio di lavorare indipendentemente dallo stato corrente (ramo attivo o testa distaccata).

OK, quindi siamo a posto, nessun commit estraneo., Comunque sia, impostiamo esplicitamente quello a cui siamo interessati (ovviamente hai un SHA1 diverso):

demo (master u-2) $ git checkout -q 0e90143

(Il-q è lì solo per risparmiarci Git blaterare su come stiamo finendo su una testa staccata. Di solito questo sarebbe un promemoria salutare, ma su questo sappiamo cosa stiamo facendo.)

Ora che il nostro sottomodulo è aggiornato, possiamo vedere il risultato nello stato del repository del contenitore:

Nella parte “classica” dello stato, vediamo un nuovo tipo di commit change, il che significa che il commit di riferimento è cambiato., Un’altra possibilità (che potrebbe essere aggravata da questa) è il nuovo contenuto, il che significherebbe che abbiamo apportato modifiche locali alla directory di lavoro del sottomodulo.

La parte inferiore, abilitata dal nostro stato.submoduleSummary = true setting in precedenza, afferma esplicitamente i commit introdotti (poiché usano una parentesi angolare rivolta a destra>) dal nostro ultimo commit del contenitore che aveva toccato il sottomodulo.

Nella famiglia “terrible default behaviors”, git diff lascia molto a desiderare:

Che cosa — ?, C’è un’opzione CLI che ci permette di vedere qualcosa di più utile:

main (master * u=) $ git diff --submodule=log
Submodule vendor/plugins/demo fe64799..0e90143:
> Pseudo-commit #2
> Pseudo-commit #1

Non ci sono altre modifiche locali al momento oltre al commit referenziato del sottomodulo Notice Nota che questo corrisponde quasi esattamente alla parte inferiore del nostro display di stato git avanzato.

Dover digitare ogni volta quel tipo di opzione CLI (che, a proposito, non appare nelle attuali offerte di completamento di Git) è piuttosto ingombrante. Fortunatamente, c’è un’impostazione di configurazione corrispondente:

Ora abbiamo solo bisogno di eseguire il commit del contenitore che finalizza l’aggiornamento del nostro sottomodulo., Se dovessi toccare il codice del contenitore per farlo funzionare con questo aggiornamento, commettilo, naturalmente. D’altra parte, evitare di mescolare le modifiche relative al sottomodulo e altre cose che riguarderebbero solo il codice del contenitore: separando ordinatamente i due, le migrazioni successive ad altri approcci di riutilizzo del codice sono rese più facili (anche, come al solito, atomic commit FTW).

Mentre stiamo per prendere questo aggiornamento del sottomodulo nel repository del nostro collega, spingeremo subito dopo il commit (che non è una buona pratica generale).

main (master * u=) $ git commit -am "Setting submodule on PC2"
main (master u+1) $ git push

Tirando un sottomodulo-usando repo

Clicca!, “Collega” cap on!

Quindi stiamo tirando gli aggiornamenti dal repository del contenitore remoto remote

(Potresti non avere il “Rebased e aggiornato con successo…” e vedere invece una “Unione fatta dalla strategia ‘ricorsiva’”. Se è così, il mio cuore va a voi, e si dovrebbe immediatamente imparare perché tira dovrebbe rebase).

Nota la seconda metà di questo display: riguarda il sottomodulo, che inizia con ” Fetching submodule…”.

Questo comportamento è diventato predefinito con Git 1.7.5, con l’impostazione di configurazione fetch.,recurseSubmodules ora predefinito su on-demand: se un progetto contenitore riceve aggiornamenti ai commit del sottomodulo di riferimento, questi sottomoduli vengono recuperati automaticamente. (Ricorda che il recupero è la prima parte di tirare.)

Ancora, e questo è fondamentale: Git recupera automaticamente, ma non si aggiorna automaticamente. La cache locale è aggiornata con il telecomando del sottomodulo, ma la directory di lavoro del sottomodulo è rimasta invariata rispetto al contenuto precedente. Almeno, puoi chiudere quel portatile, salire su un aereo e andare avanti ancora una volta offline., Sebbene questo recupero automatico sia limitato a sottomoduli già noti: tutti i nuovi, non ancora copiati nella configurazione locale, non vengono recuperati automaticamente.

Git recupera automaticamente, ma non si aggiorna automaticamente.

Il prompt corrente, con il suo asterisco (*), suggerisce modifiche locali, perché il nostro WD non è sincronizzato con l’indice, essendo quest’ultimo a conoscenza dei commit del sottomodulo appena referenziato. Controlla lo stato:

Nota come le parentesi angolari puntano a sinistra (<)?, Git vede che l’attuale WD non ha questi due commit, contrariamente alle aspettative del progetto container.

Questo è il pericolo enorme: se non si aggiorna esplicitamente la directory di lavoro del sottomodulo, il prossimo commit del contenitore regredirà il sottomodulo. Questa è una trappola del primo ordine.

È quindi obbligatorio finalizzare l’aggiornamento:

Finché stiamo cercando di formare buone abitudini generiche, il comando preferito qui sarebbe un sottomodulo git update — init — ricorsivo, al fine di avviare automaticamente qualsiasi nuovo sottomodulo e di aggiornarli ricorsivamente se necessario.,

C’è un altro caso limite: se l’URL remoto del sottomodulo è cambiato dall’ultimo utilizzo (forse uno dei collaboratori lo ha cambiato nel .gitmodules), devi aggiornare manualmente la tua configurazione locale per abbinarla a questo. In tale situazione, prima dell’aggiornamento del sottomodulo git, è necessario eseguire una sincronizzazione del sottomodulo git.

Dovrei menzionare, per completezza, che anche se l’aggiornamento del sottomodulo git è predefinito per controllare lo SHA1 referenziato, puoi cambiarlo, ad esempio, rebase qualsiasi lavoro di sottomodulo locale (ne parleremo molto presto) su di esso., Lo faresti impostando l’impostazione di configurazione di aggiornamento per il tuo sottomodulo per rebase all’interno della configurazione locale del tuo contenitore.

E mi dispiace ma no, non c’è alcuna impostazione di configurazione locale, o anche l’opzione CLI per quella materia, che può aggiornarsi automaticamente su pull. Per automatizzare tali cose, è necessario utilizzare alias, script personalizzati o ganci locali accuratamente creati., Ecco un esempio di alias spull (singola riga, divisa qui per la visualizzazione):

git config --global alias.spull '!git pull && git submodule sync --recursive && git submodule update --init --recursive'

Se vuoi mantenere la possibilità di passare argomenti personalizzati a git pull, puoi definire una funzione al volo e chiamarla, o andare con uno script personalizzato. Il primo approccio sarebbe simile a questo (di nuovo, singola riga):

Non molto leggibile, eh? Preferisco l’approccio di script personalizzato., Diciamo che metteresti un file di script git-spull da qualche parte all’interno del tuo PERCORSO (ho una directory ~/perso/bin nel mio PERCORSO solo per queste cose):

#! /bin/bash
git pull "$@" &&
git submodule sync --recursive &&
git submodule update --init --recursive

Quindi gli diamo i diritti di esecuzione:

chmod +x git-spull

E ora possiamo usarlo proprio come avremmo usato l’alias.

Aggiornamento di un sottomodulo sul posto nel contenitore

Questo è il caso d’uso più difficile, e dovresti stare lontano da esso il più possibile, preferendo la manutenzione attraverso il repository centrale e dedicato.,

Tuttavia, può accadere che il codice del sottomodulo non possa essere testato, o addirittura compilato, al di fuori del codice del contenitore. Molti temi e plugin hanno tali vincoli.

La prima cosa da capire è, perché stai per fare commit, devi iniziare da una base corretta, che sarà un suggerimento di ramo. È quindi necessario verificare che gli ultimi commit del ramo non “interrompano” il progetto del contenitore. Se lo fanno, beh, creare il proprio ramo specifico del contenitore nel sottomodulo sembra allettante, ma quel percorso porta a un forte accoppiamento tra sottomodulo e contenitore, il che non è consigliabile., Potresti voler interrompere la “sottomodulazione” di quel codice in questo particolare progetto e incorporarlo come qualsiasi contenuto normale.

Ammettiamo che è possibile, in buona coscienza, aggiungere al ramo master corrente del sottomodulo. Iniziamo sincronizzando il nostro stato locale sul telecomando:

Un altro modo per farlo sarebbe, dal repository del contenitore, sincronizzare esplicitamente il ramo locale del sottomodulo sul suo ramo remoto tracciato (singola riga in alto, ultima seguita da spazi bianchi):

Ora possiamo modificare il codice, farlo funzionare, testarlo, ecc., Una volta che siamo tutti pronti, possiamo quindi eseguire i due commit e le due spinte necessarie (è super facile, e in pratica troppo frequente, dimenticarne alcune).

Aggiungiamo semplicemente un lavoro falso e facciamo i due commit correlati, a livello di sottomodulo e contenitore:

A questo punto, il pericolo principale è dimenticare di spingere il sottomodulo. Si torna al progetto contenitore, lo si impegna e si spinge solo il contenitore. È un errore facile da fare, specialmente all’interno di un IDE o GUI. Quando i tuoi colleghi cercano di ottenere aggiornamenti, si scatena l’inferno., Guarda il primo passo:

Non c’è assolutamente alcuna indicazione che Git non possa recuperare il commit di riferimento dal telecomando del sottomodulo. Il primo suggerimento di questo è nello stato:

Nota l’avviso: apparentemente, il commit appena referenziato per il sottomodulo non si trova da nessuna parte. Infatti, se tentiamo di aggiornare la directory di lavoro del sottomodulo, otteniamo:

main (master * u=) $ git submodule update
fatal: reference is not a tree: 12e3a529698c519b2fab790630f71bd531c45727
Unable to checkout '12e3a529698c519b2fab790630f71bd531c45727' in submodule path 'vendor/plugins/demo'

Puoi chiaramente vedere quanto sia importante ricordare di spingere anche il sottomodulo, idealmente prima di spingere il contenitore., Facciamolo nel collega e riproviamo l’aggiornamento:

Dovrei notare che c’è un’opzione CLI che verificherà se anche i commit dei sottomoduli attualmente referenziati devono essere spinti, e in tal caso li spingerà: è git push — recurse-submodules=on-demand (piuttosto un boccone, certo). Ha bisogno di avere qualcosa a livello di contenitore per spingere al lavoro, però: solo i sottomoduli non lo taglieranno.

Inoltre, (non ci sono impostazioni di configurazione per questo, quindi dovresti standardizzare le procedure attorno a un alias, ad esempio spush:) — a partire da Git 2.7.0, ora c’è una spinta.,recurseSubmodules configurazioni impostazione è possibile definire (su richiesta o controllare).

git config --global alias.spush 'push --recurse-submodules=on-demand'

Rimozione di un sottomodulo

Ci sono due situazioni in cui vorresti “rimuovere” un sottomodulo:

  • Vuoi solo cancellare la directory di lavoro (forse prima di archiviare il WD del contenitore) ma vuoi mantenere la possibilità di ripristinarlo in seguito (quindi deve rimanere in .gitmodules e .git / modules);
  • Si desidera rimuovere definitivamente dal ramo corrente.

Vediamo ogni caso a turno.,

Rimozione temporanea di un sottomodulo

La prima situazione è facilmente gestibile dal sottomodulo git deinit. Guarda tu stesso:

Questo non ha alcun impatto sullo stato del contenitore. Il sottomodulo non è più conosciuto localmente (è passato da .git / config), quindi la sua assenza dalla directory di lavoro passa inosservata. Abbiamo ancora la directory vendor / plugins / demo ma è vuota; potremmo spogliarla senza conseguenze.

Il sottomodulo non deve avere alcuna modifica locale quando si esegue questa operazione, altrimenti è necessario forzare la chiamata.,

Qualsiasi sottocomando successivo del sottomodulo git ignorerà beatamente questo sottomodulo finché non lo inizierai di nuovo, poiché il sottomodulo non sarà nemmeno nella configurazione locale. Tali comandi includono update, foreach e sync.

D’altra parte, il sottomodulo rimane definito in .gitmodules: un init seguito da un aggiornamento (o un singolo update — init) lo ripristinerà come nuovo:

Rimuovendo definitivamente un sottomodulo

Questo significa che vuoi sbarazzarti del sottomodulo per sempre: un normale git rm lo farà, proprio come per qualsiasi altra parte della directory di lavoro., Questo funzionerà solo se il tuo sottomodulo utilizza un gitfile (a .git che è un file, non una directory), che è il caso che inizia con Git 1.7.8. Altrimenti dovrai gestirlo a mano (ti dirò come alla fine).

Oltre a rimuovere il sottomodulo dalla directory di lavoro, il comando aggiornerà il .file gitmodules quindi non fa più riferimento al sottomodulo. Ecco qui:

Naturalmente, le informazioni sullo stato avanzato si spostano su se stesse qui, perché il gitfile per il sottomodulo è sparito (in realtà, l’intera directory demo è scomparsa).,

La cosa strana è che la configurazione locale conserva le informazioni del sottomodulo, a differenza di ciò che accade quando si deinit. Quindi, per una rimozione completa, ti consiglio di fare entrambe le cose, in sequenza, in modo da finire correttamente ripulito (non funzionerebbe dopo il nostro comando precedente, perché è stato cancellato .gitmodules già):

git submodule deinit path/to/module # ensure local config cleanup
git rm path/to/module # clean WD and .gitmodules

Indipendentemente dal tuo approccio, il repository del sottomodulo rimane presente in .git / modules/vendor/plugins / demo, ma sei libero di ucciderlo ogni volta che vuoi.

Se hai bisogno di rimuovere un sottomodulo che è stato impostato prima di Git 1.7.,8, e quindi incorpora la sua .directory git direttamente nella directory di lavoro del contenitore (invece di fare affidamento su un gitfile), dovrai rompere il bulldozer: i due comandi precedenti devono essere preceduti da una rimozione manuale della cartella, ad esempio rm-fr vendor/plugins/demo, perché detti comandi si rifiuteranno sempre di eliminare un repository effettivo.