A git sparse-checkout cone módja: egy mappa, egy index-bejegyzés

A cone mód nem csak kevesebb fájlt rak ki lemezre: sparse indexszel egy teljes mappa a git indexében egyetlen fa-objektummá zsugorodik. Végigcsináltam egy valódi repón, hogy lássam, mi történik a színfalak mögött, és mit nem old meg ez a funkció.

Van egy céges monorepód: húsz app, ötven package, néhány millió fájl a historyban, és te ebből ténylegesen kettőt nyitogatsz. Mégis minden git checkout, minden git status, minden IDE-indexelés úgy viselkedik, mintha mind a húsz appot egyszerre szerkesztenéd. A legtöbb fejlesztő ilyenkor vagy zsörtölődik és eltűri, vagy külön repóba menekül a monorepo elől. Pedig a git már évek óta hordoz egy beépített megoldást erre, amit valószínűleg soha nem próbáltál ki, mert a neve („sparse checkout”) inkább egy féloldalas man page-et sugall, nem egy tényleg használható funkciót.

Pedig van benne legalább egy technikai csavar, amiről lefogadom, hogy nem tudtál: cone módban egy teljes, ki nem választott könyvtár a git indexében nem száz fájlként, hanem egyetlen bejegyzésként létezik. Ezt nem elhiszem-alapon állítom, hanem mert egy valódi repón végigcsináltam.

Diagram: bal oldalon egy teljes repó fastruktúrája sok apró fájl-dobozzal három mappában; jobb oldalon ugyanaz cone módú sparse-checkout után, ahol az első mappa fájljai továbbra is külön dobozokként látszanak, a másik két mappa pedig egyetlen összevont, zölden világító blokká zsugorodik, jelezve, hogy egyetlen index-bejegyzésként létezik.

Nem a .gitignore ellentéte, hanem egy bit a working tree-ben

A sparse-checkout nem szűri ki a fájlokat a repóból – azok végig ott vannak a historyban és az indexben is, csak a working tree-ben nem materializálódnak. A mechanizmus a skip-worktree bit: ha be van kapcsolva egy fájlon, a git úgy tesz, mintha a hiánya rendben volna, és nem is próbálja meg kiírni a lemezre. Ez a lényegi különbség a .gitignore-hoz képest: az ignore-olt fájlok sosincsenek a repóban, a sparse-checkoutból kihagyottak igen, csak nem látod őket a mappádban.

2020 januárjában jelent meg a funkció opt-in „cone móddal” (Git 2.25), ami helyett a fejlesztőnek nem kellett .gitignore-stílusú mintákat írogatnia, csak mappaneveket adott meg. 2022 júniusában (Git 2.37) ez a cone mód lett az alapértelmezett, a régi, minta-alapú „non-cone” módot pedig a git saját dokumentációja deprecatednek nyilvánította ugyanabban a release-ben. Ha ma git sparse-checkout set-et futtatsz, cone módot kapsz, hacsak explicit ki nem kéred a régit.

Végigcsinálva egy valódi repón

Csináltam egy játék-monorepót apps/web, apps/api, apps/mobile, packages/ui, packages/utils és docs mappákkal, mindegyikben egy-egy fájllal. Ezután:

$ git sparse-checkout set apps/web packages/ui
$ find . -not -path './.git*' | sort
.
./apps
./apps/web
./apps/web/index.js
./packages
./packages/ui
./packages/ui/index.js
./README.md

A apps/api és apps/mobile mappák nemcsak üresek lettek, hanem el is tűntek a lemezről – de csak azért, mert semmi nem volt bennük, amit meg kellett volna őrizni. Amikor ugyanezt megismételtem úgy, hogy előtte tettem egy nem verziózott scratch.txt-t az apps/api alá, a git nem törölte csendben:

warning: directory 'apps/api/' contains untracked files,
but is not in the sparse-checkout cone

A verziózott index.js eltűnt (skip-worktree), a scratch.txt viszont ottmaradt, félig kint, félig bent egy mappában, amit a git sparse-checkout list szerint már nem is választottál ki. Ez pontosan az a fajta apró, csendes viselkedés, ami egy valódi projektben simán okoz egy „hol a francba van a fájlom” délutánt, ha nem tudsz róla.

A cone mód nem varázslat, csak jobban titkolja a mintáit

A dokumentáció azt sugallja, hogy cone módban „csak mappaneveket adsz meg”, és ez a felhasználói felület szintjén igaz is. De ha megnézed, mi kerül ténylegesen a .git/info/sparse-checkout fájlba a fenti parancs után, kiderül, hogy alatta ugyanaz a .gitignore-szintaxis fut, csak automatikusan generálva:

/*
!/*/
/apps/
!/apps/*/
/packages/
!/packages/*/
/apps/web/
/packages/ui/

Vagyis a cone mód nem egy másik motor, hanem egy erősen korlátozott, gépileg generált részhalmaza a régi, teljes minta-nyelvnek – éppen ez teszi lehetővé, hogy a git ne kelljen minden fájlt minden mintával összevessen. Egy másik apróság, amit érdemes tudni: a set parancs első futtatásakor a git automatikusan átemeli a beállításokat worktree-specifikus configba (.git/config.worktree), nem a sima .git/config-ba – így egy git worktree add-del létrehozott másik munkakönyvtár nyugodtan beállíthat teljesen más sparse mintákat anélkül, hogy egymást felülírnák.

