Git Subtree
Introducción a Git subtree: ventajas, desventajas y diferencias con submodule. Aprende a añadir, actualizar y contribuir cambios.
Como se mencionó en la página anterior, Git Submodule es útil para casos específicos. Para rastrear dependencias de software dentro de un único repositorio, muchos desarrolladores prefieren Git Subtree.
Esta página explica qué es un Git subtree, cuándo usarlo en lugar de un submodule, y cómo añadir, actualizar y contribuir cambios de vuelta al subtree. También cubre el rebase de un repositorio que contiene subtrees y las opciones de comando que más se utilizan.
¿Qué es Git Subtree?
Git Subtree es una alternativa a Git Submodule. Permite anidar un repositorio dentro de otro como un subdirectorio, conservando el historial del proyecto embebido ("sub"). Es una de las formas de rastrear el historial de dependencias de software.
La diferencia clave con los submodules es que un subtree es simplemente un directorio. A diferencia de los submodules, los subtrees no necesitan un archivo .gitmodules ni gitlinks especiales en tu repositorio. Los archivos viven directamente en tu árbol de trabajo, se confirman junto con todo lo demás y viajan con cada clone, branch y merge de forma automática. Cualquier persona que clone tu repositorio obtiene el código de la dependencia sin ejecutar ningún comando adicional.
Por debajo, git subtree es un comando de porcelana (un envoltorio) sobre operaciones estándar como git merge, git read-tree y git filter-branch/split. No necesitas aprender un nuevo formato de almacenamiento, solo unos pocos comandos nuevos.
Con submodules, un clone te da un directorio vacío hasta que ejecutas git submodule update --init. Con subtrees, los archivos ya están presentes. Esa única diferencia es la que genera la mayoría de las ventajas y desventajas que se mencionan a continuación.
Subtree vs. Submodule
Ambos enfoques incorporan un repositorio dentro de otro, pero presentan compromisos opuestos. La elección depende de quién consume el repositorio y con qué frecuencia el código fluye de vuelta hacia upstream.
| Aspecto | Git Subtree | Git Submodule |
|---|---|---|
Archivos tras el clone | Ya presentes | Vacíos hasta submodule update --init |
| Metadatos adicionales | Ninguno | .gitmodules + gitlinks |
| Fija un commit upstream exacto | No (el historial se fusiona) | Sí (registra un SHA) |
| Contribuir cambios upstream | Manual (git subtree split + push) | Natural (commit dentro del submodule) |
| Tamaño del repositorio | Mayor (el historial del subproyecto se copia) | Menor (solo un puntero) |
| Curva de aprendizaje para consumidores | Ninguna | Deben aprender los comandos de submodule |
Una regla general: prefiere subtrees cuando principalmente consumes una dependencia y quieres que cada clone funcione sin más; prefiere submodules cuando el subproyecto se co-desarrolla activamente y necesitas fijar o enviar cambios a un commit upstream exacto.
Por qué usar Git Subtree
Ventajas
- Compatible con Git 1.7.10 y versiones posteriores (el comando
subtreese incluye con Git). - Flujo de trabajo simple para quienes clonan tu repositorio — no hay comandos adicionales que aprender.
- El código del subproyecto está disponible inmediatamente después de clonar el superproyecto.
- No añade nuevos archivos de metadatos (por ejemplo,
.gitmodules). - Permite modificar la dependencia en el lugar sin necesidad de un checkout separado del repositorio.
Desventajas
- Requiere aprender una nueva estrategia de fusión y algunos comandos específicos de
subtree. - Contribuir código de vuelta upstream es un proceso de varios pasos (
git subtree split, luego push). - El código del superproyecto y del subproyecto se mezclan en el mismo repositorio, lo que aumenta su tamaño y puede ensuciar el historial.
Cómo añadir un Subtree
Supongamos que existe un proyecto externo y quieres añadirlo a tu repositorio bajo un directorio específico.
Por ejemplo, para añadir una extensión de Vim a un repositorio que almacena tu configuración de Vim, ejecuta:
git subtree add --prefix .vim/bundle/example https://github.com/Example/vim-example.git master --squashLas partes de este comando:
--prefix .vim/bundle/example— el directorio donde vivirá el subproyecto. Esta opción es obligatoria para cada comandosubtree.https://github.com/Example/vim-example.git— el repositorio fuente (una URL o, más adelante, un remote con nombre).master— la rama (o commit/etiqueta) de la que se extrae.--squash— colapsa el historial completo del subproyecto en un único commit para no contaminar tu log. Omítelo si deseas que todo el historial upstream se fusione.
Con --squash, Git registra el SHA-1 de master en ese momento como referencia futura y produce dos commits — la importación comprimida y la fusión:
commit 6d7054b3acea64e2e31f4d6fb2e3be12e5865e87
Merge: 87fa91e ef86deb
Author: Ann Smith<[email protected]m>
Date: Tue Jun 10 13:37:03 2016 +0200
Merge commit 'fe67ddf158faccff4082d78a25c45d8cd93e8ba8' as '.vim/bundle/example'
commit fe67ddf158faccff4082d78a25c45d8cd93e8ba8
Author: Ann Smith<[email protected]m>
Date: Tue May 12 13:37:03 2015 +0200
Squashed '.vim/bundle/example/' content from commit b999b09
git-subtree-dir: .vim/bundle/example
git-subtree-split: b999b09cd9d69f359fa5668e81b09dcfde455ccaActualizar un Subtree
Para actualizar la subcarpeta a la última versión del repositorio hijo, ejecuta un subtree pull con el mismo prefijo y fuente:
git subtree pull --prefix .vim/bundle/example https://github.com/Example/vim-example.git master --squashEsto recupera la rama upstream y la fusiona en tu directorio de subtree, creando un nuevo commit de fusión. Usa siempre el mismo --prefix y la misma elección de --squash/sin---squash que usaste en subtree add, o Git no alineará los historiales correctamente.
Ten en cuenta que git subtree almacena los IDs de commit del subproyecto en los metadatos del mensaje de commit, no referencias simbólicas. Para encontrar el nombre de rama o etiqueta asociado a un commit almacenado, consulta el remote:
git ls-remote https://github.com/Example/vim-example.git | grep <commit-sha>Sustituye <commit-sha> por el hash de commit real de la línea git-subtree-split: de tu commit de importación.
Rebase tras Git Subtree
Para rebasar un repositorio que contiene subtrees, usa el modo --interactive de git rebase:
git rebase --interactive HEAD~5En el editor, puedes eliminar o comprimir los commits de fusión del subtree, luego guarda y ejecuta:
git rebase --continueDado que el rebase reescribe el historial, los commits de fusión del subtree pueden eliminarse o modificarse. Tras dicha reescritura, generalmente es necesario restablecer el subtree volviendo a ejecutar git subtree add o git subtree pull. Ten en cuenta que la estructura de commits modificada también puede provocar conflictos de fusión durante el rebase, por lo que se recomienda rebasar ramas que no hayan sido publicadas ni compartidas.
Opciones comunes
| Opción | Descripción |
|---|---|
-q, --quiet | Suprime los mensajes de resultado innecesarios en stderr. |
-d, --debug | Produce mensajes de depuración adicionales en stderr. |
-P <prefix>, --prefix=<prefix> | Define la ruta en el repositorio al subtree que deseas manipular. Obligatorio para todos los comandos. |
-m <message>, --message=<message> | Especifica <message> como el mensaje de commit para el commit de fusión. Válido para add, merge y pull. |
--squash | Importa el subproyecto como un único commit en lugar de fusionar su historial completo. |
Uso de Git Subtree sin seguimiento remoto
Añade el git subtree en una carpeta de prefijo específica. Usa el indicador --squash para preservar todo el historial del subproyecto en tu repositorio principal:
git subtree add --prefix .vim/bundle/vim-double-upon https://hostname.org/example/vim-plugins.git master --squashEl comando realiza una recuperación y comprime el historial. La salida típica muestra el progreso de la recuperación seguido de la confirmación de la adición:
git fetch https://hostname.org/example/vim-plugins.git master
warning: no common commits
remote: Counting objects: 325, done.
remote: Compressing objects: 100% (145/145), done.
remote: Total 325 (delta 101), reused 313 (delta 89)
Receiving objects: 100% (325/325), 61.47 KiB, done.
Resolving deltas: 100% (110/110), done.
From https://hostname.org/vim-plugins.git
* branch master -> FETCH_HEAD
Added dir '.vim/bundle/vim-double-upon'Esto crea un commit de fusión al comprimir todo el historial del subproyecto en uno solo:
3bca0ad [4 minutes ago] (HEAD, stree) Merge commit 'fa2f5dc4f1b94356bca8a440c786a94f75dc0a45' as '.vim/bundle/vim-double-upon' [John Brown]
fa2f5dc [4 minutes ago] Squashed '.vim/bundle/vim-double-upon/' content from commit 13189ec [John Brown]Para actualizar el código del plugin desde el repositorio upstream, ejecuta un git subtree pull:
git subtree pull --prefix .vim/bundle/vim-double-upon https://hostname.org/example/vim-plugins.git master --squashContribuir cambios de vuelta a upstream
Dado que el código del subproyecto está mezclado en tu repositorio, no puedes simplemente enviarlo de vuelta. Primero debes extraer los cambios que afectaron al directorio del subtree en una rama independiente con git subtree split:
git subtree split --prefix .vim/bundle/vim-double-upon -b split-branchEsto reescribe solo los commits que afectan a .vim/bundle/vim-double-upon en una nueva rama (split-branch) cuyas rutas son relativas a la raíz del subproyecto. Luego puedes hacer push de esa rama al repositorio upstream:
git push https://hostname.org/example/vim-plugins.git split-branch:masterEsta extracción unidireccional es la razón por la que "contribuir cambios upstream" aparece como desventaja — no existe sincronización bidireccional integrada.
Para acortar los comandos cotidianos de add/pull/push, añade el subproyecto como un remote.
Añadir el subproyecto como remote
Registrar la fuente como un remote con nombre acorta cada comando posterior — referencias vim-double-upon en lugar de la URL completa. El indicador -f lo recupera inmediatamente:
git remote add -f vim-double-upon https://hostname.org/example/vim-plugins.gitAñade el subtree:
git subtree add --prefix .vim/bundle/vim-double-upon vim-double-upon master --squashActualiza el subproyecto así:
git fetch vim-double-upon master
git subtree pull --prefix .vim/bundle/vim-double-upon vim-double-upon master --squashResumen
Git Subtree es una alternativa a Git Submodule. Mientras que un submodule mantiene el subproyecto como un puntero separado que debe inicializarse, un subtree lo mantiene como un directorio regular presente en cada clone. Usa git subtree add para importar una dependencia, git subtree pull para actualizarla y git subtree split + push para enviar cambios upstream — no existe sincronización bidireccional nativa. Para aprender más, explora Git Branch y Git Merge, que son las operaciones subyacentes que impulsan los comandos de subtree.