Tag: GitHub

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.

Cómo revisar y probar un Pull Request (PR) en GitHub, GitLab o Gogs

Hay gente que a día de hoy sigue sin entender la diferencia entre Git (herramienta de versionado) y GitHub (plataforma para alojar proyectos Git, ahora propiedad de Microsoft). Y mucho menos entiende el valor que aportan plataformas web como son GitHub, GitLab o Gogs, que añaden todo el valor social y de discusión sobre los cambios propuestos por otros desarrolladores o usuarios, permitiendo así el Desarrollo Colaborativo y auge que hoy en día presenta la filosofía de software Open Source.

Siendo así, vamos a aclarar que Git es una estupenda herramienta de versionado y sincronización de código, o cualquier fichero que no sea binario, permitiendo establecer la sucesión cronológica y relacionar los cambios entre sí para una control y revisión del avance de un proyecto.

Pero por otro lado Git solo, es bastante "cojo" por así decirlo. Las personas no nos relacionamos y trabajamos mediante protocolos o herramientas que llevamos instaladas y que nos permitan trabajar en equipo de manera eficaz 🤣. Es aquí donde entran las plataformas web que dan esa capa social que permiten a los desarrolladores y contribuidores al proyecto comunicarse, establecer criterios, marcar pautas, organizarse, revisar secciones de código... En este artículo nos vamos a centrar en el proceso de revisión de aportes de código Pull Request, abreviados y conocidos como PR.

Esquema básico del proceso de revisión:

Antes de empezar vamos a aclarar una serie de términos que creo que es conveniente definir. Lo primero explicar lo que es un Fork, que no es más que un clon de un proyecto cuya propiedad (en términos de administración) es de otro usuario u otra organización, pero permanece relacionado con su proyecto originario del que se hizo fork (normalmente se denomina al proyecto origen upstream). Y luego lo que es un Pull Request (de ahora en adelante PR), que es una petición que se hace al proyecto originario (upstream) para que incluya los cambios que tenemos en una de las ramas de nuestro repositorio "forkeado".

Entre el PR y la aceptación del mismo está lo que se conoce como discusión y revisión de los cambios propuestos. Los procesos de revisión son importantes porque permiten afinar los cambios, corregir metodologías de trabajo, realizar pruebas de integración con plataformas como AppVeyor o TravisCI... Podríamos decir que es el punto en el que se comunican las personas involucradas en la propuesta de PR.

Entrando en el terrenos práctico la discusión se genera en la sección de PR y es donde se produce toda la conversación. Aquí podéis ver un ejemplo:

- https://github.com/RDCH106/pycoinmon/pull/5

Creo bastante evidente como se debate, ya que es como un foro en el que dependiendo de la plataforma (GitHub, GitLab o Gogs...) será más completo o menos.

La pregunta del millón es cómo probamos el código que está en el PR. No se encuentra en una rama del repositorio el PR, realmente se encuentra en la rama del que nos hace el PR (a donde podemos ir para probar los cambios). De hecho lo interesante es que el PR puede ir cambiando, si el autor del PR en su fork, en la rama que usó para el PR, realiza cambios. Es decir, un PR no es un elemento estático sujeto a un commit concreto, sino que es la referencia a una rama del proyecto fork que contiene cambios que el upstream no tiene. El truco reside en que comparten la misma base de código y los cambios del PR son la diferencia entre la rama del proyecto upstream al que se le propone el PR y la rama del proyecto fork que contiene los cambios.

Pero esto es una verdad a medias... y digo a medias porque un PR sí es una rama, realmente es una referencia en el repositorio remoto (donde hacemos pull y push). Y eso es bueno 😁. La referencia entre un fork y su upstream nunca se rompe de manera oficial (existen por ejemplo mecanismos en GitHub para solicitar que un fork de un proyecto deje de estar relacionado con su upstream), por lo que asumiremos que un fork es como un padre y un hijo, pueden no llevarse bien pero eso nunca hará que desaparezca la relación parental padre-hijo. Cada vez que la rama del fork desde la que se hace PR se actualiza, la del proyecto upstream también queda actualizada. Por eso resulta conveniente usar ramas para hacer PR de diferentes features (características) que vamos a desarrollar, ya que así podremos mantener de manera independiente ambos paquetes de trabajo sin que haya interferencias entre ellos, y pudiendo dejar la rama el tiempo que haga falta hasta que finalmente se incluyan los cambios al proyecto upstream.

A causa de lo comentado antes podemos revisar desde la consola las referencias remotas con las que cuenta un proyecto git con el comando "git ls-remote". Voy a usar el proyecto pyCOINMON como ejemplo totalmente práctico:

rdch106@RDCH106 MINGW64 /d/GIT/pycoinmon (develop)
$ git ls-remote
From https://github.com/RDCH106/pycoinmon.git
be75cbb0e449383fd42c9033a8b76c844fcfef17        HEAD
d01ecf133c342f490030b56066ed8ff18b36f124        refs/heads/develop
be75cbb0e449383fd42c9033a8b76c844fcfef17        refs/heads/master
ec9c10b6d88274a1a006ae139856de478876572c        refs/pull/1/head
c0cb91e1776632494a23cf181584c1903516a758        refs/pull/11/head
46f2e6db3d22a4a48dcf3680157c95641123095d        refs/pull/2/head
035e64652c533a569c6b236f54e12aff35ad82b1        refs/pull/3/head
0f08bd644912e7de4a3bc53e3a8d0dbd64d6fc34        refs/pull/4/head
87ef8ca9af3eb5a523d3ef532fccee91aeedcd44        refs/pull/5/head
896d697e846649c7f23b9af3430067451ab5c089        refs/pull/7/head
d57455a8acf719cd3acea623f0759c6e11baada1        refs/pull/7/merge
d6f4c5a4b3e2b0025d74e9feed2609ba50835950        refs/tags/v0.1.0
be413f410cfa23e1f0f2715f2714874a22757b0d        refs/tags/v0.1.0^{}
cdc5e356b6d7f829e83a1b0a25e2a7304844223b        refs/tags/v0.1.4
4b69c059196f1f41bafc4f5970afb8a34817b509        refs/tags/v0.1.4^{}
b5649af39db2738481751f07b6ad59585144dbd0        refs/tags/v0.2.4
c3506ed1289f3dbcddb2db68201a114a9d1cf8b3        refs/tags/v0.2.4^{}
f0c564d2617d42c0fcb3cb842122a214a6021d37        refs/tags/v0.2.6
a3fe8f8b788311c1d4f5ee906cf2a4c70ffac8db        refs/tags/v0.2.6^{}
cea535e4e663531454b437ee8d7976a06e1ae415        refs/tags/v0.2.8
080192d1d7eb33feed6b92df2b389b39592545a6        refs/tags/v0.2.8^{}
9c374d2031a9da655be44e0c79944166cbf99e8a        refs/tags/v0.3.2
f76f78252863d372fde5d6ff197ba7519f7da50b        refs/tags/v0.3.2^{}
cc248bbd64f177a9b6041cad7c269b880c21b355        refs/tags/v0.3.5
fd50d344575e54def4b841221b2b5c2e8a0a74e3        refs/tags/v0.3.5^{}
7727986a93ce65c2deb905fcd92718c43f3e61ca        refs/tags/v0.4.0
aa2df260dc9c1ae9c1aa88ddd7d2498b451bf0fc        refs/tags/v0.4.0^{}
a620d5e379d495c69926d97a33803e91b3c07783        refs/tags/v0.4.4
d0b3fa9ae73a15ff17a47804fbbac80245c028c1        refs/tags/v0.4.4^{}
f12102dfacc530f90600082a97a0533ba862bd2c        refs/tags/v0.4.7
4329b9f5a9782071fe661376225df6d8d6dfa47e        refs/tags/v0.4.7^{}
1d3dbf367ab319f0d9a515c007ff07b90de6ca1c        refs/tags/v0.4.8
d01ecf133c342f490030b56066ed8ff18b36f124        refs/tags/v0.4.8^{}

Nos vamos a centrar por ejemplo en el PR #7 que es uno de los que tenemos abiertos y cuyos cambios supongamos que queremos probar:

Lo que vamos a hacer es traernos los cambios del PR basándonos en su ID (en nuestro caso #7), creando una nueva rama (PR_7) para poder probarlo por separado:

rdch106@RDCH106 MINGW64 /d/GIT/pycoinmon (develop)
$ git fetch origin pull/7/head:PR_7
remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Total 10 (delta 7), reused 7 (delta 7), pack-reused 3
Unpacking objects: 100% (10/10), done.
From https://github.com/RDCH106/pycoinmon
 * [new ref]         refs/pull/7/head -> PR_7

Ahora sólo resta hacer un checkout a PR_7:

rdch106@RDCH106 MINGW64 /d/GIT/pycoinmon (develop)
$ git checkout PR_7
Switched to branch 'PR_7'

O hacerlo desde el maravilloso SourceTree el cual os recomiendo como herramienta profesional para la gestión de vuestros repositorios Git locales y remotos (si eres usuario GNU/Linux no me olvido de ti, te recomiendo GitKraken).

A partir de aquí ya es hacer lo que quieras. Puedes probar los cambios, hacer algunas pruebas, hacer un "merge" para incluir los cambios en otra rama o incluso en la propia rama master.

En lo que a mí respecta, recomiendo simplemente probar los cambios y en la zona de discusión que comentaba antes, realizar todas las apreciaciones necesarias y dejar las cosas lo más claras posible, siendo responsabilidad del desarrollador que hizo el PR realizar los cambios para evitar conflictos. Una vez esté todo correcto, también recomiendo realizar el merge desde GitHub, GitLab o Gogs...

Además todo este proceso forma parte del aprendizaje y democratización de los conocimientos, haciendo de cada PR un sitio al que se puede volver para aprender y revisar lo ocurrido gracias a la trazabilidad en la discusión de los cambios.

Personalmente los procesos de revisión es una base estupenda junto al Planning Poker (prueba https://scrumpoker.online), para igualar las habilidades y competencias que permiten acortar divergencias entre las estimaciones de un grupo de desarrollo ante la planificación de las cargas de trabajo. Yo personalmente soy partidario de que el que lanzó la estimación más alta en la partida de poker realice el paquete de trabajo para que baje su estimación y aumenten sus habilidades, gracias a la supervisión (revisión) de quien dio la cifra más baja, que se asume que dio una estimación más ajustada por unas habilidades mayores.