Articles

Mastering Git submodules

Submodules, stap voor stap

we verkennen nu elke stap van het gebruik van submodules in een samenwerkingsproject, waarbij we ervoor zorgen dat we standaard gedrag, traps en beschikbare verbeteringen markeren.

om het volgen te vergemakkelijken, heb ik een paar voorbeeldrepo ‘ s samengesteld met hun “remotes” (eigenlijk alleen mappen)., U kunt decomprimeren het archief waar u wilt, dan opent een shell (of Git Bash, als je op Windows) in de git-subs map wordt:

Download het voorbeeld repo ‘ s

vindt U drie mappen in:

  • main acts als de container repo, lokaal op de eerste medewerker
  • plugin fungeert als het centrale onderhoud repo voor de module, en
  • afstandsbedieningen bevat het bestandssysteem afstandsbedieningen voor de vorige twee repo ‘ s.

in het voorbeeld hieronder geeft de prompt altijd weer in welke repo we zitten.,

het toevoegen van een submodule

laten we beginnen met het toevoegen van onze plugin als een submodule in onze container (die in main). De plugin zelf heeft een eenvoudige structuur:

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

dus laten we naar main gaan en het GIT submodule add commando gebruiken. Het neemt de URL van de remote en een subdirectory waarin de submodule “instantiate”.

omdat we paden gebruiken in plaats van URL ’s hier voor onze remotes, raken we een vreemde, zij het bekende, addertje onder het gras: relatieve paden voor remotes worden geïnterpreteerd relatief aan onze hoofd remote, nee aan onze repo’ s root directory., Dit is super raar, nergens beschreven, maar ik heb het elke keer zien gebeuren. Dus in plaats van te zeggen ../remotes / plugin, we zeggen gewoon ../plug.

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

Dit voegde enkele instellingen toe aan onze lokale configuratie:

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


url = ../remotes/plugin

en dit stageerde ook twee bestanden:

Huh?! Wat is dit .gitmodules bestand? Laten we eens kijken:

main (master + u=) $ cat .gitmodules

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

Dit lijkt op onze lokale configuratie … dus waarom de duplicatie? Nou, juist omdat onze lokale config … lokaal is., Onze medewerkers zullen het niet zien (wat volkomen normaal is), dus hebben ze een mechanisme nodig om de definities te krijgen van alle submodules die ze in hun eigen repo ‘ s moeten opzetten. Dit is wat .gitmodules is er voor; het zal later worden gelezen door het GIT submodule init commando, zoals we zo zullen zien.

terwijl we op status staan, merk op hoe minimalistisch het is als het gaat om onze submodule: het gaat gewoon met een overdreven generiek nieuw bestand in plaats van ons meer te vertellen over wat er in het bestand gebeurt., Onze submodule werd inderdaad geïnjecteerd in de subdirectory:


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

Status, zoals logs en diffs, is beperkt tot de actieve repo (op dit moment, de container), niet tot submodules, die geneste repo ‘ s zijn. Dit is vaak problematisch (het is super gemakkelijk om een regressie te missen wanneer deze weergave beperkt is), dus ik raad je aan om eens en voor altijd een submodule-bewuste status in te stellen:

git config --global status.submoduleSummary true

en nu:

Aaaah, dit is veel beter., De status breidt zijn basisinformatie uit om toe te voegen dat de submodule aanwezig op vendor/plugins/demo 3 nieuws commits heeft (zoals we het zojuist hebben aangemaakt, betekent dit dat de remote branch slechts drie commits had), de laatste is een toevoeging (let op de rechterwijzende hoek bracket >) met een eerste commit berichtregel die luidt “Fix repo naam…”.

om echt duidelijk te maken dat we hier met twee afzonderlijke repo ‘ s te maken hebben, gaan we naar de map van de submodule:

De actieve repo is veranderd, omdat een nieuwe .,git neemt het over: in de huidige directory (demo, de directory van de submodule), a .git bestaat inderdaad, ook een enkel bestand, geen directory. Laten we eens naar binnen kijken:

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

nogmaals, sinds Git 1.7.8 laat Git geen repo directory ‘ s achter in de werkdirectory van de container, maar centraliseert deze in die van de container .Git directory (binnenin .git / modules), en gebruikt een gitdir referentie in submodules.,

de reden hiervoor is eenvoudig: het staat de container repo toe om submodule-loze branches te hebben, zonder dat de repo van de submodule uit de werkdirectory moet worden verwijderd en later moet worden hersteld.

