Chapters ▾ 2nd Edition

7.11 Ferramentas do Git - Submódulos

Submódulos

Costuma acontecer de, enquanto você trabalha em um projeto, você precise utilizar um outro projeto dentro dele. Pode ser uma biblioteca que um terceiro desenvolveu ou que você esteja desenvolvendo separadamente e usando em múltiplos projetos-pai. Um problema comum surge nesses cenários: você quer ser capaz de tratar os dois projetos como sendo separados e ainda assim poder utilizar um dentro do outro.

Aqui vai um exemplo. Suponha que você está desenvolvendo um website e criando feeds Atom. Em vez de escrever o seu próprio código gerador de Atom, você decide utilizar uma biblioteca. Você provavelmente terá que ou incluir esse código de uma biblioteca compartilhada como uma instalação CPAN ou Ruby gem, ou copiar o código-fonte para dentro da árvore do seu próprio projeto. O problema de incluir a biblioteca é que é difícil customizá-la de alguma forma e frequentemente mais difícil de fazer o deploy dela, porque você precisa garantir que cada cliente tenha aquela biblioteca disponível. O problema de copiar o código para o seu próprio projeto é que quaisquer customizações que você faça são difíceis de fazer merge quando alterações do upstream ficarem disponíveis.

O Git resolve esse problema com submódulos. Submódulos permitem que você mantenha um repositório Git como um subdiretório de um outro repositório Git. Isso permite a você clonar um outro repositório para o seu projeto e manter os seus commits separados.

Começando com Submódulos

Nós caminharemos pelo desenvolvimento de um projeto simples que foi dividido em um projeto principal e alguns sub-projetos.

Vamos começar adicionando um repositório Git existente como um submódulo do repositório no qual estamos trabalhando. Para adicionar um novo submódulo você usa o comando git submodule add com a URL absoluta ou relativa do projeto que você gostaria de começar a rastrear. Neste exemplo, nós adicionaremos uma biblioteca chamada “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.

Por padrão, submódulos irão adicionar o subprojeto em um diretório de mesmo nome que o repositório, neste caso “DbConnector”. Você pode adicionar um caminho diferente no final do comando se você quiser que ele vá para outro lugar.

Se você rodar git status a este ponto, você vai notar algumas coisas.

$ 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

Primeiro você deve notar o novo arquivo .gitmodules. Esse é um arquivo de configuração que armazena o mapeamento entre a URL do projeto e o subdiretório local para o qual você fez o pull dele:

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

Se você tiver múltiplos submódulos, você terá múltiplas entradas neste arquivo. É importante notar que este arquivo é controlado por versão junto com seus outros arquivos, como o seu arquivo .gitignore. Ele sofre push e pull com o resto do seu projeto. É assim que outras pessoas que clonarem este projeto vão saber de onde baixar os projetos do submódulo.

Note

Como a URL no arquivo .gitmodules é a partir de onde outras pessoas tentarão dar clone/fetch primeiro, certifique-se de usar uma URL que elas consigam acessar, se possível. Por exemplo, se você usa uma URL para push diferente daquela que os outros utilizariam para pull, use aquela que os outros têm acesso. Você pode sobrescrever esse valor localmente com git config submodule.DbConnector.url PRIVATE_URL para o seu próprio uso. Quando aplicável, uma URL relativa pode ser útil.

A outra listagem na saída do git status é a entrada da pasta do projeto. Se você rodar git diff nisso, você vê algo interessante:

$ 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

Embora DbConnector seja um subdiretório no seu diretório de trabalho, o Git o enxerga como um submódulo e não rastreia o seu conteúdo quando você não está naquele diretório. Em vez disso, o Git o vê como um commit em particular daquele repositório.

Se você quiser uma saída de diff um pouco mais amigável, você pode passar a opção --submodule para o 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)

Quando você comita, você vê algo assim:

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

Note o modo 160000 para a entrada DbConnector. Esse é um modo especial no Git que basicamente significa que você está gravando um commit como uma entrada de diretório em vez de um subdiretório ou um arquivo.

Por fim, dê push nessas alterações:

$ git push origin master

Clonando um Projeto com Submódulos

Aqui nós vamos clonar um projeto com um submódulo dentro dele. Quando você clona tal projeto, por padrão você obtém os diretórios que contêm submódulos, mas nenhum dos arquivos dentro deles ainda:

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

O diretório DbConnector está lá, mas vazio. Você tem que rodar dois comandos a partir do projeto principal: git submodule init para inicializar o seu arquivo local de configurações, e git submodule update para buscar (fetch) todos os dados daquele projeto e fazer o checkout do commit apropriado listado no seu superprojeto (projeto-pai):

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

Agora o seu subdiretório DbConnector está no estado exato em que ele estava quando você comitou mais cedo.

No entanto, existe uma outra forma de fazer isso que é um pouco mais simples. Se você passar --recurse-submodules para o comando git clone, ele vai automaticamente inicializar e atualizar cada submódulo no repositório, incluindo submódulos aninhados se algum dos submódulos no repositório tiver submódulos também.

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

Se você já clonou o projeto e esqueceu de --recurse-submodules, você pode combinar os passos do git submodule init e git submodule update rodando git submodule update --init. Para inicializar, fazer fetch e checkout também de quaisquer submódulos aninhados, você pode usar o infalível git submodule update --init --recursive.

Trabalhando em um Projeto com Submódulos

Agora nós temos uma cópia de um projeto com submódulos e iremos colaborar com nossos colegas de equipe tanto no projeto principal quanto no projeto do submódulo.

Puxando (Pulling) Alterações do Upstream a partir do Remoto do Submódulo

O modelo mais simples de se usar submódulos em um projeto seria se você simplesmente estivesse consumindo um subprojeto e quisesse obter atualizações dele de vez em quando mas não estivesse na verdade modificando nada no seu checkout. Vamos passar por um exemplo simples disso.

Se você quiser checar se há novos trabalhos em um submódulo, você pode entrar no diretório e rodar git fetch e fazer git merge da branch de upstream para atualizar o código local.

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

Agora, se você voltar ao projeto principal e rodar git diff --submodule, você pode ver que o submódulo foi atualizado e obter uma lista de commits que foram adicionados a ele. Se você não quer digitar --submodule toda vez que você roda git diff, você pode defini-lo como formato padrão setando o valor de config diff.submodule como “log”.

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

Se você comitar nesse ponto então você vai travar o submódulo para ter o novo código quando as outras pessoas atualizarem.

Existe também uma maneira mais fácil de fazer isso, caso você prefira não fazer o fetch e o merge manualmente no subdiretório. Se você rodar git submodule update --remote, o Git vai entrar nos seus submódulos e fazer fetch e update para você.

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

Este comando vai por padrão assumir que você quer atualizar o checkout para a branch padrão do repositório do submódulo remoto (aquela apontada pelo HEAD no remoto). Você pode, no entanto, mudar isso para algo diferente se você quiser. Por exemplo, se você quer que o submódulo DbConnector rastreie a branch “stable” daquele repositório, você pode configurá-lo tanto no seu arquivo .gitmodules (para que todos os outros também o rastreiem), quanto apenas no seu arquivo local .git/config. Vamos defini-lo no arquivo .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'

Se você omitir o -f .gitmodules ele fará a alteração apenas para você, mas provavelmente faz mais sentido rastrear essa informação junto ao repositório para que todos os outros também a tenham.

Quando rodamos git status neste ponto, o Git nos mostrará que temos “new commits” no submódulo.

$ 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")

Se você definir a configuração status.submodulesummary, o Git também mostrará a você um curto sumário das alterações aos seus submódulos:

$ 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

A este ponto se você rodar git diff nós podemos ver tanto que nós modificamos nosso arquivo .gitmodules quanto que há uma quantidade de commits que nós baixamos e que estão prontos para o commit em nosso projeto submódulo.

$ 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

Isso é bem bacana pois nós podemos ver, na prática, o log dos commits que estamos prestes a comitar no nosso submódulo. Uma vez comitado, você pode ver esta informação depois do fato também quando você roda 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

O Git tentará, por padrão, atualizar todos os seus submódulos quando você rodar git submodule update --remote. Se você tiver muitos deles, você pode querer passar o nome de apenas o submódulo que você quer tentar atualizar.

Puxando (Pulling) Alterações do Upstream a partir do Remoto do Projeto

Vamos agora nos colocar no lugar do seu colaborador, que tem seu próprio clone local do repositório MainProject. Simplesmente executar git pull para pegar suas alterações recém-comitadas não é o bastante:

$ 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")

Por padrão, o comando git pull faz o fetch recursivo de alterações de submódulos, conforme podemos ver na saída do primeiro comando acima. Entretanto, ele não atualiza (update) os submódulos. Isso é mostrado pela saída do comando git status, a qual mostra que o submódulo está modificado (“modified”), e que possui novos commits (“new commits”). Além do mais, os parênteses exibindo os novos commits apontam para a esquerda (<), indicando que estes commits estão registrados no MainProject mas não estão presentes no checkout local do DbConnector. Para finalizar a atualização, você precisa rodar o 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

Note que, para estar mais seguro, você deveria rodar git submodule update com a flag --init caso os commits do MainProject de que você acabou de dar pull tenham adicionado novos submódulos, e com a flag --recursive caso algum submódulo tenha submódulos aninhados.

Se você quiser automatizar esse processo, você pode adicionar a flag --recurse-submodules ao comando git pull (desde o Git 2.14). Isto fará com que o Git rode git submodule update logo após o pull, colocando os submódulos no estado correto. Ainda mais, se você deseja fazer com que o Git sempre dê pull com --recurse-submodules, você pode configurar a opção submodule.recurse como true (isso funciona para o git pull a partir do Git 2.15). Esta opção vai fazer o Git usar a flag --recurse-submodules para todos os comandos que lhe derem suporte (exceto clone).

Existe uma situação especial que pode acontecer ao se puxar atualizações do superprojeto: poderia ser que o repositório upstream mudou a URL do submódulo no arquivo .gitmodules em um dos commits de que você deu pull. Isso pode ocorrer por exemplo se o projeto do submódulo mudar sua plataforma de hospedagem. Nesse caso, é possível para o git pull --recurse-submodules, ou git submodule update, falhar caso o superprojeto faça referência a um commit do submódulo não encontrado no remoto do submódulo configurado localmente em seu repositório. A fim de remediar essa situação, o comando git submodule sync se faz necessário:

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

Trabalhando em um Submódulo

É bem provável que se você está usando submódulos, você esteja fazendo isso porque você realmente quer trabalhar no código dentro do submódulo ao mesmo tempo em que está trabalhando no código dentro do projeto principal (ou ao longo de vários submódulos). De outro modo, você estaria provavelmente, ao invés disso, usando um sistema de gerenciamento de dependências mais simples (como Maven ou Rubygems).

Então agora vamos prosseguir com um exemplo de fazer alterações no submódulo ao mesmo tempo que no projeto principal e comitar e publicar (publish) essas alterações todas juntas.

Até agora, quando rodamos o comando git submodule update para fazer o fetch das alterações a partir dos repositórios dos submódulos, o Git traria as alterações e atualizaria os arquivos no subdiretório mas deixaria o sub-repositório no que se chama de estado de “detached HEAD” (HEAD separado). Isto significa que não há nenhuma branch local de trabalho (como master, por exemplo) rastreando alterações. Sem uma branch de trabalho rastreando alterações, isso significa que mesmo se você comitar alterações para o submódulo, as ditas alterações serão bem possivelmente perdidas da próxima vez que você rodar um git submodule update. Você tem que executar alguns passos extras se quiser que alterações num submódulo sejam rastreadas.

A fim de configurar o seu submódulo para ser mais fácil de se entrar e codar nele, você precisa fazer duas coisas. Você precisa entrar em cada submódulo e fazer o checkout de uma branch para se trabalhar. Aí você precisa contar ao Git o que fazer se você tiver feito alterações e mais tarde git submodule update --remote puxar um novo trabalho do upstream. As opções são de que você pode fazer um merge delas no seu trabalho local, ou você pode tentar fazer o rebase do seu trabalho local em cima das novas alterações.

Primeiro de tudo, vamos entrar no nosso diretório de submódulo e fazer checkout de uma branch.

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

Vamos tentar atualizar o nosso submódulo com a opção de merge (opção “merge”). Para especificá-la de forma manual, podemos apenas adicionar a opção --merge à nossa chamada do comando update. Aqui veremos que houve uma alteração no servidor para este submódulo e ela sofre 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'

Se formos para dentro do diretório DbConnector, teremos as novas alterações já mescladas (merged) em nossa branch stable local. Agora vamos ver o que acontece quando fazemos nossa própria alteração local para a biblioteca e alguma outra pessoa faz push de uma outra mudança para o upstream, tudo ao mesmo tempo.

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

Agora, se atualizarmos nosso submódulo, podemos ver o que acontece quando tivermos feito uma alteração local e o upstream também tiver uma mudança que precisamos incorporar.

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

Se você se esquecer de --rebase ou --merge, o Git vai só atualizar o submódulo para aquilo que se encontre no servidor e resetar seu projeto para o estado detached HEAD.

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

Caso isso aconteça, não se preocupe, você pode simplesmente retornar ao diretório e dar checkout da sua branch de novo (a qual continuará contendo o seu trabalho) e aplicar um merge ou um rebase com origin/stable (ou qualquer outro branch remoto que você quiser) manualmente.

Se você não tiver comitado suas modificações em seu submódulo e você rodar um submodule update que possa causar problemas, o Git irá buscar (fetch) as modificações, contudo, sem sobrescrever qualquer trabalho que não tenha sido salvo, dentro do diretório de seu submódulo.

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

Se as alterações que você realizou conflitarem com algo que mudou no upstream, o Git irá informá-lo quando você rodar o update.

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

Você pode ir para o diretório de submódulo e resolver o conflito, exatamente como você faria normalmente.

Publicando Modificações do Submódulo

Agora, nós temos algumas modificações no nosso diretório de submódulo. Algumas delas foram trazidas desde o upstream através de nossos updates, já outras foram criadas localmente e ainda não estão à disposição de qualquer outra pessoa, uma vez que ainda não realizamos o push.

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

Se nós dermos um commit no projeto principal e aplicarmos o push nele sem aplicar o push às modificações do submódulo juntas, as outras pessoas, ao tentarem realizar check out em nossas alterações, vão ter problemas, visto que elas não possuirão meio de obter aquelas atualizações do submódulo, das quais elas dependem. Aquelas alterações, as do submódulo, só existirão na nossa cópia local.

Para ter a certeza que isto não ocorra, você tem de pedir que o Git verifique, previamente à submissão do projeto principal, se em todos os seus submódulos foi aplicado o push corretamente. O comando git push pode ser acompanhado pelo argumento --recurse-submodules o qual pode assumir valores como: “check” ou “on-demand”. A opção “check” fará push simplesmente falhar se alguma alteração commitada no submódulo ainda não tiver sido enviada por 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.

Como você pode ver, ele também oferece orientações úteis sobre o que fazer em seguida. A opção simples consiste em adentrar cada submódulo e, manualmente, realizar push nos remotes para nos certificar que tais submódulos estarão disponíveis de maneira externa e, então, tentar efetuar esse push novamente. Se você desejar que o comportamento verificado em “check” suceda em todo e qualquer push, você pode deixá-lo como o comportamento padrão mediante este comando git config push.recurseSubmodules check.

A outra opção compreende a utilização do valor “on-demand”, que tentará executar as ações anteriores por você.

$ 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

Dali, pelo que você foi capaz de ver, o Git foi ao módulo DbConnector e ali aplicou o push antes de dar push no projeto central. Se esse push no submódulo vier a falhar em razão de algum motivo, o push do projeto pai vai também falhar. É possível deixar esse procedimento de modo default, fazendo git config push.recurseSubmodules on-demand.

Modificações de Submódulo por Merging

Se você alterar uma referência de submódulo ao mesmo tempo que outra pessoa, poderá encontrar alguns problemas. Isso quer dizer: se as histórias do submódulo divergiram e os commits acompanharam essas ramificações, que divergem, em superprojetos; vai ser preciso um pouco de trabalho, de sua parte, com vistas a reparar isto.

Se um dos commits for ancestral direto do outro (um merge fast-forward), o Git simplesmente escolherá o segundo para o merge, e tudo funcionará bem.

Com isso, o Git não intentará mesmo num tipo banal de merging a seu dispor. Se os commits no submódulo divergirem e precisarem que lhes faça um merge, você terá algo semelhante a isto, como resposta:

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

Em suma: o que aconteceu, aqui, é que o Git imaginou que ambas the branches registraram pontos da história do submódulo que são incompatíveis e cujas pontas hão de ser amarradas, via um merge. Ele explica o problema como “merge following commits not found”, o que é confuso, mas logo explicaremos o motivo.

De modo a solucionar esse imbróglio, você terá de descobrir em qual estado o seu submódulo deveria estar. Estranhamente, o Git não fornece muitas informações úteis aqui, nem mesmo os SHA-1s dos commits dos dois lados do histórico. Felizmente, é simples descobri-los. Se você submeter o comando git diff, poderá extrair os SHA-1s, dos commits anotados nos dois branches nos quais tem buscado realizar a fusão (merge).

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

Em sendo assim, neste caso, eb41d76 é o commit em nosso submódulo que nós possuíamos e c771610 é aquele commit que o upstream tinha. Se entrarmos no diretório do submódulo, ele já deverá estar em eb41d76, pois o merge não o teria alterado. Por outro lado, caso não seja, você poderá apenas criar um branch, apontando em direção dele e então submeter um checkout logo a seguir.

O que importa é o que se tira do SHA-1 de autoria do commit que vêm da parte que nos é oposta. É isso que você precisará mesclar e resolver. Você pode simplesmente tentar o merge diretamente com o SHA-1 ou criar um branch para ele e então tentar mesclá-lo. Sugerimos a segunda opção, ainda que seja apenas para produzir uma mensagem de commit de merge melhor.

Assim, entraremos no diretório do submódulo, criaremos um branch chamado “try-merge” com base no segundo SHA-1 de git diff e faremos o merge manualmente.

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

Encontramos um conflito de merge real; se o resolvermos e fizermos seu commit, poderemos simplesmente atualizar o projeto principal com o resultado.

$ 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. Primeiro, nós resolvemos o conflito.

  2. Depois nós retornamos ao diretório do projeto pai.

  3. Podemos realizar uma nova checagem dos SHA-1s.

  4. Tratar a entrada do submódulo que gerou o conflito.

  5. Faça commit do nosso merge.

Isso pode parecer um pouco confuso, mas na verdade não é muito difícil.

Curiosamente, há outro caso que o Git consegue tratar. Se houver no diretório do submódulo um commit de merge cujo histórico contenha ambos os commits, o Git o sugerirá como possível solução. Ele percebe que, em algum ponto do projeto do submódulo, alguém mesclou branches que continham esses dois commits; talvez seja esse o resultado que você queira.

É por isso que a mensagem de erro anterior era “merge following commits not found”: o Git não conseguiu fazer isso. É confuso, pois quem esperaria que ele sequer tentasse fazer isso?

Se ele encontrar um único commit de merge aceitável, você verá algo assim:

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

O comando sugerido pelo Git atualizará o index como se você tivesse executado git add (o que elimina o conflito) e então fará o commit. No entanto, provavelmente você não deveria fazer isso. É igualmente fácil entrar no diretório do submódulo, examinar a diferença, fazer fast-forward para esse commit, testá-lo corretamente e então fazer o commit.

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

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

Isso alcança o mesmo resultado, mas dessa forma você ao menos pode verificar que tudo funciona e, ao terminar, terá o código no diretório do submódulo.

Dicas de Submódulos

Há algumas coisas que você pode fazer para tornar o trabalho com submódulos um pouco mais fácil.

Foreach de Submódulo

Há um comando de submódulo foreach para rodar algum comando arbitrário em cada submódulo. Isso pode ser realmente útil se você tem um número de submódulos no mesmo projeto.

Por exemplo, digamos que nós queiramos iniciar um novo recurso ou fazer uma correção de bug e nós temos trabalhos ocorrendo em vários submódulos. Podemos facilmente fazer stash de todo o trabalho em todos os submódulos.

$ 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

Então nós podemos criar uma nova branch e mudar para ela em todos os nossos submódulos.

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

Você entendeu a ideia. Uma coisa realmente útil que você pode fazer é produzir um diff unificado do que foi alterado em seu projeto principal e todos os seus subprojetos também.

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

Aqui nós podemos ver que estamos definindo uma função num submódulo e a chamando no projeto principal. Isso é, obviamente, um exemplo simplificado, mas esperançosamente te dá uma ideia de como isso pode ser útil.

Aliases Úteis

Você pode querer configurar alguns apelidos (aliases) para alguns destes comandos dado que eles podem ser muito grandes e não é possível configurar opções para a maioria deles a fim de os tornarem padrões. Abordamos a configuração de aliases do Git em Aliases (Apelidos) no Git, mas aqui está um exemplo do que você talvez queira configurar se pretende trabalhar muito com submódulos no 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'

Dessa forma, você pode simplesmente executar git supdate quando quiser atualizar seus submódulos ou git spush para fazer push com verificação das dependências de submódulos.

Problemas com Submódulos

No entanto, o uso de submódulos não é isento de contratempos.

Mudando de ramos (branches)

Por exemplo, alternar entre branches que contêm submódulos também pode ser complicado em versões do Git anteriores à 2.13. Se você criar um novo branch, adicionar um submódulo nele e então voltar para um branch sem esse submódulo, o diretório do submódulo permanecerá como um diretório não rastreado:

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

Remover o diretório não é difícil, mas pode ser um pouco confuso vê-lo ali. Se você o remover e depois voltar para o branch que contém o submódulo, precisará executar submodule update --init para preenchê-lo novamente.

$ 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

Novamente, não é realmente difícil, mas pode ser um pouco confuso.

Nas versões contemporâneas da ferramenta (Git >= 2.13), toda esta gama de ações encontrou simplificação diante do acréscimo de --recurse-submodules, como flag a acompanhar o comando git checkout; a incumbência dessa se dá por repousar os submódulos ao estado ideal àquele correspondente no ramo que então tencionamos.

$ 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

Usar a flag --recurse-submodules de git checkout também pode ser útil quando você trabalha em vários branches do superprojeto e, em cada um deles, o submódulo aponta para commits diferentes. De fato, se você alternar entre branches que registram o submódulo em commits diferentes, ao executar git status o submódulo aparecerá como “modified” e indicará “new commits”. Atribui-se isso ao fato de, como padrão, o submódulo ser preterido quanto a acompanhamento de trânsito ao flutuarmos nestes ramos.

Isso pode ser muito confuso, portanto é uma boa ideia sempre usar git checkout --recurse-submodules quando o projeto contém submódulos. Em versões antigas do Git que não possuem a flag --recurse-submodules, você pode usar git submodule update --init --recursive depois do checkout para colocar os submódulos no estado correto.

Felizmente, você pode instruir o Git (>=2.14) a sempre usar a flag --recurse-submodules definindo a opção de configuração submodule.recurse: git config submodule.recurse true. Como observado acima, isso também fará o Git percorrer recursivamente os submódulos em todo comando que possua a opção --recurse-submodules (exceto git clone).

Modificando de Subdiretórios para Submódulos

Outra ressalva importante que muitas pessoas encontram envolve a mudança de subdiretórios para submódulos. Se você rastreava arquivos no projeto e quer movê-los para um submódulo, deve ter cuidado, ou o Git ficará irritado com você. Suponha que existam arquivos em um subdiretório do projeto e que você queira transformá-lo em um submódulo. Se excluir o subdiretório e então executar submodule add, o Git reclamará:

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

Primeiro, você precisa retirar o diretório CryptoLibrary da área de preparação. Depois, poderá adicionar o submódulo:

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

Agora suponha que você tenha feito isso em um branch. Se tentar voltar para um branch em que esses arquivos ainda estejam na árvore real, e não em um submódulo, receberá este erro:

$ 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

Você pode forçar a mudança com checkout -f, mas tome cuidado para não ter alterações não salvas ali, pois esse comando poderá sobrescrevê-las.

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

Então, ao voltar, por algum motivo você encontrará um diretório CryptoLibrary vazio, e talvez nem git submodule update consiga corrigi-lo. Pode ser necessário entrar no diretório do submódulo e executar git checkout . para recuperar todos os arquivos. Você pode executar isso em um script submodule foreach para aplicá-lo a vários submódulos.

É importante observar que, hoje em dia, os submódulos mantêm todos os dados Git no diretório .git do projeto principal; portanto, ao contrário de versões muito antigas do Git, destruir o diretório de um submódulo não fará você perder commits ou branches.

Com essas ferramentas, os submódulos podem ser um método bastante simples e eficaz para desenvolver simultaneamente vários projetos relacionados, mas ainda separados.