Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:git_control_codigo_fuente_programadores:conceptos_avanzados

¡Esta es una revisión vieja del documento!


Tabla de Contenidos

Conceptos Avanzados

Módulo perteneciente al curso Git: Control de código fuente para programadores

En este módulo vamos a estudiar algunos comandos avanzados, como enmendar commits, revertir cambios, resetear ramas, elegir granularmente cambios a transportar… Sin ellos, por supuesto, podrías sobrevivir en el mundo de Git, pero seguro que te será muy útil tenerlos en cuenta para cualquier contratiempo que sufras a la hora de trabajar con nuestro gestor de código fuente favorito.

También vamos a ver una breve introducción a los git-hooks, que te permiten ejecutar tareas de manera automática cuando haya cambios en un repositorio.

Por el medio estudiaremos algunas técnicas que, si bien no vas a utilizar seguramente en tu día a día, cuando las necesites te alegarás mucho de conocer.

Adicionalmente también estudiaremos el flujo de trabajo más famoso de Git, Git Flow, que ya hemos mencionado en alguna ocasión. En Git Flow se juntan todos los conceptos que hemos visto hasta ahora para formar el flujo de trabajo por excelencia que, aunque también criticado y no vale para todo el mundo ni para todos los casos, sí o sí debes conocer.

Al final del módulo, junto con las prácticas, te entregamos un PDF con un diagrama de decisión con el que podrás decidir, en multitud de situaciones, cuál es el mejor camino para solucionar un lío que hayas causado en tu repositorio, y que seguro que te resulta de gran utilidad.

Amend y Revert

Pueden existir casos en los que necesitemos realizar modificaciones sobre un commit que hayamos hecho recientemente y del que no estemos muy satisfechos. Para ello Git nos proporciona dos técnicas con las que podremos jugar a la hora de modificar dicho registro, aunque no sin ciertas restricciones, lógicamente.

Amend

Amend es una opción del propio comando git-commit que nos servirá para modificar el último commit que hayamos realizado.

git commit --amend -m "Nuevo mensaje"

Esta instrucción modifica el mensaje del último commit que tenga en local y que no hayas enviado todavía al repo remoto. Su uso típico es corregir alguna errata o registrar algo que se te olvidó y que recuerdas justo tras haberle dado a ENTER o poco después.

También se puede utilizar para añadir archivos al último commit en caso de que se te haya olvidado alguno. En ese caso llega con añadir los que te falten al área de staging:

git add archivo1.ext archivo2.ext

Acuérdate de que puedes usar patrones para añadir varios archivos con un nombre similar de golpe, o simplemente hacer una lista como se ve en lo anterior. Lo vimos al principio del todo del curso con el comando git-add.

Cuando hayas añadido los archivos que te faltasen, indicas que quieres modificar el commit anterior en base a esto, escribiendo:

git commit --amend

lo cual añadirá los archivos al último commit que todavía está en tu equipo.

Si haces esto así, sin indicar un mensaje, desde línea de comandos de Git (Git Bash) o en Linux, la opción --amend te abre un editor vi por si quieres modificar el mensaje también. Para salir y terminar con la operación debes pulsar ESC y a continuación :x o :q para que salga y grabe los cambios.

Este comando tiene implicaciones que hay que tener en cuenta a la hora de usarlo. Hay que pensar que Git por encima de todo es un VCS, por tanto su principal función es la de mantener un control de los cambios que ocurren en el repositorio.

El comando amend, al igual que el rebase, se consideran comandos destructivos ya que modifican el registro de los commits sin dejar rastro. Por tanto su uso debe ser controlado.

Si cuando te des cuenta de que quieres hacer la corrección, ya hubieses hecho push del commit al repositorio remoto, este comando se comportaría de forma diferente. En vez de rehacer el commit sin dejar rastro del cambio, lo que hará será un nuevo commit a través de un merge para evitar la pérdida del histórico. Es decir, te quedará rastro del commit anterior, pero habrá uno nuevo para enmendarlo si es importante para ti hacerlo.

Revert

El comando revert es un comando mucho más controlado que amend. Su función es la de crear un commit que rehace los cambios introducidos por otro. ¿Qué significa esto? Pues si un commit añade o elimina un cambio, git-revert creará otro commit haciendo justamente lo contrario para dejarlo como estaba antes:

git revert <id-commit>

Una gran diferencia entre el amend y el revert es que el segundo sí deja un registro del cambio que se está aplicando, lo que se adapta mucho mejor a la filosofía de Git, y además lo puedes aplicar sobre cualquier commit del repositorio (aunque por supuesto cuanto más antiguo más probabilidades de conflicto).

Reset y Cherry Pick

Estos son comandos adicionales que pueden serte útiles en algunos momentos en concreto para resolver algunas situaciones difíciles.

Reset

El comando reset se utiliza para eliminar un cambio. Es otro comando destructivo al igual que amend o rebase, aunque hay modos más destructivos que otros. El comando típico es el siguiente:

git reset --hard HEAD^

Que se utiliza para eliminar todos los cambios que tienes en tu área de preparación o directorio de trabajo, para dejar el repositorio limpio de cambios, por decirlo más claramente.

Recuerda que HEAD^ significaba el commit anterior al actual en la primera rama en caso de haber más de una. O sea, reajusta el historial del repositorio para dejarlo justo en el punto anterior de donde nos encontramos, que es a donde apunta HEAD^. Podrías usar otra referencia relativa si quisieras ir aún más atrás.

Otra variante es:

git reset HEAD^ --soft

que eliminará lo que está en el último commit que hayas hecho, pero dejará los archivos modificados de nuevo en el área de trabajo. Esto es muy útil si no has subido el commit todavía al repositorio remoto, quieres deshacer el último commit y mantener todos los cambios que había, quizá para hacer un retoque adicional o para eliminar solo algún pequeño detalle.

Finalmente, otra variante más, intermedia entre las dos anteriores es:

git reset --mixed HEAD^

Este último comando deshará también el último commit que hayas hecho, y además añadirá sus cambios al área de preparación. Es muy útil para deshacer un commit que hubieras realizado sin querer, y al mismo tiempo impide que pierdas los cambios que tenía, pero sin meterlos de nuevo en el área de trabajo.

¿Y qué pasa si ya he enviado el commit al remoto?

Todos estos comandos que acabamos de ver sólo actúan sobre la historia local de tu repositorio, es decir, se utilizan para deshacer commits que NO se hayan subido todavía a un repositorio remoto. Entonces, ¿cómo podemos arreglar el commit en caso de que ya esté en el repositorio a la vista de todos nuestros compañeros?

En primer lugar debemos destruir el commit localmente como hemos visto antes, con:

git reset --hard HEAD^

Así tenemos lo que queríamos en local.

A continuación debemos forzar el envío de este cambio local al repositorio remoto de origen con:

git push origin -f

En condiciones normales un push al origen falla si el commit que está “arriba” no es un ancestro del commit que se está intentado subir. Es decir, si el repo remoto va por delante del repo local. Con el parámetro -f (o --force, es lo mismo) forzamos a que esta comprobación no se haga.

Este comando es muy peligroso y puede tener efectos destructivos y que se pierdan los commits que van por delante de nosotros, cosa que es justo lo que queremos en este caso. Además si alguien ha sincronizado ya el repositorio y tiene en local el commit que queremos destruir, podemos causarle problemas.

Esto solamente hay que hacerlo cuando el problema que queremos arreglar sea grave como para justificar su uso. Habría que llevarlo a cabo lo antes posible para evitar que el repositorio remoto siga recibiendo cambios, y si trabajamos con otras personas, lo suyo sería avisarles para que eviten tocar el remoto mientras realizamos la operación.

Cherry Pick

El comando cherry-pick sirve para reproducir el mismo commit en una rama distinta. Es muy útil para llevar “parches” o commits en concreto a otros sitios:

git cherry-pick <commit>

Este comando reproducirá el commit que especifiques en la rama en la que te encuentres en ese momento.

Vamos a verlos en acción….

Plantillas para los commits

Escribir mensajes asociados a los commits con los cambios que guardas en tu sistema de control de código fuente es muy parecido a comentar el código de tus aplicaciones: un arte que requiere sentido común, capacidad de síntesis y una pizca de empatía. Y cuando se necesitan estas tres cosas a la vez para hacer algo, lo mejor es asumir que mucha gente lo va a hacer mal…

Por eso, los proyectos de tu equipo pueden acabar con commits llenos de mensajes de este estilo:

  • Se ha modificado el código
  • Pequeños cambios
  • Se ha añadido una nueva característica
  • Solucionado el bug
  • Retocada documentación
  • El cielo es azul y el agua moja 😜

Es decir mensajes que, si bien estrictamente hablando describen lo que se ha hecho, no dejan de ser obviedades sin interés. Es como decir en los comentarios de tu código: “Declaro una variable de tipo entero”. Es cierto, refleja lo que se hace, pero no sirve absolutamente para nada.

No hacer este tipo de comentarios estériles es uno de los 10 mandamientos del control de código fuente.

Por este motivo es muy interesante que, a la hora de hacer un commit, todas las personas que trabajan en nuestro equipo puedan tener presentes las normas para escribir los mensajes de commit:

  • Cómo deben escribirlos
  • Qué información básica y extra deberían incluir
  • Cómo deberían prefijar cada mensaje
  • Qué implicaciones tiene el código
  • Etc…

Es decir, es muy importante disponer de una metodología clara y concisa que regule cómo deben ser estos mensajes para todo el equipo, y no dejarlo al azar o confiar en que todo el mundo va a reunir las cualidades mencionadas al principio de esta lección.

¿Cómo podemos conseguirlo y garantizar que todo el mundo tenga las normas en mente cada vez que realiza un commit?

Gracias a las plantillas de commit de Git.

Definiendo una plantilla de commit para Git

Generalmente, cuando vamos a hacer un commit tras añadir los archivos a la zona de staging, utilizamos la instrucción:

git commit -m "Mi mensaje de commit"

Pero, como sabes, si omitimos los parámetros y ponemos simplemente:

git commit

lo que ocurre es que se abre nuestro editor de código predeterminado con una plantilla de texto de Git con algunas instrucciones básicas, como esta:

Lo que tenemos es una primera línea en blanco, que será donde escribamos el verdadero mensaje de commit, seguida de una serie de comentarios con un resumen de los archivos que vamos a incluir en la operación.

