Articles

stăpânirea submodulelor Git

submodule, pas cu pas

vom explora acum fiecare pas al utilizării submodulelor într-un proiect colaborativ, asigurându-ne că evidențiem comportamentele implicite, capcanele și îmbunătățirile disponibile.

pentru a facilita urmărirea dvs. de-a lungul, am pus împreună câteva exemple de repo-uri cu „telecomenzile” lor (de fapt, doar directoare)., Puteți decomprima arhiva oriunde doriți, apoi deschide un shell (sau Git Bash, daca esti pe Windows) în git-subs director se creează:

Descărcați exemplu repo

Veți găsi trei directoare de acolo:

  • principalele acte ca recipientul repo, local, la primul colaborator,
  • plugin acționează ca o centrală de întreținere repo pentru modulul, și
  • telecomenzi conține fișiere telecomenzi pentru cele două anterioare repos.

în exemplul de comenzi de mai jos, promptul afișează întotdeauna ce repo suntem în.,

adăugarea unui submodul

Să începem prin adăugarea pluginului nostru ca submodul în interiorul containerului nostru (care este în principal). Pluginul în sine are o structură simplă:

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

deci, să mergem în main și să folosim submodul git add command. Este nevoie de URL-ul telecomenzii și de un subdirector în care să” instanțieze ” submodul.

deoarece folosim căi în loc de URL-uri aici pentru telecomenzile noastre, am lovit un ciudat, deși bine-cunoscut, snag: căile relative pentru telecomenzi sunt interpretate în raport cu telecomanda noastră principală, nu cu directorul rădăcină al repo-ului nostru., Acest lucru este foarte ciudat, nu este descris nicăieri, dar am văzut că se întâmplă de fiecare dată. Deci, în loc de a spune ../ telecomenzi / plugin, spunem doar ../ modul.

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

aceasta a adăugat câteva setări în configurația noastră locală:

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


url = ../remotes/plugin

și aceasta a pus în scenă și două fișiere:

Huh?! Ce-i asta .fișier gitmodules? Să ne uităm la ea:

main (master + u=) $ cat .gitmodules

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

Acest lucru seamănă cu furie config nostru local … deci, de ce duplicarea? Ei bine, tocmai pentru că configurația noastră locală este… locală., Colaboratorii noștri nu o vor vedea (ceea ce este perfect normal), așa că au nevoie de un mecanism pentru a obține definițiile tuturor submodulelor pe care trebuie să le configureze în propriile repo-uri. Aceasta este ceea ce .gitmodules este pentru; acesta va fi citit mai târziu de comanda git submodule init, așa cum vom vedea într-un moment.

în timp ce suntem în stare, rețineți cât de minimalist este atunci când vine vorba de submodul nostru: merge doar cu un fișier nou prea generic, în loc să ne spună mai multe despre ce se întâmplă în interiorul acestuia., Nostru submodul a fost într-adevăr injectat în subdirectorul:


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

Stare, cum ar fi busteni si diff, este limitată la activ repo (acum, container), să nu submodule, care sunt imbricate repos. Acest lucru este adesea problematică (e foarte ușor să dor de o regresie atunci când este limitat la acest punct de vedere), așa că am recomandăm să configurați un submodul conștient de statutul odată și pentru totdeauna:

git config --global status.submoduleSummary true

Și acum:

Aaaah, acest lucru este mult mai bine., Statutul își extinde baza de informații pentru a adăuga că submodul prezent la furnizor/plugin-uri/demo 3 știri angajează în (ca doar ne-am creat-o, înseamnă că la distanță de ramură avut doar trei comite), ultima fiind un plus (notă dreapta-unghiul de indicare a suportului >), cu o prima comite mesaj de linie pe care scrie „Repara repo numele…”.pentru a aduce cu adevărat acasă că avem de-a face cu două repo-uri separate aici, să intrăm în directorul submodulului:

repo-ul activ s-a schimbat, pentru că este nou .,git preia: în directorul curent (demo, directorul submodul lui), a .git există într-adevăr, un singur fișier prea, nu un director. Să ne uităm în interior:

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

Din nou, deoarece Git 1.7.8, Git nu lasă directoarele repo în directorul de lucru al containerului, ci le centralizează în container .director git (interior .git / module), și utilizează o referință gitdir în submodule.,

raționamentul Din spatele acest lucru este simplu: acesta permite recipient repo de a avea submodul-mai puțin ramuri, fără a fi nevoie să renunțe la submodul e repo din directorul de lucru și restabilirea asta mai târziu.în mod natural, atunci când adăugați submodul, puteți alege să utilizați o ramură specifică sau chiar o comitere specifică, folosind opțiunea-B CLI (ca de obicei, implicit este master). Notă nu suntem, chiar acum, pe un cap detașat, spre deosebire de ceea ce se va întâmpla mai târziu: acest lucru se datorează faptului că Git verificat master, nu un SHA1 specific. Ar fi trebuit să specificăm un SHA1 la-b pentru a obține un cap detașat de la început.,

Deci, înapoi la container repo, și să finalizeze submodul plus și împinge de la distanță:

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

Hapsân un repo care folosește submodule

În scopul de a ilustra problemele cu care colaborează pe un repo care folosește submodule, vom împărți personalități și să acționeze ca și colegul nostru, care clonele recipientul de la distanță pentru a începe să lucreze cu noi. Vom clona asta într-un director de colegi, astfel încât să putem spune imediat ce cap de personalitate avem la un moment dat.,

primul lucru de observat este că submodul lipsește de la directorul de lucru; numai directorul de baza este aici:

vendor
└── plugins
└── demo

Cum s-a întâmplat? Acest lucru se datorează pur și simplu faptului că, până în prezent, noul nostru repo (coleg) nu este încă conștient de submodul nostru: informațiile pentru acesta nu sunt nicăieri în configurația sa locală (verificați-l .git / config dacă nu mă crezi). Va trebui să completăm asta, în funcție de ce .gitmodules are de spus, care este exact ceea ce face submodul git init:

nostru .git / config este acum conștient de submodul nostru., Cu toate acestea, încă nu l-am preluat de la distanță, pentru a nu spune nimic de a-l prezenta în directorul nostru de lucru. Și totuși, statutul nostru apare ca curat!

A se vedea, avem nevoie pentru a apuca comite relevante manual. Nu este ceva ce clona noastră inițială a făcut, trebuie să o facem la fiecare tragere. Vom reveni la asta într-un minut, deoarece aceasta este o clonă de comportament care poate automatiza, atunci când este apelată corect.,

În practică, atunci când se ocupă cu submodul-folosind repos, noi, de obicei, grupa doua comenzi (init și actualizare) într-una:

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

Aceasta este încă o rușine că Git a face asta singur. Imaginați-vă, pe proiecte mai mari de ață, când submodulele au propriile submodule și așa mai departe și așa mai departe… acest lucru ar deveni rapid un coșmar.

se întâmplă ca Git să ofere o opțiune CLI pentru clonă să actualizeze automat submodul git recursiv imediat după clonare: opțiunea destul de potrivită-numită — recursivă.,

deci, să încercăm din nou totul:

acum este mai bine! Rețineți că suntem acum pe un cap detașat în interiorul submodul (așa cum vom fi de acum încolo):

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

Vezi dublu set de paranteze în prompt, în loc de un singur set?, Dacă dumneavoastră prompt nu este configurat ca a mea, pentru a afișa cap detașat ca descrie (cu Git built-in script prompt, tu ar trebui să definească GIT_PS1_DESCRIBE_STYLE=ramură variabila de mediu), veți vedea mai degrabă ceva de genul:

demo ((fe64799...)) $

În orice caz, statutul confirmă unde suntem:

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

devine o actualizare de submodul de la distanță

OK, acum că avem propriile noastre repo (principal) și „coleg” (coleg) să colaboreze, să-și intensifice în pantofi de o a treia persoană: una care susține plugin-ul., Acum, să adăugăm două pseudo-comiteri și să le publicăm pe telecomandă:

în cele din urmă, să punem din nou capacul „primului Dezvoltator”:

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

Să presupunem că acum dorim să obținem aceste două comiteri în submodul nostru. Pentru a realiza acest lucru, trebuie să actualizăm repo-ul local, începând prin mutarea în directorul său de lucru, astfel încât acesta să devină repo-ul nostru activ.

pe o notă laterală, nu aș recomanda utilizarea pull pentru acest tip de actualizare., Pentru a obține corect actualizările în directorul de lucru, această comandă necesită să vă aflați pe ramura activă corespunzătoare, pe care de obicei nu o faceți (sunteți pe un cap detașat de cele mai multe ori). Ar trebui să începi cu o verificare a sucursalei. Dar, mai important, sucursala de la distanță ar fi putut foarte bine să se deplaseze mai departe de la comiterea pe care doriți să o setați și o tragere ar injecta comiteri pe care nu le doriți în baza de cod locală.,prin urmare, vă recomand să împărțiți procesul manual: mai întâi git fetch pentru a obține toate datele noi de la telecomandă în memoria cache locală, apoi conectați-vă pentru a verifica ce aveți și efectuați plata pe SHA1 dorit. În plus față de un control mai fin, această abordare are avantajul suplimentar de a lucra indiferent de starea dvs. actuală (sucursală activă sau cap detașat).

OK, deci suntem buni, nu se angajeze străine., Fie ca aceasta poate, sa stabilit în mod explicit pe cel care ne intereseaza (evident trebuie un alt SHA1):

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

(A -q este acolo doar pentru a ne scuti Git de prunci despre cum vom ajunge pe un cap detașat. De obicei, acest lucru ar fi un memento sănătos, dar pe aceasta știm ce facem.acum, că submodul nostru este actualizat, putem vedea rezultatul în starea containerului repo:

în partea „clasică” a stării, vedem un nou tip de schimbare a angajărilor, ceea ce înseamnă că comiterea de referință a fost modificată., O altă posibilitate (care ar putea fi agravată de aceasta) este conținutul nou, ceea ce ar însemna că am făcut modificări locale în directorul de lucru al submodului.

partea inferioară, activată de statutul nostru.submoduleSummary = true setare mai devreme, în mod explicit introduse comite (ca ei folosesc un drept-unghiul de indicare a suportului >) de la ultima noastră recipient se angajează că au atins submodul.

în familia „comportamente implicite teribile”, git diff lasă mult de dorit:

ce — ?, E o CLI opțiune care ne permite să vedem ceva mai util:

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

nu Există alte modificări locale acum pe langa submodul se face referire comite… Observa acest lucru corespunde aproape exact la partea inferioară a noastră îmbunătățită git display.

trebuie să tastați acest tip de opțiune CLI de fiecare dată (care, apropo, nu apare în ofertele actuale de finalizare ale Git) este destul de greoi. Din fericire, există o setare de configurare potrivită:

acum trebuie doar să efectuăm comiterea containerului care finalizează actualizarea submodulului nostru., Dacă a trebuit să atingeți codul containerului pentru a face să funcționeze cu această actualizare, comite-l de-a lungul, în mod natural. Pe de altă parte, pentru a evita amestecarea submodul modificări legate și alte chestii care doar se referă la container cod: prin separarea frumos două, mai târziu migrații pentru alt cod-reutilizarea abordări sunt realizate mai ușor (de asemenea, ca de obicei, atomic se angajează FTW).

pe măsură ce suntem pe cale să apucăm această actualizare submodule în repo-ul colegului nostru, vom împinge imediat după comitere (ceea ce nu este o bună practică generală).

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

trăgând un submodule-folosind repo

Faceți clic!, „Coleg” cap pe!

deci tragem actualizări din telecomanda containerului repo…

(este posibil să nu aveți „rebased și actualizat cu succes…” și să vedeți o „îmbinare făcută de strategia” recursivă „” în schimb. Dacă da, inima mea iese la tine, și ar trebui să învețe imediat de ce trage ar trebui să rebase).

Rețineți a doua jumătate a acestui afișaj: este vorba despre submodul, începând cu „preluarea submodului…”.

acest comportament a devenit implicit cu Git 1.7.5, cu setarea de configurare fetch.,recurseSubmodules acum implicit la on-demand: în cazul în care un proiect container devine actualizări la submodule de referință comite, aceste submodule obține preluat automat. (Amintiți-vă preluarea este prima parte a tragerii.)

totuși, și acest lucru este critic: Git preia automat, dar nu se actualizează automat. Cache-ul dvs. local este actualizat cu telecomanda submodului, dar directorul de lucru al submodului s-a lipit de conținutul său anterior. Cel puțin, puteți închide acel laptop, hop pe un avion, și încă merge mai departe o dată offline., Deși această preluare automată este limitată la submodule deja cunoscute: toate cele noi, care nu sunt încă copiate în configurația locală, nu sunt preluate automat.

git auto-preia, dar nu auto-actualizare.

promptul curent, cu asteriscul său (*), face aluzie la modificările locale, deoarece WD-ul nostru nu este sincronizat cu indexul, acesta din urmă fiind conștient de submodul nou referit comite. Verificați starea:

observați cum parantezele unghiulare punct stânga (<)?, Git consideră că actualul WD nu are aceste două angajamente, contrar așteptărilor proiectului container.

acesta este pericolul masiv: dacă nu actualizați în mod explicit directorul de lucru al submodului, următorul dvs. container va regresa submodul. Aceasta este o capcană de prim ordin.

este, prin urmare, obligatoriu să finalizați actualizarea:

atâta timp cât încercăm să formăm obiceiuri generice bune, comanda preferată aici ar fi un submodule git update — init — recursiv, pentru a auto-init orice submodule nou și pentru a le actualiza recursiv dacă este necesar.,

există un alt caz de margine: în cazul în care URL-ul de la distanță submodul schimbat de la ultima utilizare (probabil, unul dintre colaboratori a schimbat în .gitmodules), trebuie să actualizați manual configurația locală pentru a se potrivi cu aceasta. Într-o astfel de situație, înainte de actualizarea submodulului git, va trebui să rulați o sincronizare a submodulului git.

ar trebui să menționez, de dragul completitudinii, că, chiar dacă actualizarea submodulului Git este implicită pentru a verifica SHA1 de referință, puteți schimba acest lucru, de exemplu, pentru a rebase orice lucrare locală submodule (vom vorbi despre asta foarte curând)., Ați face acest lucru setând setarea de configurare a actualizării pentru submodul dvs. pentru a rebase în configurația locală a containerului.

și îmi pare rău, dar nu, nu există nici o setare de configurare locală, sau chiar opțiunea CLI pentru care contează, care poate auto-actualizare pe pull. Pentru a automatiza astfel de lucruri, va trebui să utilizați fie aliasuri, scripturi personalizate, fie cârlige locale atent lucrate., Iată un exemplu spull alias (o singură linie, split aici pentru afișare):

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

Dacă doriți să-și păstreze capacitatea de a trece de argumente personalizate la git pull, puteți fie să se definească o funcție pe-the-fly și numesc, sau du-te cu un script personalizat. Prima abordare ar arăta astfel (din nou, o singură linie):

nu este foarte lizibilă, nu-i așa? Prefer abordarea script personalizat., Să zicem că ai pus un git-spull fișier script undeva în CALEA ta (am o ~/perso/bin director în CALEA mea doar pentru astfel de lucruri):

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

apoi dai executarea drepturi:

chmod +x git-spull

Și acum putem folosi doar ca am folosit numele.

actualizarea unui submodule în loc în container

acesta este cel mai greu caz de utilizare și ar trebui să stați departe de el cât mai mult posibil, preferând întreținerea prin repo-ul central, dedicat.,cu toate acestea, se poate întâmpla ca codul submodulului să nu poată fi testat sau chiar compilat în afara codului containerului. Multe teme și plugin-uri au astfel de constrângeri.primul lucru pe care trebuie să-l înțelegeți este că, pentru că veți face angajamente, trebuie să începeți de la o bază adecvată, care va fi un vârf de ramură. Prin urmare, trebuie să verificați dacă cele mai recente angajamente ale sucursalei nu vor „rupe” proiectul dvs. de container. Dacă o fac, bine, Crearea propriei ramuri specifice containerului în submodule sună tentant, dar această cale duce la o cuplare puternică între submodule și container, ceea ce nu este recomandabil., Poate doriți să opriți „submodularea” acelui cod în acest proiect Special și să îl încorporați ca orice conținut obișnuit.să recunoaștem că puteți, cu bună conștiință, să adăugați la ramura principală actuală a submodulului. Un alt mod de a face acest lucru ar fi, din repo — ul containerului, să sincronizăm Explicit sucursala locală a submodulului peste sucursala de la distanță urmărită (o singură linie deasupra, ultima-urmată de spațiul alb):

acum putem edita codul, să-l facem să funcționeze, să-l testăm etc., Odată ce suntem pregătiți, putem efectua apoi cele două comiteri și cele două împingeri necesare (este foarte ușor și, în practică, prea frecvent, să uităm o parte din asta).

să adăugăm pur și simplu munca falsă și să facem cele două comiteri conexe, la nivelul submodului și al containerului:

în acest moment, pericolul major este să uităm să împingem submodul. Te întorci la proiectul containerului, îl comiți și împingi doar containerul. Este o greșeală ușor de făcut, mai ales în interiorul unui IDE sau GUI. Când colegii tăi încearcă să obțină actualizări, tot iadul se dezlănțuie., Uitați-vă la primul pas:

nu există absolut nici un indiciu că Git nu a putut prelua comiterea de referință de la telecomanda submodului. Primul indiciu în acest sens este în stare:

observați avertismentul: aparent, comiterea nou menționată pentru submodule nu este de găsit nicăieri. Într-adevăr, dacă vom încerca actualizarea submodul este directorul de lucru, obținem:

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

puteți vedea în mod clar cât de important este să-ți amintești împingând submodul de asemenea, în mod ideal, înainte de a împinge recipientul., Hai să facem asta în colegul și încercați din nou actualizarea:

eu ar trebui să rețineți există o CLI opțiune care va verifica dacă în prezent face referire submodul se obligă trebuie să fie împins prea, și dacă așa va împingeți-le: e git push — recurse-submodulele=la cerere (destul de greu de pronunțat, desigur). Trebuie să aibă ceva la nivel de container pentru a împinge la lucru, totuși: doar submodulele nu o vor tăia.

Ce e mai mult, (nu e nici o setare de configurare pentru acest lucru, așa că ar trebui să standardizeze procedurile în jurul valorii de un alias, de exemplu, spush:) — începând cu Git 2.7.0, există acum o împinge.,configurațiile recurseSubmodules setarea pe care o puteți defini (la cerere sau verificare).

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

de a Scoate un submodul

Există două situații în care ai vrea să „elimina” un submodul:

  • vrei doar pentru a șterge directorul de lucru (probabil înainte de arhivare container e WD), dar vrea să-și păstreze posibilitatea de a restaura mai târziu (deci trebuie să rămână în .gitmodules și .git / module);
  • pe care doriți să-l eliminați definitiv din ramura curentă.

să vedem fiecare caz pe rând.,

eliminarea temporară a unui submodul

prima situație este ușor de gestionat de submodulul git deinit. Acest lucru nu are niciun impact asupra stării containerului. Submodul nu mai este cunoscut local (a plecat de la .git / config), astfel încât absența sa din Directorul de lucru trece neobservată. Încă mai avem directorul demo / plugins / demo, dar este gol; am putea să-l dezbrăcăm fără nicio consecință.

submodul nu trebuie să aibă modificări locale atunci când faceți acest lucru, altfel ar trebui să forțați apelul.,

mai târziu subcomanda de git submodul va ignora fericire acest submodul până init din nou, ca submodul nu va fi chiar în local config. Astfel de comenzi includ actualizare, foreach și sincronizare.

pe de altă parte, submodul rămâne definit în .gitmodules: o init urmat de un update (sau un singur update — init) va restaura ca noi:

eliminarea Permanentă a un submodul

Acest lucru înseamnă că vrei să scapi de submodul pentru totdeauna: un regulat git rm va face, la fel ca pentru orice altă parte de directorul de lucru., Acest lucru va funcționa numai dacă submodul dvs. utilizează un gitfile (a .git care este un fișier, nu un director), care este cazul începând cu Git 1.7.8. În caz contrar, va trebui să vă ocupați de acest lucru manual (vă voi spune cum la sfârșit).

în plus față de stripping submodul din Directorul de lucru, comanda Va actualiza .fișierul gitmodules astfel încât să nu mai facă referire la submodul. Aici te duci:

în mod natural, advanced status info excursie peste ei înșiși aici, pentru că gitfile pentru submodul este plecat (de fapt, întregul director demo dispărut).,

ce e ciudat, deși, este faptul că config locale păstrează informații submodule, spre deosebire de ceea ce se întâmplă atunci când deinit. Deci, pentru o eliminare cuprinzătoare, vă recomand să faceți ambele, în ordine, astfel încât să ajungeți curățate corespunzător (nu ar funcționa după comanda noastră anterioară, deoarece s-a șters .gitmodules deja):

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

Indiferent de abordare, submodul e repo rămâne prezent în .git/module/furnizor/plugin-uri / demo, dar ești liber să-l omoare ori de câte ori doriți.

Dacă aveți vreodată nevoie să eliminați un submodul care a fost configurat înainte de Git 1.7.,8, și, prin urmare, încorporează sale .git director direct în recipientul este directorul de lucru (în loc de bazându-se pe o gitfile), va trebui să buldozer: ultimele două comenzi trebuie să fie precedată de un manual de ștergere folder, de exemplu, rm-fr furnizor/plugin-uri/demo-ul, pentru că a spus comenzi va refuza întotdeauna să ștergeți un real depozit.