Subtree, монорепозитории и sparse-checkout

Рейтинг: 49% · 10 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

Subtree, монорепозитории и sparse-checkout

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout (вы здесь)
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Представь: у тебя основной сервис, а внутри живёт чужая библиотека логирования. Ты хочешь, чтобы её код был прямо в дереве - открыл файл, поправил, собрал, и ничего лишнего скачивать не надо. Но при этом иногда хочется подтягивать апдейты из апстрима и иногда отдавать свои патчи обратно. Подмодули (submodule) ты уже знаешь - они держат чужой код отдельным репозиторием по ссылке. А есть второй путь - git subtree, когда чужой код физически вшит в твоё дерево, без файла .gitmodules и без отдельного клона. И есть третья большая тема - когда наоборот, репозиторий гигантский, и ты хочешь выгрузить только кусок. Это монорепозиторий git и sparse checkout. Разберём всё это как механику, а не как набор заклинаний.

git subtree: чужой код прямо в твоём дереве

Идея subtree простая до неприличия. Ты берёшь историю чужого репозитория и приклеиваешь её в подкаталог своего, склеивая две истории merge-коммитом. После этого у тебя в рабочей копии лежит обычная папка с обычными файлами. Никаких указателей на внешний коммит, никакого второго .git, никакого git submodule update. Клонировал репо - и сразу всё на месте.

Добавляем библиотеку в каталог vendor/logger.

Код: Выделить всё

git remote add logger-upstream https://example.com/acme/logger.git
git subtree add --prefix=vendor/logger logger-upstream main --squash
Что произойдёт. Git сходит за историей удалённого logger, и с флагом --squash сплющит всю его историю в один синтетический коммит (чтобы не тащить к тебе сотни чужих коммитов), а затем сольёт это в подкаталог vendor/logger. Смотрим лог.

Код: Выделить всё

git log --oneline -3
a91f0c2 Merge commit 'b3d7e1f' as 'vendor/logger'
b3d7e1f Squashed 'vendor/logger/' content from commit 4c8a210
7e21d45 init service skeleton
Коммит b3d7e1f - это и есть сплющенный снимок чужого кода, а a91f0c2 - merge, который вживил его в путь vendor/logger. Файлы уже в рабочей копии, можно открывать и редактировать как свои.

Изображение

subtree pull, push и split: жизнь после add

Через месяц апстрим logger выпустил фиксы. Подтягиваем.

Код: Выделить всё

git subtree pull --prefix=vendor/logger logger-upstream main --squash
Git возьмёт новые коммиты апстрима, опять сплющит их в один и сделает merge в твой vendor/logger. Если ты сам правил файлы внутри этого каталога, возможен конфликт - решаешь его как обычный merge-конфликт, руками.

Обратная дорога - отдать свои изменения в апстрим. Тут работает split: Git выкусывает из твоей истории только коммиты, которые трогали vendor/logger, и пересобирает их в отдельную линию истории так, будто они изначально жили в корне чужого репозитория.

Код: Выделить всё

git subtree push --prefix=vendor/logger logger-upstream my-fixes
Под капотом push - это split плюс git push получившейся ветки. Можно вызвать split явно и посмотреть результат.

Код: Выделить всё

git subtree split --prefix=vendor/logger -b logger-only
git log --oneline logger-only -2
e0c4a98 fix: race in async flush
4c8a210 release 2.3.0
Обрати внимание: в ветке logger-only пути уже без префикса vendor/logger - файлы лежат в корне, как ждёт апстрим. Это и есть магия subtree: он умеет переписывать пути туда-сюда. Минус - split на большой истории считается долго, он реально проходит коммиты и фильтрует их.

subtree vs submodule: критерий выбора

