Подмодули: внешние репозитории внутри проекта

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

Подмодули: внешние репозитории внутри проекта

Сообщение 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 и как из них выбираться
Зачем вообще нужны подмодули git

Представь: ты пишешь сервис, и тебе нужна внутренняя библиотека, которая живёт в отдельном репозитории. Команда её правит независимо, у неё свой релизный цикл, свои тесты. Тебе не нужен её весь хаос внутри своей истории - тебе нужна КОНКРЕТНАЯ версия, зафиксированная намертво, чтобы сборка была воспроизводимой и сегодня, и через год.

Скопировать файлы руками? Потеряешь связь с апстримом и историю. Подключить как пакет (composer, npm)? Отлично, если у библиотеки есть нормальный реестр и версионирование. А если это твой же приватный код, форк чужого проекта или вендоринг C-зависимости без пакетного менеджера? Вот тут на сцену выходят подмодули git.

Подмодуль - это ссылка на чужой репозиторий по конкретному коммиту, вшитая в твою историю. Не копия файлов, не ветка - именно указатель: "вот этот репозиторий, вот этот SHA". Ты фиксируешь не "последнюю версию", а ровно тот коммит, на котором всё работало. Хочешь обновиться - двигаешь указатель осознанно и коммитишь это движение.

Сразу честно: подмодули болезненны. Это самая частая тема "почему git меня ненавидит" среди мидлов. Половина боли - от непонимания механики. Поэтому разберём именно механику, а не набор заклинаний.

Изображение

Что физически создаёт git submodule add

Заведём суперпроект (так называют родительский репозиторий) и подключим библиотеку.

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

git init super && cd super
git commit --allow-empty -m "init"
git submodule add https://example.com/libfoo.git vendor/libfoo
Смотрим, что появилось:

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

git status
On branch main
Changes to be committed:
  new file:   .gitmodules
  new file:   vendor/libfoo
Два объекта, и оба важны.

Первый - файл .gitmodules в корне. Это обычный текстовый файл под версионным контролем, карта всех подмодулей:

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

[submodule "vendor/libfoo"]
	path = vendor/libfoo
	url = https://example.com/libfoo.git
Он коммитится и едет вместе с проектом - именно по нему другие узнают, ОТКУДА клонировать подмодуль и КУДА его класть.

Второй - сам vendor/libfoo. Обрати внимание: git показывает его как один "new file", хотя это целая директория с кодом. Загляни в дерево:

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

git ls-files --stage vendor/libfoo
160000 a1b2c3d4e5f6...  0	vendor/libfoo
Видишь режим 160000? Это не обычный файл (100644) и не дерево (040000). Это специальный тип записи - gitlink. По сути в дереве суперпроекта лежит не содержимое библиотеки, а единственный SHA-1 (или SHA-256 в новых репах) коммита подмодуля. Весь "вес" подмодуля в индексе родителя - это одна 40-символьная строка.

Где же реальный .git подмодуля? В современном git он не внутри vendor/libfoo, а вынесен в служебную папку родителя:

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

ls super/.git/modules/vendor/libfoo
HEAD  config  objects  refs  index ...
А в самой рабочей директории vendor/libfoo лежит файл .git (не папка), и внутри одна строка:

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

gitdir: ../../.git/modules/vendor/libfoo
Запомни этот факт - именно из-за раздвоения "рабочая копия здесь, а .git вон там" растут самые мерзкие грабли с перемещением и удалением. Вернёмся к ним.

Клонирование: git подмодуль recursive и пустые папки

Классическая первая боль новичка. Коллега склонировал твой репозиторий обычным git clone, зашёл в vendor/libfoo - а там пусто. Сборка падает. "У тебя что-то не закоммичено!"

Нет, всё закоммичено. Просто обычный clone тянет суперпроект, видит gitlink, но НЕ лезет внутрь подмодулей. Папки создаются пустыми. Лечится одним из двух способов.

Сразу при клонировании:

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

git clone --recurse-submodules https://example.com/super.git
Или, если уже склонировал пустышку, инициализируем и подтягиваем содержимое:

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

