Git 🌙
Chapters ▾ 2nd Edition

5.11 Git инструменти - Подмодули

Подмодули

Често се случва докато работите по един проект да трябва да използвате друг такъв в него. Може да е библиотека разработена от други или такава, която сте написали вие, но я използвате в множество проекти. В такива случаи възниква тривиален проблем: искате да третирате двата проекта като отделни, но и да можете да използвате единия в другия.

Ето пример. Разработвате уебсайт и създавате Atom потоци. Вместо да пишете собствен Atom генериращ код, решавате да използвате библиотека. Може да вмъквате кода от споделени библиотеки като CPAN install или Ruby gem или да копирате сорса в дървото на собствения си проект. Проблемът с включването на библиотеката е, че е трудно да я настроите според вашите нужди и често е още по-трудно да я внедрите в production среда, защото трябва да сте сигурни, че всеки клиент я има при себе си. Проблемът с копирането във вашия проект пък е, че всички ваши промени по библиотеката ще станат трудни за сливане когато upstream източника се промени.

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. Това значи, че ще се тегли и публикува с останалата част от проекта. Това се прави, за да може други хора, които клонират проекта да ви да знаят откъде да вземат проектите на подмодулите.

Забележка

Понеже URL-ите в .gitmodules файла са мястото, от което другите хора първо ще опитат да клонират/теглят, уверете се доколкото е възможно, че това са адреси до които те имат достъп. Например, ако използвате различен URL за публикуване от този който другите ще ползват за теглене, постарайте се да ползвате този, до който другите имат достъп. Можете да коригирате тази стойност локално с git config submodule.DbConnector.url PRIVATE_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 я третира като отделен къмит.

Ако искате малко по-приятен diff изход, може да подадете --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 '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 ще инициализира и обнови всеки подмодул в хранилището, включително вложените такива, ако съществуват в хранилището.

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

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

Сега имаме копие от проекта с подмодули в него и ще работим заедно с колегите си както по основния проект, така и по подмодула.

Издърпване на Upstream промени от Submodule Remote

Най-простият модел на работа с подмодули е просто да ги ползвате в проекта си без да ги модифицирате и от време на време да проверявате за промени по тях. Да видим малък пример.

Ако искате да проверите за нова работа в даден подмодул, можете да влезете в директорията му и да изпълните git fetch и git merge към upstream клона.

$ 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 клона в хранилището на подмодула (този, към който сочи HEAD в отдалеченото хранилище). Но можете да промените това поведение, ако желаете. Например, ако искате 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 ще ни покаже, че имаме “нови къмити” в подмодула.

$ 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 файла ни и че имаме няколко къмита, които сме изтеглили и са готови да бъдат къмитнати в submodule проекта.

$ 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. Ако те са повечко, можете да подадете само името на този, който желаете да обновите.

Издърпване на Upstream промени от Project Remote

Нека сега се поставим в ролята на вашия сътрудник, който има собствено локално копие на 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 (от версия на Git 2.14). Това ще накара Git д аизпълни git submodule update веднага след изтеглянето, което да постави подмодулите в коректен режим. Освен това, ако желаете Git винаги да тегли с --recurse-submodules, може да укажете това с конфигурационната опция submodule.recurse (това ще важи от версия 2.15 и нагоре). Така Git ще използва флага --recurse-submodules за всички команди, които го поддържат (с изключение на clone).

Съществува специална ситуация, която може да възникне при издърпване на промени от подпроект: може upstream хранилището да е променило адреса на подмодула в .gitmodules файла в някой от къмитите, които теглите. Това може да стане, ако например проектът на подмодула е със сменена хостинг платформа. В такъв случай е възможно командите git pull --recurse-submodules, или git submodule update да не успеят, ако главният проект сочи към къмит на подмодул, който не се намира в локално конфигурирания в хранилището submodule remote. За да се избегне тази ситуация е необходима командата 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

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

Ако ползвате подмодули, твърде вероятно е да искате да работите и по техния код едновременно с основния проект. В противен случай може да предпочетете да ползвате по-проста dependency management система като Maven или Rubygems.

Да разгледаме пример с промяна на кода на подмодул и на основния проект, къмитване и публикуване на промените.

Досега, когато изпълнявахме git submodule update командата за да изтеглим промените от хранилищата на подмодулите, Git ще взема новия код и ще обновява файловете в поддиректориите, но ще ги остави в режим известен като “detached HEAD”. Това означава, че нямаме работещ локален клон (като master например) следящ промените. Липсата на такъв клон означава, че дори да къмитнете промени по подмодула, тези промени е твърде вероятно да се загубят при следващото изпълнение на git submodule update. Необходими са малко допълнителни стъпки, ако искате промените в подмодул да бъдат следени.

Две неща трябва да се направят. Трябва да отидете в директорията на всеки подмодул и да стартирате клон, по който да работите. След това трябва да инструктирате Git как да действа, ако сте направили промени и след това git submodule update --remote внесе нови данни от upstream хранилището. Опциите са да можете да ги слеете с локалната ви работа или да се опитате да пребазирате локалната работа върху новите промени.

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

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

Нека опитаме с опцията “merge”. За да я подадем ръчно, добавяме --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 клон. Сега нека видим какво се случва, ако направим наша собствена промяна в библиотеката и някой друг публикува друга upstream промяна по същото време.

$ 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 просто ще обнови подмодула до съдържанието на сървъра и ще върне проекта в режим detached 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'

Също, ако сте направили промени, които водят до конфликт с нещо променено в upstream-а, 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'

Можете да влезете в поддиректорията и да оправите конфликта по нормалния начин.

Публикуване на промени по подмодул

Сега имаме промени в директорията на подмодула. Някои от тях идват от upstream-а след обновяване а други са направени ръчно локално и не са достъпни за никой друг, защото все още не сме ги публикували.

$ 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” ще направи така, че публикуването да бъде отказано, ако произволна къмитната промяна в подмодул не е била публикувана успешно.

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

Както се вижда, получавате и напътствия как бихте могли да продължите. Най-простата възможност е да преминете през директориите на всички подмодули и ръчно да публикувате съдържанието им, след което да се върнете и да пробвате да публикувате главния проект. Ако искате това поведение с проверка да е валидно за всички публикувания, можете да изпълните 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 модула и е публикувал него преди да опита Push за главния проект. Ако по някаква причина този подмодул не успее да се публикува, главния проект също няма да успее. За да направите това поведение подразбиращо се, изпълнете git config push.recurseSubmodules on-demand.

Сливане на промени в подмодул

Ако промените референция на подмодул по същото време като някой друг, може да възникнат проблеми. Това е ситуация, при която историите на подмодул са се разделили и са къмитнати в разделящи се клонове в суперпроект.

Ако единият от къмитите е директен предшественик на другия (fast-forward merge), тогава 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 е upstream къмита. Ако отидем в поддиректорията на модула, той вече трябва да е на eb41d76, защото сливането не го е променило. Ако по някаква причина не е, може просто да създадете нов клон сочещ към него.

Важната част е SHA-1 стойността на къмита от другата страна. Това е, което трябва да слеете и коригирате. Можете или да опитате сливане с SHA-1 стойността директно или може да създадете клон за нея и да опитате сливането в него. Бихме препоръчали второто, дори само за да създадете по-добро merge commit съобщение.

И така, ще отидем в поддиректорията, ще създадем клон наречен “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. Разрешаваме конфликтния submodule обект.

  5. Къмитваме сливането ни.

Може да е леко смущаващо, но не е толкова трудно.

Съществува и още един интересен случай, който Git обработва. Ако в директорията на подмодула съществува merge къмит, който съдържа и двата къмита в историята си, Git ще ви го предложи като възможно решение. Той вижда, че на даден етап от този submodule проект, някой вече е слял клоновете съдържащи тези два къмита, така че може да искате този къмит.

Ето защо объркващото съобщение от по-горе гласеше “merge following commits not found”, защото системата не може да направи това. Объркващият текст идва, защото едвали някой би очаквал 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 (което изчиства конфликта) и след това да къмитнете. Но вероятно не трябва да правите това. Можете също толкова лесно да влезете в поддиректорията, да видите каква е разликата, да направите fast-forward до този къмит, да го тествате и след това да къмитнете.

$ 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 команда, която ви позволява да изпълнявате други команди за всеки подмодул, като в цикъл. Това е полезно, ако имате много подмодули в проекта.

Да кажем, че стартираме работа по нова функционалност или оправяме грешка и същевременно имаме текущо свършена работа в няколко подмодула. Можем лесно да маскираме (stash) работата по всички подмодули наведнъж.

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

В общи линии получавате представа. Едно наистина полезно нещо е, че можете да изведете добре изглеждащ унифициран diff на това какво се е променило в главния проект и също във всички подмодули.

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

Тук се вижда, че дефинираме функция в подмодул и я викаме в главния проект. Очевидно примерът е прост, но да се надяваме че получавате представа как foreach може да бъде полезна опция.

Псевдоними

Някои от тези команди може да са доста дълги и неудобни за писане, а и не за всички може да направите подразбиращи се настройки в конфигурацията. Така че, вероятно бихте искали да създадете псевдоними за тях. Ние погледнахме как се правят псевдоними в 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 за да публикувате с dependancy checking за тях.

Възможни проблеми

Ползването на подмодули си има и неудобства.

Превключване на клонове

Така например, превключването на клонове с подмодули в тях може да е проблематично при версии на 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 и нагоре опростяват всичко това добавяйки флага --recurse-submodules към командата git checkout, която се грижи за поставянето на подмодулите в правилния статус за клона, към който превключваме.

$ 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” и ще индикира “new commits”. Това е така, защото по подразбиране статуса на подмодула не се обслужва по време на превключване между клоновете.

Това може да е наистина смущаващо, така че добра идея е винаги да използваме git checkout --recurse-submodules, когато проектът съдържа подмодули. За по-стари версии на Git, които не поддържат флага --recurse-submodules, след извличането може да изпълните git submodule update --init --recursive за да поставите подмодулите в правилния режим.

За щастие, може да инструктирате Git (>=2.14) винаги да използва флага --recurse-submodules през конфигурационната опция submodule.recurse: 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.

Сега представете си, че сте направили това в отделен клон. Ако се опитате да превключите обратно към клон (примерно master), в който тези файлове все още са в действителното дърво, а не в подмодул — ще получите грешка:

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

С тези инструменти, подмодулите в Git могат да са ефективен и прост начин за разработка в множество взаимно свързани, но все пак независими проекта едновременно.

scroll-to-top