Это главный вопрос темы. Не существует ответа "всегда так". Есть критерий, по которому выбираешь.
  • Кто чаще клонирует и кто чаще правит. Если на чужой код смотрят как на готовую зависимость и почти не трогают - подмодуль чище: он хранит ровно один SHA-1/SHA-256 указатель, дерево лёгкое. Если потребитель должен сразу получить рабочий код без лишних телодвижений и иногда его патчить - subtree удобнее.
  • Сложность для пользователя. Submodule перекладывает сложность на того, кто клонирует: забыл --recurse-submodules или git submodule update - получил пустые папки и сломанную сборку. Subtree перекладывает сложность на мейнтейнера: команды subtree pull/push с длинным --prefix надо помнить и не путать. Для большой команды джунов subtree безопаснее - клонировал и работаешь.
  • Размер и история. Subtree вшивает чужую историю (пусть и сквошнутую) в твой репозиторий навсегда, дерево пухнет. Submodule оставляет историю снаружи. На десятке мелких вендоров разница незаметна, на жирной библиотеке с гигабайтами ассетов - решающая.
  • Точность версии. Submodule пиннит ровно один коммит зависимости - воспроизводимость идеальная. Subtree после нескольких pull --squash превращает версию в "то, что мы намержили", и отследить, какой именно апстрим внутри, сложнее.
Короткое правило. Зависимость, которую вы потребляете и редко патчите, плюс важна точная версия - submodule. Код, который должен ехать вместе с репозиторием и иногда дорабатываться у себя, плюс хочется чтобы клон сразу работал - subtree. И помни про граф из прошлых уроков: узел subtree - это обычный коммит в твоей истории со стрелкой-родителем, а узел submodule - всего лишь ярлык, ссылка на коммит в чужом графе.

Монорепозиторий git: один репо на много проектов

Subtree и submodule - это про "много репозиториев, иногда вместе". Монорепо - идея противоположная: всё в одном. Бэкенд, фронтенд, мобилка, инфраструктура, общие либы - в одном дереве, в одной истории. Так живут Google, Meta и куча компаний поменьше.

За что любят монорепо в 2026:
  • Атомарный коммит через границы проектов. Поменял API и его клиента одним коммитом - не бывает состояния, где половина системы уже на новой версии, а половина ещё нет.
  • Один источник правды по версиям зависимостей, общий тулинг, общий CI, единый код-ревью.
  • Лёгкий рефакторинг "поперёк" - переименовал функцию во всех проектах сразу, а не выкатывал восемь PR в восемь репозиториев.
Против:
  • Дерево пухнет до миллионов файлов, и обычный git status начинает тормозить - Git физически обходит весь рабочий каталог.
  • Сборка и CI должны уметь считать, какие проекты затронул коммит, иначе на каждый чих гоняется вся вселенная.
  • Права доступа: в одном репо тяжело спрятать секретную папку команды от остальных - git штатно не умеет частичные права на подкаталоги.
Именно тяжесть большого дерева и привела к sparse-checkout и partial clone. Это инструменты выживания в большом репозитории git.

git sparse-checkout: выгружаем только нужные каталоги

Ключевая мысль: история и так у тебя вся (после обычного clone все коммиты на диске), но рабочая копия может содержать лишь часть файлов. Остальное Git помечает как "пропущено" и физически не раскладывает на диск. Включаем cone-режим и задаём набор каталогов.

Код: Выделить всё

git sparse-checkout init --cone
git sparse-checkout set apps/web libs/ui
После этого в рабочем каталоге останутся только apps/web, libs/ui и файлы в корне (корневые файлы вроде README и package.json cone-режим всегда оставляет - без них репозиторий бесполезен). Всё остальное исчезнет из рабочей копии, но не из истории. Проверим.

Код: Выделить всё

git sparse-checkout list
apps/web
libs/ui

ls
apps  libs  package.json  README.md
ls apps
web
Видишь - в apps только web, хотя в репозитории там может быть тридцать приложений. Cone-режим (--cone, дефолт с современных версий) работает по простому правилу: ты задаёшь каталоги, Git включает их целиком плюс файлы по пути к ним. Он быстрый, потому что сравнивает по префиксам путей, а не гоняет каждый файл через gitignore-подобные шаблоны. Старый non-cone режим с произвольными паттернами помечен как deprecated - не используй его в новых проектах, он медленный и не дружит с sparse-index.

