Git submodules как менеджер пакетов
- title
- Git submodules как менеджер пакетов
- type
- summary
- summary
- Несбитт видит в submodules менеджер пакетов - gitlink как lockfile, .gitmodules как манифест, - где резолв, хранение и обновления реализованы хуже
- tags
- git, package-management, dependencies, security
- created
- 2026-09-13
- updated
- 2026-09-13
- lang
- ru
- translation_of
- git-submodules-as-package-manager
- source_updated
- 2026-09-13
- translated
- 2026-09-14
- translator
- lllm/antigravity/gemini-3.7-flash-medium
Эндрю Несбитт пришёл к этому из-за мелкого неудобства. Он добавил worktree, выполнил в нём git submodule update --init, после чего git worktree remove завершился с отказом. В man-странице для submodules есть отдельный пункт рядом с незакоммиченными изменениями: удаление такого worktree требует --force, а git worktree move не перемещает его вовсе. Эта оговорка существовала с момента появления git worktree в git 2.5 в июле 2015 года, а в worktree add позже пришлось внести патч, игнорирующий submodule.recurse, поскольку его учёт приводил к тому, что внутренний reset --hard рекурсивно заходил в пустые пути submodule'ей.
Это натолкнуло его на мысль взглянуть на submodules как на менеджер пакетов. Большинство компонентов на месте. Gitlink - SHA коммита, записанный по пути с режимом 160000, - служит записью в lockfile. `.gitmodules`, сопоставляющий пути с URL, выступает манифестом. Команда git submodule update отвечает за этап установки. Фиксация версии столь же точна, как и в любом lockfile. Но почти во всём остальном это хуже настоящего менеджера пакетов.
Резолв
Gitlink указывает, какой коммит нужно извлечь, а URL в .gitmodules сообщает, откуда его взять. Других механизмов резолва нет. Если апстрим переименован, перенесён на другой хост или сделан приватным, любая ссылка на него ломается, хотя SHA остался прежним, а сами объекты есть в каждом клоне, где они уже были скачаны.
К тому же git submodule init копирует каждый URL в .git/config, и последующие команды читают его оттуда. Изменение .gitmodules в сторону зеркала ничего не даёт существующему клону без вызова git submodule sync. В CI привычным обходным решением остаётся глобальная перезапись через url.<base>.insteadOf - например, GitHub HTTPS на SSH, чтобы применился deploy-ключ.
Здесь полезен контраст с cursed-bundler-go-get-ruby-gems: модуль-прокси в Golang - это как раз тот поиск серверов по идентификатору содержимого, которого submodules лишены.
Установка
Обычный clone сохраняет gitlink и оставляет директорию пустой вплоть до запуска git submodule update --init или ключа --recurse-submodules. Настройка submodule.recurse, заставляющая checkout, fetch, pull и grep работать рекурсивно, по умолчанию выключена. update извлекает зафиксированный коммит в состоянии detached HEAD; флаг --remote вместо этого берёт верхушку отслеживаемой ветки, так что за одним именем команды скрываются и "установить зафиксированное", и "обновить до последней версии".
Переключение веток в суперпроекте обновляет gitlink, но оставляет рабочее дерево submodule'я нетронутым, из-за чего git status сразу показывает его как изменённый. В заметке проекта Rust за июнь 2026 года о переносе подпроектов компилятора с submodules перечислены вытекающие проблемы: пустые или ошибочные checkout'ы после клонирования, случайные обновления submodule'ей, просачивающиеся в pull request'ы после смены ветки, а также самодельная логика в bootstrap, выставляющая нужный коммит для каждого подмодуля перед сборкой.
Хранение
Git-директория submodule'я находится в $GIT_DIR/modules/<name>/ со своими refs, index, config, хуками и, по умолчанию, собственным хранилищем объектов; в рабочем дереве лежит файл .git, указывающий обратно. Удаление состоит из трёх шагов: git rm, git submodule deinit и описанного в документации ручного rm -rf оставшейся директории модуля.
Из-за такой структуры worktree и submodules конфликтуют между собой. Связанные worktree разделяют $GIT_DIR, но имеют собственные HEAD и index, поэтому два worktree на разных ветках ссылаются на один и тот же подмодуль на разных коммитах через хранилище, которое частично общее, а частично раздельное. Git запрашивает --force вместо того, чтобы выяснить, можно ли безопасно выбросить это состояние, а move выдаёт отказ, так как перезапись указателей попросту не реализована. После того как Ксавье Морель (Xavier Morel) спросил в рассылке git в марте 2026 года, может ли checkout подмодуля быть worktree общего клона, в апреле появились RFC и серия из трёх патчей с предложением git worktree add --recurse-submodules, где директории submodule'ей создаются под $GIT_COMMON_DIR/worktrees/<id>/modules/ для каждого worktree отдельно и делят объекты через хардлинки.
То же самое дублирование происходит и без worktree. Два submodule'я, зависящие от одного репозитория, получают две директории модулей, два хранилища объектов (если вручную не настроены alternates) и две независимые фиксации. Кэш реестра Cargo, контентно-адресуемое хранилище pnpm и кэш модулей Golang сохраняют байты ровно один раз.
Обновление
Сдвиг зафиксированной версии требует зайти в submodule, выполнить fetch, переключить коммит, выйти и сделать git add по нужному пути; update --remote сокращает шаги посередине. В манифесте можно указать имя ветки, но в нём нет диапазонов версий, шаблонов тегов или минимальных коммитов, поэтому ветка остаётся единственной плавающей ссылкой. Экосистема gitsubmodule в Dependabot и менеджер git-submodules в Renovate (отключённый по умолчанию) по этой причине просто отслеживают верхушки веток. Семантика диапазонов версий, о которой спорят авторы других пакетных менеджеров (описанная в default-version-bound-constraints), здесь отсутствует вовсе.
Безопасность
Файл .gitmodules контролируется апстримом и разбирается во время clone --recurse-submodules ещё до того, как пользователь успел что-либо посмотреть, что неоднократно приводило к удалённому выполнению кода. В CVE-2018-11235 использовался ../ в имени подмодуля для записи его git-директории вместе с хуками за пределы modules/. В CVE-2018-17456 использовался URL, начинающийся с -, который дочерний clone разбирал как параметр команды. В CVE-2022-39253 использовалась символическая ссылка на директорию объектов для копирования локальных файлов при клонировании через локальный транспорт, и в исправлении значение protocol.file.allow по умолчанию изменили на user. В CVE-2024-32002 симлинк в сочетании с нечувствительной к регистру файловой системой позволял записать хук в .git/. Общая проблема путей checkout как поверхности атаки рассматривается в supply-chain-security.
Абстракция
Submodules выставляют внутренности git напрямую: ID объектов вместо пинов, detached HEAD, структуру директорий modules, транспортные URL в манифесте. Пакетный менеджер закрывает аналогичные части форматом манифеста, резолвером и кэшем. Список того, чего не хватает по мнению Несбитта, в других местах уже в основном решён: общий кэш объектов, рекурсия по умолчанию, единый жизненный цикл добавления и удаления, а также ограничения диапазонов версий. Апрельская серия патчей решает один частный случай с хранением. Сложнейшей проблемой он считает резолв: SHA уже служит независимым от хоста идентификатором контента, но URL в .gitmodules остаётся единственным доступным для git способом найти сервер, на котором этот контент лежит. dependency-vendoring предлагает альтернативный выход, при котором байты зависимостей хранятся прямо в суперпроекте, и этот вопрос вообще не возникает.