Feliz Navidad y Próspero Año 2022

Otro año más nos encontramos en el "día de la marmota", seguimos con el COVID19 y casi parece que vamos a tener que aprender a convivir con él. No obstante partiendo de la reflexión del año pasado, siento que mi orden de prioridades ahora sí es el correcto. He podido alimentar Mascando Bits con algunas entradas interesantes, a la vez que como siempre, me sigo quedando con ganas y sin tiempo de hacer otras que se me quedan en la recámara (cosas que pasan 😅 ).

He tenido la mala suerte de haber pasado este año el COVID19, pero también la buena suerte de haberlo pasado de una manera razonablemente buena, gracias a la nueva situación en la que nos encontramos, tras un año de vacunas y medidas, a veces acertadas y otras muchas contradictorias y descordinadas. Seguir siempre es un motivo para mirar hacia delante y estar agradecido. 😄

Como años anteriores y sabiendo que me repito, no quiero dejar de agradecer a todos aquellos que habéis dedicado unos minutos durante este año, para plasmar en comentarios de los distintos artículos que os han gustado, os han sido de ayuda, o incluso algunas correcciones y aportes… Anima mucho y siempre seguirá animando. 😊 A los lectores, muchas veces silenciosos, también daros las gracias por leerme y compartir mis artículos cuando así lo habéis creido oportuno. Puede parecer que no os tenga en cuenta, pero el número de visitas, claramente me dice que estáis ahí. !Gracias por ello! 😊

Sólo me resta desearos con mis mejores deseos ¡Felices Fiestas! (para los agnósticos y ateos), ¡Feliz Navidad! (para los creyentes) y ¡Feliz Año 2022! (para TODOS 😉 ). ¡Nos vemos a la vuelta!

Cambiar de Branch en un Shallow Clone

Si estas usando sistemas de integración continua (Continuous Integration), trabajas con repositorios pesados o simplemente no quieres pasar por un un clonado de Git que te traiga todo el árbol de un repositorio, seguro que estás familiarizado con el concepto shallow clone o clonado superficial.

Un shallow clone o cloando superficial permite traerse los últimos commits y no todo el histórico del repositorio Git. Aunque pueda parecer una solución maravillosa para desprenderse de los problemas de un clonado completo del histórico del árbol Git , presenta ciertos problemas a la hora de operarlo como un repositorio clonado de manera normal cuando se ha clonado bajo estas condiciones específicas de clonado.

🐑 Crear un clonado superficial

Para ejecutar un clonado superficial o shallow clone lo haremos con el siguiente comando:

git clone -–depth [depth] [remote-url]

Donde depth la profundidad es el número de commits que nos vamos a traer durante el clonado y remote-url es la dirección URL de origen de donde vamos a clonar el repositorio. 👀 El uso de --depth implica --single-branch.

Para ejecutar clonado superficial o shallow clone de una rama o branch podemos hacerlo con:

git clone [remote-url] -–branch [name] -–single-branch

Donde name es el nombre de la rama o branch que queremos clonar.

Si aún queremos hilar más fino y resulta que tenemos repositorios con submódulos (repositorios Git incluidos en otro repositorio), los cuales tienes su propio árbol Git y se inicializan usando alguno de los siguientes comandos:

git clone -–recursive [remote-url]  # Git version >= 1.6.5
git clone -–recurse-submodules -–jobs [num-jobs] [remote-url]   # Git version >= 2.13

La primera sintaxis con recursive puede resultar más cómoda, aunque resulta más rápida y eficiente la segunda con recurse-submodules, la cual es la sintaxis vigente que permite especificar el número de submódulos operados concurrentemente mediante jobs.

Si este es tu caso, no tiene sentido hacer un clonado superficial si se hace un clonado completo de los submódulo. Para ello ejecutaremos el siguiente comando:

git clone -–depth [depth] -–shallow-submodules [remote-url]

La opción --shallow-submodules implica que todos los submódulos se clonarán con una profundidad de 1.

🐏 Convertir un repositorio con clonado superficial en uno de tipo clonado completo

Si has seguido los pasos anteriores, te darás cuenta que si quieres cambiarte por ejemplo a otra rama no puedes. Eso es debido a que se ha omitido el resto del histórico según se lo hemos especificado. ¿Eso significa que no existe? No, eso sólo quiere decir que no lo conocemos.

Si ejecutamos el siguiente comando sobre un repositorio clonado superficialmente, veremos los remotos que conocemos y los remotos existentes en el local:

git branch -–all

Si intentamos hacer un fetch del remoto veremos que tampoco conseguimos ver el histórico completo del responsorio:

git fetch -–verbose

Esto es debido a que nuestro remoto no está convenientemente configurados debido al clonado superficial. Podemos restaurar su funcionalidad completa partiendo de que nuestro remoto origin, de donde clonamos el repositorio, contiene la remote -url que metimos. Por ello podemos restaurar el acceso a todo el histórico del remoto para nuestro repositorio, usando el siguiente comando:

git remote set-branches origin '*'

Mediante la opción set-branches podemos cambiar la lista de ramas que son seguidas por el remoto conocido y por defecto que es origin.

Ahora volvemos a ejecutar el comando fetch de nuevo y en este caso podremos apreciar que la totalidad de ramas aparecen:

git fetch -–verbose

Con esto ya tenemos acceso al historial completo del repositorio y podermeos cambiarnos a otra rama a la cual antes no podíamos:

git checkout rama-que-estaba-buscando

Si quieres comprobar el último commit de la rama local para cotejarlo con el último commit disponible el remoto y comprobar que está todo correcto, puedes hacerlo con:

git show

💡 Conclusiones

El clonado superficial o shallow clone es una gran herramienta, pero puede echarnos el lazo al cuello si no sabemos y necesitamos deshacerlo. No obstante aunque siempre existe la opción de hacer un clonado clásico de nuevo, no resulta una opción elegante y eficiente que nos obliga a duplicar y volver a clonar el repositorio.

Frase Memorable 11


Antonio Machado dijo, “En cuestiones de cultura y de saber sólo se pierde lo que se guarda; sólo se gana lo que se da”.


Inclusión de aportes mediante las diferentes estrategias de Merge en Git

Cada vez suele ser más normal ver la figura del ingeniero de software que se dedica a la gestión de proyectos, gestionando las ramas de desarrollo (develop & features) y la rama estable master (recientemente normalizada como main).

Imagen obtenida de la presentación Desarrollo Colaborativo de Software

Muchas veces sin querer se usan las herramientas de "merge" de código de manera incorrecta sin pararse a pensar en los beneficios y "contraprestaciones" que puede tener hacerlo de una forma u otra. Es por ello que este artículo, pretende arrojar algo de luz en qué nos puede convenir hacer en cada momento.

Antes de nada vamos a aclarar la notación para que resulte todo más sencillo. Nos referiremos a la rama Base, como la rama a la que se desea fusionar los cambios en el proceso de merge. Por otro lado nos referiremos a la rama Head como la rama que contiene los cambios que queremos incluir en la rama Base. Estos cambios en términos de plataformas para el desarrollo colaborativo de software como GitHub, GitLab, Bitbucket o Gogs, se suele aplicar con frecuencia en los conocidos como Pull Request (PR). Una solicitud formal a la inclusión de código de un desarrollador o grupo de desarrolladores externo al equipo de desarrolladores de un repositorio, la cual puede ser revisada y probada antes de realizar el merge.

La rama Base más habitual es la master o main que es asumida además como rama estable del desarrollo en un repositorio de manera generalizada. Los puntos más estables y que deberías usar son los denominados Tags de versión que son puntos específicos donde se ha trabajado una estabilidad específica que suele cerrar una iteración de desarrollo que incluye mejoras y soluciones a fallos conocidos desde la anterior versión.

Porque sale fuera de alcance de este artículo, no mencionaremos toda la problemática y gestión inherente que puede aparecer en forma de conflicto cuando realizamos un merge de código. Conflictos que deberán ser resueltos para asegurar la coherencia y estabilidad de la rama Base. Y que dicho sea de paso, suelen ser producidos muchas veces por falta de comunicación o por no ser "aséptico" en la forma de programar, refactorizando el código más de la cuenta o cuando no toca, por tener un mal diseño de clases o funciones... o incluso por aplicar la regla del scout que se resume en "Dejar las cosas mejor de como te las encontraste". Esto último puede convertir en un infierno la tarea de la persona que se dedica a integrar cambios y aportes externos si no se gestiona correctamente o falla el proceso de comunicación. Los potenciales conflictos por cambios en el código, es la causa número uno por la que una propuesta de cambio (PR) acaba por no integrarse al código finalmente y por consecuencia genera frustración y pérdida de tiempo.

Merge

La opción de hacer un merge de varios commits de una rama (Head) o hacer un merge de un PR es la acción habitual y predeterminada. Al realizar el merge todos los commits de la rama Head se fusionarán con los de la rama Base.

Estado inicial

Tras aplicar el merge

En esta ilustración, la rama Head se bifurca desde el segundo commit en la rama Base. Se sugieren algunos cambios como nuevas confirmaciones en la rama Head y ahora deben actualizarse en la rama Base. Mediante el merge, las confirmaciones se agregan a la rama Base como se muestra en la imagen superior.

No obstante el ejemplo presentado resulta una visión simplificada, existiendo dos posibles formas de hacer el merge dependiendo de la casuística en la inclusión o no del merge commit.

Fast-Forward o No Fast-Forward 🤔

En el primer caso, el merge fusiona lo cambios y añade los commits de la rama Head en la rama Base. Esto es posible siempre y cuando los commits se produzcan en la rama Head. Básicamente lo que se produce es un desplazamiento del puntero (fast-forward) de la rama Base al último commit de la rama Head.

master = Base | feature = Base

En el segundo caso, con el merge se añade un commit adicional en la rama Base que deja constancia de la unión de la rama Head con la rama Base. Este commit adicional aparece, bien porque lo forzamos para que no se produzca un fast forward (--no-ff); o bien porque la rama Base también contenía cambios, existiendo cambios en ambas ramas en el momento de la fusión.

Squash + Merge

Squash y merge combina todos los commits de la rama Head en un único commit y luego fusiona la rama Head con la rama Base. De esta manera, el historial de commits de la rama Head queda simplificado en un único commit en la rama Base.

Estado inicial

Tras aplicar squash + merge

Se puede observar que la rama Head se bifurca desde el segundo commit de la rama Base y se agregan dos nuevos commits que se añaden a la cabecera de la rama Base. Mediante squash y merge, ambos commits (6 y 7) se combinan en una único commit y luego se fusionan en la rama Base como se muestra en la imagen superior.

Rebase + Merge

Rebase y merge agrega todas los commits en la rama Head de manera individual a la rama Base sin un merge commit. Para todas los hotfix y commits puntuales que no se puedan fusionar con otros commits, esta es la opción de referencia. Esto es debido a que a no existe relación de parentesco con los commits preexistentes del propio árbol.

Cuando se hace una reorganización con rebase en el flujo de los commits de una rama, el SHA único de cada commit cambia debido a que además del contenido del commit para la formación del hash SHA, también se tiene en cuenta el parentesco con el commit inmediatamente anterior y por consiguiente del árbol completo. Este hecho provoca que el uso de rebase provoque más conflictos y su gestión sea más complicada. Esta gestión es complicada, es debido a que Git principalmente usa los commits comunes como base de parentesco, el contenido del propio commit y su contexto (líneas de código anteriores y posteriores a cada cambio de un commit); y en el caso del rebase por causa de la reorganización, no se cuenta con la base de parentesco.

Estado inicial

Tras aplicar rebase + merge de la rama Base a la rama Head

Tras aplicar rebase + merge de la rama head a la rama Base

Atendiendo a la ilustración, la rama Base se bifurca en el segundo commit para formar la rama Head. Posteriormente, se agrega un nuevo commit a la rama Base. Mientras tanto, los commits se realizan en la rama Head.

Por causa del rebase y merge de la rama de la Base a la rama Head, la base de la rama Head se vuelve a colocar. Es decir, ahora la rama Head se bifurca desde el nuevo tercer commit para que el nuevo commit de la rama Base se incluya en la rama Head. Y luego, se aplican los commits en la rama Head.

Ahora, para actualizar la rama Base con los últimos commits de la rama Head, el rebase se realiza de la rama Head a la rama Base como se muestra en la imagen superior.

GitHub

En GitHub podemos ver estas 3 opciones, siendo el ejemplo más habitual e ilustrativo cuando estamos revisando un PR y finalmente decidimos incorporar los cambios:

Haciendo click en la flecha que apunta hacia abajo al lado del "Merge pull request", podremos ver las tres formas de hacer merge que hemos mencionado. De manera análoga GitLab, Bitbucket o Gogs presentan las mismas opciones, o muy similares.

Conclusiones

Tras ver todas las opciones de merge, es recomendable tener en Git cuanta más traza mejor, eso incluye la creación del merge commit evitando el fast-forward si con ello podemos mejorar la traza de la rama donde existían los cambios que se fusionan a la rama Base.

Pero si trabajas en proyectos grandes o eres el gestor de la ramas de desarrollo de un repositorio con suficientes desarrolladores implicados, es más que seguro usarás squash y merge para compartimentar mejor los commits de cada desarrollador. Eso sí asegúrate de tener un buen sistema de CI como Travis CI o AppVeyor, así como suficientes pruebas unitarias, porque el problema de juntar varios commits en uno, es la imposibilidad de revertir los commits parciales una vez que se han juntado.

Por último el uso de rebase y merge es recomendable hacerlo como última opción y en caso de que squash y merge no sean suficiente o se quiera tener la trazabilidad manteniendo los commits parciales de las ramas que se fusionan (hotfix, commits puntales, excesiva densidad de ramas del árbol de versionado Git...), pudiendo realizar así una regresión limpia y más fácil de seguir. La principal desventaja de este método es que se pierde trazabilidad cronológica por la reorganización y potencialmente aumentan los conflictos, pero a la vez se gana simplicidad y claridad en el árbol debido a que no genera una densidad de ramas que complique su gestión. Además, también es posible revertir cualquier commit parcial, pero esto pierde relevancia, cuando se asume que existen servicios de CI que deberías estar ya usando que pueden hacer correr tus pruebas unitarias y ahorrarte sufrimiento y sobresfuerzo innecesario.