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
- Во-первых, мы разрешили конфликт.
- Затем мы вернулись в каталог основного проекта.
- Мы снова проверили хеши SHA-1.
- Обновили указатель на подмодуль с учётом разрешенного конфликта.
- Зафиксировали наше слияние, создав новый коммит.
Это может немного запутать, но на самом деле здесь нет ничего сложного.
Интересно, что существует ещё один случай, с которым 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, удаление каталога подмодуля не приведёт к потере каких-либо коммитов или веток.
Все эти инструменты делают подмодули довольно простым и эффективным методом работы одновременно над несколькими тесно связанными, но разделёнными проектами.