linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Часть IV — Совместная работа

$ глава 13 · 50 минут

Submodules, worktrees и sparse checkout

Иногда один репозиторий - это слишком тесно. Несколько проектов зависят друг от друга, и хочется их как-то связать. Или наоборот: один репозиторий слишком большой, и хочется работать только с частью.

В Git есть три механизма для этих задач: submodules (репо внутри репо), worktrees (несколько working tree из одного репо) и sparse checkout (выбрать, какую часть скачивать). Каждый - компромиссный: решает одну проблему, создаёт другую.

Эта глава - про то, что внутри у этих механизмов, в каких сценариях они выручают, и какой ценой. С особым акцентом на submodules: они больно кусают, если ими пользоваться не понимая, и сценариев, где они правда нужны, меньше, чем кажется.

13.1 Submodules: репо внутри репо

Submodule - это указатель из одного репозитория на конкретный коммит другого репозитория. Не копия, не зависимость в смысле пакетного менеджера, а именно «пиннинг» на SHA.

Внутри репо submodule выглядит как обычная директория, но в .gitmodules записан URL источника, а в индексе записан SHA коммита из этого источника:

[submodule "vendor/awesome"]
    path = vendor/awesome
    url = https://github.com/foo/awesome.git

Команды:

bash
git submodule add https://github.com/foo/awesome vendor/awesome
git submodule init                    # инициализация (после clone)
git submodule update                  # подтянуть до SHA из индекса
git submodule update --remote         # подтянуть до HEAD upstream
git submodule deinit vendor/awesome   # деинициализировать

Типичный заход на свежем клоне:

bash
git clone https://github.com/foo/bar
cd bar
# vendor/awesome пустая - нужно подтянуть
git submodule update --init --recursive

Или сразу при clone:

bash
git clone --recursive https://github.com/foo/bar

Что физически записано

В индексе родительского репо vendor/awesome лежит как специальный объект «gitlink» - указатель на коммит SHA. Не tree, не blob, а ссылка на коммит. При git status ты видишь:

modified:   vendor/awesome (new commits)

Это значит: ты зашёл в submodule, сделал там что-то, теперь родительскому репо нужно обновить указатель.

Где submodules адекватны

  • Вендоринг библиотеки, которая редко меняется. Пиннинг на конкретный SHA, защита от внезапного breaking change.
  • Шаринг кода между несколькими проектами. Например, общий протобуф-пакет.
  • Огромная third-party зависимость. Когда package manager слишком тяжёл (или его нет, например в C/C++).

Где submodules болят

  • Каждая активная разработка. Если внутри submodule ты тоже часто коммитишь, цикл «обновить родительский указатель» - отдельная работа на каждое изменение.
  • Сложность для новых разработчиков. Без --recursive репо просто не собирается, без объяснений где что.
  • CI должен помнить про submodules. Без --recurse-submodules в pipeline - собирается пустота.
  • Branch / submodule mismatch. Ветки родительского репо могут указывать на разные SHA submodule. При переключении ветки git submodule update нужен явно.

Полезный конфиг

bash
# Автоматически обновлять submodules при checkout/pull
git config --global submodule.recurse true
# Показывать diff внутри submodule, не только «modified»
git config --global diff.submodule log

С этими настройками submodules ведут себя ощутимо менее агрессивно.

Подводный камень: detached HEAD внутри submodule

git submodule update делает checkout по SHA, не по ветке. Внутри submodule у тебя detached HEAD (см. detached-head). Если ты там что-то коммитишь, не переключаясь на ветку - твои коммиты висят на отдельном указателе, и при следующем git submodule update их можно потерять.

Правильный цикл «изменить код внутри submodule»:

bash
cd vendor/awesome
git switch -c fix/x      # выйти из detached, создать ветку
# ... правки ...
git commit -am "fix"
git push
cd ..
git add vendor/awesome   # обновить указатель в родителе
git commit -m "bump awesome"

13.2 Альтернативы submodule: subtree, monorepo

Submodule - не единственный способ держать несколько кодовых баз вместе. Альтернативы:

git subtree

Subtree копирует содержимое одного репо внутрь другого как обычные файлы. Без gitlink, без .gitmodules. Внешне это выглядит как обычная директория, и git clone тащит её целиком, без отдельной команды.

bash
git subtree add --prefix=vendor/awesome https://github.com/foo/awesome main --squash
git subtree pull --prefix=vendor/awesome https://github.com/foo/awesome main --squash

Плюсы:

  • Новый разработчик клонирует и сразу всё на месте.
  • CI не нужно знать про submodule.
  • Можно править файлы внутри, и они не теряются.

Минусы:

  • Каждый pull увеличивает размер репо (история копируется).
  • Команды длинные, легко забыть --prefix.
  • Чтобы отправить изменения обратно в upstream - отдельная процедура git subtree push.

Subtree подходит для разового вендоринга, когда дальнейшие обновления редки. Если планируется регулярный sync - subtree становится утомительным.

Monorepo

Альтернативный подход: отказаться от идеи «несколько репо», положить всё в один. Все приложения, все библиотеки, общие зависимости - в одном дереве.

monorepo/
  apps/
    web/
    mobile/
    api/
  libs/
    auth/
    utils/
    models/
  tools/
    deploy/
    ci/

Плюсы:

  • Один clone, всё на месте.
  • Refactoring через всё дерево - атомарный коммит.
  • Никакого pinning'а - все приложения видят актуальные библиотеки.

Минусы:

  • Требует системы сборки, понимающей зависимости (Bazel, Nx, Turborepo, Pants).
  • Репо растёт быстро. Часто > гигабайта.
  • CI должен выбирать, что собирать (incremental builds), иначе каждое изменение запускает всю сборку.

Monorepo выбирают большие компании (Google, Meta, Stripe) и многие фронтенд-команды (Nx-based). Mid-size команды чаще сидят на multi-repo. Это политическое решение, не техническое - и часто принимается «потому что у Google monorepo», что плохой аргумент.

Сравнение

Чтоsubmodulessubtreemonorepo
Pinning на SHAданет (есть копия)нет
Нужен --recursive при cloneданетнет
Простота для нового разработчиканизкаясредняявысокая
Размер репомаленькийрастётбольшой
Atomic refactor через всё деревонетнетда
Подходит для редких обновленийдада-
Подходит для активной разработки в нескольких компонентахнетнетда

Правило большого пальца: submodules - для read-only зависимостей. Для активного кода - monorepo или multi-repo с package manager'ом.

13.3 Worktrees: параллельная работа без переключения веток

Обычно у репозитория одно working tree - одна директория с файлами, которая отражает HEAD. Чтобы посмотреть другую ветку - git switch, и working tree переключается.

Это раздражает, когда нужно одновременно работать с двумя ветками. Типичный сценарий: ты пишешь фичу в feat/x, прилетает срочный hotfix-запрос на main. Варианты:

  • Stash текущие изменения, переключиться на main, починить, вернуться, unstash. Работает, но контекст рассыпается.
  • Клонировать репо ещё раз в другую директорию - оверхед на clone, две .git/ директории на диск.
  • Worktree - несколько working tree из одного .git/.
bash
git worktree add ../bar-hotfix main
# создаёт ../bar-hotfix с checkout main

Что произошло: в ../bar-hotfix появилась полная working copy, как будто это отдельный клон. Но .git/ шарится с основной директорией - никакого второго fetch'а, никакого дублирования объектов.

В этой директории ты делаешь hotfix, коммитишь, пушишь, как обычно. В основной директории всё это время продолжается работа в feat/x - без переключения.

Когда закончил - удалить worktree:

bash
git worktree remove ../bar-hotfix
# либо просто rm -rf, если внутри ничего не сохраняется

Список worktree

bash
git worktree list
# /path/to/main         abc123 [feat/x]
# /path/to/bar-hotfix   def456 [main]