Добавить каталог, не перезатирая список - флаг --add.

Код: Выделить всё

git sparse-checkout add services/auth
Выключить всё и вернуть полное дерево:

Код: Выделить всё

git sparse-checkout disable
sparse-index: чтобы git status не умирал на гигантском дереве

Тут тонкий момент, который многие пропускают. Даже когда ты выгрузил один каталог из мегарепо, в служебном файле .git/index по умолчанию всё ещё перечислены ВСЕ миллионы файлов репозитория - просто помеченные как skip-worktree. Git их не кладёт на диск, но при каждом git status и git add вынужден пробегать весь этот список. Индекс раздут, операции тормозят.

Лечит это sparse-index. Он сворачивает целые пропущенные каталоги в одну запись индекса - вместо тысячи файлов внутри tools/ там лежит один узел "дерево tools/". Включается так:

Код: Выделить всё

git sparse-checkout init --cone --sparse-index
или на уже настроенном репозитории:

Код: Выделить всё

git config index.sparse true
git sparse-checkout reapply
Проверить, что индекс реально сжался, можно через служебную команду.

Код: Выделить всё

git ls-files --sparse | grep -c '/$'
Записи, оканчивающиеся на слэш - это свёрнутые каталоги-деревья в sparse-index. На большом монорепо разница в скорости git status измеряется не процентами, а разами: индекс на тысячи записей вместо миллионов. Связка git sparse-checkout плюс sparse-index - это то, что делает работу в монорепо терпимой.

Связка sparse-checkout + partial clone

sparse-checkout экономит рабочий каталог и индекс, но clone всё равно тащит к тебе ВСЮ историю и ВСЕ blob-объекты всех файлов за всё время. На монстро-монорепо это десятки гигабайт. Добивает проблему partial clone - клон без содержимого файлов, blob докачиваются по требованию.

Код: Выделить всё

git clone --filter=blob:none --sparse https://example.com/org/megarepo.git
cd megarepo
git sparse-checkout set apps/web
Что тут происходит. Флаг --filter=blob:none говорит "скачай коммиты и деревья, но НЕ скачивай содержимое файлов" - blob прилетят лениво, только когда понадобятся. Флаг --sparse сразу ставит sparse-checkout в минимальный набор. Дальше set apps/web докачает blob только для нужного каталога. Так из 40-гигабайтного репозитория ты получаешь рабочую копию в сотни мегабайт за минуту вместо часа. Подробности производительности (cruft-паки, обслуживание, geometric-обслуживание которое в 2.54 стало дефолтом ручного maintenance) - в модуле про масштаб, тут важно понять схему: partial clone режет загрузку по сети, sparse-checkout режет рабочую копию, sparse-index режет индекс. Три разных рычага, и в монорепо включают все три сразу.

Реальные инструменты монореп 2026

Голым git большой монорепо тоже тянут, но обычно поверх ставят оснастку. Тулзы сборки и зависимостей по затронутым проектам - Bazel, Nx, Turborepo, pnpm workspaces. Сам git притащил scalar (встроен в git с версии 2.38, команда git scalar clone) - это профиль настроек, который сразу включает partial clone, sparse-checkout, sparse-index и фоновое обслуживание для огромных репозиториев. На стороне Git-хостинга reftable-бэкенд (production-ready с 2.51) ускоряет операции со ссылками на репозиториях с десятками тысяч веток - тоже про монорепо. То есть в 2026 монорепо это не "один git clone", а связка git + scalar/partial clone/sparse + сборочный граф.