Miért kellett egyáltalán ez a mód

A régi, teljes minta-alapú sparse-checkout minden checkoutnál minden mintát minden fájllal összevet – ez O(N×M), ahol N a minták, M a fájlok száma. A GitHub engineering blogja egy konkrét, valós méretű repón mérte ezt: hárommillió fájl, ezer mappa, hétszázezer materializálandó fájl esetén a régi mintaillesztés öt percig futott egyetlen checkoutnál. A cone mód, mivel a mintákat mappaszintre korlátozza, O(M + N×d) komplexitású (d a maximális mappamélység) – ugyanaz a checkout egy másodperc alatt lefutott. Ez nem marketingszám, hanem a GitHub saját méréssorozata a saját monorepójukon.

A sparse index: amikor egy mappa egy sor lesz

Ez volt a rész, amitől tényleg meglepődtem. Alapból a sparse-checkout csak a working tree-t ritkítja, az index (a .git/index) minden fájlt továbbra is önálló bejegyzésként tárol. A --sparse-index kapcsoló ezt is megváltoztatja: a kiválasztott cone-on kívüli mappák a fájljaik helyett egyetlen fa-objektumként (tree, mode 040000) kerülnek az indexbe. Íme a különbség ugyanazon a repón, git ls-files --sparse --stage-dzsel:

100644 8178c76... 0	README.md
040000 5d52ff2... 0	apps/api/
040000 d0cc3b1... 0	apps/mobile/
100644 52a3442... 0	apps/web/index.js
040000 d647919... 0	docs/
100644 9fad0cf... 0	packages/ui/index.js
040000 a4c951b... 0	packages/utils/

Az apps/api, apps/mobile, docs és packages/utils – mind kívül esnek a cone-on – mindegyike egy darab index-bejegyzés, függetlenül attól, hány ezer fájl lapul alattuk a valóságban. Ez az igazi ok, amiért egy nagy monorepóban a git status vagy a git add érdemben gyorsabb lehet sparse indexszel: nincs mit bejárni azokban a mappákban, amikről tudod, hogy nem érdekelnek.

Van egy csapdája is: nem minden parancs sparse-index-tudatos. Amikor egy sima git ls-files --stage-et futtattam (a --sparse flag nélkül) egy sparse-index repón, a git csendben kibontotta a teljes indexet, és közben ki is írta, hogy ez lassú:

hint: The sparse index is expanding to a full index,
a slow operation.

Vagyis pont az az előny veszik el egy pillanat alatt, amiért bekapcsoltad a funkciót – nem hibaüzenettel, csak egy figyelmeztető sorral, amit könnyű átlapozni egy CI logban.

Amit ez NEM old meg

A sparse-checkout a working tree-t és bizonyos parancsok sebességét ritkítja – a .git mappa, a letöltött history, a blob-ok mérete változatlan marad. Ha a klónozás lassúsága vagy a lemezhely a probléma, az a --filter=blob:none partial clone feladata, ami külön funkció, és a kettő tökéletesen kombinálható. Ezt a kombinációt csomagolta be a Microsoft Scalar nevű eszközébe, ami 2022 októberében (Git 2.38) a git saját disztribúciójának része lett – ha nem akarsz kézzel babrálni cone mintákkal és fetch-filterekkel, a scalar clone ezt egyben intézi.

Egy másik, kevésbé kényelmes tény: a git saját dokumentációja a mai napig, szó szerint ezzel a mondattal kezdi a parancs leírását: „THIS COMMAND IS EXPERIMENTAL. ITS BEHAVIOR […] WILL LIKELY CHANGE IN THE FUTURE.” Ez négy évvel az után van így, hogy a funkció alapértelmezetté vált, és olyan cégek épülnek rá éles gyakorlatban, mint a GitHub és a Microsoft. Nem hibás állítás, csak érdemes komolyan venni: az interakció más git-funkciókkal (submodule, merge edge case-ek, néhány GUI kliens) időnként még mindig meglepetést okoz.

Használnád production-ben?

Ha van egy valódi monorepód, ahol a csapatok jól elkülönített mappákban dolgoznak, és a fejlesztők/CI-jobok tényleg csak egy-két almappát érintenek – igen, minden fenntartás nélkül. A cone mód beállítása néhány perc, a nyereség (gyorsabb checkout, gyorsabb status, kevesebb fájl az IDE indexelőjének) azonnal érezhető, és a partial clone-nal kombinálva ez az, ami ténylegesen skálázhatóvá tesz egy tízezer fájlos monorepót.

Amit viszont nem tennék: kis-közepes repóknál bevezetni „elővigyázatosságból”. Extra mentális terhet ad (mindenkinek tudnia kell, hogy épp mit lát és mit nem, és a fenti untracked-fájl csapda pont ott üt be, amikor a legkevésbé számítasz rá), miközben a haszna nulla, ha a repód amúgy is másodpercek alatt checkoutolható. És ha a csapat olyan IDE-t vagy build-eszközt használ, ami nem sparse-checkout-tudatos (végigmegy a fájlrendszeren, nem a git indexen), a látszólagos gyorsulásból semmi nem jön át – sőt, „hiányzó fájl” hibákat kezdesz kapni ott, ahol a tool azt feltételezte, hogy minden a helyén van.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük