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 initInitialized empty Git repository in /Users/example/git-submodule-demo/.git/Добавьте подмодуль командой git submodule add, передав URL репозитория, который хотите встроить:
git submodule add https://somehost/example/textexampleCloning 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 statusOn 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.