Типичные грабли
  • Забыл --cone и получил non-cone с паттернами - status тормозит, sparse-index не включается. Всегда init --cone.
  • Ждёшь, что sparse-checkout ускорит сам по себе на монорепо, но не включил sparse-index - индекс остался на миллион записей, тормоза никуда не делись. Нужны ОБА.
  • subtree push без предварительного pull последних изменений апстрима - получишь отказ non-fast-forward, как при обычном push. Сначала pull, потом push.
  • Перепутал --prefix в subtree-командах (например vendor/logger и vendor/loggers) - Git молча создаст второй subtree или намержит не туда. Префикс копируй, не печатай руками.
  • Думаешь, sparse-checkout удаляет файлы из репозитория - нет, он прячет их только из рабочей копии. git log, git show, история - всё на месте. disable вернёт всё назад.
  • После git subtree add --squash потерял связь, какой именно коммит апстрима внутри - записывай версию в CHANGELOG руками, squash её прячет.
Мини-лаба: пощупать руками
  • Создай пустой репозиторий-апстрим: mkdir lib; в нём git init -b main, положи файл и закоммить.
  • В другом каталоге заведи основной репо, сделай первый коммит, потом git subtree add --prefix=vendor/lib ../lib main --squash. Посмотри git log --oneline - найди merge и squash-коммит.
  • Поправь файл внутри vendor/lib, закоммить. Сделай git subtree split --prefix=vendor/lib -b lib-only и убедись через git log lib-only, что в этой ветке пути без префикса.
  • Теперь про sparse. На любом репо с несколькими каталогами: git sparse-checkout init --cone --sparse-index, затем set на один каталог. Сделай ls - лишние каталоги пропали. Проверь git sparse-checkout list.
  • Запусти git status - он быстрый, потому что индекс сжат. Затем git sparse-checkout disable и убедись, что всё дерево вернулось.
Контрольные вопросы
  • Чем дерево репозитория с subtree отличается от дерева с submodule - что лежит на месте чужого кода в каждом случае?
  • По какому критерию ты выберешь subtree, а не submodule, для библиотеки, которую твоя команда часто патчит у себя?
  • Зачем нужен sparse-index, если sparse-checkout уже не выгружает лишние файлы на диск? Что именно он сжимает?
  • Какие три разных ресурса режут partial clone, sparse-checkout и sparse-index соответственно?
Итог

Subtree вшивает чужой код прямо в дерево (add/pull/push/split, без .gitmodules) и выигрывает там, где важно "клонировал и работает" плюс свои патчи; submodule выигрывает там, где важна точная версия и лёгкое дерево. Монорепо складывает всё в один репозиторий ради атомарных изменений поперёк проектов, но требует инструментов выживания: sparse-checkout прячет лишние каталоги из рабочей копии (только --cone), sparse-index сжимает раздутый индекс, partial clone не тащит лишние blob по сети. В большом репозитории git их включают вместе - и тогда гигантское дерево становится рабочим.
👍4 ❤️3 🔥1 😄 🤔2
Аватара пользователя
proxmoxsre
Сообщения: 1
Зарегистрирован: 22 май 2026, 00:27

Re: Subtree, монорепозитории и sparse-checkout

Сообщение proxmoxsre »

А если в subtree намержить апстрим через pull без --squash - чем плохо? Дерево же будет точнее по версиям, но история разбухнет, правильно понимаю?
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
robfranx
Сообщения: 1
Зарегистрирован: 24 май 2026, 19:23

Re: Subtree, монорепозитории и sparse-checkout

Сообщение robfranx »

Спасибо, наконец дошло почему один sparse-checkout не ускорял status - забыл про sparse-index. Включил index.sparse и reapply, status реально летает теперь.
👍1 ❤️1 🔥1 😄 🤔1
Ответить
← Предыдущая глава
Подмодули: внешние репозитории внутри проекта
Следующая глава →
Git LFS и большие файлы

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: git clone: клонирование репозиторияgit submodule: подмодулиМонорепозиторий и большие репозитории Git

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость