Git
Chapters ▾ 2nd Edition

7.11 Гит алати - Подмодули

Подмодули

Док радите на једном пројекту, често имате потребу да унутар њега користите неки други пројекат. Можда је то библиотека коју је развио неко други или коју ви развијате одвојено користећи више родитељских пројеката. У овим случајевима долази до заједничког проблема: желите имати могућност да два пројекта третирате као одвојене, а да у исто време један од њих можете користити из другог.

Ево примера. Претпоставимо да развијате веб сајт и креирате Атом вести. Уместо да пишете свој сопствени кôд који генерише Атом, одлучујете да употребите библиотеку. Највероватније ћете морати или да укључите тај кôд из дељене библиотеке као што је CPAN install или Руби gem, или да у своје стабло пројекта копирате тај изворни кôд. Проблем са додавањем библиотеке преставља тешкоћа у прилагођавању библиотеке на било који начин, а често је још теже и да се библиотека достави јер морате обезбедити да библиотека буде доступна сваком клијенту. Проблем са копирањем кода у ваш пројекат је у компликованом спајању ваших локалних промена кода библиотеке са новим узводним променама.

Програм Гит решава овај проблем употребом подмодула. Подмодули вам омогућавају да Гит репозиторијум чувате као поддиректоријум неког другог Гит репозиторијума. На тај начин можете да клонирате други репозиторијум у свој пројекат и да ваше комитове држите раздвојене.

Први кораци са подмодулима

Проћи ћемо кроз развој једноставног пројекта који је подељен на неколико потпројеката.

Хајде да почнемо тако што је ћемо постојећи Гит репозиторијум додати као подмодул репозиторијума у којем радимо. Да бисте додали нови подмодул, користите 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.

Submodules ће подразумевано додати потпројекат у директоријум са истим именом као и репозиторијум, у овом случају „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 адресе пројекта и локалног директоријума у који сте га повукли:

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

Ако имате више подмодула, постојаће више ставки у овом фајлу. Важно је приметите да је овај фајл део контроле верзије заједно са осталим фајловима, као што је ваш .gitignore фајл. Гура се и повлачи заједно са остатком вашег пројекта. На тај начин остали људи који клонирају овај пројекат знају одакле да преузму подмодул пројекте.

Белешка

Пошто је URL у .gitmodules фајлу прво место одакле други људи покушају да клонирају/добаве, ако је то могуће, обезбедите да се користи 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 поддиректоријум у вашем радном директоријуму, програм Гит га види као подмодул и не прати његов садржај када се не налазите у том директоријуму. Уместо тога, Гит поддиректоријум види као одређени комит из тог репозиторијума.

Ако желите лепши приказ из команде 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 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 поддиректоријум у потуно истом стању као што је био када сте малопре комитовали.

Постоји и један мало једноставнији начин да се ово уради. Ако команди git clone наведете --recurse-submodules, она ће аутоматски да иницијализује и ажурира сваки подмодул у репозиторијуму, чак и угњеждене подмодуле у случају да неки од подмодула у репозиторијуму и сами поседују подмодуле.

$ 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 кораке можете комбиновати извршавањем by running 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 видећете да је подмодул ажуриран и исписаће вам се листа комитова који су му додати. Ако не желите да куцате --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, програм Гит ће нам приказати да имамо „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 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, програм Гит ће подразумевано покушати да ажурира све ваше подмодуле. Ако их имате доста, вероватно ћете пожелети да проследите име оног подмодула за који желите да се покуша ажурирање.

Повлачење узводних измена са удаљеног репозиторијума пројекта

Хајде да сада ускочимо у ципеле вашег сарадника који има сопствени локални клон репозиторијума 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 заставицом за случај да неки од подмодула имају угњеждено подмодуле.

Ако овај процес желите да аутоматизујете, команди git pull (почевши од Гит верзије 2.14) можете да додате заставицу --recurse-submodules. Ово ће наложити програму Гит да изврши git submodule update непосредно након повлачења, постављајући подмодуле у исправно стање. Уз то, ако желите да програм Гит увек повлачи са --recurse-submodules, можете да поставите конфигурациону опцију submodule.recurse на true (ово функционише за git pull почевши од верзије 2.15 програма Гит). Ова опција ће навести програм Гит да заставицу --recurse-submodules употребљава за све команде које је подржавају (осим clone).