git submodule update --init --recursive
Разберём по слоям, потому что эти ключи путают:
  • --init копирует настройки из .gitmodules в локальный .git/config. До этого подмодуль "не активирован" - git про него знает из карты, но не считает своим.
  • update клонирует/фетчит подмодуль и переводит его на тот самый зафиксированный коммит из gitlink.
  • --recursive делает то же самое для подмодулей внутри подмодулей. Да, они вкладываются.
Поэтому в README любого проекта с подмодулями строчка git submodule update --init --recursive - почти ритуал. Добавь её в инструкцию по сборке, сэкономишь людям час.

Detached HEAD внутри подмодуля - это не баг, это контракт

Заходишь в подмодуль после update, делаешь git status и видишь:

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

cd vendor/libfoo && git status
HEAD detached at a1b2c3d
nothing to commit, working tree clean
Паника новичка: "я что-то сломал, HEAD оторвался!". Спокойно. Это нормальное и единственно правильное состояние. Подумай: суперпроект фиксирует КОММИТ, а не ветку. Ветка - это движущийся ярлык, сегодня она на одном коммите, завтра на другом. Если бы подмодуль стоял на ветке, фиксация "версии" теряла бы смысл. Поэтому git ставит подмодуль ровно на нужный SHA в detached-состоянии - буквально "стой здесь, не на ветке, а на этом коммите".

Грабли отсюда: если ты внутри подмодуля что-то накоммитишь в detached HEAD, а потом сделаешь в нём checkout/switch на ветку - твои коммиты повиснут без ярлыка, и их легко потерять. Поэтому если реально правишь подмодуль, сначала встань на ветку (git switch main), а потом коммить.

Обновление подмодуля и фиксация нового указателя

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

Удобный способ - дать git'у самому сходить за свежими коммитами:

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

git submodule update --remote vendor/libfoo
Ключ --remote важен. Без него git submodule update возвращает подмодуль на коммит, ЗАПИСАННЫЙ в gitlink (то есть откатывает твои локальные движения к зафиксированной версии). С --remote он идёт в апстрим и берёт верхушку отслеживаемой ветки (по умолчанию ветка по умолчанию удалёнки, можно задать через branch в .gitmodules). Это противоположные операции, их регулярно путают.

После обновления смотрим в суперпроекте:

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

git status
	modified:   vendor/libfoo (new commits)

git diff
diff --git a/vendor/libfoo b/vendor/libfoo
index a1b2c3d..f6e5d4c 160000
--- a/vendor/libfoo
+++ b/vendor/libfoo
@@ -1 +1 @@
-Subproject commit a1b2c3d4e5f6...
+Subproject commit f6e5d4c3b2a1...
Видишь? Изменился ровно один указатель - старый SHA на новый. Чтобы зафиксировать новую версию для всей команды, коммитим это в РОДИТЕЛЕ:

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

git add vendor/libfoo
git commit -m "bump libfoo to f6e5d4c"
Теперь любой, кто сделает git submodule update, получит именно этот коммит библиотеки. Версия зафиксирована.

Главная боль: забытый push подмодуля и рассинхрон версий

Вот сценарий, который ломает CI у каждого второго. Ты правишь и сам подмодуль, и родителя:
  • в vendor/libfoo сделал коммит на ветке;
  • в super сделал git add vendor/libfoo, закоммитил новый указатель;
  • сделал git push в super.
И всё красиво у тебя локально. А у коллеги при git submodule update - ошибка "fatal: reference is not a tree". Почему? Ты запушил РОДИТЕЛЯ с указателем на коммит библиотеки, которого НЕТ на сервере библиотеки - ты забыл запушить сам подмодуль. Указатель есть, объекта нет.

Защита встроена в git. Перед пушем родителя проверяй подмодули:

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

git push --recurse-submodules=check
Этот режим откажется пушить, если хоть один подмодуль ссылается на ещё не отправленный коммит. А режим on-demand попытается сам запушить подмодули перед родителем:

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

git push --recurse-submodules=on-demand
Закрепи это в конфиге, чтобы не держать в голове:

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