Видно, какие directories связаны с какими ветками.

Ограничения

  • Одна ветка - одна worktree. Нельзя checkout одну и ту же ветку в двух worktree одновременно. Git защищается от двух одновременных коммитов на одну ветку.
  • Не для submodules-сценариев. Submodules внутри worktree работают, но с нюансами; лучше избегать.
  • git worktree remove отказывается с грязными изменениями. Либо commit/stash, либо --force.

Когда worktree оправданы

  • Hotfix во время фича-работы.
  • Сравнение поведения двух веток вживую (запустить обе одновременно).
  • Долгий тест на одной ветке, продолжение работы на другой.
  • Code review большой ветки в отдельной директории, чтобы IDE не мешалась.

В commercial-проектах worktree часто недооценён. В большом проекте, где ребилд занимает 5 минут, два рабочих копии могут экономить десятки минут в день.

13.3.1 Подводный камень: relative refs внутри worktree

.git/ шарится между worktrees, но reflog и некоторые refs - per-worktree:

.git/
  HEAD                  ← per-worktree (main worktree)
  worktrees/
    bar-hotfix/
      HEAD              ← per-worktree (hotfix worktree)
      index
      logs/

Это значит:

  • HEAD в основной директории != HEAD в hotfix.
  • git stash в одной не видит stash другой (stash хранится в per-worktree-refs).
  • reflog HEAD в одной не показывает события другой.

Эти изоляции по большей части незаметны, но иногда удивляют. Когда «не нахожу stash в новом worktree» - это потому, что он на самом деле в старом.

13.4 Sparse checkout: только нужная часть

Sparse checkout управляет тем, какие файлы Git кладёт в рабочее дерево. По умолчанию sparse checkout сам по себе не уменьшает то, что качается с remote, - чтобы не тащить blob'ы всего репо, его комбинируют с partial clone (--filter=blob:none, см. ниже). Полезно, когда репо большое (монорепо), а нужна только конкретная директория.

Включение:

bash
git clone --filter=blob:none --no-checkout https://github.com/foo/monorepo
cd monorepo
git sparse-checkout init --cone
git sparse-checkout set apps/web libs/auth
git checkout main