Podemos aprovechar esta capacidad de Git para definir nuestra propia plantilla de commit de modo que, cada vez que alguien vaya a hacer uno, se abra el editor de código y nos muestre la información que nosotros queramos en lugar de la información por defecto.

Para lograrlo lo primero es definir esa plantilla que vamos a utilizar.

Crea un nuevo archivo de texto en blanco, en donde quieras, y ábrelo con tu editor de texto favorito. Puedes ponerle el nombre que desees, por ejemplo plantilla-commit.txt.

Invierte un buen rato en pensar bien qué vas a meter dentro, considerando la forma de trabajar que tenéis en tu organización y qué objetivos te interesa conseguir.

Al final, deberás reflejar con comentarios las normas que quieres que sigan las personas de tu equipo a la hora de hacer commits a Git, y dejando líneas en blanco en donde quieras que escriban.

Nota: se considera un comentario aquella línea que comience con un # o un ;.

Por ejemplo, te dejamos este de ejemplo que he hemos creado nosotros, y que quizá te pueda servir como base:

# ----------------------------------------------------------
# Primera línea: Algo breve pero descriptivo
#                de lo que se ha hecho
#                ¡Que sirva para algo! :-S
#                Que facilite las búsquedas si es preciso
# ----------------------------------------------------------
#    Debe empezar por uno de estos prefijos:
#    - WIP: si lleva trabajo sin terminar (Work In Progress)
#    - END: si es código final, listo para revisar
#
#    Además, según el tipo de código, debe llevar un de estos:
#    - #NNN: número de tarea a la que pertenece el trabajo
#    - BUG: cuando corrige un bug
#    - DOC: si se está tocando documentación
#    - REF: si es solo una refactorización
#    - IMG: si son retoques estéticos/de imagen
#    - PRF: retoques de rendimiento
#    - TST: código relacionado con tests
#    - CID: cambios al proceso de CI/CD
# ----------------------------------------------------------
#    Por ejemplo:
#    - WIP:#123: Nuevo formulario de crear usuario
#    - END:BUG: Problema con cálculo de hash
#    - WIP:DOC: Actualizado manual del alumno
# ----------------------------------------------------------


# ----------------------------------------------------------
# Info extra (opc)
# ----------------------------------------------------------
#    Si la línea anterior no tiene todo lo necesario
#    o si el id de tarea no existe, ofrecer detalles sobre
#    - por qué se han hecho los cambios
#    - detalles importantes sobre cambios
#    - posible rotura de compatibilidad
#    - cambios de funcionalidad
#    - etc...
# ----------------------------------------------------------


# ----------------------------------------------------------
# Recursos relacionados - Donde ver información extra (opc)
# ----------------------------------------------------------
#    Por ejemplo, urls a documentos internos/externos
#    sobre técnicas utilizadas, funcionalidad,
#    casos de uso, docs técnicos, etc..
# ----------------------------------------------------------

Es un poco largo, pero eso no te debe preocupar ya que los comentarios se eliminan automáticamente antes de hacer el commit, por lo que nunca llegarán al repositorio remoto ni ocuparán espacio allí 🎉

Como ves, en este caso describimos cómo se debe etiquetar el mensaje, con uno o dos prefijos que luego permiten localizar rápidamente los tipos de cosas que se hacen en el código

Se les recuerda que la frase principal, en la primera línea, debe ser breve pero descriptiva, o no servirá de nada. Luego pueden meter la información extra que sea precisa, pero esta primera es fundamental para saber rápidamente de qué va cada uno y para poder localizar sin dificultad, ojeando un listado de commits o con una búsqueda, aquello que nos interesa.

Recuerda que los commits siempre deben ser muy atómicos, pequeños y no mezclar tareas diferentes.

Bien, ahora solo nos queda hacer que Git utilice esta plantilla para mostrarla en el editor de commits. Esto se consigue editando la configuración, abriendo una línea de comandos en uno de los repositorios donde la queramos utilizar, con una línea como esta:

git config commit.template ./plantilla-commit.txt

poniendo al final la ruta absoluta o relativa del archivo de plantilla que hemos creado. En este caso, como la plantilla la hemos copiado en la raíz del repositorio local, llega con ./ seguido del nombre del archivo.

Esto hará que, a partir de este momento, si hacemos un git commit, sin parámetros, se abra el editor por defecto con la plantilla de texto que hemos definido:

Ahora basta con escribir el mensaje siguiendo las directivas que aparecen en los comentarios, guardar y cerrar el archivo: se realizará el commit de manera normal.

Incluir la plantilla en cada repositorio - Opción 1

Esto está muy bien, pero requiere configuración manual en cada uno de los repositorios o en cada uno de los ordenadores para lograrlo, ya que Git (a propósito, por seguridad) no incluye ninguna manera de distribuir configuraciones específicas a través de un repositorio. Es decir, no permite incluir un archivo de configuración en el propio repo y que así todos los usuarios usen la misma automáticamente.

Podríamos haber modificado la configuración global de modo que este ajuste afecte a todos los repositorios del usuario actual, utilizando el modificador --global en el comando de configuración que acabamos de ver. Pero al final tenemos el mismo problema: cada desarrollador tiene que hacerlo en su propio equipo para que funcione, y además necesitamos tener el archivo en una ruta accesible por cada usuario en cada equipo.

Para solucionar esto, la mejor manera de poner en marcha esto para todo el que trabaje en un proyecto, es incluir en cada repositorio de código dos archivos:

  • La plantilla que hayamos creado.
  • Un archivo .bat (en Windows) o .sh (en Linux/Mac) que contenga el comando necesario para incluir la configuración en el repositorio.

En Linux/Mac:

#!/bin/bash
git config commit.template ./plantilla-commit.txt

En Windows:

@echo off
git config commit.template .\plantilla-commit.txt

Debemos instruir a todo el equipo que, cada vez que clonen un repositorio, ejecuten el .bat o .sh para dejarlo bien configurado. No es lo ideal, ya que es posible que se olviden o sean negligentes al hacerlo, pero es la menos mala de las soluciones.

Además, la principal ventaja de hacerlo por repositorio y no mediante la configuración global, es que podemos hacer que cada repositorio tenga su propia plantilla de commit, que puede ser diferente según el tipo de proyecto.

Incluir la plantilla en cada repositorio - Opción 2

Otra opción, si nos interesase distribuir una configuración más compleja para Git, con muchos otros valores, sería incluir un archivo .gitconfig en la raíz del repo (o con el nombre que queramos). Este contendrá los ajustes exactos que nos interese distribuir: la plantilla de commit que nos ocupa, el editor predeterminado, los alias que nos interesen, etc…

Estos valores los podemos copiar de nuestro archivo de configuración global o de dónde sea: es solo texto.

Ahora, para aplicarlo, podemos hacer una inclusión de estos valores en la configuración del repositorio:

git config --local include.path ../.gitconfig

Este comando, tal cual, lo metemos en un .bat (para Windows) y en un .sh (para los que usen Linux o Mac), y tendrán que ejecutarlo tras clonar el repositorio. Lo que hace es incluir nuestra configuración, tomada de este archivo, en la configuración local del repositorio.

Lo bueno de hacerlo así es que podemos meter configuraciones más complejas de manera muy simple con un solo comando, e ir cambiándolas más adelante, con el tiempo, si es que nos interesa.

Mejorar la productividad con alias

Aunque en muchas ocasiones vas a utilizar herramientas gráficas para trabajar con Git, también es cierto que en muchas otras deberás utilizar la terminal. Y recordar los parámetros exactos para muchos de los comandos de Git puede ser un reto.

Los alias de Git son una herramientas muy potente que te puede facilitar la vida. Al simplificar comandos complejos y permitir personalizaciones, los alias no solo ahorran tiempo, sino que también reducen la posibilidad de errores al escribir comandos repetitivos. Además, facilitan la adopción de buenas prácticas y flujos de trabajo más eficientes, lo que se traduce en una mayor productividad.

Gestionar "Alias" en Git

Los alias se establecen siempre a nivel global de tu máquina de desarrollo, para poder utilizarlos desde cualquier sitio.

Para crear un alias debemos utilizar el comando config que ya hemos visto en varias ocasiones. Por ejemplo:

git config --global alias.p 'push'

En este caso estamos definiendo un alias llamado simplemente p, que es un atajo para el comando push, que enviará los commits locales al repositorio remoto.

Fíjate en que hemos utilizado el switch --global, de modo que lo estamos definiendo para todo el equipo, no solo para el repositorio en el que nos encontramos ahora posicionados.

Una vez definido, podemos hacer un “push” simplemente escribiendo:

git p

Nos ahorramos escribir 3 letras! 😆 De acuerdo, no es mucho, pero por algo debemos empezar. Eso solo un ejemplo. En breve veremos alias más interesantes, no te preocupes.

Podemos conocer los alias que tenemos definidos actualmente escribiendo:

git config --global --get-regexp alias

que mostrará en tu terminal todos los alias y los comandos para los que sirven de atajo.

Finalmente, puedes eliminar un alias concreto con el parámetro --unset de la configuración:

git config --global --unset alias.p

que eliminará el alias indicado (p en este caso).

Nota: los alias se almacenan en el archivo de configuración global del sistema, que puedes encontrar en la ruta %userprofile%\.gitconfig en Windows o en ~/.gitconfig en Linux/macOS. Podemos editar directamente ese archivo en lugar de utilizar los comandos que acabamos de ver, pero ¡cuidadito!

Ahora que ya sabemos manejar alias, vamos a crear unos cuantos para subir de nivel nuestra productividad con Git…

Algunos alias útiles

1.- Hacer checkouts

Una tarea muy común en Git es cambiar de rama o examinar un commit usando el comando checkout. Este comando es largo (8 letras) y además complicado de escribir en español, por lo que un alias como este:

git config --global alias.co 'checkout'

nos vendrá muy bien.

Ahora podemos escribir simplemente:

git co develop/v2.3

para cambiarnos a la rama de desarrollo de la versión 2.3 de la aplicación que estamos desarrollando (suponiendo que les llamemos de ese modo develop/vx.y, claro).

O también podemos usar un simple guion como parámetro:

git co -

para movernos a la anterior rama en la que estuviésemos posicionados (un buen truco).

2.- Listar ramas remotas

Salvo que queramos crear ramas locales propias (ver punto siguiente), lo más habitual es que queramos crear una rama local que “trackee” una rama remota, de modo que nos traigamos sus cambios y luego todos los que hagamos localmente queden reflejados en el repositorio remoto cuando hagamos push.

Pero para esto, primero tenemos que saber qué ramas remotas tenemos disponibles y cuáles de ellas ya tenemos en local. Este alias nos ayudará:

git config --global alias.lb 'branch -a'

que se usaría como:

git lb

Esto nos mostrará un listado de todas las ramas disponibles en el repositorio actual, tanto locales como remotas (“lb” es de “List Branches”, o sea, listar ramas).

Si lo que queremos es ver tan sólo qué ramas remotas tenemos disponibles, podemos definir este otro alias:

git config --global alias.lrb 'ls-remote'

que nos mostraría un listado de todas las ramas y tags remotos que haya (lrb = List Remote Branches).

3.- Crear ramas y cambiar a éstas

Aunque lo anterior está muy bien, generalmente, para lo que más usaremos el comando checkout es para crear nuevas ramas y empezar a trabajar con ellas, por lo que a lo mejor preferirás definir otro alias de esta manera:

git config --global alias.cb 'checkout -b'

que se usaría así:

git cb NuevaRama

Lo que hará será crear una nueva rama local y cambiarte a ella de inmediato para seguir trabajando (lo de “cb” es por Create Branch, o sea, crear rama).

De todos modos, desde la versión 2.23 de Git tenemos un comando mucho más útil para hacer lo mismo: switch. Este es específico para cambiar de ramas y nos ayuda más que usar git branch y git checkout ya que lo que hace es cambiar a una rama local, pero también crearla y seguir a una remota en caso de que exista. Por ello, salvo que estés usando por algún motivo una versión antigua de Git, mi recomendación sería definir el alias cb, así:

git config --global alias.cb 'switch'

De este modo, si queremos crear una nueva rama o simplemente cambiarnos a una versión local de una rama remota será mucho más fácil (en este caso “cb” es por Change Branch, o sea, cambiar de rama, que de paso la crea si es necesario).

Si el nombre de rama que le pasamos no existe en local pero sí en remoto, ya de manera automática nos creará la rama local y empezará a “trackear” la rama remota desde la local, sin que tengamos que lanzar comandos largos y complicados, como pasaba antes con branch y checkout.

4.- Añadir todos los cambios

Lo primero que hay que hacer antes de un commit es añadir los archivos que necesitamos al área de staging de Git, mediante el comando git add -A. Esto añade todos los archivos nuevos, modificados y eliminados y los deja listos para hacer el commit.

Si queremos ahorrarnos todo eso puedes crear el alias:

git config --global alias.aa 'add -A'

y simplemente escribir:

git aa

cuando quieras añadir todos los cambios al área de staging (lo de “aa” es de “Add All”, pero puedes usar lo que prefieras, o incluso solo una “a” para hacer git a y listo).

Este acorta muy poco el comando, pero como se usa tanto a lo largo del día tiene un impacto importante en acelerar el trabajo.

5.- Deshacer el último commit local

Algo bastante común es que hagamos un commit y nos demos cuenta de que nos hemos olvidado de algo, o hemos metido cosas de más, etc…. En ese caso lo suyo es que, si no hemos enviado todavía el commit al repositorio remoto, usemos un reset para deshacerlo. Así dejamos de nuevo en el área de staging todos los cambios. antes.

O sea, se trata de volver a dejarlo todo como antes de hacer el commit, para poder rehacerlo a nuestro gusto.

Nota: un poco más adelante vamos a ver cómo arreglar casi cualquier cosa que te pueda ocurrir con Git, y veremos un ejemplo como este, incluyendo el caso de que el commit ya lo hayas enviado al remoto.

Si definimos este alias:

git config --global alias.rlc 'reset HEAD^ --soft'

Ahora un simple:

git rlc

y adiós commit erróneo.

Nota: a este alias le hemos llamado rlc porque es la abreviatura de Reset Last Commit, o deshacer el último commit. En este y todos los demás ejemplos crea alias que tengan sentido para ti y te resulten fáciles de recordar ¿vale?

Otro clásico es que estemos trabajando mucho tiempo en un commit, la cosa se complique, no lleguemos al resultado esperado y decidamos abandonar. En ese caso lo que nos suele interesar es dejarlo todo tal cual estaba antes de empezar a trabajar, de modo que se deshagan todos los cambios y aquí no ha pasado nada…

En este caso podemos definir otro alias muy similar al anterior, pero más destructivo, ya que no solo deshace el commit, sino que también deshace todos los cambios que hubiera (incluyendo recuperar archivos borrados):

git config --global alias.rlc 'reset HEAD^ --hard'

Mucho ojo al utilizarlo porque es muy destructivo.

6.- Enviar todos los cambios al remoto con un mensaje: el 3 en 1

Si hay algo que hacemos constantemente en Git es precisamente esta combinación de pasos:

  1. Añadir todos los cambios al staging
  2. Hacer commit de dichos cambios
  3. Enviar todo al servidor con push

Esto implica hacer uso de 3 comandos concretos (add, commit y push) con sus correspondientes parámetros, y en ese orden.

Por eso, un alias muy interesante que vas a utilizar todo el rato, consiste en crear la combinación de todos esos pasos en uno solo. A ese alias le hemos llamado send.

La forma de definirlo implica combinar más de un comando (hasta ahora habíamos utilizado solo 1).

En los alias de Git es posible combinar más de un comando. Pero para conseguirlo hay que ejecutarlos como comandos de terminal. Es decir, no sirve lo que hemos hecho hasta ahora (poner el nombre de un comando de Git), sino que es necesario utilizar el comando git completo.

Esto en la práctica implica que tenemos que crear un pequeño script de una línea que se ejecutará al invocar al alias.

Esto en Git se hace poniendo el prefijo ! delante del contenido del alias.

Así, por ejemplo, si quisiésemos hacer un tan solo el add y el commit con un mensaje genérico haríamos esto:

git config --global alias.wip '!git add -A && git commit -m ''Texto commit'''

Fíjate en que el texto va entre comillas simples dos veces, no comillas dobles. Es decir, estamos “escapeando” las comillas con otras comillas. Los && se usan para ejecutar tareas en serie, una tras otra.

Esto es muy útil pues nos permite crear un commit con todos los cambios actuales y un mensaje genérico (siempre el mismo).

Incluso podríamos añadir al final un && git push para que enviase el commit al repo remoto.

Pero y si queremos personalizar el mensaje que vamos a asignar al commit?

En ese caso necesitaremos obtenerlo de un parámetro, pero no podemos hacerlo directamente como hemos hecho hasta ahora, ya que los alias de Git no permiten pasar parámetros directamente.

La solución pasa por definir una función dentro del alias y luego llamarla, usando los expansores de parámetros de la terminal para tomar sus valores por posición. Así:

git config --global alias.send '! send() { git add -A && git commit -m \"$1\" && git push ; }; send'

En este caso lo que hemos hecho en el alias es definir una función send, cuyo contenido es todo lo que va entre las llaves. Lo que hace esta función es ejecutar los 3 comandos de Git que necesitamos. Tras definirla, se llama, y es esta llamada la que realmente recibe el parámetro. El $1 representa el primer parámetro que le pasemos (si se le pasasen más serían $2, $3…).

Nota: puedes conocer más sobre la expansión de parámetros aquí: Expansión de parámetros de bash

Con esta definición podemos hacer simplemente:

git send "El mensaje de mi commit"

cuando queramos, de un solo golpe, añadir todos los cambios, hacer el commit y enviarlo al servidor.

Superútil y este sí que nos puede ahorrar mucho tiempo.

Evitar problemas por el tipo de cambio de línea

Como seguramente sabrás, cada vez que en tu teclado pulsas la tecla ENTER para cambiar de línea en tu código o en cualquier documento de texto, lo que ocurre es que se inserta un carácter de control que representa ese fin de línea. Estos caracteres de control no se ven, pero están ahí y ocupan memoria (o lo que es lo mismo, espacio en disco).

Como en muchas otras cosas, existen diferencias entre los sistemas operativos a la hora de tratar este tipo de caracteres de control. Y en concreto, Windows, macOs y Linux utilizan caracteres de control diferentes para hacerlo.

Si tienes acceso a dos sistemas de estos, es muy fácil de comprobar. Por ejemplo, abre el bloc de notas en Windows y escribe unas cuantas líneas de texto con un solo carácter en cada una de ellas. Ahora haz lo mismo en un editor de texto plano de Linux o de macOS, escribiendo exactamente las mismas líneas:

Si te fijas en sus tamaños, resaltados en rojo en la figura, el de Windows ocupa un poco más. En concreto 5 bytes más que el de macOS.

El motivo de esto es que en macOs y Linux se utiliza como carácter de control para cambio de línea un Avance de línea (Line Feed o LF). En Windows, por otro lado, se utilizan dos caracteres: el mismo LF y además, delante de éste, otro llamado Retorno de Carro (en inglés Carriage Return o CR). Es decir, Windows usa CRLF, dos caracteres, como control para el cambio de línea. Por eso ocupa 1 byte más por línea y por tanto un 50% más en este caso extremo.

Nota friki #1: esta terminología viene de los tiempos de las máquinas de escribir. El carro era un cilindro que agarraba el papel y que se movía en horizontal con cada letra y hacia arriba por cada línea. Cuando en una de estas máquinas terminabas de escribir una línea, le dabas un golpe al carro con la mano izquierda, que lo que hacía eran dos cosas: moverlo a la derecha (retorno de carro) y girar el carro un poco hacia arriba para subir el papel una línea (avance de línea). En este vídeo se pueden ver ambas cosas en acción: primero el avance de línea y luego el retorno de carro con un golpe más fuerte.

Nota friki #2: en los antiguos sistemas Mac “clásicos” que se comercializaron hasta 2001, el cambio de línea se hacía con un retorno de carro, CR, solamente, al igual que ocurría con los clásicos ZX Spectrum o Commodore 64. El CRLF de Windows se usaba también en sistemas clásicos como los Amstrad CPC, en OS/2, sistemas Atari y muchos otros. Y muchos sistemas embebidos o para mainframes, todavía en uso en bancos y grandes aseguradoras, utilizan sus propios caracteres de control para cambio de línea. De hecho, el estándar Unicode recoge hasta 8 caracteres de control que se deberían reconocer como terminadores de línea.

Si abrimos los 3 archivos (Windows, Mac clásico y Linux) con Notepad++ (solo para Windows), que es capaz de mostrar los caracteres de control, vemos las diferencias claramente:

Problemas de diferencias de archivos en Git debidos a retornos de carro

Cuando trabajamos en equipo con el Git, podemos llegar a tener problemas debido a estas diferencias.

Dependiendo de cómo tengamos configurado Git, si en el equipo tenemos a unos en Windows, a otros en macOS o en Linux, según quién edite en cada momento los archivos, pueden registrarse cambios que son debidos tan solo a estos elementos de control para cambio de línea.

Por regla general, los programas de comparación de archivos hacen caso omiso de los caracteres de control, por lo que no notaremos la diferencia. Pero sí que veremos archivos cambiados en los commits, y a la hora de copiar archivos a otro lado podremos sobrescribir algunos que realmente no han cambiado.

Si, por ejemplo, copiamos archivos a otro servidor por la red local o por FTP y el programa de copiado decide si debe sobrescribir o no en función de si ha variado la fecha de modificación del archivo o su tamaño, podríamos copiar muchos más archivos de los necesarios solo por esa diferencia en los cambios de línea. O si hacemos despliegue continuo y se van a desplegar en el servidor mediante el propio Git, se podrían cambiar archivos sin querer sólo por estos cambios de línea.

En el asistente de instalación de Git en Windows y Linux se nos muestra una opción específica para controlar este comportamiento:

Se nos ofrecen 3 opciones que ya vienen bien explicadas en la propia pantalla pero que se detallan a continuación:

  • Checkout Windows-Style, commit Unix-style: esto implica que cuando nos traigamos los archivos desde el repositorio remoto, Git convertirá todos los cambios de línea en CRLF, es decir, en el estilo para Windows. Esto es lo que tendremos para trabajar localmente. A la hora de enviarlos al repositorio de nuevo, Git los convertirá de nuevo al estilo UNIX, es decir, a LF.
  • Checkout as is, commit Unix-style: como en la opción anterior, en este caso se convertirán todos los cambios de línea a LF al enviarlos al repositorio remoto y no se hará conversión alguna al traerse los archivos, es decir, se obtendrán tal cual están en el repositorio remoto.
  • Checkout as-is, commit as-is: con esta opción, Git no hace cambio alguno sobre los cambios de línea.

La última opción es la peor de todas y la que no deberíamos elegir casi nunca. Si en el proyecto trabajan personas con diferentes sistemas o, si nosotros mismos trabajamos con más de un sistema operativo, acabaremos con el problema que describía antes, con varios tipos de cambios de línea mezclados.

Lo que deberíamos hacer es, marcar la opción 1 si trabajamos en Windows y la opción 2 si trabajamos en Linux o Mac. De este modo tendríamos siempre el tipo de cambio de línea apropiado para nuestro sistema, y al mismo tiempo se guardaría de manera homogénea en los repositorios remotos.

Puedes saber qué opción tienes establecida si abres una terminal y escribes:

git config --global core.autocrlf

Por ejemplo, en Windows la opción recomendada, la veríamos establecida a true:

Y los compañeros que trabajen con macOS o Linux la deben establecer como input, para que siempre se use LF como cambio de línea.

Si tienes otro valor, puedes cambiarlo globalmente usando la misma instrucción que antes, pero indicando el valor que le vas a ajustar, por ejemplo:

git config --global core.autocrlf true

para Windows si no la tuvieras correctamente establecida, o con input para Linux/macOS.

No dejar nada al azar: establecer por repositorio

El problema de lo anterior es que dejamos a la responsabilidad de cada desarrollador el establecer la opción correcta, lo cual es propenso a errores. Si no conocen la problemática o cuál es la opción correcta podríamos tener problemas.

La solución a esto es no dar la libertad de elegir y establecer esta opción directamente en todos los repositorios que tengamos. Para ello podemos hacer uso del archivo .gitattributes.

Este archivo lo estudiaremos con detalle un poco más adelante, pero de momento lo que debes sabe de él es que se crea en la raíz del repositorio y permite incluir configuración de Git que afectará tan solo al repositorio actual, prevaleciendo sobre lo que haya en la configuración global de Git.

En el caso que nos ocupa podemos establecer el modo en que gestionará Git cada archivo para los cambios de línea añadiendo patrones de archivos y un valor de configuración concreto.

Si queremos la configuración correcta equivalente a autocrlf, lo que debemos hacer es escribir esto en ese archivo:

* text=auto

Esto significa que, para todos los archivos de texto, Git decidirá de manera automática qué hacer respecto de los cambios de línea, lo cual es equivalente a la situación ideal que mencionaba antes: que en Windows usará CRLF y en macOS/Linux usará LF. Además, al enviar los cambios a un repositorio remoto los homogeneizará a LF.

Esto asegura consistencia para los cambios de línea entre todos los desarrolladores, sin importar en qué sistema operativo estén trabajando.

Es una gran idea tener un archivo .gitattributes con configuraciones como esta como parte de las plantillas de proyectos que usemos. Como se mete bajo control de código, aparecerá en los repositorios locales de todos los desarrolladores.

Si queremos configurarlo de manera distinta para algún tipo de archivo concreto podemos hacerlo también. Por ejemplo, los archivos .sln de Visual Studio que guardan información sobre los proyectos que forman parte de una solución son archivos de texto, y dado que Visual Studio sólo funciona en Windows, podríamos forzar a que siempre utilicen CRLF como separador de línea escribiendo esto en el archivo .gitattributes:

* text=auto

# Archivos de soluciones de Visual Studio
*.sln text eol=crlf

De este modo le estamos indicando a Git que lo archivos con la extensión .sln son de texto, pero que debe utilizar siempre CRLF para los cambios de línea, independientemente del sistema operativo en el que se clonen. Así evitamos que si lo abre un colaborador en macOS, cambie ese ajuste. No es que sea necesario, pero podríamos forzarlo así.

Arreglar un repositorio que no esté con cambios de línea homogéneos

Bien, con lo que hemos visto ya podemos crear una configuración homogénea para todo nuestro equipo (o para nosotros si trabajamos en varias máquinas con sistemas operativos diferentes).

Pero, ¿qué pasa si el mal ya está hecho y cuando hemos puesto este remedio en marcha ya tenemos centenares de archivos de código con diferentes estilos de cambio de línea?

Por suerte, Git ya tiene este problema contemplado también, así que podemos hacer lo siguiente:

git add --renormalize .

¡Fíjate en el punto del final, es importante!

Al hacer esto, Git recorrerá todos los archivos de contenido textual que haya en nuestro repositorio y se asegurará de que tienen los cambios de línea homogéneos, según la configuración que corresponda.

Tras hacerlo, tendremos en el área de preparación (staging) de Git todos los archivos que se han modificado y sólo tendríamos que hacer un commit y un push al repositorio remoto para persistir los cambios.

Con estas técnicas sencillas podremos tener la seguridad de que no vamos a tener problemas por culpa de algo tan aparentemente inofensivo como unos cambios de línea.

El archivo .gitattributes

Aunque la configuración por defecto de Git suele funcionar bien en muchos casos, a veces necesitaremos ajustarlo para que se adapte mejor a las necesidades de un proyecto concreto, sobre todo si trabajas en equipo. Para tener un control más preciso, el archivo .gitattributes es tu aliado, aunque a veces se subestima.

Vamos a ver para qué sirve exactamente, y veremos algunos ejemplos de cómo puedes usarlo en tu día a día.

¿Qué es .gitattributes?

El archivo .gitattributes es un archivo de configuración especial de Git que te permite definir atributos para ciertas rutas o nombres de archivo dentro de tu repositorio. Estos atributos cambian la forma en que Git gestiona esos archivos, afectando cosas como la normalización de los saltos de línea (que ya hemos visto), cómo trata los espacios en blanco, si considera un archivo como binario o de texto, o qué estrategia usa para hacer merge.

Normalmente, el archivo .gitattributes está en la raíz del repositorio. Pero puedes ponerlo en subdirectorios si quieres que los atributos se apliquen solo a los archivos de ese directorio y sus subdirectorios.

Git mira los archivos .gitattributes de forma jerárquica: los atributos que definas en un archivo .gitattributes más cercano a un archivo concreto tienen prioridad sobre los que estén en archivos .gitattributes de directorios superiores. Si no hay ningún archivo .gitattributes en el directorio actual ni en los de arriba, Git usa los atributos por defecto.

La función principal de .gitattributes es darte control para personalizar cómo Git gestiona tus archivos, más allá de la configuración general del sistema o del repositorio. Esto te ayuda a solucionar problemas típicos cuando desarrollas software en equipo, como por ejemplo:

  • Consistencia de saltos de línea: esto ya lo hemos estudiado.
  • Manejo de espacios en blanco: si los espacios en blanco no son consistentes, los diffs se llenan de ruido y es más difícil revisar el código. .gitattributes te permite configurar cómo Git debe tratar los espacios en blanco, e incluso puedes hacer que los corrija automáticamente cuando haces commits.
  • Identificación de archivos binarios y de texto: Git necesita saber si un archivo es de texto o binario para usar las herramientas correctas para comparar y fusionar. .gitattributes te permite decirle explícitamente si un archivo es binario o de texto, incluso si Git no lo detecta bien automáticamente.
  • Control de exportación de archivos: cuando creas archivos comprimidos (archives) del repositorio, .gitattributes te permite decir qué archivos quieres excluir o cómo quieres que se procesen al exportarlos.
  • Estrategias de merge personalizadas: para algunos tipos de archivos, puede que quieras usar una estrategia de merge diferente a la que usa Git por defecto. .gitattributes te permite elegir estrategias de merge específicas para ciertos archivos.

Gracias a este archivo podrás adaptar Git a las peculiaridades de tu proyecto, haciendo que todo sea más consistente, que trabajar en equipo sea más fácil y que te ahorres problemas por diferencias entre entornos de desarrollo.

Usos Habituales de .gitattributes

Vamos a ver algunos usos habituales que te serán muy útiles en tu día a día:

1.- Gestión de Espacios en Blanco

.gitattributes te permite controlar cómo Git trata los espacios en blanco al final de las líneas y otros problemas parecidos. El atributo whitespace te deja definir reglas para detectar y, si quieres, corregir problemas de espacios en blanco.

Aquí tienes algunas opciones útiles para el atributo whitespace:

  • whitespace=fix: elimina automáticamente los espacios en blanco al final de las líneas cuando haces commits.
  • whitespace=warn: te avisa si hay espacios en blanco problemáticos (por ejemplo, espacios al final de línea, espacios antes de tabulaciones) pero no los corrige solo.
  • whitespace=-trailing-space: hace caso omiso de los espacios en blanco al final de las líneas al generar diffs.
  • whitespace=-space-before-tab: hace caso omiso de los espacios antes de las tabulaciones al generar diffs.

Para que Git corrija automáticamente los espacios en blanco al final de línea en todos los archivos de texto, puedes usar:

* text whitespace=fix

2.- Tratamiento de Archivos Binarios

Es muy importante decirle a Git qué archivos son binarios para que no intente compararlos y fusionarlos como si fueran texto, porque eso no tiene sentido y puede dar errores o incluso corromper los archivos. Si el tipo de archivo no es muy conocido es posible que tengamos que hacerlo manualmente.

Para indicar que un archivo es binario, usa el atributo binary:

*.docx binary

Así te aseguras de que Git trate los archivos de Microsoft Word como binarios, gestionándolos de forma eficiente y sin intentar generar diffs de texto.

A veces, Git puede pensar que un archivo de texto es binario por error. En ese caso, puedes forzar que lo trate como texto con el atributo text:

*.log text

3.- Control de Exportación de Archivos

Cuando generas archivos comprimidos del repositorio (por ejemplo, con git archive), .gitattributes te permite controlar qué archivos se incluyen o se excluyen, y también aplicar cambios durante la exportación.

El atributo export-ignore te permite excluir archivos o patrones de archivos de los archivos comprimidos que generas:

*.psd export-ignore
temp/ export-ignore

El atributo export-subst te permite reemplazar comodines de formato (como $Format:%H$, $Format:%an$, etc.) en el contenido de los archivos al exportarlos. Esto es útil para poner información de la versión o del commit en los archivos exportados.

*.txt export-subst

4.- Estrategias de Merge Personalizadas

Aunque no es tan común, .gitattributes también te permite elegir estrategias de merge personalizadas para archivos específicos. Esto puede ser útil si las estrategias de merge que Git usa por defecto no son las mejores para ciertos tipos de archivos, como archivos de configuración o archivos que se generan automáticamente.

El atributo merge te permite indicar qué estrategia de merge quieres usar. Git tiene varias estrategias ya hechas (como resolve, recursive, ours, theirs), y también puedes crear estrategias de merge personalizadas con scripts.

config.ini merge=ours

En este ejemplo, le decimos a Git que cuando haga merge en el archivo config.ini, use la estrategia ours, es decir, que mantenga tu versión del archivo y ignore los cambios remotos.

Conclusión

Como ves, el archivo .gitattributes no es para todos los días, pero es una herramienta clave para afinar y personalizar cómo Git gestiona tus repositorios. Te da control sobre cosas tan importantes como los saltos de línea, los espacios en blanco, cómo trata los archivos binarios y la exportación de archivos, lo que lo convierte en un recurso muy útil para proyectos en equipo y que funcionan en diferentes sistemas operativos.

Si entiendes y usas .gitattributes bien, puedes mejorar mucho tu forma de trabajar, evitar problemas por diferencias entre entornos y asegurarte de que tu código sea consistente y de calidad.

En breve veremos otros uso adicional de este archivo, para el manejo de archivos de gran tamaño con Git.

Técnicas de trabajo con repositorios grandes

Cuando empiezas un proyecto nuevo con Git, todo va como la seda. Creas commits, branches, merges… Git es rápido y eficiente. Pero, ¿qué pasa cuando tu proyecto crece? ¿Qué ocurre cuando tienes miles de commits, un historial enorme y un equipo grande trabajando a la vez?

Ahi es donde entran en juego dos técnicas diferentes que nos proporciona Git para trabajar con repositorios enormes: el shallow clone y scalar. Vamos a ver qué son, cómo funcionan y cuándo usarlos.

Shallow clone

Alguna vez, cuando te toque trabajar con un repositorio Git remoto muy grande, tendrás la sensación de que te estás descargando todo Internet de lo mucho que tarda en bajar. Todos, tarde o temprano pasamos por esa experiencia desagradable de ver cómo nuestra conexión se arrastra mientras Git se baja hasta el último commit de hace años que, realmente no vamos a necesitar para nada. Pero no te preocupes, hay una forma de evitarlo: el modo de clonado superficial de Git o Git shallow clone.

En pocas palabras, un clonado superficial te trae solo los commits más recientes de un repositorio, en lugar de todo su historial. Hacer un Git shallow clone te da lo esencial de un repo, lo que necesitas aquí y ahora, sin todo el bagaje histórico que, seamos sinceros, probablemente nunca vas a mirar. Es perfecto para cuando quieres empezar a trabajar rápido o cuando tu disco duro está tan lleno que debes ahorrar cada byte que puedas. Es la forma ideal de clonar un repositorio para muchos casos, como veremos luego.

Gracias a esta técnica, podrás clonar ese proyecto gigante en cuestión de segundos en lugar de minutos, y ocupando muy poco espacio en disco.

Entendiendo Git shallow clone

Imagina que estás viendo una serie, pero que para ver el último capítulo tienes que descargarte la temporada completa. No tendría mucho sentido. Pues con Git hacemos esto casi siempre… ¿Por qué nos tenemos que descargar todo el historial de un proyecto cuando en el 99% de los casos solo vas a necesitar la última versión del código (o como mucho, unas pocas versiones anteriores)?

En términos técnicos, un shallow clone es una copia parcial de un repositorio Git que contiene solo una parte limitada del historial de commits. Cuando realizas un clonado superficial, especificas una “profundidad” (depth) que indica cuántos commits quieres incluir en tu copia local, contando desde el más reciente hacia atrás.

Imagina que estás clonando el repositorio de Linux (sí, el kernel completo del sistema operativo). Con un clon completo, estarías descargando más de 1 millón de commits que se remontan hasta el principio de los tiempos: varios GB de historia. Si lo que quieres es ver el código más reciente y haces un shallow clone con una profundidad de 1, solo obtienes el último commit y el estado actual de sus archivos: unos pocos MB y unos segundos para traértelo. Mucho mejor.

Las principales diferencias entre un clonado normal y uno superficial son:

  • Tamaño del repositorio: los shallow clones son mucho más pequeños porque solo contienen una parte del historial.
  • Velocidad de clonado: al descargar menos información, el proceso es mucho más rápido. Es especialmente útil en entornos CI/CD donde el tiempo es oro (porque te lo cobran) y el espacio en disco también.
  • Operaciones Git disponibles: algunas operaciones no estarán disponibles, como por ejemplo hacer un git blame más allá de tu profundidad establecida o un git rebase con commits antiguos.
  • Dependencia del servidor remoto: para operaciones que requieren acceso al historial completo, necesitarás conectarte al repositorio remoto, ya que no tienes toda la información en local.

Cómo utilizar los clonados superficiales de Git

El comando básico es súper sencillo:

git clone --depth 1 https://github.com/usuario/repositorio.git

En ese --depth 1 está toda la “magia”. Le dices a Git: “dame solo el último commit”.

Nota: pongo un ejemplo con GitHub pero esto sirve con cualquier repositorio Git, claro.

Es así de sencillo. Pero tenemos otras opciones interesantes…

¿Quieres traerte algunos commits más porque se te quedan cortos? Usa el comando fetch para traerlos usando una profundidad mayor:

git fetch --depth=N

Siendo N el número de commits que quieres traerte.

Si en un momento determinado te arrepientes del clonado superficial y quieres traerte la historia completa, no tienes que crear otra copia local del repo. Basta con que uses el comando contrario, unshallow:

git fetch --unshallow

Esto es lo básico. pero se puede ser más selectivo todavía. Por ejemplo, si solo vas a utilizar una rama del código, digamos main, ¿para qué quieres traerte todas, aunque solo sean unos pocos commits?

Para clonar de manera superficial tan solo una rama concreta, simplemente indica qué rama te interesa con --branch:

git clone --depth 1 --branch main https://github.com/usuario/repositorio.git

¿Y si lo que quieres es un tag concreto? Por ejemplo, la última versión estable… Pues exactamente lo mismo:

git clone --depth 1 --branch v2.0.0 https://github.com/usuario/repositorio.git

Sparse Checkout: el complemento perfecto para el shallow clone

Un truco adicional es combinar shallow clone con sparse checkout.

Un sparse checkout en Git es la funcionalidad que permite traerte tan solo algunos archivos y/o directorios concretos desde un repositorio, en lugar de descargar todo el contenido. Esto te permite reducir el espacio de almacenamiento y mejorar el rendimiento si solo tienes interés en ciertos archivos. Así que si lo combinamos con el clonado superficial para traernos solo una parte de los archivos del repo podemos adelgazar a tope el tamaño del repositorio local:

# Primero, hacemos el shallow clone
git clone --depth 1 --filter=blob:none https://github.com/usuario/repositorio.git
cd repositorio

# Configuramos sparse checkout
git sparse-checkout init
git sparse-checkout set carpeta/que/necesito

# Actualizamos el repositorio local
git pull

El truco aquí está en el modificador --filter=blob:none del primer comando. Al ponerlo en un clonado es como decirle a Git: “De momento no me descargues ningún archivo, ya te diré cuáles quiero”. Es súper útil si andas escaso de espacio en disco, te tienes que traer un repositorio muy grande pero solo necesitas un par de archivos.

Casos de uso ideales para el shallow cloning

El shallow cloning es una técnica muy interesante que presenta grandes ventajas pero que, como todo, también tiene sus inconvenientes. Pero hay algunos casos concretos donde es fundamental:

1. Entornos CI/CD: aquí el shallow clone es ideal. ¿Para qué necesitas toda la historia cuando solo vas a compilar y hacer test? Configura tu pipeline con:

git clone --depth 1 --single-branch --branch main tu-repo.git

Fíjate: en este caso le hemos puesto el modificador --single-branch además de --branch porque sin él se clonarían todas las ramas del repositorio aunque solo se traería la información de la rama main. Con este modificador solo descargará la información de la rama main y no las demás, lo que ahorra (aunque sea poco) tiempo y espacio en disco.

2. Servidores de despliegue: en producción, solo necesitas el código que vas a ejecutar. Punto. Un shallow clone con el tag de la versión específica es lo que necesitas:

git clone --depth 1 --branch v2.1.0 tu-repo.git

3. Pruebas rápidas: ¿quieres probar rápidamente ese proyecto de GitHub que pesa varios GB? Usa shallow clone y ahórrate 20 minutos de descarga.

4. Trabajo en itinerancia: si estás de viaje en el extranjero, conectado con el móvil u otro tipo de conexión lenta o que te cobra por transferencia, querrás traerte la menor cantidad de información posible.

Cuándo NO usar shallow clone

  • Si estás desarrollando activamente en el proyecto. Básicamente porque te va a dificultar hacer merges de ramas y te va a complicar también el traerte cambios que otras personas hagan en el proyecto.
  • Cuando necesitas acceso frecuente al historial completo
  • Si trabajas con muchas ramas y merges complejos
  • En proyectos pequeños donde el ahorro de espacio es insignificante

En definitiva, el clonado superficial de repositorios en Git es una herramienta que, bien utilizada, puede transformar tu flujo de trabajo, especialmente en entornos automatizados. Como hemos visto, no se trata de sustituir los clonados tradicionales, sino usarlos de manera inteligente en las ocasiones apropiadas.

La clave está en el equilibrio: optimiza donde puedas y tenga sentido, mantén la historia completa donde la necesites, y sobre todo, asegúrate de entender cuándo y por qué estás usando cada enfoque. Al final, como con tantas otras cosas en desarrollo, no es la herramienta lo que importa, sino cómo la utilizas para hacer mejor y más eficiente tu trabajo.

Introducción a los Git Hooks

Git se puede extender de forma programática añadiéndole funcionalidad que responda ante ciertos eventos gracias a una característica llamada hooks (muchas veces llamados Git hooks). Permiten realizar acciones automáticas junto a muchos de los comandos típicos de Git.

Importante: los hooks son un concepto avanzado y no son para todo el mundo ni se usan a menudo en un proyecto convencional. Además, se suelen utilizar fundamentalmente en Linux, aunque puedes usarlos en los demás sistemas también. En esta lección los comandos que usaremos son todos para Bash sobre Linux. Por eso se trata de una lección opcional. De todos modos es recomendable que al menos la leas para conocer el concepto.

Los hooks son programas que se ejecutan antes o después que algún evento importante de Git. Por ejemplo, antes de completar un git-commit, tras cambiar de rama o antes de enviar los cambios durante un git push. Se almacenan en el directorio .git/hooks/ de cada proyecto clonado, y es necesario que sean programas o scripts ejecutables (en sistemas tipo Unix han de tener el permiso de ejecución).

Para añadir un hook a tu proyecto, simplemente copia o enlaza a dicho directorio el programa que quieras ejecutar automáticamente con el nombre del “evento” (por ejemplo, pre-commit. Todos los posibles nombres están disponibles en la documentación de Git). Si el programa es un script y quieres que el resto de colaboradores puedan usarlo, es conveniente incluirlo en la raíz del proyecto y crear un acceso directo a él en .git/hooks/:

ln -s ../../pre-commit.sh .git/hooks/pre-commit

Vamos a ver algunas aplicaciones prácticas de los hooks de Git.

1. Comprobar la calidad del código antes del commit

Los hooks que se ejecutan previos a la confirmación de un commit son útiles puesto que nos permiten realizar comprobaciones sobre el código añadido o eliminado justo antes de reflejar dichos cambios en el árbol de versiones del repositorio. Si el hook falla o devuelve código de error, el commit no se efectuará.

En el siguiente ejemplo (válido para sistemas Unix, como Linux o macOS), utilizamos el paquete jslint de Node.js para comprobar la calidad del código de una aplicación JavaScript antes de completar el commit:

#!/bin/bash
# pre-commit.sh
 
# Guardar los cambios no confirmados
STASH_NAME="pre-commit-$(date +%s)"
git stash save -q --keep-index $STASH_NAME
 
# Comprobaciones y tests
jslint my_application.js || exit 1
 
# Recuperar los cambios guardados
STASHES=$(git stash list)
if [[ $STASHES == "$STASH_NAME" ]]; then
  git stash pop -q
fi

Se podría proceder con la misma estrategia a lanzar una suite de tests sobre la aplicación u otras comprobaciones necesarias, por ejemplo, una búsqueda de claves o tokens secretos para evitar introducirlos al repositorio.

2. Generar la documentación conforme se suben cambios

Si en nuestro proyecto contamos con un generador de documentación a partir del código, podemos ejecutarlo regularmente conforme desarrollamos mediante un hook de tipo pre-push. Simplemente lanzamos el comando necesario para componer toda la documentación en algún directorio (por ejemplo, docs/) y la añadimos a un nuevo commit.

En el siguiente listado te muestro varios ejemplos de los posibles comandos que podrías usar para tal fin:

#!/bin/bash
# pre-push.sh
 
# Genera la documentación
doxygen Doxyfile
 
# Otro ejemplo, con Python:
pydoc3 -w docs/
 
# Otro ejemplo, con R:
Rscript -e 'pkgdown::build_site()'
 
# Añade y confirma los cambios relativos a la documentación
git add docs/
git commit -m "Update documentation ($(date +%F@%R))"

La ventaja de esto es que, si utilizas GitHub Pages o un servicio similar para servir tu documentación online, siempre estará al día con los cambios de tu código y no se quedará obsoleta.

3. Comprueba dependencias al cambiar de rama

Por último, una aplicación de los hooks muy interesante es actualizar las dependencias instaladas al cambiar de rama en un proyecto. Si utilizas gestores de paquetes y dependencias para tu lenguaje de desarrollo (Pip en Python, npm en Node.js, Nuget en .NET, Bundler en Ruby, Cargo en Rust…), te puede ser muy útil el siguiente ejemplo.

El listado de código a continuación correspondería a un hook post-checkout, y lo que consigue es comprobar si entre la rama anterior y la nueva ha cambiado el archivo de dependencias (en este caso, Gemfile para Ruby), en cuyo caso ejecuta el instalador conveniente (en este caso, bundle):

#!/bin/bash
# post-checkout.sh
 
if [ $1 = 0000000000000000000000000000000000000000 ]; then
  # Si estamos en un proyecto recién clonado, comparar con el directorio vacío
  old=$(git hash-object -t tree /dev/null)
else
  # El primer argumento es el hash de la anterior HEAD
  old=$1
fi
if [ -f Gemfile ] &&
  git diff --name-only $old $2 | egrep -q '^Gemfile|\.gemspec$'
then
  # Vaciar $GIT_DIR evita problemas si bundle llama a git
  (unset GIT_DIR; exec bundle)
  # Se completa el checkout aunque falle bundler
  true
fi

Puedes adaptar este código para tu propio uso simplemente cambiando Gemfile por el archivo de dependencias que utilices (package.json en desarrollo web Front-End, por ejemplo) y bundle por el comando correspondiente (npm install o pip install -r requirements.txt, según el caso).

Fuentes y más información:

Flujo de trabajo para productos con GitFlow

GitFlow es un modelo de creación de ramas para trabajar con Git, diseñado y propuesto como “estándar” por el programador holandés Vincent Driessen en 2010.

Desde que se propuso ha sido adoptado por miles y miles de proyectos en todo el mundo, dado que ofrece un modo sencillo y claro de trabajar, del que todo el mundo es consciente, ofreciendo entre otras ventajas:

  • Facilita el desarrollo en paralelo de diversas características, arreglo de bugs, etc… ya que aísla los nuevos desarrollos del trabajo finalizado.
  • Disminuye los conflictos al trabajar en la misma característica, ya que la rama de una de éstas es como un “jardín cerrado” en el que solo se realizan cambios que tienen que ver con dicha característica.
  • Facilita la gestión de versiones, puesto que cada vez que se completa algún nuevo desarrollo, sus cambios se mezclan con la rama develop (que veremos enseguida), que actúa como un área de almacenamiento de todas las características que todavía no han salido a producción. Cuando vamos a lanzar una nueva versión, en la rama develop tendremos todas las nuevas características desde la anterior versión, listas para ser desplegadas.
  • Da soporte sencillo para arreglar bugs en versiones en producción. Cuando se abre una rama para arreglar un bug desde una versión que ya tenemos tageada, ésta contendrá exactamente lo que hay en producción y no corremos el riesgo de mezclarlo con algo que esté en desarrollo.

Es por esto que te lo vas a encontrar en casi cualquier sitio en el que trabajes, por lo que te conviene conocerlo bien. Así sabrás cómo empezar a trabajar en un proyecto con control de código basado en Git desde el primer día.

Nota: GitFlow no es para todo el mundo, y de hecho tiene sus detractores. Aunque existen otros tipos de workflow y desde luego los hay más sencillos, que usan menos ramas y menos operaciones, sigue siendo seguramente el más popular y el más utilizado, válido para muchos tipos de proyectos. Por eso te conviene conocerlo. Si luego en tu empresa o en algún proyecto decidís usar uno más simple, con sólo una rama principal, por ejemplo, lo aprendido con él te sigue valiendo igual, así que no es tiempo perdido.

Las ramas en GitFlow

El funcionamiento de GitFlow en realidad es bastante sencillo. Se basa en la existencia de ciertas ramas en el repositorio y luego en que todos los desarrolladores involucrados lleven a rajatabla cierta forma de trabajar con ellas.

Las ramas básicas que necesitamos son dos: master y develop, combinadas con ramas temporales para crear desarrollos de características nuevas, de resolución de bugs y de nuevas versiones. Además utilizaremos tags para señalar los commits que marcan el lanzamiento de nuevas versiones.

El siguiente gráfico, original de Vincent Driessen, muestra el proceso en un golpe de vista, pero vamos a verlo ahora con más detalle:

Como vemos existen las dos ramas principales que comentábamos antes, develop y master, que será de las que partamos para trabajar y que se mantienen sincronizadas al cabo de cada iteración de trabajo con GitFlow.

Nuevas características y bugs no importantes

Para crear cualquier nuevo desarrollo de nuestro producto (una nueva característica o incluso el arreglo de un bug que no sea crítico y se deje para la siguiente versión) lo que tenemos que hacer es crear una nueva rama de característica (feature branch) desde la rama develop.

Por ejemplo, supongamos que tenemos un directorio de código bajo Git de un producto del que ya hemos lanzado su versión 1.0 y estamos trabajando en nuevas características. El aspecto del árbol de commits de Git sería como el que sigue:

Nota: todas las figuras como la anterior usadas en esta lección son originales de Vincent Driesden, tomadas de su trabajo original con su permiso explícito.

Como vemos, en este directorio se han desarrollado ya un par de características (representadas por los puntos amarillos en la rama develop, que son commits tras haber cerrado una rama de desarrollo), y en un momento abrimos dos ramas a partir de develop, cada una de ellas dedicada al desarrollo de una característica.

Todo desarrollo se realiza siempre sobre una de estas nuevas ramas, de modo que lo aislamos de todo lo demás hasta que se finaliza.

Nota: si durante el desarrollo en una de estas ramas debes dejarlo todo para atender, por ejemplo, un bug urgente o se te asigna otra característica más urgente, harías un commit de todo lo que tuvieras para guardarlo, crearías una nueva rama para el nuevo desarrollo, cambiándote a ésta, y ya retomarías el desarrollo de la anterior cuando pudieras, volviendo a cambiarte a ella. Eso o, si va a ser rápido, puedes usar un stash, claro.

Una vez finalizado el desarrollo de la característica de una rama, ésta se mezcla con la rama principal develop, creando un commit y desapareciendo como rama, pero permaneciendo todos los puntos intermedios. En la siguiente figura podemos ver el momento en el que se cierran las dos ramas que habíamos creado en la figura anterior:

Sacando versiones nuevas

Una vez que tenemos las características de la siguiente versión desarrolladas, es hora de lanzar una nueva versión de nuestro producto, sitio web o lo que sea que estamos desarrollando.

La forma de hacerlo con GitFlow es sacar una nueva rama de release que se llamará release-*, siendo el asterisco el número de versión que queremos lanzar.

Por ejemplo, supongamos que en nuestro repositorio, al terminar la primera característica que habíamos abierto, decidimos lanzar ya una nueva versión 1.1 de nuestro producto. Crearíamos una rama release-1.1 en la que probaríamos los nuevos desarrollos ya en conjunto, y corregiríamos los últimos detalles directamente en esta nueva rama, antes de lanzar la versión definitiva.

Importante: fíjate en que, una vez que abrimos una rama de release, todos los nuevos cambios que se hagan en ésta se deben mezclar en la rama develop, de modo que en ésta siempre esté la versión más reciente de todo.

Hasta ahora no hemos usado la rama master para nada. Su objetivo en GitFlow es el de contener exclusivamente código que ha sido lanzado a producción, de modo que podamos ver en cualquier momento el estado de la aplicación que se ha entregado a los clientes y usuarios o que están en el servidor en el caso de una aplicación Web o un servicio.

Bien, debido a ello, una vez tenemos todos los detalles corregidos y estamos listos para lanzar la nueva versión (1.1 en nuestro ejemplo), lo que tenemos que hacer es mezclar la rama release-* tanto con develop como con master:

Además debemos crear un tag en master para marcar ese hito en el tiempo. Observa que en la figura anterior se crea un tag tras haber hecho la mezcla de modo que queda marcada la versión 1.1.0.

En este instante las ramas develop y master son idénticas. Esta última quedará congelada hasta que haya un bug importante o se saque alguna versión nueva. Mientras tanto todo el desarrollo se seguirá realizando a partir de la rama develop siguiendo la metodología que acabamos de describir en puntos anteriores.

Solucionando bugs importantes

Cuando se descubre un bug en producción que impacta mucho a los usuarios y se debe corregir cuanto antes, sin esperar a lanzar una nueva versión mayor o menor, deberemos utilizar la rama master para arreglarlo.

Hemos quedado en que esta rama siempre tiene el código exacto que se ha llevado a producción, por lo que nos aseguramos de que estamos trabajando con el código apropiado.

Lo que tenemos que hacer en este caso es sacar una nueva rama para trabajar en el bug que llevará siempre el nombre hotfix-*, siendo el asterisco el número de versión que se generará debido al bug, siguiendo normalmente el versionado semántico.

Nota: recuerda que SemVer define tres números de versión, mayor para cuando hay cambios incompatibles hacia atrás, menor con nueva característica compatibles, y hotfix, precisamente para ir aumentándolo solo cuando se corrigen bugs.

Por ejemplo, si en nuestro proyecto se descubre un error grave mientras estamos en la versión 1.0, y por lo tanto desarrollando características de la siguiente versión, y necesitamos corregirlo, abriremos una nueva rama llamada hotfix-1.0.1, en la que solucionaríamos el problema. Ahora, antes de llevarlo a producción hacemos una mezcla de esa rama tanto a master (para llevarlo a producción) como a develop, para que no se pierdan esos cambios en versiones posteriores, fíjate en la parte superior derecha del siguiente diagrama:

En resumen

Como puedes comprobar, GitFlow es en realidad una metodología muy sencilla.

Solamente existen dos ramas persistentes, que son master y develop. La primera de ellas contiene solamente código que ha ido a producción alguna vez, y se marcan esos commits con un tag para localizarlos fácilmente. La otra rama, develop, es la que se usa todo el tiempo para realizar el trabajo principal y desarrollar nuevas características. Ambas se sincronizan y son idénticas cada vez que se lanza una nueva versión.

Las otras ramas “virtuales” llamadas features, release y hotfixes, en realidad no existen como tales sino que están formadas por multitud de pequeñas otras ramas que aparecen y desaparecen según el desarrollo que haya actualmente en marcha, para crear nuevas características, dejar listas nuevas versiones y arreglar bugs urgentes respectivamente.

En el siguiente vídeo vamos a ver cómo gestionar GitFlow de manera sencilla usando una herramienta visual (en concreto SourceTree). Te dejaremos como ejercicio el ver cómo realizar este flujo de trabajo con otras herramientas.

Cómo arreglar problemas con Git

A continuación te presentamos un diagrama de decisión para la resolución de la mayor parte de los problemas que te puedan surgir en el día a día a la hora de trabajar con Git. Dado que es una gráfica hemos preferido facilitártelo en formato PDF para poder utilizarlo con mayor facilidad. Puedes descargarlo en PDF desde las referencias cruzadas del lateral. Procura repasarlo ahora para ver si todo te encaja o si te surge alguna duda. Y conviene siempre tenerlo a mano 😉

Caso práctico: eliminar el último commit en el origen

Como ejemplo, vamos a ver varios casos que se refieren exclusivamente a cuando se trata del último commit realizado.

En Git existen muchas maneras de hacer las cosas. Estas que cuento a continuación no tienen por qué ser las únicas, pero son las que generalmente son más directas y sencillas.

El commit NO está en el remoto todavía

Bueno, este es el caso fácil. Si hemos hecho el commit y todavía no se ha enviado al repositorio de origen (en Github, GitLab, Bitbucket o donde sea), podemos solucionarlo de varias maneras.

Volver al último commit empezando de cero

Si lo único que necesitas es eliminar este último commit y empezar de nuevo solo tienes que abrir una línea de comandos en la carpeta del repositorio y escribir:

git reset HEAD^ --hard

Lo que hace esto es eliminar el último commit de la historia, dejando la carpeta en el punto anterior en el que estaba antes de hacer los cambios correspondientes a dicho commit.

El ^ del final de la cabecera le indica a Git que el punto de la historia en donde deseas dejarla (resetearla), es el padre del último commit en la rama actual (que se llama siempre HEAD). Es importante indicarlo.

Ahora bien, esto hará que pierdas todos los cambios que había en éste.

Volver al último commit pero no perder los cambios actuales

Lo anterior será lo que desees hacer en muchos casos, pero en muchos otros no. A lo mejor lo que necesitas corregir está en un solo archivo y los cambios de los demás te parecen bien. En estos casos está bien eliminar el commit pero al mismo tiempo evitar perder los cambios. Conseguirlo es casi idéntico a lo anterior, y solo debes escribir:

git reset HEAD^ --soft

De este modo deja todo tal cual está.

Te has olvidado de uno o varios archivos

Si no te hace falta algo tan radical y simplemente te has olvidado de añadir algún archivo al último commit, no hace falta que deshagas todo como acabamos de ver.

Puedes escribir tan solo este comando para añadir los que te falten:

git add archivo1.ext archivo2.ext ...

Ten en cuenta que puedes usar patrones para añadir varios archivos con un nombre similar de golpe, o simplemente hacer una lista como se ve en lo anterior.

Cuando hayas añadido los archivos que te faltasen, indicas que quieres modificar el commit anterior en base a esto, escribiendo:

git commit --amend

lo cual añadirá el o los archivos al último commit que todavía está en tu equipo.

Nota: si haces esto así, desde línea de comandos, la opción --amend te abre un editor vi por defecto (o el editor que tengas configurado en tu equipo) por si quieres modificar el mensaje también. Para salir y terminar con la operación debes pulsar ESC y a continuación :x o :q para que salga y grabe los cambios.

El commit ya se ha enviado al servidor remoto

Esta es una pregunta muy frecuente de la que hay muchas versiones diferentes online para su respuesta, muchas de ellas incorrectas. Se trata de hacer lo mismo que acabamos de ver, pero esta vez teniendo en cuenta que no solo está en nuestro equipo local, sino que dicho commit también se ha enviado al servidor de origen, sea éste Github, Bitbucket, Gitlab o similar.

En este caso en realidad es bastante sencillo de hacer también.

El repo local y el remoto están sincronizados

Este es el caso más habitual. Es decir, estamos trabajando en un repositorio, hacemos cambios, realizamos un commit para guardarlos en la historia y lo enviamos al remoto usando push. Acto seguido nos damos cuenta de que va con un error muy gordo y queremos echarnos para atrás. ¿Cómo lo hacemos?

Pues en primer lugar destruyendo el commit localmente como hemos visto en el apartado anterior, con:

git reset HEAD^ --hard

y a continuación forzando los cambios al repositorio remoto de origen con:

git push origin -f

En condiciones normales un push al origen falla si el commit que está “arriba” no es un ancestro del commit que se está intentado hacer. Es decir, si el repo remoto va por delante del repo local.

Con el parámetro -f (o --force que es lo mismo) forzamos a que esta comprobación no se haga. Esto puede tener efectos destructivos y que se pierdan los commits que van por delante de nosotros, cosa que es justo lo que queremos en este caso: con la instrucción reset estamos situados en un commit inmediatamente anterior al último y con el push forzado estamos haciendo que éste desaparezca.

Submódulos en Git: reutilización de repositorios

Imagina que estás desarrollando una aplicación y necesitas incluir una biblioteca de código que mantienes en otro repositorio (o incluso que es un repositorio de terceros, ubicado en otro sitio). Para conseguirlo, podrías copiar y pegar el código, pero ¿qué pasaría cuando necesites actualizar esa biblioteca en todos tus proyectos? Aquí es donde entran en juego los submódulos de Git.

Los submódulos son repositorios Git completos que puedes incluir dentro de otro repositorio Git principal.

Es como tener una carpeta especial en tu proyecto que apunta a un commit específico de otro repositorio.

Esta característica, aunque a veces incomprendida, es muy potente a la hora de gestionar dependencias que están bajo tu control.

Hay varios escenarios donde resultan fundamentales:

  • Cuando mantienes bibliotecas compartidas entre varios proyectos y quieres tener control total sobre las actualizaciones.
  • En el desarrollo de plugins o extensiones que necesitan mantener su propio control de versiones
  • Para gestionar temas o plantillas que se utilizan en múltiples proyectos
  • Cuando necesitas incluir código de terceros pero quieres mantener la capacidad de actualizarlo fácilmente

La principal ventaja de los submódulos frente a otras alternativas como copiar código o usar gestores de dependencias es que mantienen una referencia exacta a una versión específica del código que estás incluyendo. Esto garantiza que tu proyecto siempre utilizará la versión correcta de cada dependencia, evitando sorpresas desagradables cuando alguien más clone tu repositorio.

Además, los submódulos te permiten trabajar en el código de la dependencia directamente desde tu proyecto principal, facilitando el proceso de desarrollo cuando necesitas hacer modificaciones en ambas partes simultáneamente. Esto es especialmente útil en entornos empresariales donde diferentes equipos trabajan en componentes que deben integrarse en múltiples proyectos.

Aunque tienen fama de complicados, no lo son tanto una vez que entiendes sus conceptos básicos y su flujo de trabajo. En los siguientes apartados, veremos paso a paso cómo trabajar con ellos de manera efectiva, empezando por cómo añadirlos a tu proyecto.

Añadiendo submódulos a tu repositorio

Para incorporar un submódulo a tu repositorio Git proporciona el comando git submodule con una sintaxis bastante intuitiva:

git submodule add <url_repositorio> [ruta_destino]

Además de esta sintaxis básica, ofrece algunos parámetros adicionales:

  • -b <rama>: para especificar una rama concreta del submódulo.
  • --name: para usar un nombre diferente al del repositorio.
  • --depth: para hacer un clon superficial y ahorrar espacio.

Por ejemplo:

git submodule add -b desarrollo --name utils https://github.com/tu-usuario/biblioteca-comun.git libs/utils

Así, por ejemplo, si quieres añadir un submódulo que apunte a una biblioteca de utilidades común que tienes en GitHub, harías:

git submodule add https://github.com/tu-usuario/biblioteca-comun.git libs/comun

Lo que hace este comando es:

  1. Clona el repositorio indicado en la ruta especificada (libs/comun en este ejemplo).
  2. Se crea un archivo .gitmodules en la raíz de tu proyecto (si no existía).
  3. Se registra el submódulo en el archivo .gitmodules con su URL y ruta.
  4. El directorio del submódulo se marca de forma especial en el repositorio principal.

El contenido del archivo .gitmodules será algo así:

[submodule "libs/comun"]
    path = libs/comun
    url = https://github.com/tu-usuario/biblioteca-comun.git

Una vez añadido el submódulo, necesitas confirmar los cambios en tu repositorio principal:

git add .gitmodules libs/comun
git commit -m "Añadido submódulo biblioteca-comun"

Pero, algo muy importante que ocurre en este caso es que el repositorio principal no almacena el código del submódulo, sino únicamente una referencia al commit específico del submódulo que estás utilizando.

Esto significa que tu repositorio principal se mantiene ligero, ya que solo guarda un puntero al estado exacto del submódulo.

Por supuesto, si utilizas diversos componentes en tu proyecto, que están repositorios diferentes, puedes añadir múltiples submódulos a tu repositorio. Tan solo debes repetir la operación mas de una vez:

git submodule add https://github.com/tu-usuario/modulo1.git libs/modulo1
git submodule add https://github.com/tu-usuario/modulo2.git libs/modulo2

Cada uno se gestionará de forma independiente, permitiéndote mantener diferentes versiones de cada submódulo según las necesidades de tu proyecto.

Es importante mencionar que cuando añades un submódulo, este se fija en el último commit de la rama principal del repositorio del submódulo. Si necesitas una versión específica, puedes cambiar al commit deseado dentro del directorio del submódulo y luego confirmar ese cambio en el repositorio principal.

Clonación de repositorios con submódulos

Tienes dos opciones principales para clonar por primera vez un repositorio que contiene submódulos:

1. Clonación en dos pasos:

git clone https://github.com/usuario/proyecto.git
git submodule update --init --recursive

2. Clonación en un solo paso:

git clone --recurse-submodules https://github.com/usuario/proyecto.git

La segunda opción es más directa, pero es importante conocer ambas porque la primera te permite más control sobre el proceso.

Actualización de submódulos

Para mantener tus submódulos actualizados con las últimas versiones del código, tienes varias opciones según lo que necesites:

1. Actualizar todos los submódulos a la última versión de sus respectivas ramas:

git submodule update --remote

2. Actualizar un submódulo específico:

git submodule update --remote nombre_submodulo

3. Actualizar submódulos al estado exacto registrado en el repositorio principal:

git submodule update --init

Es decir, el submódulo quedará en el commit específicamente referenciado cuando lo añadimos, no en la última versión.

A veces es muy importante tenerlo en una versión concreta. Y, desde luego, siempre es recomendable probarlo a fondo antes de hacer la actualización a las versiones más recientes por si existen incompatibilidades o bugs.

Verificación del estado de los submódulos

Puedes comprobar el estado de tus submódulos de varias formas:

# Ver el estado general de los submódulos
git submodule status
 
# Ver cambios pendientes en los submódulos
git submodule foreach git status
 
# Ver los commits de los submódulos
git submodule foreach git log -1

Sincronización después de cambios remotos

Cuando otros miembros del equipo han actualizado los submódulos (es decir, le han cambiado el commit al que apuntan), necesitarás sincronizar estos cambios:

# Obtener cambios del repositorio principal
git pull
 
# Actualizar submódulos según los cambios recibidos
git submodule update

Ojo: este comando no inicializa submódulos que aún no han sido clonados. Si necesitas inicializar submódulos que no están presentes, debes usar git submodule update --init primero.

Configuración de comportamientos automáticos

Para hacer tu vida más fácil, puedes configurar Git para que siempre actualice los submódulos automáticamente:

# Configurar pull automático de submódulos
git config --global submodule.recurse true
 
# Para push automático de submódulos
git config --global push.recurseSubmodules on-demand

Recuerda que los submódulos normalmente apuntan a commits específicos, no a ramas. Esto significa que no se actualizarán automáticamente cuando haya nuevos cambios en el repositorio del submódulo, lo cual es una característica de seguridad para mantener la estabilidad de tu proyecto. Tendrás que actualizar explícitamente cuando quieras incorporar nuevos cambios.

Un consejo práctico es siempre verificar que los submódulos están en el estado correcto antes de hacer commits importantes en tu repositorio principal. Esto evitará problemas de consistencia en el futuro.

Realizando cambios en un submódulo

Cuando necesitas modificar el código de un submódulo, el proceso es el siguiente:

# 1. Entrar al directorio del submódulo
cd ruta/al/submodulo
 
# 2. Asegurarte de estar en la rama correcta
git checkout main  # (o la rama que corresponda)
 
# 3. Realizar tus cambios y confirmarlos
git add .
git commit -m "Descripción de los cambios en el submódulo"
 
# 4. Subir los cambios al repositorio del submódulo
git push origin main

Actualizando el repositorio principal

Después de modificar un submódulo, necesitas actualizar la referencia en el repositorio principal:

# Desde el directorio raíz del repositorio principal
git add ruta/al/submodulo
git commit -m "Actualizada referencia del submódulo a la última versión"
git push

Trabajando con ramas en submódulos

Por ejemplo, si necesitas trabajar en una característica nueva, puedes abrir una rama y trabajar en ella, como en un repositorio normal:

# Dentro del submódulo
git checkout -b nueva-caracteristica
# Realizar cambios
git commit -am "Nuevos cambios en característica"
git push -u origin nueva-caracteristica
 
# En el repositorio principal
git add ruta/al/submodulo
git commit -m "Submódulo ahora apunta a la rama nueva-caracteristica"

Gestión de cambios paralelos

Cuando varios desarrolladores modifican submódulos, es importante mantener una buena coordinación:

# Antes de empezar a trabajar
git submodule update --remote --merge
 
# Si hay conflictos en el submódulo
cd ruta/al/submodulo
git status
# Resolver conflictos normalmente
git add .
git commit

Tracking de ramas específicas

Puedes configurar submódulos para seguir ramas específicas:

# En el archivo .gitmodules
[submodule "ruta/al/submodulo"]
    path = ruta/al/submodulo
    url = https://github.com/usuario/submodulo.git
    branch = desarrollo
 
# O mediante comando
git config -f .gitmodules submodule.ruta/al/submodulo.branch desarrollo

Comprobación de cambios pendientes

Es importante verificar regularmente el estado de tus submódulos:

# Ver cambios pendientes en todos los submódulos
git submodule foreach git status
 
# Ver diferencias en submódulos
git diff --submodule

Recomendaciones importantes

  1. Siempre haz commit y push en el siguiente orden:
    • Primero en el submódulo
    • Luego en el repositorio principal
  2. Comunica los cambios:
    • Informa a tu equipo cuando actualices submódulos
    • Documenta las dependencias entre versiones
  3. Mantén un versionado coherente:
    • Usa tags en los submódulos para marcar versiones estables
    • Considera usar ramas específicas para diferentes versiones de tu proyecto principal
  4. Evita cambios circulares:
    • No crees dependencias circulares entre submódulos
    • Mantén una jerarquía clara de dependencias

Una referencia circular ocurre cuando dos o más repositorios se incluyen mutuamente como submódulos, de manera directa o indirecta. Por ejemplo, si el repositorio A incluye como submódulo al repositorio B, y este a su vez incluye como submódulo al repositorio A, se crea un ciclo infinito que puede causar problemas graves en la inicialización, actualización y gestión de los repositorios.

Recuerda que los cambios en submódulos son especialmente delicados porque afectan a todos los proyectos que los utilizan. Por eso es crucial mantener una buena comunicación con el equipo y seguir un proceso claro al realizar modificaciones.

Prácticas propuestas para el módulo

informatica/programacion/cursos/git_control_codigo_fuente_programadores/conceptos_avanzados.1740468017.txt.gz · Última modificación: por tempwin