git config push.recurseSubmodules check
Рассинхрон версий - вторая сторона той же медали. Один разработчик подвинул gitlink вперёд, другой - назад (например, при неаккуратном merge). В merge-конфликте gitlink выглядит странно, потому что конфликтуют два SHA. Разрешается просто: заходишь в подмодуль, ставишь его на ПРАВИЛЬНЫЙ коммит руками (git checkout нужный_sha), выходишь, git add path, коммитишь. Главное - осознанно выбрать, какая версия библиотеки верная, а не тыкать наугад.

submodule foreach и массовые операции

Когда подмодулей несколько, бегать по папкам руками невыносимо. Есть встроенный обход:

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

git submodule foreach 'git fetch'
git submodule foreach --recursive 'git switch main && git pull'
Внутри команды доступны переменные окружения: $name (имя из .gitmodules), $sm_path (путь), $sha1 (зафиксированный коммит), $displaypath. Например, быстрый аудит, на каком коммите каждый подмодуль:

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

git submodule foreach 'echo "$sm_path -> $(git rev-parse --short HEAD)"'
Entering 'vendor/libfoo'
vendor/libfoo -> f6e5d4c
Полезный обзорный взгляд без захода внутрь даёт git submodule status: префикс минус значит "не инициализирован", плюс - "рабочий коммит не совпадает с зафиксированным в родителе", пусто - "всё синхронно".

Перемещение и удаление: те самые грабли с .git/modules

Раньше удаление подмодуля было адом из шести ручных шагов. С современным git стало терпимо, но осадок и подводные камни остались.

Переместить подмодуль в другую папку:

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

git mv vendor/libfoo libs/foo
Git сам обновит path в .gitmodules и поправит служебный gitdir-указатель. На старых версиях это приходилось чинить руками - помни об этом, если работаешь в legacy-окружении.

Удаление - аккуратно, по шагам:

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

git submodule deinit -f vendor/libfoo
git rm -f vendor/libfoo
rm -rf .git/modules/vendor/libfoo
git commit -m "remove libfoo submodule"
Что тут происходит и почему именно так:
  • deinit деактивирует подмодуль - убирает его секцию из .git/config и чистит рабочую копию, но карту .gitmodules не трогает.
  • git rm удаляет gitlink из индекса и запись из .gitmodules. Начиная с git 2.12 это убирает большую часть мусора автоматически.
  • rm -rf .git/modules/... - вот это самое забываемое. Реальный .git подмодуля живёт в служебной папке родителя и git rm его НЕ удаляет.
Грабли во всей красе: ты удалил подмодуль, не почистив .git/modules. Через месяц добавляешь ДРУГОЙ подмодуль по тому же пути vendor/libfoo. Git находит старую служебную папку, решает, что подмодуль уже есть, и подцепляет мёртвые objects/refs от прошлой жизни. Результат - "вроде добавил, а внутри чужой код" или загадочные ошибки про несуществующие коммиты. Если что-то подобное видишь после пере-добавления - первым делом проверяй .git/modules/<path>.

Отдельно про absorbgitdirs. Если тебе достался старый репозиторий, где .git подмодуля лежит ВНУТРИ рабочей папки (а не в .git/modules родителя), команда git submodule absorbgitdirs перенесёт его в служебную папку и проставит правильный gitdir-указатель. Это лечит целый класс проблем с перемещением в наследованных проектах.

Когда подмодуль - плохая идея (мост к subtree)

Честный разговор. Подмодули хороши, когда:
  • зависимость - это реально отдельный репозиторий со своей жизнью и релизами;
  • нужна жёсткая фиксация конкретной версии-коммита для воспроизводимости;
  • ты лишь ПОТРЕБЛЯЕШЬ библиотеку и редко её правишь.
И превращаются в кару, когда:
  • команда вперемешку правит и родителя, и подмодуль каждый день - двойные пуши и забытые указатели изматывают;
  • много новичков - detached HEAD и пустые папки бьют по ним постоянно;
  • хочется атомарных коммитов "одна правка через несколько компонентов" - с подмодулями это всегда два коммита в двух репозиториях.
Если это твой случай, у git есть встроенная альтернатива - subtree. Он наоборот ВКЛАДЫВАЕТ содержимое чужого репозитория прямо в твою историю как обычные файлы: никаких gitlink, никаких пустых папок при клонировании, никаких отдельных пушей. Платишь за это раздутой историей и более громоздким обновлением апстрима. А на другом полюсе - монорепозиторий, где всё просто лежит вместе. Ровно про subtree и этот компромисс - следующий урок.