После этого в рабочем каталоге будут только apps/web/* и libs/auth/*. Файлы из apps/mobile/, libs/utils/ и т.д. физически не на диске - только в .git/objects/. Это экономит место и время первого checkout.

--cone - режим, в котором sparse-checkout работает быстро на больших репах (по директориям, не по файловым паттернам). --no-cone (старый) поддерживает gitignore-style patterns, но медленнее.

Когда полезен

  • Монорепо. Frontend-команда работает только с apps/web и несколькими libs/. Sparse checkout убирает остальное с диска.
  • Очень большие репозитории. Linux kernel клонируется целиком ~3 GB; если нужна только определённая поддерева драйверов - sparse сокращает до сотен MB.
  • CI на одну часть. Если в pipeline проверяется только frontend/, sparse checkout сокращает время clone в разы.

Ограничения

  • Не для всех команд прозрачно. git log показывает всю историю (как и должен), но если кто-то спросит «покажи мне изменения в файле X» - а файл вне sparse, тебе нужно расширить sparse, чтобы увидеть его.
  • Не «virtualизация», как Git LFS или VFS. Файлы или есть на диске, или нет; чтобы получить - нужно поменять sparse-set.

Partial clone

Sparse checkout + --filter=blob:none (partial clone) - мощная пара. Partial clone не качает blob'ы (содержимое файлов) до checkout. Sparse checkout говорит, какие blob'ы понадобятся. Вместе - clone в несколько раз быстрее на больших репах.

Поддерживается Git 2.25+. На GitHub partial clone работает, sparse checkout - локальная команда, не требует поддержки сервера.

13.5 Когда что выбирать

Шпаргалка для типичных сценариев:

Что нужноЧто брать
Подключить read-only зависимость (vendored library)submodule
Подключить зависимость, в которой будем активно правитьmonorepo или package manager (npm, cargo, pip)
Разовый вендоринг проектаsubtree (или просто cp -r + commit)
Несколько активных продуктов с общими библиотекамиmonorepo с системой сборки
Срочный hotfix во время feature-работыworktree
Тестировать две версии одновременноworktree
Работать только с частью большого репоsparse checkout
CI на часть монорепоsparse + partial clone
Огромная история, нужен только последний коммит--depth 1 (shallow clone)

Что НЕ делать

  • Не использовать submodule «потому что коллеги их видели». Это специфический инструмент, не общая практика. Если ты не уверен - скорее всего, не нужны.
  • Не лечить submodule-боль ещё более глубокими submodule. Если submodule болит, рассмотри subtree или monorepo.
  • Не делать worktree «временно навсегда». Через год у тебя в директории-сестре старый код, на котором не сделан pull полгода. Удаляй worktree после задачи.
  • Не путать sparse checkout с .gitignore. .gitignore не даёт Git'у видеть файлы. Sparse checkout говорит, какие из видимых файлов класть на диск. Это разные слои.

Уроки в sandbox

lab-13.1. Worktree для hotfix без потери контекста

Цель - отработать сценарий «работаю над фичей, прилетел срочный hotfix». С worktree можно решить hotfix параллельно, не трогая фича-ветку. Лаба не требует remote, всё локально.

  1. Создай репо с двумя коммитами: mkdir -p /tmp/worktree-lab && cd /tmp/worktree-lab && git init && echo 'v1' > app.txt && git add . && git commit -m 'v1' && echo 'v2' >> app.txt && git commit -am 'v2'.

  2. Создай feature-ветку и сделай в ней «незаконченную работу»: git switch -c feat/big-refactor && echo 'WIP refactor' >> app.txt && git commit -am 'wip refactor'. Это твоя текущая, длинная задача. У тебя в working tree «грязные» (в смысле работы) изменения.

  3. Прилетает срочный hotfix-запрос на main. Без worktree пришлось бы stash + switch. Сделай worktree вместо этого: git worktree add ../hotfix main. Создаст параллельную директорию ../hotfix с checkout'ом main.

  4. Проверь: git worktree list. Увидишь оба worktree - основной (с feat/big-refactor) и хотфикс (с main). Они шарят одну .git/ директорию: место на диске не дублируется.

  5. Сделай hotfix в отдельной директории: cd ../hotfix && echo 'CRITICAL FIX' >> app.txt && git commit -am 'fix: critical' && cd -.

  6. Вернувшись в основной worktree, ты всё ещё на feat/big-refactor, твоя WIP-работа на месте. Запусти git log --oneline --graph --all: увидишь, что на main появился коммит 'fix: critical', а твоя фича от него отделена.

  7. Удали worktree, когда hotfix задеплоен: git worktree remove ../hotfix. Директория удалится, .git/ останется.

  8. Итог для головы: ты решил две задачи параллельно, без stash, без потери контекста. Worktree - это инструмент против stash'а в сценариях «нужно временно отвлечься на другую ветку».

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Submodule - это указатель на конкретный SHA другого репо. Подходит для read-only вендоринга, но болит при активной разработке внутри (см. [[detached-head]] в submodule).
  • Subtree копирует содержимое внутрь, без gitlink. Проще для нового разработчика, но история растёт, и push в upstream неудобен.
  • Monorepo - отказ от идеи «много репо» в пользу одного дерева. Требует системы сборки с incremental builds (Bazel, Nx, Turborepo). Политическое решение, не техническое.
  • Worktree даёт несколько working tree из одного .git/. Идеально для hotfix во время feature-работы и параллельного тестирования веток. Одна ветка - одна worktree.
  • Sparse checkout позволяет работать только с частью большого репо. В паре с partial clone (`--filter=blob:none`) сокращает clone больших монорепо в разы.
  • Шпаргалка выбора: submodule = пиннинг read-only. Subtree = разовый вендоринг. Monorepo = активная разработка нескольких компонентов. Worktree = параллельные ветки. Sparse = часть монорепо.

Контрольные вопросы

  1. Я добавил submodule, запушил, коллега клонировал - у него директория submodule пустая. Что забыли?

    Показать ответ

    git submodule update --init --recursive после clone. Submodule хранится в индексе родительского репо как указатель (gitlink), но содержимое не клонируется автоматически. Нужно сказать Git'у «теперь клонируй submodules».

    Альтернатива - клонировать сразу с --recursive:

    git clone --recursive <url>

    Это часто забывают. Для команд, активно использующих submodules, полезно добавить инструкцию в README или сделать Makefile target make init, который делает git submodule update --init за пользователя.

  2. Submodule болит: каждое изменение в нём ломает CI родительского репо. Что делать?

    Показать ответ

    Скорее всего, submodule не подходит к твоей задаче. Submodule проектировался под редкие обновления вендоренной зависимости, а не под активную разработку.

    Если код в submodule меняется часто и связан с родительским репо логически:

    • Если это один продукт в разных папках - переходи на monorepo. Один репо, один CI, atomic коммиты через всё.
    • Если это общая библиотека - заверни в проперный пакет (npm, cargo, pip) и подключай через package manager.
    • Если это разовый вендоринг (нужно зафиксировать конкретную версию для воспроизводимости) - рассмотри subtree или просто cp -r && git commit.

    submodule оправдан для read-only зависимостей: «здесь конкретный SHA внешней библиотеки, мы его не правим, только обновляем раз в полгода». Если у тебя другой сценарий - инструмент не тот.

  3. Чем worktree лучше второго clone?

    Показать ответ

    Тремя вещами:

    1. Объекты не дублируются. .git/objects/ шарится между worktrees. На репозиториях по гигабайту это экономит место.
    2. Один fetch обновляет всё. Не нужно ходить по двум клонам и делать git fetch в каждом.
    3. Конфиг общий. Один git config, один список remote, одни SSH-ключи. В двух клонах это два разных репо со своими настройками, которые легко рассинхронизировать.

    Минусы worktree: чуть менее изолирован (одна .git/ - общая точка отказа), нельзя checkout одну ветку в двух worktree одновременно. Для большинства сценариев плюсы перевешивают.

  4. Я в monorepo, но мне нужно работать только с одной директорией. Как обойтись без скачивания всего репо?

    Показать ответ

    Partial clone + sparse checkout:

    git clone --filter=blob:none --no-checkout <url>
    cd <repo>
    git sparse-checkout init --cone
    git sparse-checkout set apps/web libs/auth
    git checkout main

    --filter=blob:none говорит «не качай содержимое файлов до checkout». --no-checkout - не делай checkout сразу. sparse-checkout set - какие директории положить на диск.

    В итоге clone в разы быстрее, диск тратится только на нужные директории, история (коммиты, tree-объекты) всё равно вся на месте - нужна для git log, git blame и т.д.

    Файлы вне sparse-set можно «добавить позже»: git sparse-checkout add libs/utils подкачает их с remote.

  5. Я случайно сделал коммит внутри submodule, теперь не могу его запушить - submodule в detached HEAD. Что делать?

    Показать ответ

    Создать ветку на текущей позиции и запушить с неё:

    cd path/to/submodule
    git switch -c fix/my-change      # из detached в новую ветку
    git push -u origin fix/my-change

    Теперь твой коммит на ветке fix/my-change, не висит на detached. Дальше - открыть PR в upstream submodule, ждать мержа.

    После того как submodule примет коммит в свой main:

    cd path/to/submodule
    git switch main
    git pull
    cd ..
    git add path/to/submodule        # обновить указатель в родителе
    git commit -m "bump submodule to <new-sha>"

    Урок: внутри submodule всегда работай на нормальной ветке. См. detached-head про общую механику.

← Предыдущая12-remoteСледующая →14-hooks
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки