Introducción
Para cerrar esta serie de entradas que he dedicado al control de versiones me gustaría hablar de un tema bastante importante a la hora de gestionar versiones de nuestro código fuente y que curiosamente es un tema del que no se habla mucho. El tema en cuestión es el Branching & Merging. Existen momentos a lo largo del ciclo de vida de un desarrollo en el que por diferentes cuestiones necesitamos separar nuestro código y abrir nuevos frentes de desarrollo. En este post voy a tocar brevemente los conceptos de Branching & Merging y repasaremos algunos de los casos en los que se hace necesario que nuestro código siga caminos diferentes.
Branching & Merging
El concepto de Branch en si es un concepto bastante simple. Consiste en hacer que un único conjunto de ficheros pueda evolucionar de dos o más formas diferentes que divergen, esto se conoce como Paralell Development y es un mecanismo que nos permite aislar estos diferentes caminos unos de otros sin que se afecten entre si. Crear una rama de un bloque de código dentro de un software de control de versiones es análogamente similar a crear una copia de una carpeta en un sistema de ficheros a diferencia de en este caso se guarda la relación entre el código base del que se hizo el Branch y la nueva rama que se ha creado, permitiendo así combinar los cambios en las dos ramas cuando sea necesario, proceso conocido como Merge.
A diferencia de otros productos Team Foundation Version Control tiene una forma curiosa de gestionar las ramificaciones del código fuente desde el punto de vista técnico. El hecho de que el medio de almacenamiento del código fuente en TFVC sea una base de datos dota al producto de una capacidad de relación entre versiones impresionante. En TFVC cuando se crea un Branch de un fichero, no se crea una copia de éste, sino que se crea una rama que apunta a un fichero origen en un estado concreto y lo que hace es crear dos caminos de versionado diferentes, siempre guardando la relación entre padre e hijo y su versionado de forma independiente. Si tenemos un fichero 1Mb y se crean 10 ramas diferentes sobre el mismo fichero esto no se traduce en 10Mb de espacio ocupados en el repositorio. El espacio no se ocupa hasta que el fichero es modificado y es necesario crear el versionado del mismo, lo cual ahorra un espacio bastante considerable ya que no todos los ficheros se modifican en todas las ramas.
Crear un branch de nuestro código en Team Foundation Server es sencillo, desde el explorador de código fuente elegimos la carpeta o fichero que queremos ramificar elegimos la opción Branch.
Esta operación nos abre un cuadro de diálogo que nos permite crear la rama basándonos en una fecha concreta, una etiqueta, un changeset o simplemente de la última versión del código que hay en el repositorio. Además nos permite elegir el nombre con el que se almacenará en el repositorio esta nueva rama que estamos creando.
Una vez hecho estamos lo única cosa más que necesitamos para disfrutar de las ventajas del desarrollo en paralelo que comentaba antes es hacer CheckIn de todo el código fuente que hemos ramificado. En este momento ya podremos evolucionar un mismo código base de dos formas diferentes y con su versionado de forma aislada. Al haber creado dos ramas diferentes del mismo código y haberlo evolucionado de formas diferentes se crea una desincronización entre las versiones que tarde o temprano hay que arreglar. Es aquí donde entre en juego el concepto de Merging. Hacer merging entre dos branches no es más que sincronizar los cambios que creamos oportunos producidos en las ramas. Esto es que si por ejemplo en una de las ramas se corrige un bug se puede pasar esa modificación del código a la otra rama. Esta sincronización o merge se puede hacer de forma bi-direcional, aunque según que estrategias ocurre más en un sentido que en otro.
Para llevar a cabo este sincronización entre ramas solo tenemos que elegir la opción Merge que nos aparece en el menú contextual de cualquier carpeta o fichero, ya sea de la rama o de la versión base, que nos mostrará una wizard para llevar a cabo la combinación de los cambios entre ramas. En este asistente se nos permite especificar el origen y el destino del Merge y también si queremos hacerlo de una versión específica del código o si queremos únicamente sincronizar uno o varios changesets.
Escenarios y Estrategias
La idea de crear ramificaciones de código en base a escenarios que pueden producirse a lo largo del ciclo de vida de un desarrollo no es nuevo. Al igual que con la arquitectura de software se reconocen ciertos patrones de comportamiento y existen una serie patrones de diseño que cubren esas necesidades en el tema que nos ocupa también existen una serie de escenarios, y sus correspondientes estrategias, que aplicadas pueden ayudarnos a mejorar la gestión de nuestro preciado código fuente.
Dado a que este post esta ya cogiendo un tamaño considerable, voy a comentar brevemente por ahora varios de los escenarios y estrategias que podemos adoptar ante situaciones que nos surgen a lo largo del ciclo de vida del desarrollo de un proyecto.
Escenarios
Release Isolation: Este escenario se produce cuando tenemos que mantener el código fuente de varias versiones del mismo producto. Este escenario es el que más se repite y es por el que la mayoría de empresas comienza a tener ramas de un mismo producto. Estas ramas no tienen porque ser versiones diferentes a nivel del producto en si, por ejemplo v1.0 o v2.0, sino que pueden ser la misma versión del producto para la línea de desarrollo general, para la rama de pruebas o integración o la de producción.
Feature Isolation: En este caso las ramas se hacen necesarias si se quieren desarrollar nuevas características del producto sin que afecten a las que ya existen y hacer merge cuando la característica este lista para ser integrada en la rama principal.
Team Isolation: Otro de los escenarios posibles es que un mismo producto lo esté desarrollando más de un equipo de forma simultanea, produciendo esto muchos quebraderos de cabeza con los bloqueos de ficheros y las consiguientes combinaciones que se tendrían que dar a diario. De esta forma permitimos trabajar a distintos equipos sin que interfieran los cambios hechos por cada uno de ellos.
Integration Isolation: Hacer merge entre ramas, por ejemplo en el caso del escenario en el que varios equipos trabajan sobre un mismo producto, puede ser muy costoso y llevar varios días de trabajo, implicando a varias personas. En este caso, la solución es crear una rama más para integrar el resto de ramas y no entorpecer a los equipos de desarrollo al tener bloqueada su rama mientras se intenta hacer el merge o resolver conflictos.
Estrategias
La estrategia básica a seguir es tener tres bloques de ramas bien diferenciados, estos bloques se suelen denominar Dev, Main y Production. Cada uno de ellos tiene su función y en base al escenario que tengamos se pueden crear más ramas hijas dentro de cada una de ellas. El código fuente sigue un modelo de promoción entre estas ramas Dev -> Main -> Production, aunque como dije antes este modelo de promoción también puede producirse de forma bi-direccional. Cada una de estas ramas tiene una finalidad que vamos a ver a continuación
Dev: Esta rama es la del desarrollo activo, donde los desarrolladores crean nuevo código, ejecutan pruebas unitarias y realizan la corrección de bugs que se van produciendo al lo largo del desarrollo. En esta rama se suele recomendar hacer compilaciones en periodos de tiempo muy cortos o incluso hacer uso de la ya conocida integración continua.
Main: A medida que se van alcanzando hitos en nuestro proyecto, el código de la rama Dev se puede ir promocionando a la rama Main, que es la rama donde los testers realizaran pruebas más intensivas a nivel funcional, de rendimiento, de seguridad y de calidad del código. En muchos casos esta rama es también conocida con la rama Test o Integration. Una vez que el código alcanza la calidad necesaria y esta listo puede promocionarse a la siguiente de las ramas, Production.
Production: A esta rama llega el código listo para ponerse en producción y es en esta donde si fuese necesario corregiríamos los bugs críticos que surgiesen en la aplicación.
En estas tres ramas principales se basan la mayoría de estrategias, si necesitasemos versionar nuestro código fuente basado en diferentes versiones del producto (Release Isolation) no necesitaríamos crear mas ramas. Sin embargo si estuviésemos en el caso de aislamiento por características (Feature Isolation), nos bastaría con crear diferentes ramas dentro de la rama Dev para cubrir cada una de las características e ir promocionandolas a la rama Main cuando fuese necesario. De la misma manera poco esfuerzo más también se cubren los otros dos escenarios que describía antes.
Como nota final deciros que al igual que existen una serie de buenas prácticas asociadas al uso del Branching & Merging también las existen malas y siempre debemos analizar a fondo nuestro escenario e intentar prever las situaciones que nos pueden causar problemas para evitar caer en el uso de malas prácticas.
Por último pediros "perdón" por la extensión del post y también por ser en algunos casos demasiado breve con las definiciones o descripciones. Sabéis que estaría encantado de ampliarle más información acerca del tema a quién me lo pida ya que es un tema muy extenso.