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.

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
- git-scm.com – git sparse-checkout dokumentáció
- git forrás – git-sparse-checkout.adoc (cone mód belső működése, non-cone deprecation)
- GitHub Blog – Bring your monorepo down to size with sparse-checkout
- git-scm.com – Partial Clone
- GitHub Blog – The story of Scalar (partial clone + sparse-checkout kombináció)
- git – 2.37.0 release notes (non-cone mode deprecation)