Постоји специјална ситуација која може да се догоди када се повлаче ажурирања суперпројекта: може се десити да је у једном од комитова које повлачите узводни репозиторијум променио URL адресу подмодула у .gitmodules фајлу. Ово може да се деси, на пример, ако This can happen for example if the submodule project changes its hosting platform. У том случају је могуће да git pull --recurse-submodules или git submodule update не успеју да се изврше ако суперпројекат указује на комит подмодула који не може да се нађе у удаљеном репозиторијуму подмодула локално конфигурисаном у вашем репозиторијуму. Да би се решила ова ситуација, потребна је команда git submodule sync:

# копира нову URL адресу у вашу локалну конфигурацију
$ git submodule sync --recursive
# ажурира подмодул са нове URL адресе
$ git submodule update --init --recursive

Рад на подмодулу

Врло је вероватно да ако користите подмодуле, то чините јер заиста желите да радите на коду у подмодулу истовремено уз рад на коду главног пројекта (или преко неколико подмодула). У супротном бисте сигурно користили једноставнији систем за управљање зависностима (као што је Maven или Rubygems).

Па хајде да сада прођемо кроз пример прављења измена у подмодулу истовремено са изменама у главном пројекту и комитовање и објављивање тих измена у исто време.

До сада, кадгод смо извршавали команду git submodule update да преузмемо измене из репозиторијума подмодула, програм Гит би преузео измене и ажурирао фајлове у поддиректоријуму, али би под-репозиторијум оставио у такозваном „detached HEAD” (одвојен HEAD) стању. То значи да нема локалне радне гране (као што је master, на пример) која прати измене. Када нема радне гране која прати измене, чак и ако комитујете измене у подмодулу, оне ће највероватније изгубити када следећи пут извршите git submodule update. Морате урадите неке додатне кораке ако желите да се измене у подмодулу прате.

Ако подмодул желите подесити тако да буде лакше да само уђете у њега и почнете да дељете, морате урадити две ствари. Потребно је да уђете у сваки подмодул и одјавите грану у којој ћете радити. Затим програму Гит морате рећи шта да ради ако сте направили измене, а git submodule update --remote он да повуче нови рад са узводног репозиторијума. Имате две опције: можете да их спојите у свој локални рад, или можете покушати да ребазирате свој локални рад на врх нових измена.

Најпре, хајде да одемо у директоријум подмодула и одјавимо грану.

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

Покушајмо да „merge” опцијом ажурирамо наш подмодул. Ручно је наводимо тако што update позиву једноставно додамо опцију --merge. Овде ћемо видети да је на серверу дошло до промене за овај подмодул и она се спаја.

$ 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, програм Гит ће једноставно ажурирати подмодул на оно што се налази на серверу и ресетоваће ваш пројекат на стање одвојен HEAD.

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

Ако се то догоди, не брините, једноставно се можете вратити назад у директоријум и поново одјавити своју грану (која још увек садржи ваш рад) и ручно спојити или ребазирати origin/stable (или коју год удаљену грану желите).

Ако своје измене подмодула још увек увек нисте комитовали, па покренете ажурирање подмодула, имаћете проблема, програм Гит ће преузети измене али неће преписати несачуван рад у вашем директоријуму подмодула.

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

Као што видите, команда вам приказује и корисне савете у вези могућих наредних корака. Једноставна могућност је да пређете у сваки подмодул и да ручно гурнете на удаљене репозиторијуме и тако обезбедите да су доступни споља када поново покушате гурање главног пројекта Ако желите да се ово понашање провере дешава за сва гурања, поставите га да буде подразумевано са 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

Као што видите, програм Гит је прешао у DbConnector модул у гурнуо га пре него што је гурнуо главни пројекат. Ако из неког разлога ро гурање не успе, неће успети ни гурање главног пројекта. Ово понашање можете поставити као подразумевано извршавањем git config push.recurseSubmodules on-demand.

Спајање измена подмодула

