Articles

Mastering av Git-undermoduler

undermoduler, steg för steg

vi utforskar nu varje steg för att använda undermoduler i ett samarbetsprojekt, så att vi markerar standardbeteenden, fällor och tillgängliga förbättringar.

för att underlätta din följande längs, jag har sammanställt några exempel repor med sina ”Fjärrkontroller” (faktiskt bara kataloger)., Du kan packa upp arkivet var du vill, öppna sedan ett skal (eller Git Bash, om du är på Windows) i Git-subs-katalogen Det skapar:

ladda ner exemplet repos

Du hittar tre kataloger där:

  • huvud fungerar som container repo, lokal till den första collaborator,
  • plugin fungerar som den centrala underhåll repo för modulen, och
  • Fjärrkontroller innehåller filsystemet fjärrkontroller för de två tidigare repor.

i exempelkommandona nedan visar prompten alltid vilken repo vi är i.,

lägga till en undermodul

låt oss börja med att lägga till vår plugin som en undermodul inuti vår behållare (som är i huvudsak). Insticksprogrammet själv har en enkel struktur:

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

så låt oss gå in i main och använda Git submodule add-kommandot. Det tar fjärrens URL och en underkatalog för att ”instantiate” undermodulen.

eftersom vi använder sökvägar i stället för webbadresser här för våra Fjärrkontroller, vi träffar en konstig, om än välkända, hake: relativa sökvägar för fjärrkontroller tolkas i förhållande till vår huvudsakliga fjärrkontroll, nej till vår repo rotkatalog., Detta är super konstigt, beskrivs inte någonstans, men jag har sett det hända varje gång. Så istället för att säga ../ Fjärrkontroller / plugin, vi säger bara ../insticksprogram.

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

detta lade till några inställningar i vår lokala konfiguration:

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


url = ../remotes/plugin

och detta iscensatte också två filer:

Huh?! Vad är det här?gitmodules fil? Låt oss titta på det:

main (master + u=) $ cat .gitmodules

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

detta ursinnigt liknar vår lokala config… så varför dubbelarbete? Just för att vår lokala config är … lokal., Våra medarbetare kommer inte att se det (vilket är helt normalt), så de behöver en mekanism för att få definitionerna av alla submoduler de behöver för att ställa in i sina egna repos. Det här är vad .gitmodules är för, det kommer att läsas senare av git submodule kommandot init, som vi kommer att se i ett ögonblick.

medan vi är på status, notera hur minimalistisk det är när det gäller vår undermodule: Det går bara med en alltför Generisk ny fil istället för att berätta mer om vad som händer inuti den., Vår submodule injicerades faktiskt i underkatalogen:


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

Status, som loggar och diffs, är begränsad till den aktiva repo (just nu behållaren), inte till submoduler, som är kapslade repor. Detta är ofta problematiskt (det är super lätt att missa en regression när den är begränsad till den här vyn), så jag rekommenderar att du ställer in en submodule-medveten status en gång för alla:

git config --global status.submoduleSummary true

och nu:

Aaaah, det här är väldigt bättre., Statusen utökar sin basinformation för att lägga till att undermodulen som finns hos vendor / plugins / demo fick 3 Nyheter förbinder sig i (som vi just skapat det betyder det att fjärrgrenen bara hade tre åtaganden), den sista är ett tillägg (notera den högra vinkelfästet >) med en första begå meddelandelinje som läser ”Fix repo namn…”.

för att verkligen ta hem att vi hanterar två separata repos här, låt oss komma in i undermodulens katalog:

den aktiva repo har förändrats, eftersom en ny .,git tar över: i den aktuella katalogen (demo, undermodulens katalog), En .git existerar faktiskt, en enda fil också, inte en katalog. Låt oss titta inuti:

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

igen, eftersom Git 1.7.8, lämnar Git inte repo-kataloger i behållarens arbetskatalog, men centraliserar dessa i behållarens.git katalog (inuti .git / moduler), och använder en gitdir referens i undermoduler.,

motivet bakom detta är enkelt: det gör att behållaren repo att ha submodule-mindre grenar, utan att behöva skrapa submodulens repo från arbetskatalogen och återställa den senare.

naturligtvis, när du lägger till undermodulen, kan du välja att använda en specifik gren, eller till och med en specifik begå, med hjälp av-B CLI-alternativet (som vanligt är standardvärdet master). Obs! vi är inte just nu på ett fristående huvud, till skillnad från vad som händer senare: det beror på att Git checkade ut master, inte en specifik SHA1. Vi skulle ha varit tvungna att ange en SHA1 till-b för att få ett fristående huvud från get-go.,

så, tillbaka till behållarens repo, och låt oss slutföra undermodulens tillägg och driva det till fjärrkontrollen:

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

ta tag i en repo som använder undermoduler

för att illustrera problemen med att samarbeta på en repo som använder undermoduler, delar vi upp personligheter och agerar som vår kollega, som kloner behållarens fjärrkontroll för att börja arbeta med oss. Vi klonar det i en kollegas katalog, så vi kan omedelbart berätta vilken personlighetslock vi har på när som helst.,

det första att märka är att vår undermodul saknas från arbetskatalogen; bara dess baskatalog är här:

vendor
└── plugins
└── demo

hur hände det? Detta beror helt enkelt på det faktum att vår nya repo (kollega) hittills inte är medveten om vår undermodule än: informationen för den är ingenstans i sin lokala konfiguration (kontrollera dess .git / config om du inte tror mig). Vi måste fylla i det, baserat på vad .gitmodules måste säga, vilket är precis vad git submodule init gör:

vår .git / config är nu medveten om vår undermodule., Men vi har fortfarande inte hämtat den från dess fjärrkontroll, för att inte säga något om att ha den närvarande i vår arbetskatalog. Och ändå visar vår status som ren!

Se, vi måste ta tag i relevanta begår manuellt. Det är inte något vår första klon gjorde, vi måste göra det på varje drag. Vi kommer tillbaka till det om en minut, eftersom det här är en beteendeklon som faktiskt kan automatisera, när den är korrekt kallad.,

i praktiken grupperar vi vanligtvis de två kommandona (init och update) i en:

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

det är fortfarande synd att Git har dig att göra allt själv. Tänk dig, på större tandtråd projekt, när undermoduler har sina egna undermoduler, och så vidare och så vidare… Detta skulle snabbt bli en mardröm.

Det råkar vara så att Git ger en CLI alternativ för klon att automatiskt git submodule update — init rekursivt direkt efter kloning: ganska träffande namngivna rekursiva alternativ.,

så låt oss försöka hela saken igen:

Nu är det bättre! Observera att vi nu är på ett fristående huvud inuti undermodulen (som vi kommer att vara från och med nu):

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

se den dubbla uppsättningen parenteser i min prompt, istället för en enda uppsättning?, Om din uppmaning är inte konfigurerad som mitt, för att visa fristående huvudet som beskriver (med Git inbyggd snabb manus, skulle du definiera GIT_PS1_DESCRIBE_STYLE=filial miljövariabel), kommer du snarare se ut så här:

demo ((fe64799...)) $

I alla fall, status bekräftar vart vi är på:

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

att Få en uppdatering från submodule fjärrkontroll

OK, nu när vi har vår egen repo (main) och vår ”kollega” (kollega) alla ställa upp för att samarbeta, låt oss kliva in i skor av en tredje person: den som upprätthåller plugin., Här, låt oss flytta till det:

nu, låt oss lägga till två pseudo-begår och publicera dessa till fjärrkontrollen:

slutligen, låt oss sätta vår” första Utvecklare ” cap på igen:

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

anta att vi nu vill få dessa två begår i vår submodule. För att uppnå detta måste vi uppdatera sin lokala repo, som börjar med att flytta in i sin arbetskatalog så att den blir vår aktiva repo.

på en sidoanteckning rekommenderar jag inte att du använder pull för den här typen av uppdatering., För att korrekt få uppdateringarna i arbetskatalogen kräver det här kommandot att du är på rätt aktiv gren, som du vanligtvis inte är (du är på ett fristående huvud för det mesta). Du måste börja med en check av den grenen. Men ännu viktigare, den avlägsna grenen kan mycket väl ha gått längre framåt eftersom begäret du vill sätta på, och ett drag skulle injicera begår du kanske inte vill ha i din lokala kodbas.,

