Git
Chapters ▾ 2nd Edition

7.11 Інструменти Git - Підмодулі

Підмодулі

Часто, під час роботи з проектом трапляється, що в ньому потрібно використати інший проект. Можливо, це бібліотека, яку хтось інший або ви розробляєте окремо та використовуєте в декількох проектах. У цих ситуаціях виникає поширена проблема: ви бажаєте мати можливість розглядати два проекти як окремі, проте все одно мати можливість використовувати один з іншого.

Ось приклад. Уявіть, що ви розробляєте веб-сайт та створюєте Atom feeds. Замість того, щоб писати власний код для генерації 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

Якщо у вас декілька підмодулів, то у файлі буде декілька записів. Важливо зауважити, що цей файл знаходиться під версійним контролем, як й інші файли, наприклад файл .gitignore. Він надсилається та отримується з рештою проекту. Таким чином інші люди, які клонують цей проект, знають звідки отримати проекти підмодулі.

Note

Оскільки інші люди спершу будуть клонувати та отримувати зміни з URL у файлі .gitmodules, переконайтеся, що вони мають доступ до цього 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 розглядає її як окремий коміт з того репозиторія.

Якщо ви бажаєте трохи гарнішого вигляду різниці, то передайте опцію --submodule до git diff.

$ 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 'added DbConnector module'
[master fb9093c] added DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

Зверніть увагу на права доступу 160000 для елементу DbConnector. Це спеціальні права доступу в Git, що означають, що ви записуєте коміт як директорію, а не просто піддиректорію або файл.

Lastly, push these changes:

$ 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'

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

Тепер у нас є копія проекту з підмодулями в ньому і ми будемо співпрацювати з іншими учасниками команди як над головним проектом, як і над проектом підмодулем.

Отримання змін з першоджерела

Найпростіша модель використання підмодулів у проекті — просто користуватись підпроектом та отримувати оновлення з нього подеколи, проте не змінювати нічого у своїй копії. Розгляньмо тут простий приклад.

Якщо ви бажаєте перевірити, чи є щось новеньке у підмодулі, то можете перейти до його директорії та виконати 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, то побачите, що підмодуль було оновлено, та отримаєте список комітів, які були до нього додано. Якщо ви не бажаєте набирати --submodule щоразу при виконанні git diff, то можете встановити це типовим форматом, якщо встановите змінну конфігурації 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 status, Git покаже нам, що є нові коміти (“new commits”) у підмодулі.

$ 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 спробує оновити всі ваші підмодулі при виконанні git submodule update --remote, отже якщо у вас їх багато, то можливо варто передати імʼя саме того підмодуля, який ви бажаєте оновити.

Робота з підмодулями

Цілком імовірно, якщо ви використовуєте підмодулі, ви робите це, бо насправді збираєтесь працювати над кодом підмодуля одночасно з працею над кодом у головному проекті (чи одночасно над декількома підмодулями). Інакше, імовірно замість підмодулів ви б використали простішу систему керування залежностями (таку як Maven чи Rubygems).

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

Досі, коли ми виконували команду git submodule update щоб отримати зміни з репозиторіїв підмодулів, Git брав зміни та оновлював файли у піддиректоріях, проте залишав підрепозиторії у так званому стані “відокремлений HEAD” (detached HEAD). Це означає, що немає локальної гілки (як “master”, наприклад), яка слідкує за змінами. Без робочої гілки, що стежить за змінами, це означає, що навіть якщо ви збережете зміни в коміті підмодуля, ці зміни ймовірно будуть втрачені під час наступного виконання git submodule update. Вам доведеться виконати додаткові кроки, щоб за змінами в підмодулі стежили.

Щоб налаштувати підмодулі для легшої розробки, треба зробити дві речі. Вам треба перейти в кожен підмодуль та переключитись на гілку, над якою ви будете працювати. Потім треба сказати Git, що робити, якщо зроблено зміни, а git submodule update --remote отримує зміни з першоджерела. Є варіант зливати їх з вашою локальною роботою, або спробувати перебазувати вашу локальну роботу поверху нових змін.

Спершу, перейдімо до директорії нашого підмодуля та переключимось до гілки.

$ git checkout stable
Switched to branch 'stable'

Спробуймо варіант “зливати”. Щоб задати його вручну, можна просто додати опцію --merge до виклику update. Тут ми побачимо, що була зміна на сервері цього підмодуля, і її злито.

$ 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(+)

Тепер, якщо ми оновимо наш підмодуль, то побачимо, що станеться, якщо ми зробили локальні зміни, а першоджерело також має зміни, які нам треба обʼєднати.

$ 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
  > updated 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.

Як бачите, нам також надано корисні поради щодо того, що ми можемо зробити далі. Простий варіант — перейти до кожного підмодуля та вручну надіслати до віддалених сховищ, щоб переконатись, що вони доступні ззовні, та потім знову спробувати надіслати. Якщо ви хочете, щоб усі операції push відбувалися з поведінкою check, її можна зробити типовою за допомогою git config push.recurseSubmodules check.

Інший варіант — використати значення “on-demand”, що спробує зробити те саме за вас.

$ 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 зрозумів, що дві записані точки гілок в історії підмодуля, що розійшлися, мають бути злиті. Це видно з “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 безпосередньо, або створити гілку для нього, а потім спробувати її злити. Ми рекомендуємо останнє, навіть якщо це лише зробить повідомлення коміту гарнішим.

Отже, тепер ми перейдемо до директорії підмодуля, створимо гілку на базі другого SHA-1 з git diff та вручну зіллємо.

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610
(DbConnector) $ 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 запропонує його як можливе розвʼязання. Він бачить, що колись у проекті підмодуля, хтось зливав гілки з цими двома комітами, отже можливо вам саме він і потрібен.

Ось чому повідомлення помилки раніше було “merge following commits not found”, адже він не зміг цього зробити. Це збиває з пантелику, оскільки хто б очікував, що 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 forwarded to a common submodule child'

Це досягає такого саме результату, проте, принаймні, таким чином можливо пересвідчитись, що код у вашому підмодулі працюватиме, коли ви закінчите.

Поради щодо підмодулів

Є декілька речей, що можуть дещо полегшити вашу роботу з підмодулями.

Для кожного підмодуля (submodule 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.

$ 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 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 'adding crypto library'
[add-crypto 4445836] adding 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. Припустіть, у вас є файли в піддиректорії проекту, і ви бажаєте перенести їх до підмодуля. Якщо ви видалите піддиректорію, а потім виконаєте 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, знищення директорії підмодуля не призводить до втрати ніяких комітів чи гілок, які у вас були.

За допомогою цих інструментів, підмодулі можуть бути доволі простим та ефективним методом розробки декількох пов’язаних, проте все ж таки окремих проектів одночасно.