Ако измените референцу на подмодул у исто време кад и неко други, можете наићи на проблеме. То јест, ако су се историје подмодула разишле и комитоване у одвојене гране суперпројекта, биће потребно мало рада да то поправите.

Ако је један од комитова директни предак другог (спајање методом брзог премотавања унапред), онда ће програм Гит за спајање једноставно изабрати тај други и то лепо функционише.

Међутим, програм Гит уместо вас неће покушати чак ни тривијално спајање. Ако се комитови подмодула разилазе и потребно је да се споје, видећете нешто слично овоме:

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

Оно што се у суштини десило овде је да је програм Гит открио да све гране бележе тачке у историји подмодула које се разилазе и потребно је да се споје. Он то објашњава као „merge following commits not found” (није пронађено спајање након комитова), што донекле збуњује, па ћемо ускоро објаснити зашто је тако.

Да бисте решили проблем, потребно је да одредите стање у којем подмодул треба да се налази. Чудно, али програм Гит вам у овој ситуација не пружа доста информација које могу да вам помогну, чак ни SHA-1 суме комитова са обе стране историје. Не срећу, ово једноставно може да се одреди. Ако извршите git diff видећете SHA-1s суме комитова који су забележени у обе гране које покушавате да спојите.

$ 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

$ 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. Комитујемо спајање.

Можда помало збуњује, али заиста није тешко.

Интересантно је да постоји још један случај који програм Гит сам обрађује. Ако у директоријуму подмодула постоји комит спајања који у својој историји садржи оба комита, програм Гит ће вам то предложити као могуће решење. Он види да је у неком тренутку подмодул пројекта неко спојио гране које садрже ова два комита, па је могуће да ћете пожелети тај.

Ово је разлог што се приказује порука „merge following commits not found”, јер није био у стању да уради ово. Забуну уноси питање да ли би било ко и очекивао да програм Гит ово покуша?

Ако пронађе макар један прихватљиви комит спајања, видећете нешто слично овоме:

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

Ово постиже исти резултат, али барем можете проверити да функционише и када завршите имате кôд у директоријуму свог подмодула.

Савети за подмодуле

Постоји неколико ствари које могу да вам олакшају рад са подмодулима.

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;

Овде можемо видети да дефинишемо функцију у подмодулу и позивамо је из главног пројекта. Ово је очигледно упрошћен пример, али надамо се да вам даје представу колико може бити корисно.

Корисни алијаси

Вероватно ћете хтети да поставите неколико алијаса за неке од ових команди, јер могу бити прилично дугачке и за већину не можете поставити подразумеване опције конфигурацијом. Постављање алијаса у програму Гит смо објаснили у covered setting up Git aliases in Гит алијаси, али овде дајемо пример шта можете поставити ако планирате доста да радите са подмодулима у програму Гит.

$ 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 --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 када ваш пројекат има подмодуле. У старијим верзијама програма Гит које немају заставицу --recurse-submodules, након одјављивања можете употребити git submodule update --init --recursive и тако поставите подмодуле у одговарајуће стање.

Срећом, програму Гит (>=2.14) можете конфигурационом опцијом submodule.recurse навести да увек корсити заставицу --recurse-submodules: git config submodule.recurse true. Као што је напоменуто изнад, то ће навести програм Гит да рекурзивно посети подмодуле за свку команду која у себи има наведену опцију --recurse-submodules (осим git clone).

Претварање поддиректоријума у подмодуле

Друго главно ограничење на које многи људи наиђу се тиче претварања поддиректоријума у подмодуле. Ако сте у свом пројекту пратили фајлове и желите да их померите у подмодул, морате бити опрезни иначе ће програм Гит да се наљути на вас. Претпоставимо да имате фајлове у поддиректоријуму свог пројекта и да желите да га претворите у подмодул. Ако обришете поддиректоријум па онда извршите submodule add, програм Гит вам каже следеће:

$ 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 директоријуму основног пројекта, за разлико од много старијих верзија програма Гит, уништавање директоријума подмодула нећете изгубити ниједан комит или грану коју сте имали.

Уз ове алате, подмодули могу да буду прилично једноставан и ефективан метод за истовремено развијање неколико повезаних, али ипак одвојених пројеката.