Git 🌙
Chapters ▾ 2nd Edition

7.11 Инструменты Git - Подмодули

Подмодули

Часто при работе над одним проектом, возникает необходимость использовать в нём другой проект. Возможно, это библиотека, разрабатываемая сторонними разработчиками или вами, но в рамках отдельного проекта, и используемая в нескольких других проектах. Типичная проблема, возникающая при этом — вы хотите продолжать работать с двумя проектами по отдельности, но при этом использовать один из них в другом.

Приведём пример. Предположим, вы разрабатываете веб-сайт и создаёте ленту в формате Atom. Вместо написания собственного генератора Atom, вы решили использовать библиотеку. Скорее всего, вы либо подключите библиотеку как сторонний модуль, такой как модуль CPAN или Ruby Gem, либо скопируете исходный код библиотеки в свой проект. Проблема использования библиотеки состоит в трудности её адаптации под собственные нужды и часто более сложным является её распространение, так как нужно быть уверенным, что каждому клиенту доступна изменённая версия. При включении кода библиотеки в свой проект проблемой будет объединение ваших изменений в ней с новыми изменениями в оригинальном репозитории.

Git решает эту проблему с помощью подмодулей. Подмодули позволяют вам сохранить один Git-репозиторий, как подкаталог другого Git-репозитория. Это даёт вам возможность клонировать в ваш проект другой репозиторий, но коммиты при этом хранить отдельно.

Начало работы с подмодулями

Далее мы рассмотрим процесс разработки простого проекта, разбитого на один главный проект и несколько подпроектов.

Давайте начнём с добавления существующего Git-репозитория как подмодуля репозитория, в котором мы работаем. Для добавления нового подмодуля используйте команду git submodule add, указав относительный или абсолютный URL проекта, который вы хотите начать отслеживать. В данном примере мы добавим библиотеку «DbConnector».

$ git submodule add https://github.com/chaconinc/DbConnector
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

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

Если в данный момент выполнить git status, то можно заметить следующее.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   .gitmodules
	new file:   DbConnector

Во-первых, вы увидите новый файл .gitmodules. Это конфигурационный файл, в котором хранится соответствие между URL проекта и локальным подкаталогом, в который вы его выкачали:

[submodule "DbConnector"]
	path = DbConnector
	url = https://github.com/chaconinc/DbConnector

Если у вас несколько подмодулей, то в этом файле у вас будет несколько записей. Важно заметить, что этот файл добавлен под версионный контроль Git так же, как и другие ваши файлы, например, ваш файл .gitignore. Этот файл можно получить или отправить на сервер вместе с остальными файлами проекта. Благодаря этому другие люди, которые клонируют ваш проект, узнают откуда взять подмодули проекта.

Примечание

Поскольку другие люди первым делом будут пытаться выполнить команды clone/fetch по URL, указанным в файле .gitmodules, старайтесь проверять, что URL будут им доступны. Например, если вы выполняете отправку по URL, отличному от того, по которому другие люди получают данные, то используйте тот URL, к которому у других участников будет доступ. В таком случае для себя вы можете задать локальное значение URL с помощью команды git config submodule.DbConnector.url PRIVATE_URL. Если применимо, относительные URL могут помочь.

Следующим пунктом в списке вывода команды git status будет запись о каталоге проекта. Команда git diff покажет для него кое-что интересное:

$ git diff --cached DbConnector
diff --git a/DbConnector b/DbConnector
new file mode 160000
index 0000000..c3f01dc
--- /dev/null
+++ b/DbConnector
@@ -0,0 +1 @@
+Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

Хоть DbConnector и является подкаталогом вашего рабочего каталога, Git распознает его как подмодуль и не отслеживает его содержимое пока вы не перейдёте в него. Вместо этого, Git видит его как конкретный коммит этого репозитория.

Если вам нужен немного более понятный вывод, передайте команде git diff параметр --submodule.

$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..71fc376
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "DbConnector"]
+       path = DbConnector
+       url = https://github.com/chaconinc/DbConnector
Submodule DbConnector 0000000...c3f01dc (new submodule)

При создании коммита вы увидите следующее:

$ git commit -am 'Add DbConnector module'
[master fb9093c] Add DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

Обратите внимание на права доступа 160000 у DbConnector. Это специальные права доступа в Git, которые означают, что вы сохраняете коммит как элемент каталога, а не как подкаталог или файл.

Наконец, отправьте эти изменения:

$ git push origin master

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

Далее мы рассмотрим клонирование проекта, содержащего подмодули. Когда вы клонируете такой проект, по умолчанию вы получите каталоги, содержащие подмодули, но ни одного файла в них не будет:

$ git clone https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
$ cd MainProject
$ ls -la
total 16
drwxr-xr-x   9 schacon  staff  306 Sep 17 15:21 .
drwxr-xr-x   7 schacon  staff  238 Sep 17 15:21 ..
drwxr-xr-x  13 schacon  staff  442 Sep 17 15:21 .git
-rw-r--r--   1 schacon  staff   92 Sep 17 15:21 .gitmodules
drwxr-xr-x   2 schacon  staff   68 Sep 17 15:21 DbConnector
-rw-r--r--   1 schacon  staff  756 Sep 17 15:21 Makefile
drwxr-xr-x   3 schacon  staff  102 Sep 17 15:21 includes
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 scripts
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 src
$ cd DbConnector/
$ ls
$

Каталог DbConnector присутствует, но он пустой. Вы должны выполнить две команды: git submodule init — для инициализации локального конфигурационного файла, и git submodule update — для получения всех данных этого проекта и извлечения соответствующего коммита, указанного в основном проекте.

$ git submodule init
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
$ git submodule update
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

Теперь ваш каталог DbConnector находится в точно таком же состоянии, как и ранее при выполнении коммита.

Однако, существует немного более простой вариант сделать то же самое. Если вы передадите опцию --recurse-submodules команде git clone, то она автоматически инициализирует и обновит каждый подмодуль в этом репозитории, включая вложенные подмодули, при условии, что подмодули репозитория имеют свои подмодули.

$ git clone --recurse-submodules https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

Если вы уже клонировали проект, но забыли указать --recurse-submodules, вы можете объединить шаги git submodule init и git submodule update, запустив git submodule update --init. Также для инициализации, получения и извлечения всех существующих вложенных подмодулей можно использовать безопасную команду git submodule update --init --recursive.

Работа над проектом с подмодулями

Теперь, когда у нас есть копия проекта с подмодулями, мы будем работать совместно с нашими коллегами над обоими проектами: основным проектом и проектом подмодуля.

Получение изменений подмодуля из удалённого репозитория

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

Если вы хотите проверить наличие изменений в подмодуле, перейдите в его каталог и выполните git fetch, а затем git merge для обновления локальной версии отслеживаемой ветки.

$ git fetch
From https://github.com/chaconinc/DbConnector
   c3f01dc..d0354fc  master     -> origin/master
$ git merge origin/master
Updating c3f01dc..d0354fc
Fast-forward
 scripts/connect.sh | 1 +
 src/db.c           | 1 +
 2 files changed, 2 insertions(+)

Теперь если вы вернётесь в основной проект и выполните git diff --submodule, то увидите, что подмодуль обновился, а так же список новых коммитов. Если вы не хотите каждый раз при вызове команды git diff указывать опцию --submodule, то задайте конфигурационному параметру diff.submodule значение «log» для использования такого формата вывода по умолчанию.

$ git config --global diff.submodule log
$ git diff
Submodule DbConnector c3f01dc..d0354fc:
  > more efficient db routine
  > better connection routine

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

Если вы не хотите вручную извлекать и сливать изменения в подкаталог, то для вас существует более простой способ сделать то же самое. Если вы выполните git submodule update --remote, то Git сам перейдёт в ваши подмодули, получит изменения и обновит их для вас.

$ git submodule update --remote DbConnector
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   3f19983..d0354fc  master     -> origin/master
Submodule path 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644'

Эта команда по умолчанию предполагает, что вы хотите обновить локальную копию до состояния ветки master из репозитория подмодуля. Однако, при желании вы можете задать другую ветку. Например, если вы хотите, чтобы подмодуль DbConnector отслеживал ветку stable, то можете указать это либо в файле .gitmodules (тогда и другие люди также будут отслеживать эту ветку), либо в вашем локальном файле .git/config. Давайте настроим это в файле .gitmodules:

$ git config -f .gitmodules submodule.DbConnector.branch stable

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   27cf5d3..c87d55d  stable -> origin/stable
Submodule path 'DbConnector': checked out 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687'

Если опустить -f .gitmodules, то команда сделает изменения в локальном файле .git/config, но, вероятно, имеет смысл всё же отслеживать эту информацию в репозитории, так чтобы и все остальные участники имели к ней доступ.

Если сейчас выполнить команду git status, то Git покажет нам наличие «новых коммитов» в подмодуле.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

  modified:   .gitmodules
  modified:   DbConnector (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

Если установить в настройках параметр status.submodulesummary, то Git будет также отображать краткое резюме об изменениях в ваших подмодулях:

$ git config status.submodulesummary 1

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   .gitmodules
	modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c3f01dc...c87d55d (4):
  > catch non-null terminated lines

Если сейчас выполнить git diff, то увидите изменённый файл .gitmodules, а также наличие нескольких недавно полученных коммитов, которые готовы к включению в проект нашего подмодуля.

$ git diff
diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
 Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

Здорово, что мы можем увидеть список подготовленных коммитов в нашем подмодуле. После создания коммита вы тоже можете получить эту информацию, выполнив команду git log -p.

$ git log -p --submodule
commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Sep 17 16:37:02 2014 +0200

    updating DbConnector for bug fixes

diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

При выполнении команды git submodule update --remote Git по умолчанию будет пытаться обновить все подмодули. Поэтому, при большом количестве подмодулей вы возможно захотите указать название только того подмодуля, который необходимо обновить.

Получение изменений основного проекта из удалённого репозитория

Теперь давайте поставим себя на место нашего коллеги, у которого есть собственная копия репозитория MainProject. Простого запуска команды git pull для получения последних изменений уже недостаточно:

$ git pull
From https://github.com/chaconinc/MainProject
   fb9093c..0a24cfc  master     -> origin/master
Fetching submodule DbConnector
From https://github.com/chaconinc/DbConnector
   c3f01dc..c87d55d  stable     -> origin/stable
Updating fb9093c..0a24cfc
Fast-forward
 .gitmodules         | 2 +-
 DbConnector         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c87d55d...c3f01dc (4):
  < catch non-null terminated lines
  < more robust error handling
  < more efficient db routine
  < better connection routine

no changes added to commit (use "git add" and/or "git commit -a")

По умолчанию, команда git pull рекурсивно получает изменения для подмодулей, что можно увидеть выше в выводе первой команды. При этом, она не обновляет подмодули. Это отображается в выводе команды git status, которая показывает, что подмодуль изменён (modified) и содержит новые коммиты (new commits). Более того, новые коммиты обозначены символом «<», означающим, что эти коммиты добавлены в проект MainProject, но не извлечены в текущем состоянии подмодуля DbConnector. Для завершения обновления следует выполнить команду git submodule update:

$ git submodule update --init --recursive
Submodule path 'vendor/plugins/demo': checked out '48679c6302815f6c76f1fe30625d795d9e55fc56'

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

На всякий случай, команду git submodule update лучше запускать с параметром --init, чтобы проинициализировать новые подмодули, которые, возможно, были добавлены в новых коммитах MainProject репозитория, а так же с параметром --recursive, чтобы обновить вложенные подмодули при их наличии.

Если вы хотите автоматизировать этот процесс, то добавьте параметр --recurse-submodules при запуске команды git pull (начиная с версии 2.14). Это приведёт к выполнению команды git submodule update сразу после получения и объединения изменений, приводя подмодули в корректное состояние. Более того, если вы хотите, чтобы Git всегда добавлял параметр --recurse-submodules к команде git pull, можно установить конфигурационный параметр submodule.recurse в значение true (работает для команды git pull начиная с версии 2.15). Если этот параметр конфигурации установлен, Git будет всегда добавлять --recurse-submodules ко всем командам, которые его поддерживают (кроме clone).

Существует специфическая ситуация, которая может возникнуть при получении изменений основного проекта: в одном из коммитов изменён URL подмодуля в файле .gitmodules. Такое может произойти, например, когда проект подмодуля переехал на другой хостинг. В таком случае команды git pull --recurse-submodules или git submodule update могут завершиться с ошибкой, потому как основной проект ссылается на коммит подмодуля, который не найден в удалённом репозитории этого подмодуля, на который указывает локальная конфигурация в основном репозитории. Чтобы исправить эту ситуацию требуется запуск команды git submodule sync:

# copy the new URL to your local config
$ git submodule sync --recursive
# update the submodule from the new URL
$ git submodule update --init --recursive

Работа с подмодулем

Весьма вероятно, что вы используете подмодули, потому что хотите работать над кодом подмодуля (или нескольких подмодулей) во время работы над кодом основного проекта. Иначе бы вы, скорее всего, предпочли использовать более простую систему управления зависимостями (такую как Maven или Rubygems).

Давайте теперь рассмотрим пример, в котором мы одновременно с изменениями в основном проекте внесём изменения в подмодуль, зафиксировав и опубликовав все эти изменения в одно и то же время.

Ранее, когда мы выполняли команду git submodule update для извлечения изменений из репозитория подмодуля, Git получал изменения и обновлял файлы в поддиректории, но оставлял вложенный репозиторий в состоянии, называемом «отсоединённый HEAD» («detached HEAD»). Это значит, что локальная рабочая ветка (такая, например, как master), отслеживающая изменения, отсутствует. Отсутствие отслеживающей ветки означает, что даже если вы зафиксируете изменения в подмодуле, эти изменения, скорее всего, будут потеряны при следующем запуске git submodule update. Если хотите, чтобы изменения в подмодуле отслеживались, потребуется выполнить несколько дополнительных шагов.

Для подготовки репозитория подмодуля к такой работе с ним, нужно сделать две вещи. Нужно перейти в каждый подмодуль и извлечь ветку, в которой будете далее работать. Затем необходимо сообщить Git что ему делать, если вы внесли свои изменения, а затем получаете новые изменения из удалённого репозитория командой git submodule update --remote. Возможны два варианта — вы можете слить их в вашу локальную версию или попробовать перебазировать ваши локальные наработки поверх новых изменений.

Первым делом, давайте перейдём в каталог нашего подмодуля и переключимся на нужную ветку.

$ cd DbConnector/
$ git checkout stable
Switched to branch 'stable'

Давайте попробуем обновить подмодуль путём слияния изменений. Для этого просто добавьте опцию --merge при вызове команды update. Ниже мы видим, что с удалённого сервера получен коммит для подмодуля и слит с текущим состоянием:

$ cd ..
$ git submodule update --remote --merge
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   c87d55d..92c7337  stable     -> origin/stable
Updating c87d55d..92c7337
Fast-forward
 src/main.c | 1 +
 1 file changed, 1 insertion(+)
Submodule path 'DbConnector': merged in '92c7337b30ef9e0893e758dac2459d07362ab5ea'

Перейдя в каталог DbConnector, можно увидеть, что новые изменения уже слиты в нашу локальную ветку stable. Теперь давайте посмотрим что происходит, когда мы вносим локальные изменения в библиотеку, а кто-то другой в то же время отправляет другие изменения в удалённый репозиторий.

$ cd DbConnector/
$ vim src/db.c
$ git commit -am 'Unicode support'
[stable f906e16] Unicode support
 1 file changed, 1 insertion(+)

Теперь мы можем увидеть что происходит при обновлении подмодуля, когда локальные изменения должны включать новые изменения, полученные с удалённого сервера.

$ cd ..
$ git submodule update --remote --rebase
First, rewinding head to replay your work on top of it...
Applying: Unicode support
Submodule path 'DbConnector': rebased into '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

Если вы забудете указать параметры --rebase или --merge, то Git просто обновит подмодуль до состояния репозитория на сервере и установит его в состояние отсоединённого HEAD.

$ git submodule update --remote
Submodule path 'DbConnector': checked out '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

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

Если вы не зафиксировали ваши изменения в подмодуле и выполнили его обновление, то это приведёт к проблемам — Git получит изменения из удалённого репозитория, но не перезапишет несохранённые изменения в каталоге вашего подмодуля.

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   5d60ef9..c75e92a  stable     -> origin/stable
error: Your local changes to the following files would be overwritten by checkout:
	scripts/setup.sh
Please, commit your changes or stash them before you can switch branches.
Aborting
Unable to checkout 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

Если ваши локальные изменения в подмодуле конфликтуют с какими-либо изменениями в удалённом репозитории, то Git вам сообщит об этом при обновлении подмодуля.

$ git submodule update --remote --merge
Auto-merging scripts/setup.sh
CONFLICT (content): Merge conflict in scripts/setup.sh
Recorded preimage for 'scripts/setup.sh'
Automatic merge failed; fix conflicts and then commit the result.
Unable to merge 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

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

Публикация изменений подмодуля

Теперь у нас есть некоторые изменения в каталоге нашего подмодуля. Какие-то из них мы получили при обновлении из удалённого репозитория, а другие были сделаны локально и пока никому не доступны, так как мы их ещё никуда не отправили.

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > Update setup script
  > Unicode support
  > Remove unnecessary method
  > Add new option for conn pooling

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

Чтобы гарантированно избежать такой проблемы, вы можете попросить Git проверять отправлены ли изменения всех подмодулей до отправки изменений основного репозитория на сервер. Команда git push принимает аргумент --recurse-submodules, который может принимать значения либо «check», либо «on-demand». Использование значения «check» приведёт к тому, что push просто завершится ошибкой, если какой-то из подмодулей не был отправлен на сервер.

$ git push --recurse-submodules=check
The following submodule paths contain changes that can
not be found on any remote:
  DbConnector

Please try

	git push --recurse-submodules=on-demand

or cd to the path and use

	git push

to push them to a remote.

Как видите, эта команда также даёт нам некоторые полезные советы о том, что мы могли бы делать дальше. Самый простой вариант — это пройти по всем подмодулям и вручную отправить изменения на удалённые серверы, чтобы гарантировать доступность изменений другим людям, а затем попытать отправить изменения снова. Если вы хотите использовать поведение “check” при каждом выполнении git push, то его можно установить поведением по умолчанию, выполнив команду git config push.recurseSubmodules check.

Другой вариант — это использовать значение «on-demand», тогда Git попытается сделать всё вышеописанное за вас.

$ git push --recurse-submodules=on-demand
Pushing submodule 'DbConnector'
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 3), reused 0 (delta 0)
To https://github.com/chaconinc/DbConnector
   c75e92a..82d2ad3  stable -> stable
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
To https://github.com/chaconinc/MainProject
   3d6d338..9a377d1  master -> master

Как видите, перед отправкой на сервер основного проекта Git перешёл в каталог модуля DbConnector и отправил его изменения на сервер. Отправка изменений основного репозитория также завершится неудачей если во время отправки изменений подмодуля возникла ошибка. Установить такое поведение по умолчанию можно с помощью команды git config push.recurseSubmodules on-demand.

Слияние изменений подмодуля

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

Если один из коммитов является прямым предком другого (слияние может быть выполнено перемоткой вперёд), то Git просто выберет последний для выполнения слияния, то есть всё отработает хорошо.

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

$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Unpacking objects: 100% (2/2), done.
From https://github.com/chaconinc/MainProject
   9a377d1..eb974f8  master     -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

Здесь говорится о том, что Git понял, что в этих двух ветках содержатся указатели на разошедшиеся записи в истории подмодуля и их необходимо слить. Git поясняет это как «слияние последующих коммитов не найдено» («merge following commits not found»), что несколько обескураживает, но вскоре мы объясним почему так происходит.

Для решения этой проблемы, мы должны разобраться в каком состоянии должен находиться подмодуль. Странно, но Git не предоставляет вам для этого никакой вспомогательной информации, даже SHA-1 хешей коммитов разошедшихся веток истории. К счастью, получить эту информации довольно просто. Если выполнить команду git diff, то вы получите SHA-1 хеши коммитов обоих веток, которые вы пытаетесь слить.

$ git diff
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector

В данном примере eb41d76 является нашим коммитом в подмодуле, а c771610 — коммитом из вышестоящего репозитория. Если мы перейдём в каталог нашего подмодуля, то он должен быть на коммите eb41d76, так как операция слияния его не изменяла. Если по каким-то причинам это не так, то вы можете просто создать новую ветку из этого коммита и извлечь её.

Куда более важным является SHA-1 хеш коммита второй ветки истории. Именно его мы должны будем слить и разрешить конфликты. Вы можете попытаться либо просто выполнить слияние, указав непосредственно этот SHA-1 хеш, либо создать из него отдельную ветку и слить её. Мы предлагаем использовать последний вариант, хотя бы только потому, что сообщение коммита слияния получается более читаемым.

Итак, перейдите в каталог нашего подмодуля, создайте ветку «try-merge» на основе второго SHA-1 хеша из git diff и выполните слияние вручную.

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610

$ git merge try-merge
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

Мы получили конфликт слияния, поэтому если мы разрешим его и создадим коммит, то можно просто включить результирующий коммит в основной проект.

$ vim src/main.c (1)
$ git add src/main.c
$ git commit -am 'merged our changes'
Recorded resolution for 'src/main.c'.
[master 9fd905e] merged our changes

$ cd .. (2)
$ git diff (3)
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit eb41d764bccf88be77aced643c13a7fa86714135
 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d
++Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a
$ git add DbConnector (4)

$ git commit -m "Merge Tom's Changes" (5)
[master 10d2c60] Merge Tom's Changes
  1. Во-первых, мы разрешили конфликт.

  2. Затем мы вернулись в каталог основного проекта.

  3. Мы снова проверили SHA-1 хеши.

  4. Обновили указатель на подмодуль с учётом разрешённого конфликта.

  5. Зафиксировали наше слияние, создав новый коммит.

Это может немного запутать, но на самом деле здесь нет ничего сложного.

Интересно, что существует ещё один случай, с которым Git умеет работать. Если подмодуль содержит некий коммит слияния, который в своей истории содержит оба первоначальных коммита, то Git предложит его как возможное решение. Git видит, что в проекте подмодуля ветки, содержащие эти два коммита, уже когда-то кем-то сливались и этот коммит слияния, вероятно, именно то, что вам нужно.

Именно поэтому сообщение об ошибке выше было «merge following commits not found» — Git не мог найти такой коммит. Кто бы мог подумать, что Git пытается такое сделать?

Если Git нашёл только один приемлемый коммит слияния, то вы увидите нечто подобное:

$ git merge origin/master
warning: Failed to merge submodule DbConnector (not fast-forward)
Found a possible merge resolution for the submodule:
 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes
If this is correct simply add it to the index for example
by using:

  git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"

which will accept this suggestion.
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

Предлагаемая команда обновляет индекс таким же образом, как если бы вы выполнили git add (что очищает список конфликтов), а затем создали коммит. Хотя, наверное, вам не стоит так делать. Будет лучше просто перейти в каталог подмодуля, просмотреть изменения, сместить вашу ветку на этот коммит, должным образом всё протестировать и только потом создать коммит в основном репозитории.

$ cd DbConnector/
$ git merge 9fd905e
Updating eb41d76..9fd905e
Fast-forward

$ cd ..
$ git add DbConnector
$ git commit -am 'Fast forward to a common submodule child'

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

Полезные советы

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

Команда Foreach

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

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

$ git submodule foreach 'git stash'
Entering 'CryptoLibrary'
No local changes to save
Entering 'DbConnector'
Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable
HEAD is now at 82d2ad3 Merge from origin/stable

Затем мы можем создать новую ветку и переключиться на неё во всех наших подмодулях.

$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Switched to a new branch 'featureA'
Entering 'DbConnector'
Switched to a new branch 'featureA'

Вы уловили идею. Ещё одна полезная вещь, которую можно сделать с помощью этой команды — создать комплексную дельту изменений основного проекта и всех подмодулей.

$ git diff; git submodule foreach 'git diff'
Submodule DbConnector contains modified content
diff --git a/src/main.c b/src/main.c
index 210f1ae..1f0acdc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -245,6 +245,8 @@ static int handle_alias(int *argcp, const char ***argv)

      commit_pager_choice();

+     url = url_decode(url_orig);
+
      /* build alias_argv */
      alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1));
      alias_argv[0] = alias_string + 1;
Entering 'DbConnector'
diff --git a/src/db.c b/src/db.c
index 1aaefb6..5297645 100644
--- a/src/db.c
+++ b/src/db.c
@@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len)
        return url_decode_internal(&url, len, NULL, &out, 0);
 }

+char *url_decode(const char *url)
+{
+       return url_decode_mem(url, strlen(url));
+}
+
 char *url_decode_parameter_name(const char **query)
 {
        struct strbuf out = STRBUF_INIT;

Здесь видно, что мы определили в подмодуле функцию и вызываем её в основном проекте. Это, конечно, упрощённый пример, но надеемся, что мы смогли донести до вас всю полезность этой функции.

Полезные псевдонимы

Возможно, вы захотите настроить псевдонимы для некоторых из этих команд, так как они могут быть довольно длинными и для большинства из них вы не можете задать значения по умолчанию. Мы рассмотрели настройку псевдонимов Git в разделе Псевдонимы в Git главы 2, однако ниже приведён пример, который вы наверняка захотите использовать, если планируете часто работать с подмодулями Git.

$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'"
$ git config alias.spush 'push --recurse-submodules=on-demand'
$ git config alias.supdate 'submodule update --remote --merge'

Таким образом, для обновления подмодулей достаточно выполнить команду git supdate, а для отправки всех изменений с учётом подмодулей — команду git spush.

Проблемы с подмодулями

Однако, использование подмодулей не обходится без проблем.

Переключение веток

Например, переключение веток с подмодулями в них может оказаться довольно запутанным, особенно для версий Git старше 2.13. Если вы создадите новую ветку и добавите в ней подмодуль, а затем переключитесь обратно на ветку без подмодуля, то каталог подмодуля всё равно останется и будет неотслеживаемым:

$ git --version
git version 2.12.2

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

Удалить каталог не сложно, но может показаться странным, что он вообще присутствует. Если удалить каталог и снова переключиться на ветку с подмодулем, то потребуется выполнить submodule update --init, чтобы в нём появились файлы.

$ git clean -ffdx
Removing CryptoLibrary/

$ git checkout add-crypto
Switched to branch 'add-crypto'

$ ls CryptoLibrary/

$ git submodule update --init
Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'

$ ls CryptoLibrary/
Makefile	includes	scripts		src

Опять же, это не сильно сложно, но может немного сбивать с толку.

В более новых версиях (Git >= 2.13) всё несколько упрощено за счёт поддержки командой git checkout параметра --recurse-submodules, который отвечает за приведение подмодулей в состояние, соответствующее извлекаемой ветке.

$ git --version
git version 2.13.3

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout --recurse-submodules master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

nothing to commit, working tree clean

Использование параметра --recurse-submodules команды git checkout помогает при работе с несколькими ветками основного проекта, когда в каждой из них используется разное состояние подмодуля. Действительно, если вы переключитесь между ветками, в которых зафиксированы разные состояния подмодуля, то команда git status отметит подмодуль как изменённый («modified») и покажет наличие новых коммитов в нём. Это происходит из-за того, что по умолчанию состояние подмодуля не изменяется при переключении веток.

Такое поведение может сильно сбивать с толку, поэтому рекомендуется всегда использовать git checkout --recurse-submodules если в вашем проекте есть подмодули. В более старых версиях Git, в которых команда git checkout не поддерживает параметр --recurse-submodules, для приведения подмодулей в ожидаемое состояние используйте команду git submodule update --init --recursive сразу после извлечения ветки.

К счастью, вы можете указать Git (начиная с версии 2.14) всегда использовать параметр --recurse-submodules с помощью изменения конфигурации: git config submodule.recurse true. Как упоминалось выше, это заставит Git автоматически добавлять параметр --recurse-submodules ко всем командам (кроме git clone), которые его поддерживают, и рекурсивно обрабатывать подмодули.

Переход от подкаталогов к подмодулям

Другая большая проблема возникает при переходе от использования подкаталогов к использованию подмодулей. Если у вас были отслеживаемые файлы в вашем проекте и вы хотите переместить их в подмодуль, то вы должны быть осторожны, иначе Git будет ругаться на вас. Предположим, у вас есть файлы в каком-то каталоге вашего проекта и вы хотите переместить их в подмодуль. Если вы удалите подкаталог, а затем выполните submodule add, то Git ругнётся на вас:

$ rm -Rf CryptoLibrary/
$ git submodule add https://github.com/chaconinc/CryptoLibrary
'CryptoLibrary' already exists in the index

Первым делом вы должны удалить каталог CryptoLibrary из индекса. Затем можно добавить подмодуль:

$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

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

$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
  CryptoLibrary/Makefile
  CryptoLibrary/includes/crypto.h
  ...
Please move or remove them before you can switch branches.
Aborting

Вы всё же можете извлечь ветку принудительно, используя команду checkout -f, только убедитесь перед этим в отсутствии несохранённых изменений, так как они могут быть перезаписаны.

$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'

Если вернуться обратно на предыдущую ветку, то по какой-то причине каталог CryptoLibrary будет пустым, а команда git submodule update не сможет это исправить. Чтобы вернуть свои файлы, вам понадобится перейти в каталог подмодуля и выполнить команду git checkout .. Можно использовать эту команду вместе с submodule foreach для извлечения сразу всех подмодулей.

Важно отметить, что в современных версиях Git подмодули сохраняют все служебные данные в каталоге .git основного проекта, поэтому, в отличие от более старых версий Git, удаление каталога подмодуля не приведёт к потере каких-либо коммитов или веток.

Все эти инструменты делают подмодули довольно простым и эффективным методом работы одновременно над несколькими тесно связанными, но разделёнными проектами.

scroll-to-top