W3docs

Git Submodule

Краткое введение в Git submodule: как добавлять, клонировать, обновлять и удалять подмодули, а также типичные сценарии их использования.

Подмодуль позволяет встраивать один репозиторий Git в другой, сохраняя их истории полностью раздельными. На этой странице объясняется, что такое подмодуль, когда его стоит применять, и рассматривается полный жизненный цикл команд: добавление, клонирование, получение изменений, обновление, отправка и удаление подмодуля, а также типичные подводные камни, которые делают работу с подмодулями непростой.

Что такое подмодуль

Очень часто репозиторий с кодом зависит от внешнего кода из других репозиториев. Можно напрямую скопировать внешний код в основной репозиторий или воспользоваться системой управления пакетами языка. Однако оба способа имеют недостаток: они не отслеживают изменения во внешнем репозитории.

Git позволяет включать другие репозитории Git — называемые подмодулями — внутрь одного репозитория. Подмодуль располагается по определённому пути в рабочей директории родительского репозитория и сам по себе является полным клоном другого репозитория со своей собственной историей .git.

Ключевая идея состоит в том, что подмодуль зафиксирован на конкретном коммите, а не на ветке или теге. Родительский репозиторий хранит только путь, URL и SHA коммита, который ожидается в подмодуле. Именно это позволяет зависеть от внешнего кода в известной, воспроизводимой точке во времени.

Эти отношения отслеживаются двумя файлами:

  • .gitmodules — отслеживаемый файл в корне родительского репозитория. Он сопоставляет путь каждого подмодуля с его удалённым URL (и опционально с веткой). Этот файл фиксируется в коммите и доступен всем участникам.
  • .git/config и запись gitlink в дереве — локальная информация для каждого клона, фиксирующая фактически извлечённый коммит (gitlink отображается с режимом 160000).

Подмодули поддерживают добавление, синхронизацию, обновление и клонирование, но поскольку родительский репозиторий хранит только SHA коммита, обновление подмодуля всегда является намеренным, двухшаговым действием — никогда автоматическим.

Когда использовать подмодули

Работа с подмодулями непроста, поэтому ниже приведены наиболее подходящие сценарии их применения.

  • Если подпроект меняется слишком быстро или грядущие изменения нарушат API, зафиксируйте код на конкретном коммите ради безопасности.
  • Если компонент обновляется не очень часто и вы хотите отслеживать его как стороннюю зависимость.
  • Если вы предоставляете часть проекта третьей стороне и хотите интегрировать их работу в конкретный момент времени (подходит только при нечастых обновлениях).
  • Если технологический контекст допускает упаковку и формальное управление зависимостями, следует использовать менеджеры пакетов вместо подмодулей.
  • Если кодовая база огромна и вы не хотите скачивать её целиком каждый раз, используйте подмодули, чтобы участники загружали только нужные части.

Добавление подмодуля

Сначала создайте (или перейдите в) репозиторий, который будет содержать подмодуль:

mkdir git-submodule-demo
cd git-submodule-demo/
git init
Initialized empty Git repository in /Users/example/git-submodule-demo/.git/

Добавьте подмодуль командой git submodule add, передав URL репозитория, который хотите встроить:

git submodule add https://somehost/example/textexample
Cloning into '/Users/example/git-submodule-demo/textexample'...
remote: Counting objects: 8, done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 8 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (8/8), done.

Git немедленно клонирует репозиторий textexample в папку с таким же именем и создаёт файл .gitmodules. Чтобы разместить подмодуль по другому пути, добавьте его в качестве последнего аргумента, например git submodule add <url> vendor/textexample.

Теперь проверьте состояние репозитория с помощью git status:

git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   .gitmodules
	new file:   textexample

Обратите внимание, что textexample добавляется в индекс как единая запись, а не как отдельные файлы внутри него. Родительский репозиторий отслеживает только указатель коммита подмодуля. Зафиксируйте оба файла с помощью git add и git commit:

git add .gitmodules textexample
git commit -m "Add textexample submodule"
[master (root-commit) d5002d0] Add textexample submodule
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 textexample

Режим 160000 особенный: он помечает textexample как gitlink (указатель на коммит), а не как обычную директорию.

Проверка состояния подмодуля

Прежде чем вносить изменения, посмотрите, на каком коммите находится каждый подмодуль:

git submodule status
 a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 textexample (v1.2.0)

Первый символ имеет значение: пробел означает, что подмодуль находится на ожидаемом коммите; + означает, что он переключён на другой коммит, нежели записан в родительском репозитории; - означает, что подмодуль ещё не инициализирован.

Обновление подмодулей