Мини-лаба: подмодуль руками за 5 минут

Повтори по шагам, локально, без удалёнок (используем file://):
  • 1. Сделай "удалённую" библиотеку: mkdir -p /tmp/libfoo && cd /tmp/libfoo && git init && echo "v1" > foo.txt && git add . && git commit -m "v1"
  • 2. Сделай суперпроект: mkdir /tmp/super && cd /tmp/super && git init && git commit --allow-empty -m init
  • 3. Подключи: git submodule add file:///tmp/libfoo vendor/libfoo
  • 4. Посмотри новые объекты: открой .gitmodules и выполни git ls-files --stage vendor/libfoo (найди режим 160000 - это gitlink).
  • 5. Закоммить: git add . && git commit -m "add libfoo"
  • 6. В библиотеке сделай новый коммит: cd /tmp/libfoo && echo "v2" > foo.txt && git commit -am "v2"
  • 7. Подтяни в суперпроект: cd /tmp/super && git submodule update --remote vendor/libfoo, затем git diff - убедись, что поменялся только Subproject commit.
  • 8. Зафиксируй указатель: git add vendor/libfoo && git commit -m "bump libfoo to v2"
  • 9. Сэмулируй коллегу: git clone --recurse-submodules /tmp/super /tmp/clone, проверь, что в /tmp/clone/vendor/libfoo лежит foo.txt с v2.
  • 10. Удали начисто: git submodule deinit -f vendor/libfoo && git rm -f vendor/libfoo && rm -rf .git/modules/vendor/libfoo && git commit -m "drop libfoo" - и убедись, что .git/modules/vendor пуст.
После такого прогона руками gitlink, .gitmodules и .git/modules перестанут быть магией.

Контрольные вопросы
  • 1. Чем физически отличается запись подмодуля в дереве суперпроекта от обычного файла и обычной папки? Какой у неё режим и что хранится внутри?
  • 2. В чём разница между git submodule update и git submodule update --remote? Какая из команд может незаметно откатить твои локальные правки в подмодуле?
  • 3. Почему после git submodule update подмодуль оказывается в detached HEAD, и чем это грозит, если коммитить прямо в этом состоянии?
  • 4. Какой шаг при удалении подмодуля чаще всего забывают и к какому конкретному багу это приводит при будущем добавлении подмодуля по тому же пути?
Итог

Подмодуль - это вшитый в твою историю указатель на конкретный коммит чужого репозитория: gitlink-запись (режим 160000) плюс карта .gitmodules. Всё остальное - производные от этой одной идеи. Detached HEAD - это контракт фиксации версии, а не поломка. Update без --remote возвращает к зафиксированному коммиту, с --remote - тянет свежак. Главные грабли - пустые папки без recursive, забытый push подмодуля и недочищенный .git/modules при удалении. Подмодули мощны для вендоринга независимых репозиториев с жёсткой фиксацией версий, но мучительны при ежедневной совместной правке - и там лучше смотреть в сторону subtree, чем мы и займёмся дальше.
👍5 ❤️3 🔥2 😄 🤔1
Аватара пользователя
ScalaPilot
Сообщения: 1
Зарегистрирован: 18 май 2026, 00:05

Re: Подмодули: внешние репозитории внутри проекта

Сообщение ScalaPilot »

Вот про .git/modules реально граблями ловил - удалил подмодуль, потом добавил по тому же пути и получил чужие коммиты. Теперь понятно почему, спасибо.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
redispilot
Сообщения: 1
Зарегистрирован: 17 май 2026, 12:03

Re: Подмодули: внешние репозитории внутри проекта

Сообщение redispilot »

А detached HEAD внутри меня всегда пугал, думал что-то сломал. Оказывается это by design под фиксацию версии. Хороший момент про git switch перед правкой.
👍 ❤️1 🔥 😄 🤔1
Ответить
← Предыдущая глава
Теги, релизы, archive и bundle
Следующая глава →
Subtree, монорепозитории и sparse-checkout

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

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

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

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

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