Git
Chapters ▾ 2nd Edition

7.11 Herramientas de Git - Submódulos

Submódulos

A menudo ocurre que mientras trabaja en un proyecto, necesita usar otro proyecto desde adentro. Tal vez se trate de una biblioteca desarrollada por un tercero, o que usted está desarrollando por separado y que se utiliza en múltiples proyectos principales. Un problema común surge en estos escenarios: desea poder tratar los dos proyectos como separados y aún así poder usar uno desde el otro.

Aquí hay un ejemplo. Supongamos que está desarrollando un sitio web y creando feeds Atom. En lugar de escribir su propio código de generación de Atom, decide usar una biblioteca. Es probable que tenga que incluir este código de una biblioteca compartida, como una instalación CPAN o Ruby gem, o copiar el código fuente en su propio árbol de proyectos. El problema con la inclusión de la biblioteca es que es difícil personalizar la biblioteca de alguna manera y, a menudo, es más difícil de implementar, porque debe asegurarse de que cada cliente tenga esa biblioteca disponible. El problema con el envío del código a su propio proyecto es que cualquier cambio personalizado que realice es difícil de fusionar cuando estén disponibles los cambios de upstream.

Git aborda este problema utilizando submódulos. Los submódulos le permiten mantener un repositorio de Git como un subdirectorio de otro repositorio de Git. Esto le permite clonar otro repositorio en su proyecto y mantener sus commits separados.

Comenzando con los Submódulos

Pasaremos por el desarrollo de un proyecto simple que se ha dividido en un proyecto principal y algunos subproyectos.

Comencemos agregando un repositorio de Git existente como un submódulo del repositorio en el que estamos trabajando. Para agregar un nuevo submódulo, use el comando git submodule add con la URL del proyecto que desea empezar a rastrear. En este ejemplo, agregaremos una biblioteca llamada “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 defecto, los submódulos agregarán el subproyecto a un directorio llamado igual que el repositorio, en este caso “DbConnector”. Puede agregar una ruta diferente al final del comando si desea que vaya a otra parte.

Si ejecuta git status en este punto, notará algunas cosas.

$ 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

En primer lugar, debe observar el nuevo archivo .gitmodules. Este es un archivo de configuración que almacena la asignación entre la URL del proyecto y el subdirectorio local en el que lo ha insertado:

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

Si tiene múltiples submódulos, tendrá múltiples entradas en este archivo. Es importante tener en cuenta que este archivo está controlado por la versión con sus otros archivos, como su archivo .gitignore. Se empuja y hala con el resto de su proyecto. Así es como otras personas que clonan este proyecto saben de dónde obtener los proyectos de submódulos.

Note

Dado que la URL en el archivo .gitmodules es lo que otras personas intentarán primero clonar/buscar, asegúrese de usar una URL a la que puedan acceder si es posible. Por ejemplo, si usa una URL diferente a la que presionar para que otros la utilicen, utilice aquella a la que otros tienen acceso. Puede sobrescribir este valor localmente con git config submodule.DbConnector.url PRIVATE_URL para su propio uso.

La otra lista en el resultado git status es la entrada de la carpeta del proyecto. Si ejecuta git diff sobre eso, verá algo interesante:

$ 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

Aunque DbConnector es un subdirectorio en su directorio de trabajo, Git lo ve como un submódulo y no rastrea su contenido cuando usted no está en ese directorio. Sin embargo, Git sí lo ve como un “commit” particular de ese repositorio.

Si quiere una mejor salida de diff, puede pasar la opción --submodule a 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)

Cuando hace “commit”, ve algo como esto:

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

Observe el modo 160000 para la entrada` DbConnector`. Ese es un modo especial en Git que básicamente significa que está registrando una confirmación como una entrada de directorio en lugar de un subdirectorio o un archivo.

Clonación de un Proyecto con Submódulos

Aquí clonaremos un proyecto con un submódulo. Cuando clona tal proyecto, de forma predeterminada obtiene los directorios que contienen submódulos, pero ninguno de los archivos dentro de ellos aún:

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

El directorio DbConnector está ahí, pero está vacío. Debe ejecutar dos comandos: git submodule init para inicializar su archivo de configuración local, y git submodule update para buscar todos los datos de ese proyecto y que verifique el “commit” adecuado que figura en su superproyecto:

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

Ahora su subdirectorio DbConnector está en el estado exacto en el que estaba cuando hizo “commit” antes.

Sin embargo, hay otra manera de hacer esto que es un poco más simple. Si pasa --recursive al comando git clone, se inicializará y actualizará automáticamente cada submódulo en el repositorio.

$ git clone --recursive 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'

Trabajando en un Proyecto con Submódulos

Ahora tenemos una copia de un proyecto con submódulos y colaboraremos con nuestros compañeros de equipo tanto en el proyecto principal como en el proyecto de submódulo.

Llegada de los Cambios de Upstream

El modelo más simple de usar submódulos en un proyecto sería si simplemente consumiera un subproyecto y quisiera obtener actualizaciones de él de vez en cuando, pero en realidad no estuviera modificando nada en el proceso. Veamos un ejemplo simple de esto.

Si desea buscar trabajo nuevo en un submódulo, puede acceder al directorio y ejecutar git fetch y git merge en la rama upstream para actualizar el 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(+)

Ahora, si vuelve al proyecto principal y ejecuta git diff --submodule puede ver que el submódulo se actualizó y obtener una lista de commits que se le agregaron. Si no desea escribir --submodule cada vez que ejecuta git diff, puede establecerlo como el formato predeterminado configurando el valor de configuración diff.submodule en “log”.

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

Si hace “commit” en este punto, bloqueará el submódulo para que tenga el nuevo código cuando otras personas lo actualicen.

También hay una forma más sencilla de hacer esto si prefiere no buscar y fusionar manualmente en el subdirectorio. Si ejecuta git submodule update --remote, Git irá a sus submódulos y buscará y actualizará todo por usted.

$ 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 asumirá de forma predeterminada que desea actualizar la rama master del repositorio de submódulos. Sin embargo, puede establecer esto en algo diferente si lo desea. Por ejemplo, si desea que el submódulo DbConnector rastree la rama “stable” del repositorio, puede configurarlo en su archivo .gitmodules (para que todos los demás también lo rastreen), o simplemente en su archivo .git/config local. Vamos a configurarlo en el archivo .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'

Si deja de lado los -f .gitmodules, solo hará el cambio por usted, pero probablemente tenga más sentido rastrear esa información con el repositorio para que todos los demás también lo hagan.

Cuando ejecutamos git status en este punto, Git nos mostrará que tenemos “new commits” en el 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")

Si configura status.submodulesummary, Git también le mostrará un breve resumen de los cambios a sus 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

En este punto, si ejecuta git diff podemos ver que hemos modificado nuestro archivo .gitmodules y también que hay un número de commits que hemos eliminado y estamos listos para hacer “commit” a nuestro proyecto de 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

Esto es muy bueno ya que podemos ver el registro de los commits a los que estamos a punto de hacer “commit” en nuestro submódulo. Una vez hecho el “commit”, puede ver esta información también cuando ejecuta 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 intentará por defecto actualizar todos sus submódulos cuando ejecute git submodule update --remote así que si tiene muchos de ellos, quizás quiera pasar el nombre del submódulo que desea intentar actualizar.

Trabajando en un Submódulo

Es bastante probable que si está utilizando submódulos, lo esté haciendo porque realmente desea trabajar en el código dentro del submódulo al mismo tiempo que está trabajando en el código dentro del proyecto principal (o en varios submódulos). De lo contrario, probablemente usaría un sistema de administración de dependencias más simple (como Maven o Rubygems).

Así que ahora veamos un ejemplo de cómo hacer cambios en el submódulo al mismo tiempo que al proyecto principal y de hacer “commit” y publicar esos cambios conjuntamente.

Hasta ahora, cuando ejecutamos el comando git submodule update para obtener cambios de los repositorios de submódulos, Git obtendría los cambios y actualizaría los archivos en el subdirectorio, pero deja el sub-repositorio en lo que se llama un estado “detached HEAD”. Esto significa que no hay una rama de trabajo local (como “master”, por ejemplo) que rastrea los cambios. Por lo tanto, los cambios que realice no serán rastreados correctamente.

Para configurar su submódulo para que sea más fácil entrar y piratear, necesita hacer dos cosas. Necesita ingresar a cada submódulo y verificar una rama para trabajar. Entonces necesita decirle a Git qué hacer si ha hecho cambios y luego git submodule update --remote extrae un nuevo trabajo de upstream. Las opciones son que puede combinarlas en su trabajo local, o puede tratar de volver a establecer la base de su trabajo local además de los nuevos cambios.

Primero que todo, vayamos a nuestro directorio de submódulos y verifiquemos una rama.

$ git checkout stable
Switched to branch 'stable'

Probemos con la opción “merge”. Para especificarlo manualmente, solo podemos agregar la opción --merge a nuestra llamada update. Aquí veremos que hubo un cambio en el servidor para este submódulo y se fusionó.

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

Si vamos al directorio de DbConnector, tenemos los nuevos cambios ya fusionados en nuestra rama stable local. Ahora veamos qué sucede cuando hacemos nuestro propio cambio local a la biblioteca y alguien más impulsa otro cambio en sentido ascendente al mismo tiempo.

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

Ahora, si actualizamos nuestro submódulo podemos ver qué sucede cuando hemos realizado un cambio local y upstream también tiene un cambio que necesitamos incorporar.

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

Si olvida --rebase o --merge, Git simplemente actualizará el submódulo a lo que esté en el servidor y restablecerá su proyecto a un estado detached HEAD.

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

Si esto sucede, no se preocupe, simplemente puede volver al directorio y verificar su rama nuevamente (que aún contendrá su trabajo) y combinar o rebasar origin/stable (o cualquier rama remota que desee) manualmente.

Si no ha hecho “commit” a los cambios en su submódulo y ejecuta una actualización de submódulo que podría causar problemas, Git buscará los cambios pero no sobrescribirá el trabajo no guardado en el directorio de su 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'

Si realizó cambios que entran en conflicto con algo cambiado en upstream, Git le informará cuándo ejecutar la actualización.

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

Puede acceder al directorio de submódulos y solucionar el conflicto tal como lo haría normalmente.

Publicando Cambios de Submódulo

Ahora tenemos algunos cambios en nuestro directorio de submódulos. Algunos de estos fueron enviados desde el inicio por nuestras actualizaciones y otros fueron hechos localmente y todavía no están disponibles para nadie, ya que aún no los hemos promocionado.

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > updated setup script
  > unicode support
  > remove unnecessary method
  > add new option for conn pooling

Si hacemos “commit” en el proyecto principal y lo elevamos sin elevar los cambios del submódulo también, otras personas que intenten verificar nuestros cambios estarán en problemas ya que no tendrán forma de obtener los cambios del submódulo de los que dependen . Esos cambios solo existirán en nuestra copia local.

Para asegurarse de que esto no ocurra, puede pedirle a Git que verifique que todos sus submódulos se hayan elevado correctamente antes de empujar el proyecto principal. El comando git push toma el argumento --recurse-submodules que puede configurarse como “check” u “on-demand”. La opción “check” hará que push simplemente falle si alguno de los cambios cometidos del submódulo no ha sido elevado.

$ 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 puede ver, también nos da algunos consejos útiles sobre lo que podríamos hacer a continuación. La opción simple es ir a cada submódulo y empujar manualmente a los controles remotos para asegurarse de que estén disponibles externamente y luego probar este empuje de nuevo.

La otra opción es usar el valor “on-demand”, que intentará hacer esto por usted.

$ 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

Como puede ver allí, Git entró en el módulo DbConnector y lo empujó antes de empujar el proyecto principal. Si ese empuje del submódulo falla por algún motivo, el empuje del proyecto principal también fallará.

Fusionando Cambios de Submódulo

Si cambia una referencia de submódulo al mismo tiempo que otra persona, puede tener algunos problemas. Es decir, si los historiales de los submódulos han divergido y les han hecho “commit” a ramas divergentes en un súper proyecto, puede tomar un poco de trabajo arreglarlo.

Si uno de los commits es un antecesor directo del otro (una fusión de avance rápido), entonces Git simplemente elegirá el último para la fusión, por lo que funcionará bien.

Sin embargo, Git no intentará siquiera una fusión trivial para usted. Si los commits del submódulo divergen y necesitan fusionarse, obtendrá algo que se ve así:

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

Básicamente, lo que sucedió aquí es que Git ha descubierto que las dos ramas registran puntos en la historia del submódulo que son divergentes y necesitan fusionarse. Lo explica como “merge following commits not found”, lo cual es confuso, pero lo explicaremos en un momento.

Para resolver el problema, debe averiguar en qué estado debería estar el submódulo. Curiosamente, Git no le da demasiada información para ayudarle aquí, ni siquiera los SHA-1 de las commits de ambos lados de la historia. Afortunadamente, es fácil de entender. Si ejecuta git diff, puede obtener los SHA-1 de los commits registrados en ambas ramas que estaba intentando fusionar.

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

Entonces, en este caso, eb41d76 es el “commit' en nuestro submódulo que nosotros teníamos y c771610 es el ``commit” que tenía upstream. Si vamos a nuestro directorio de submódulos, ya debería estar en eb41d76 ya que la fusión no lo habría tocado. Si por alguna razón no es así, simplemente puede crear y verificar una rama que lo señale.

Lo que es importante es el SHA-1 del “commit” del otro lado. Esto es lo que tendrá que fusionar y resolver. Puede simplemente probar la fusión con el SHA-1 directamente, o puede crear una rama para él y luego intentar fusionar eso. Sugeriríamos esto último, aunque sea para poder hacer un mejor mensaje de “commit” de fusión.

Por lo tanto, accederemos a nuestro directorio de submódulos, crearemos una rama basada en ese segundo SHA-1 de git diff y fusionaremos manualmente.

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610
(DbConnector) $ git merge try-merge
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

Aquí tenemos un conflicto de fusión real, por lo que si resolvemos eso y le hacemos “commit”, podemos simplemente actualizar el proyecto principal con el 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. Primero resolvemos el conflicto

  2. Luego volvemos al directorio principal del proyecto

  3. Podemos verificar los SHA-1 nuevamente

  4. Resolver la entrada conflictiva del submódulo

  5. Commit a nuestra fusión

Puede ser un poco confuso, pero realmente no es muy difícil.

Curiosamente, hay otro caso que maneja Git. Si existe un “commit” de fusión en el directorio del submódulo que contiene ambos commits en su historial, Git lo sugerirá como posible solución. Se ve que en algún punto del proyecto del submódulo, alguien fusionó las ramas que contienen estos dos commits, así que tal vez querrá esa.

Esta es la razón por la cual el mensaje de error de antes era “fusionar los siguientes commits no encontrado”, porque no podía hacer esto. Es confuso porque ¿quién esperaría que intentara hacer esto?

Si encuentra un único commit de fusión aceptable, verá algo como esto:

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

Lo que está sugiriendo que haga es actualizar el índice como si hubiera ejecutado git add, que borra el conflicto y luego haga “commit”. Sin embargo, probablemente no debería hacer esto. También puede acceder fácilmente al directorio de submódulos, ver cuál es la diferencia, avanzar rápidamente a este “commit”, probarlo correctamente y luego hacerle “commit”.

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

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

Esto logra lo mismo, pero al menos de esta manera puede verificar que funcione y que tenga el código en el directorio de su submódulo cuando haya terminado.

Consejos de Submódulo

Hay algunas cosas que puede hacer para facilitar el trabajo con los submódulos.

Submódulo Foreach

Hay un comando de submódulo foreach para ejecutar algún comando arbitrario en cada submódulo. Esto puede ser realmente útil si tiene un varios submódulos en el mismo proyecto.

Por ejemplo, digamos que queremos comenzar una nueva característica o hacer una corrección de errores y tenemos trabajo sucediendo en varios submódulos. Podemos esconder fácilmente todo el trabajo en todos nuestros 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

Entonces podemos crear una nueva rama y cambiar a ella en todos nuestros 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'

¿Entiende la idea? Una cosa realmente útil que puede hacer es producir una buena diff unificada de lo que ha cambiado en su proyecto principal y todos sus subproyectos también.

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

Aquí podemos ver que estamos definiendo una función en un submódulo y llamándola en el proyecto principal. Obviamente, este es un ejemplo simplificado, pero con suerte le dará una idea de cómo esto puede ser útil.

Alias Útiles

Es posible que desee configurar algunos alias para algunos de estos comandos, ya que pueden ser bastante largos y no puede establecer opciones de configuración para la mayoría de ellos para que sean predeterminados. Cubrimos la configuración de alias de Git en Alias de Git, pero aquí hay un ejemplo de lo que puede querer configurar si planea trabajar mucho con submódulos en 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'

De esta forma, simplemente puede ejecutar git update cuando desee actualizar sus submódulos, o git push para presionar con la comprobación de dependencia de submódulos.

Problemas con los Submódulos

Sin embargo, el uso de submódulos no deja de tener problemas.

Por ejemplo, cambiar ramas con submódulos en ellos también puede ser complicado. Si crea una nueva rama, agrega un submódulo allí y luego vuelve a una rama sin ese submódulo, aún tiene el directorio de submódulo como un directorio sin seguimiento:

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'adding crypto library'
[add-crypto 4445836] adding crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

Eliminar el directorio no es difícil, pero puede ser un poco confuso tener eso allí. Si lo quita y luego vuelve a la rama que tiene ese submódulo, necesitará ejecutar submodule update --init para repoblarlo.

$ git clean -fdx
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

De nuevo, no es realmente muy difícil, pero puede ser un poco confuso.

La otra advertencia principal con la que se topan muchas personas es pasar de subdirectorios a submódulos. Si ha estado rastreando archivos en su proyecto y desea moverlos a un submódulo, debe tener cuidado o Git se enojará con usted. Supongamos que tiene archivos en un subdirectorio de su proyecto y desea cambiarlo a un submódulo. Si elimina el subdirectorio y luego ejecuta submodule add, Git le grita:

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

Primero debe abandonar el directorio CryptoLibrary. Luego puede agregar el 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.

Ahora supongamos que hizo eso en una rama. Si intenta volver a una rama donde esos archivos todavía están en el árbol en lugar de un submódulo – obtiene este error:

$ 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

Puede forzarlo a cambiar con checkout -f, pero tenga cuidado de no tener cambios no guardados allí ya que podrían sobrescribirse con ese comando.

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

Luego, cuando vuelve, obtiene un directorio CryptoLibrary vacío por alguna razón y git submodule update tampoco puede arreglarlo. Es posible que deba acceder al directorio de su submódulo y ejecutar un git checkout . para recuperar todos sus archivos. Puede ejecutar esto en un script submodule foreach para ejecutarlo en múltiples submódulos.

Es importante tener en cuenta que los submódulos de estos días mantienen todos sus datos de Git en el directorio .git del proyecto superior, por lo que a diferencia de muchas versiones anteriores de Git, la destrucción de un directorio de submódulos no perderá ninguna rama o “commit” que tenga.

Con estas herramientas, los submódulos pueden ser un método bastante simple y efectivo para desarrollar en varios proyectos relacionados pero separados simultáneamente.