därför rekommenderar jag att dela upp processen manuellt: först git Hämta för att få alla nya data från fjärrkontrollen i lokal cache, logga sedan för att verifiera vad du har och kassan på önskad SHA1. Förutom finkornig kontroll har detta tillvägagångssätt den extra fördelen att arbeta oavsett ditt nuvarande tillstånd (aktiv gren eller fristående Huvud).

OK, så vi är bra, ingen främmande begå., Var det som det kan, låt oss uttryckligen ställa in den vi är intresserade av (självklart har du en annan SHA1):

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

(- q är bara där för att bespara oss Git blabbering om hur vi hamnar på ett fristående Huvud. Vanligtvis skulle detta vara en hälsosam påminnelse, men på den här vet vi vad vi gör.)

nu när vår submodule uppdateras kan vi se resultatet i container Repos status:

i den ”klassiska” delen av statusen ser vi en ny begår förändringstyp, vilket innebär att den refererade begå ändrats., En annan möjlighet (som kan förvärras av detta) är nytt innehåll, vilket skulle innebära att vi gjorde lokala ändringar i undermodulens arbetskatalog.

den nedre delen, aktiverad av vår status.submoduleSummary = true inställning tidigare anges uttryckligen de införda åtagandena (eftersom de använder en vinkelfäste med rätt vinkel >) sedan vår sista container begår som hade berört undermodulen.

i familjen ”terrible default behaviors” lämnar git diff mycket att önska:

vad är det — ?, Det är ett ERSÄTTNINGSPROGRAM för ” alternativ som låter oss se något mer användbart:

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

Det finns inga andra lokala förändringar just nu förutom submodule refereras begå… Märker detta motsvarar nästan exakt den nedre delen av vår förbättrade git status display.

att behöva skriva den typen av CLI-alternativ varje gång (vilket förresten inte visas i Git: s aktuella kompletteringserbjudanden) är ganska obehagligt. Lyckligtvis finns det en matchande konfigurationsinställning:

Vi behöver nu bara utföra container commit som slutför vår undermoduls uppdatering., Om du var tvungen att röra behållarens kod för att få det att fungera med den här uppdateringen, begå det naturligtvis. Å andra sidan, undvik att blanda submodulerelaterade förändringar och andra saker som bara skulle gälla behållarkoden: genom att noggrant separera de två, görs senare migreringar till andra kodåtervändningsmetoder enklare (som vanligt begår atomic FTW).

eftersom vi är på väg att ta tag i denna submodule uppdatering i vår kollegas repo, kommer vi att driva direkt efter att ha begått (vilket inte är en allmän god praxis).

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

dra en submodule-använda repo

Klicka!, ”Kollega” keps på!

Så vi drar uppdateringar från container Repos fjärrkontroll…

(Du kanske inte har ”framgångsrikt rebased och uppdaterad…” och ser en ”sammanfogning gjord av” rekursiv ”strategi” istället. Om så är fallet, mitt hjärta går ut till dig, och du bör omedelbart lära sig varför drar bör rebase).

notera den andra halvan av den här skärmen: det handlar om undermodulen, som börjar med”Hämta undermodulen…”.

detta beteende blev standard med Git 1.7.5, med konfigurationsinställningen hämta.,recurseSubmodules nu standard till on-demand:om ett containerprojekt får uppdateringar till refererade undermoduler begår, hämtas dessa undermoduler automatiskt. (Kom ihåg att hämta är den första delen av att dra.)

fortfarande, och det här är kritiskt: git hämtar automatiskt, men uppdaterar inte automatiskt. Din lokala cache är uppdaterad med undermodulens fjärrkontroll, men undermodulens arbetskatalog fastnat på dess tidigare innehåll. Åtminstone kan du stänga den bärbara datorn, hoppa på ett plan och fortfarande gå framåt en gång offline., Även om den här automatiska hämtningen är begränsad till redan kända undermoduler: alla nya, ännu inte kopierade till lokal konfiguration, hämtas inte automatiskt.

git hämtar automatiskt, men uppdaterar inte automatiskt.

den aktuella prompten, med dess asterisk (*), antyder lokala ändringar, eftersom vår WD inte synkroniseras med indexet, den senare är medveten om den nyligen refererade undermodulen begår. Kolla in statusen:

Lägg märke till hur vinkelfästena pekar åt vänster (<)?, Git ser att den nuvarande WD inte har dessa två åtaganden, i motsats till behållarprojektets förväntningar.

det här är den massiva faran: om du inte uttryckligen uppdaterar undermodulens arbetskatalog, kommer din nästa containeruttag att återgå till undermodulen. Det här är en första ordningens fälla.

är därför obligatoriskt att du slutför uppdateringen:

så länge vi försöker bilda generiska goda vanor, skulle det föredragna kommandot här vara en git submodule update — init — rekursiv, för att automatiskt init någon ny undermodul, och att rekursivt uppdatera dessa om det behövs.,

det finns ett annat kantfall: om undermodulens fjärr-URL ändrats sedan den senast användes (kanske en av samarbetspartnerna ändrade den i .gitmodules), du måste manuellt uppdatera din lokala config för att matcha detta. I en sådan situation, innan git submodule-uppdateringen, måste du köra en git submodule-synkronisering.

jag bör nämna, för fullständighetens skull, att även om git submodule update Standard för att kolla in den refererade SHA1, kan du ändra det till, till exempel, rebase någon lokal undermodule arbete (vi kommer att prata om det mycket snart) ovanpå det., Du skulle göra det genom att ställa in inställningen för uppdateringskonfiguration för din undermodule för att rebase inuti behållarens lokala konfiguration.

och jag är ledsen men nej, det finns ingen lokal konfigurationsinställning, eller till och med CLI-alternativ för den delen, som kan automatiskt uppdatera vid pull. För att automatisera sådana saker måste du använda antingen Alias, anpassade skript eller noggrant utformade lokala krokar., Här är ett exempel på ett spullalias (Enkelrad, delad här för visning):

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

om du vill behålla möjligheten att skicka egna argument till git pull kan du antingen definiera en funktion on-the-fly och kalla den, eller gå med ett eget skript. Det första tillvägagångssättet skulle se ut så här (igen, enda rad):

inte särskilt läsbar, va? Jag föredrar det anpassade skriptet., Låt oss säga att du skulle lägga en git-spull-skriptfil någonstans i din sökväg (jag har en ~/perso/bin-katalog i min sökväg bara för sådana saker):

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

vi ger det sedan körningsrättigheter:

chmod +x git-spull

och nu kan vi använda det precis som vi skulle ha använt aliaset.

uppdatera en undermodule på plats i behållaren

detta är det svåraste användningsfallet, och du bör hålla dig borta från det så mycket som möjligt, och föredra underhåll genom den centrala, dedikerade repo.,

det kan dock hända att undermodulkoden inte kan testas, eller till och med kompileras, utanför containerkoden. Många teman och plugins har sådana begränsningar.

det första du måste förstå är, eftersom du kommer att göra åtaganden, måste du börja från en korrekt grund, som kommer att vara en gren tips. Du måste därför kontrollera att filialens senaste åtaganden inte kommer att ”bryta” ditt containerprojekt. Om de gör, ja, att skapa din egen containerspecifika gren i submodulen låter frestande, men den vägen leder till stark koppling mellan submodule och behållare, vilket inte är tillrådligt., Du kanske vill sluta ”submoduling” den koden i det här projektet, och bara bädda in det som vanligt innehåll istället.

låt oss erkänna att du med gott samvete kan lägga till undermodulens nuvarande huvudgren. Låt oss börja med att synkronisera vårt lokala tillstånd på fjärrkontrollens:

ett annat sätt att gå om detta skulle vara, från containerrepoen, att uttryckligen synkronisera undermodulens lokala gren över dess spårade fjärr gren (enda rad på toppen, sist-följt av blanktecken):

Vi kan nu redigera koden, få den att fungera, testa den etc., När vi är redo, kan vi sedan utföra de två begår och de två nödvändiga skjuter (det är super lätt, och i praktiken alltför ofta, att glömma en del av det).

låt oss helt enkelt lägga till falskt arbete och göra de två relaterade åtagandena, vid undermodulen och behållarnivåerna:

vid denna tidpunkt glömmer den stora faran att driva undermodulen. Du kommer tillbaka till containerprojektet, begår det och trycker bara på behållaren. Det är ett enkelt misstag att göra, särskilt inom en IDE eller GUI. När dina kollegor försöker få uppdateringar, bryter helvetet lös., Titta på det första steget:

det finns absolut ingen indikation på att Git inte kunde hämta den refererade begå från undermodulens fjärrkontroll. Den första antydan till detta är i status:

Lägg märke till varningen: tydligen är den nyligen refererade begå för undermodulen ingenstans att hittas. Om vi försöker uppdatera undermodulens arbetskatalog får vi:

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

Du kan tydligt se hur viktigt det är att komma ihåg att trycka på undermodulen också, helst innan du trycker på behållaren., Låt oss göra det i kollega och försök uppdateringen igen:

jag bör notera att det finns ett CLI — alternativ som kommer att verifiera om det för närvarande refererade submodule begår måste skjutas också, och om så kommer att driva dem: det är git push-recurse-submodules=on-demand (ganska muntligt, visserligen). Det måste ha något containernivå för att driva till jobbet, men: endast submoduler kommer inte att klippa det.

vad mer, (det finns ingen konfigurationsinställning för detta, så du måste standardisera procedurer runt ett alias, t.ex. spush:) — från och med Git 2.7.0 finns det nu ett tryck.,recurseSubmodules konfigurationer inställning Du kan definiera (till On-demand eller check).

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

ta bort en undermodul

det finns två situationer där du vill ”ta bort” en undermodul:

  • du vill bara rensa arbetskatalogen (kanske innan du arkiverar behållarens WD) men vill behålla möjligheten att återställa den senare (så det måste förbli i .gitmodules och .git / modules);
  • du vill definitivt ta bort den från den aktuella grenen.

låt oss se varje fall i sin tur.,

tillfälligt ta bort en undermodul

den första situationen hanteras enkelt av git submodule deinit. Se själv:

detta har ingen inverkan på behållarens status alls. Submodulen är inte lokalt känd längre (den är borta från .git / config), så dess frånvaro från arbetskatalogen går obemärkt. Vi har fortfarande leverantör / plugins / demo katalog men det är tomt; vi kunde ta bort det utan konsekvens.

undermodulen får inte ha några lokala ändringar när du gör det, annars måste du — tvinga samtalet.,

alla senare underkommandon av git-undermodulen kommer att ignorera denna undermodul tills du init det igen, eftersom undermodulen inte ens kommer att vara i lokal config. Sådana kommandon inkluderar uppdatering, förvarje och synkronisering.

däremot förblir undermodulen definierad i .gitmodules: en init följt av en uppdatering (eller en enda uppdatering — init) kommer att återställa den som ny:

permanent ta bort en undermodul

det betyder att du vill bli av med undermodulen för gott: en vanlig git rm kommer att göra, precis som för någon annan del av arbetskatalogen., Detta fungerar bara om undermodulen använder en gitfile (a .git som är en fil, inte en katalog), vilket är fallet som börjar med Git 1.7.8. Annars måste du hantera detta för hand (Jag ska berätta hur i slutet).

förutom att ta bort undermodulen från arbetskatalogen uppdateras kommandot .gitmodules filen så att den inte referera submodule längre. Här går du:

Naturligtvis, avancerad status info resa över sig själva här, eftersom gitfile för submodule är borta (faktiskt, hela demo katalog försvann).,

vad som är konstigt är dock att den lokala konfigurationen behåller undermodulinformation, till skillnad från vad som händer när du deinit. Så, för en omfattande borttagning, rekommenderar jag att du gör båda, i följd, för att hamna ordentligt rengjorda (det skulle inte fungera efter vårt tidigare kommando, eftersom det rensas .gitmodules redan):

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

oavsett din inställning förblir undermodulens repo närvarande i .git / moduler / leverantör / plugins / demo, men du är fri att döda det när du vill.

om du någonsin behöver ta bort en underkod som installerades före Git 1.7.,8, och därför bäddar dess .git-katalogen rakt i behållarens arbetskatalog (istället för att förlita sig på en gitfile) måste du bryta ut bulldozer: de föregående två kommandona måste föregås av en manuell mappborttagning, t.ex. rm-fr-leverantör/plugins/demo, eftersom dessa kommandon alltid kommer att vägra att ta bort ett verkligt förråd.