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 log --oneline -3
a91f0c2 Merge commit 'b3d7e1f' as 'vendor/logger'
b3d7e1f Squashed 'vendor/logger/' content from commit 4c8a210
7e21d45 init service skeleton

subtree pull, push и split: жизнь после add
Через месяц апстрим logger выпустил фиксы. Подтягиваем.
Код: Выделить всё
git subtree pull --prefix=vendor/logger logger-upstream main --squash
Обратная дорога - отдать свои изменения в апстрим. Тут работает split: Git выкусывает из твоей истории только коммиты, которые трогали vendor/logger, и пересобирает их в отдельную линию истории так, будто они изначально жили в корне чужого репозитория.
Код: Выделить всё
git subtree push --prefix=vendor/logger logger-upstream my-fixes
Код: Выделить всё
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
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 превращает версию в "то, что мы намержили", и отследить, какой именно апстрим внутри, сложнее.
Монорепозиторий git: один репо на много проектов
Subtree и submodule - это про "много репозиториев, иногда вместе". Монорепо - идея противоположная: всё в одном. Бэкенд, фронтенд, мобилка, инфраструктура, общие либы - в одном дереве, в одной истории. Так живут Google, Meta и куча компаний поменьше.
За что любят монорепо в 2026:
- Атомарный коммит через границы проектов. Поменял API и его клиента одним коммитом - не бывает состояния, где половина системы уже на новой версии, а половина ещё нет.
- Один источник правды по версиям зависимостей, общий тулинг, общий CI, единый код-ревью.
- Лёгкий рефакторинг "поперёк" - переименовал функцию во всех проектах сразу, а не выкатывал восемь PR в восемь репозиториев.
- Дерево пухнет до миллионов файлов, и обычный git status начинает тормозить - Git физически обходит весь рабочий каталог.
- Сборка и CI должны уметь считать, какие проекты затронул коммит, иначе на каждый чих гоняется вся вселенная.
- Права доступа: в одном репо тяжело спрятать секретную папку команды от остальных - git штатно не умеет частичные права на подкаталоги.
git sparse-checkout: выгружаем только нужные каталоги
Ключевая мысль: история и так у тебя вся (после обычного clone все коммиты на диске), но рабочая копия может содержать лишь часть файлов. Остальное Git помечает как "пропущено" и физически не раскладывает на диск. Включаем cone-режим и задаём набор каталогов.
Код: Выделить всё
git sparse-checkout init --cone
git sparse-checkout set apps/web libs/ui
Код: Выделить всё
git sparse-checkout list
apps/web
libs/ui
ls
apps libs package.json README.md
ls apps
web
Добавить каталог, не перезатирая список - флаг --add.
Код: Выделить всё
git sparse-checkout add services/auth
Код: Выделить всё
git sparse-checkout disable
Тут тонкий момент, который многие пропускают. Даже когда ты выгрузил один каталог из мегарепо, в служебном файле .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-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
Реальные инструменты монореп 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 их включают вместе - и тогда гигантское дерево становится рабочим.