natuurlijk, wanneer je de submodule toevoegt, kun je ervoor kiezen om een specifieke branch te gebruiken, of zelfs een specifieke commit, met behulp van de-B CLI optie (zoals gewoonlijk is de standaard master). Merk op dat we nu niet op een losstaande head zitten, in tegenstelling tot wat er later zal gebeuren: Dit komt omdat Git master heeft uitgecheckt, niet een specifieke SHA1. We zouden een SHA1 naar-b moeten specificeren om vanaf het begin een losstaand hoofd te krijgen.,

So, back to the container repo, and let ’s finalize the submodule’ s addition and push that to the remote:

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

Grabbing a repo that uses submodules

om de problemen te illustreren met het samenwerken aan een repo die submodules gebruikt, zullen we persoonlijkheden splitsen en optreden als onze collega, die de remote van de container klonen om met ons te beginnen werken. We zullen dat klonen in een collega directory, zodat we direct kunnen zien welke persoonlijkheidspet we hebben op een bepaald moment.,

het eerste wat opvalt is dat onze submodule ontbreekt in de werkmap; alleen de basismap is hier:

vendor
└── plugins
└── demo

Hoe is dat gebeurd? Dit komt simpelweg door het feit dat onze nieuwe repo (collega) tot nu toe nog niet op de hoogte is van onze submodule: de informatie hiervoor is nergens in zijn lokale configuratie (check its .git / config als je me niet gelooft). Dat moeten we invullen, gebaseerd op wat .gitmodules heeft het te zeggen, en dat is precies wat git submodule init doet:

Our .git / config is zich nu bewust van onze submodule., Echter, we hebben het nog steeds niet opgehaald van de remote, om nog maar te zwijgen over het feit dat het aanwezig is in onze werk directory. En toch, onze status verschijnt als schoon!

Zie, we moeten de relevante commits handmatig pakken. Het is niet iets wat onze eerste kloon deed, we moeten het bij elke pull doen. Daar komen we zo op terug, want dit is een gedragskloon die daadwerkelijk kan automatiseren, als hij correct wordt aangeroepen.,

in de praktijk, bij het omgaan met submodule-gebruik van repo ‘s, groeperen we meestal de twee commando’ s (init en update) in één:

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

Het is nog steeds jammer dat Git je dat allemaal zelf laat doen. Stel je eens voor, bij grotere FLOSS projecten, wanneer submodules hun eigen submodules hebben, enzovoort, enzovoort … zou dit al snel een nachtmerrie worden.

het gebeurt zo dat Git een CLI optie biedt voor clone om automatisch Git submodule update — init recursief bij te werken direct na het klonen: de nogal toepasselijke — named-recursive optie.,

dus laten we het hele ding opnieuw proberen:

nu is dat beter! Merk op dat we nu op een losgekoppelde head in de submodule zitten (zoals we vanaf nu zullen zijn):

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

zie de dubbele verzameling haakjes in mijn prompt, in plaats van een enkele set?, Als uw vraag niet is geconfigureerd als de mijne, om vrijstaande hoofd zoals beschreven (met Git ‘ s ingebouwde prompt script, dat je voor het definiëren van de GIT_PS1_DESCRIBE_STYLE=tak omgevingsvariabele), zult u eerder iets ziet zoals dit:

demo ((fe64799...)) $

In ieder geval, status bevestigt waar we op:

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

een update van de submodule ‘ s op afstand

OK, nu hebben we onze eigen repo (main) en onze “collega ‘ s” (collega) alle set-up samen te werken, laten we een stap in de schoenen van een derde persoon: de een die houdt van de plugin., Laten we hier naar toe gaan:

laten we nu twee pseudo-commits toevoegen en deze publiceren op de remote:

tot slot zetten we onze “first developer” cap opnieuw op:

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

stel dat we nu deze twee commits in onze submodule willen krijgen. Om dit te bereiken, moeten we de lokale repo updaten, te beginnen door naar de werk directory te gaan, zodat het onze actieve repo wordt.

even terzijde, ik zou niet aanraden pull te gebruiken voor dit soort update., Om de updates in de werk directory goed te krijgen, vereist dit commando dat je op de juiste actieve branch zit, wat je meestal niet doet (je zit meestal op een losstaande head). Je zou moeten beginnen met een kassa van dat filiaal. Maar nog belangrijker, de remote branch zou heel goed verder vooruit kunnen zijn gegaan sinds de commit die je wilt instellen, en een pull zou commits injecteren die je misschien niet in je lokale codebase wilt.,

daarom raad ik aan om het proces handmatig te splitsen: eerst Git fetch om alle nieuwe gegevens van de remote in de lokale cache te krijgen, log dan in om te controleren wat je hebt en checkout op de gewenste SHA1. Naast fijnere controle, heeft deze aanpak het extra voordeel van het werken ongeacht uw huidige toestand (actieve tak of losstaande hoofd).

OK, dus we zijn goed, geen externe commit., Hoe het ook zij, Laten we expliciet instellen op degene waarin we geïnteresseerd zijn (uiteraard heb je een andere SHA1):

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

(de-q is er alleen om ons te sparen Git blabbering over hoe we eindigen op een losstaande head. Meestal zou dit een gezonde herinnering zijn, maar op deze weten we wat we doen.)

nu onze submodule is bijgewerkt, kunnen we het resultaat zien in de status van de container repo:

In het “klassieke” deel van de status zien we een nieuw type commits veranderen, wat betekent dat de referentie commit veranderd is., Een andere mogelijkheid (die kan worden aangevuld met deze) is nieuwe inhoud, wat zou betekenen dat we lokale wijzigingen hebben aangebracht in de werkdirectory van de submodule.

het onderste deel, ingeschakeld door onze status.submoduleSummary = true instelling eerder op, geeft expliciet de geà ntroduceerde commits aan (omdat ze een haakje naar rechts gebruiken >) sinds onze laatste container commit die de submodule had aangeraakt.

In de “terrible default behaviors” familie laat git diff veel te wensen over:

wat de — ?, Er is een CLI optie die ons iets bruikbaars laat zien:

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

Er zijn op dit moment geen andere lokale wijzigingen behalve de commit waarnaar verwezen wordt in de submodule… merk op dat dit bijna precies overeenkomt met het onderste deel van onze verbeterde Git status display.

dat soort CLI-optie elke keer moet typen (wat trouwens niet te zien is in de huidige voltooiingsaanbiedingen van Git) is nogal log. Gelukkig is er een overeenkomende configuratie-instelling:

We hoeven nu alleen nog de container commit uit te voeren die de update van onze submodule voltooit., Als je de code van de container moest aanraken om het te laten werken met deze update, commit het dan natuurlijk mee. Aan de andere kant, vermijd het mengen van submodule-gerelateerde veranderingen en andere dingen die alleen betrekking hebben op de container code: door netjes te scheiden van de twee, worden latere migraties naar andere code-hergebruik benaderingen gemakkelijker gemaakt (ook, zoals gewoonlijk, atomic commits FTW).

terwijl we op het punt staan deze submodule update in de repo van onze collega te pakken, pushen we direct na het committen (wat geen algemene goede gewoonte is).

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

Pulling a submodule-using repo

klik!, “Collega” cap op!

dus we Pullen updates van de container repo ‘ s remote…

(je hebt misschien niet de “succesvol gerebased en bijgewerkt …” en zie in plaats daarvan een “Merge made by the ‘recursive’ strategy”. Als dat zo is, gaat mijn hart naar je uit, en je moet onmiddellijk leren waarom pulls moeten rebasen).

Let op de tweede helft van dit scherm: het gaat over de submodule, beginnend met “Fetching submodule…”.

dit gedrag werd de standaard met Git 1.7.5, met de configuratie instelling fetch.,recurseSubmodules nu standaard naar on-demand: als een containerproject updates krijgt naar gerefereerde submodule commits, worden deze submodules automatisch opgehaald. (Onthoud dat ophalen het eerste deel is van trekken.)

nog steeds, en dit is van cruciaal belang: Git haalt automatisch op, maar werkt niet automatisch bij. Je lokale cache is up-to-date met de remote van de submodule, maar de werkdirectory van de submodule blijft aan de vorige inhoud hangen. Je kunt tenminste die laptop sluiten, op een vliegtuig springen, en nog steeds vooruit gaan als je offline bent., Hoewel deze auto-fetching beperkt is tot reeds bekende submodules: alle nieuwe, nog niet gekopieerd naar de lokale configuratie, worden niet automatisch opgehaald.

Git haalt automatisch op, maar werkt niet automatisch bij.

de huidige prompt, met zijn sterretje (*), wijst op lokale wijzigingen, omdat onze WD niet synchroon is met de index, de laatste is zich bewust van de nieuw gerefereerde submodule commits. Bekijk de status:

merk op hoe de hoekhaken naar links gaan (<)?, Git ziet dat de huidige WD deze twee commits niet heeft, in tegenstelling tot de verwachtingen van het container project.

Dit is het enorme gevaar: als je de werkmap van de submodule niet expliciet bijwerkt, zal je volgende container commit de submodule regresseren. Dit is een first-order val.

Is daarom verplicht om de update af te ronden:

zolang we proberen algemene goede gewoonten te vormen, zou het commando hier de voorkeur hebben een Git submodule update — init — recursive, om elke nieuwe submodule automatisch in teit, en deze indien nodig recursief bij te werken.,

Er is nog een randgeval: als de externe URL van de submodule veranderd is sinds het laatst gebruikt (misschien heeft een van de medewerkers het veranderd in de .gitmodules), moet je je lokale configuratie handmatig updaten om hiermee overeen te komen. In een dergelijke situatie, voordat de git submodule update, zou je een Git submodule sync moeten uitvoeren.

Ik zou, voor de volledigheid, moeten vermelden dat zelfs als git submodule update standaard de gerefereerde SHA1 uitcheckt, je dat kunt veranderen om bijvoorbeeld elk lokaal submodule werk te rebasen (daar zullen we het snel over hebben)., Je zou dat doen door het instellen van de update configuratie instelling voor uw submodule om te rebasen in de lokale configuratie van uw container.

en het spijt me maar nee, er is geen lokale configuratie-instelling, of zelfs CLI-optie wat dat betreft, die automatisch kan worden bijgewerkt bij pull. Om dergelijke dingen te automatiseren, moet je ofwel aliassen gebruiken, aangepaste scripts, of Zorgvuldig vervaardigde lokale hooks., Hier is een voorbeeld spull alias (enkele regel, hier gesplitst voor weergave):

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

als je de mogelijkheid wilt behouden om aangepaste argumenten door te geven aan git pull, kun je of een functie On-The-fly definiëren en het aanroepen, of gaan met een aangepast script. De eerste benadering zou er zo uitzien (nogmaals, enkele regel):

niet erg leesbaar, eh? Ik geef de voorkeur aan de aangepaste script aanpak., Laten we zeggen dat je een Git-spull script bestand ergens in je pad plaatst (Ik heb een ~/perso/bin directory in mijn pad alleen voor zulke dingen):

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

We geven het dan uitvoerrechten:

chmod +x git-spull

en nu kunnen we het gebruiken zoals we de alias zouden hebben gebruikt.

een submodule op zijn plaats in de container bijwerken

Dit is het moeilijkste geval, en u moet er zoveel mogelijk van wegblijven, waarbij u de voorkeur geeft aan onderhoud via de centrale, dedicated repo.,

Het kan echter voorkomen dat submodule code niet kan worden getest, of zelfs gecompileerd, buiten de container code. Veel thema ‘ s en plugins hebben dergelijke beperkingen.

het eerste wat je moet begrijpen is, omdat je commits gaat maken, dat je moet beginnen met een goede basis, wat een branch tip zal zijn. Je moet daarom verifiëren dat de laatste commits van de branch je container project niet zullen “breken”. Als ze dat doen, nou, het creëren van je eigen container-specifieke branch in de submodule klinkt verleidelijk, maar dat pad leidt tot een sterke koppeling tussen submodule en container, wat niet aan te raden is., U kunt stoppen met “submoduling” die code in dit specifieke project, en gewoon insluiten zoals elke reguliere inhoud in plaats daarvan.

geef toe dat je met een gerust geweten de huidige master branch van de submodule kunt toevoegen. Laten we beginnen met het synchroniseren van onze lokale status op de remote ‘ S:

een andere manier om dit te doen zou zijn, vanuit de container repo, om expliciet de lokale tak van de submodule te synchroniseren over de getrackte remote tak (enkele regel bovenaan, laatste — gevolgd door witruimte):

We kunnen nu de code bewerken, laten werken, testen, enz., Als we eenmaal klaar zijn, kunnen we dan de twee commits en de twee noodzakelijke pushes uitvoeren (het is super eenvoudig, en in de praktijk maar al te vaak, om daar iets van te vergeten).

laten we gewoon fake werk toevoegen en de twee gerelateerde commits maken, op de submodule en container niveaus:

Op dit punt is het grootste gevaar het vergeten om de submodule te pushen. Je gaat terug naar het container project, commit het, en push alleen de container. Het is een gemakkelijke fout om te maken, vooral binnen een IDE of GUI. Als je collega ‘ s updates proberen te krijgen, breekt de hel los., Kijk naar de eerste stap:

Er is absoluut geen aanwijzing dat Git de gerefereerde commit niet kon ophalen van de remote van de submodule. De eerste hint hiervan is in de status:

Let op de waarschuwing: blijkbaar is de nieuwe commit waarnaar verwezen wordt voor de submodule nergens te vinden. Inderdaad, als we proberen de werkmap van de submodule bij te werken, krijgen we:

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

u kunt duidelijk zien hoe belangrijk het is om te onthouden dat u ook de submodule pusht, idealiter voordat u de container pusht., Laten we dat in collega doen en de update opnieuw proberen:

Ik moet opmerken dat er een CLI optie is die zal controleren of momenteel gerefereerde submodule commits ook gepusht moeten worden, en als dat zo is, zullen ze gepusht worden: het is git push — recurse-submodules=on-demand (nogal een mondvol, toegegeven). Het moet echter iets container-niveau hebben om te pushen om te werken: alleen submodules zullen het niet snijden.

wat meer is (hier is geen configuratie instelling voor, dus je zou procedures rond een alias moeten standaardiseren, bijvoorbeeld spush:) — beginnend met Git 2.7.0, is er nu een push.,recurseSubmodules configuraties instelling die u kunt definiëren (naar on-demand of check).

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

het verwijderen van een submodule

er zijn twee situaties waarin u een submodule wilt” verwijderen”:

  • u wilt alleen de werkmap wissen (misschien voordat u de WD van de container archiveert) maar u wilt de mogelijkheid behouden om deze later te herstellen (dus het moet binnen blijven .gitmodules en .git / modules);
  • u wilt het definitief verwijderen uit de huidige branch.

laten we elk geval op zijn beurt bekijken.,

tijdelijk een submodule verwijderen

de eerste situatie wordt gemakkelijk afgehandeld door GIT submodule deinit. Zie zelf:

Dit heeft geen enkele invloed op de container status. De submodule is lokaal niet meer bekend (het is weg van .git / config), dus de afwezigheid ervan uit de werkdirectory blijft onopgemerkt. We hebben nog steeds de leverancier / plugins / demo directory, maar het is leeg; we konden strippen zonder gevolg.

de submodule mag geen lokale wijzigingen hebben als u dit doet, anders moet u de aanroep forceren.,

elk later subcommando van git submodule zal deze submodule zalig negeren totdat je het opnieuw init, omdat de submodule niet eens in de lokale configuratie zal zitten. Dergelijke opdrachten omvatten update, foreach en sync.

aan de andere kant blijft de submodule gedefinieerd in .gitmodules: een init gevolgd door een update (of een enkele update-init) zal het herstellen als nieuw:

permanent verwijderen van een submodule

Dit betekent dat je voorgoed van de submodule af wilt: een reguliere Git rm zal doen, net als voor elk ander deel van de werkdirectory., Dit werkt alleen als je submodule een gitfile gebruikt (a .git wat een bestand is, geen directory), wat het geval is beginnend met Git 1.7.8. Anders zul je dit met de hand moeten afhandelen (Ik zal je vertellen hoe aan het einde).

naast het verwijderen van de submodule uit de werkmap, zal het commando de .gitmodules bestand dus het verwijst niet meer naar de submodule. Hier ga je:

natuurlijk struikelt geavanceerde status info over zichzelf hier, omdat het gitfile voor de submodule weg is (eigenlijk is de hele demo directory verdwenen).,

wat vreemd is, is dat de lokale configuratie submodule informatie behoudt, in tegenstelling tot wat er gebeurt als je deinit. Dus, voor een uitgebreide verwijdering, adviseer ik je beide te doen, in volgorde, om uiteindelijk goed opgeschoond (het zou niet werken na onze vorige opdracht, omdat het gewist .gitmodules already):

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

ongeacht uw aanpak, de repo van de submodule blijft aanwezig in .git/modules/vendor/plugins / demo, maar je bent vrij om dat te doden wanneer je maar wilt.

als je ooit een submodule moet verwijderen die was ingesteld voor Git 1.7.,8, en sluit daarom zijn .Git directory direct in de werk directory van de container (in plaats van te vertrouwen op een gitfile), moet je de bulldozer tevoorschijn halen: de vorige twee commando ’s moeten worden voorafgegaan door een handmatige map verwijdering, bijvoorbeeld rm-fr vendor/plugins/demo, omdat genoemde commando’ s altijd zullen weigeren om een echte repository te verwijderen.