Участники команды должны обновлять код подмодуля, когда он был изменён в другом месте. Нельзя полагаться только на git pull, потому что получение изменений родительского репозитория меняет лишь записанный коммит подмодуля — оно не затрагивает файлы, извлечённые внутри подмодуля. Чтобы переключиться на коммит, который ожидает родительский репозиторий, выполните:

git submodule update

Без флага --remote эта команда извлекает коммит, записанный в родительском репозитории, и не получает новые изменения из upstream.

Чтобы вместо этого получить последний коммит из отслеживаемой ветки подмодуля (main по умолчанию или ветку, заданную в .gitmodules), используйте --remote:

git submodule update --remote textexample

Это получает изменения из upstream подмодуля и продвигает его вперёд. Родительский репозиторий теперь видит новый указатель коммита, поэтому необходимо выполнить git add и зафиксировать изменения подмодуля, чтобы записать их.

Чтобы выполнить обновление для всех подмодулей, включая вложенные, добавьте --init --recursive:

git submodule update --init --recursive

Если файл .gitmodules изменился (например, URL подмодуля переехал), выполните git submodule sync, чтобы скопировать новые URL в локальный .git/config перед обновлением:

git submodule sync --recursive

Клонирование репозитория с подмодулями Git

Для клонирования проекта с подмодулями используйте команду git clone. По умолчанию она клонирует родительский репозиторий, оставляя директории подмодулей пустыми. После этого необходимо выполнить git submodule init и git submodule update. Первая команда обновляет локальный .git/config, добавляя сопоставления из .gitmodules, а вторая загружает данные подмодуля и извлекает зафиксированный коммит.

Если вы клонировали без --recurse-submodules, заполните подмодули вручную:

git clone /url/to/repo/with/submodules
git submodule init
git submodule update

Сокращение git submodule update --init объединяет эти две последние команды. Ещё лучше — клонировать всё за один шаг с помощью --recurse-submodules:

git clone --recurse-submodules /url/to/repo/with/submodules

Это клонирует родительский репозиторий и автоматически инициализирует и извлекает каждый подмодуль, так что рабочее дерево сразу оказывается полным.

Получение кода подмодуля

Когда вы получаете изменения родительского репозитория, в котором появились новые подмодули, эти подмодули оказываются неинициализированными. Сначала загрузите актуальное состояние родительского репозитория:

git pull

Если появились новые подмодули, инициализируйте и загрузите их за один шаг:

git submodule update --init --recursive

Команда git submodule init сама по себе только копирует сопоставления в локальный .git/config; она не скачивает никакой код. Именно шаг update фактически загружает подмодуль и извлекает зафиксированный коммит. Чтобы git pull делал это автоматически, задайте настройку:

git config submodule.recurse true

Отправка изменений в подмодуле

Подмодуль — это полноценный, независимый репозиторий Git, поэтому фиксацию и отправку изменений внутри его директории выполняют точно так же, как и в любом другом репозитории:

cd textexample
git checkout main
# ...edit files...
git commit -am "Fix typo in textexample"
git push
cd ..

После фиксации изменений внутри подмодуля родительский репозиторий по-прежнему указывает на старый коммит. Запуск git status в родительском репозитории теперь показывает:

	modified:   textexample (new commits)

Запишите новый указатель, добавив путь подмодуля в индекс и зафиксировав изменения в родительском репозитории, затем отправьте их:

git add textexample
git commit -m "Bump textexample to latest"
git push

Чтобы не отправлять родительский репозиторий до того, как коммиты подмодуля появятся на удалённом сервере (что оставило бы коллег с неработающим, недостижимым указателем), отправляйте всё вместе:

git push --recurse-submodules=on-demand

Это сначала отправляет все неотправленные коммиты подмодулей, а затем родительский репозиторий.

Удаление подмодуля

Удалить папку вручную недостаточно — Git хранит служебные данные. Чтобы удалить подмодуль корректно, выполните:

git submodule deinit -f textexample
git rm textexample
git commit -m "Remove textexample submodule"

deinit отменяет регистрацию подмодуля и удаляет его рабочее дерево, git rm удаляет gitlink и запись в .gitmodules, а коммит фиксирует удаление.

Итоги

Подмодули — хороший способ хранить проекты в отдельных репозиториях, при этом ссылаясь на них как на папки в рабочем дереве другого репозитория. Главная ментальная модель проста: родительский репозиторий хранит указатель коммита, каждое обновление является намеренным, а --recurse-submodules спасает от частично заполненных клонов. Для часто меняющихся зависимостей обычно лучше использовать настоящий менеджер пакетов. Чтобы глубже изучить связанные рабочие процессы, смотрите git clone, git pull и git status.

Практика

Практика
Каковы ключевые аспекты использования Git submodule?
Каковы ключевые аспекты использования Git submodule?
Was this page helpful?