Git (II) - Ramificaciones

Lectura de ~8 minutos.

Hace poco vimos como dar los primeros pasos con Git. Allí vimos los aspectos básicos de Git. Ahora veremos la potencia detallada de las ramificaciones.

Primero la pregunta más obvia, ¿que es una ramificación? Como se mencionó en el post anterior, Git almacena instantáneas del trabajo realizado, y entre otras cosas uno o varios apuntadores a confirmaciones padre. De este modo se crea una estructura en árbol, de modo que cada rama de esta estructura se puede considerar una ramificación de desarrollo.

¿Cómo se crea una rama? Realmente no se crea una rama, se crea un nuevo apuntador para que se pueda mover. Por ejemplo si queremos crear una rama llamada “prueba” debemos ejecutar el siguiente comando:

$ git branch prueba

Git sabe en que rama te encuentras actualmente en todo momento gracias al apuntador especial llamado HEAD. Este apuntador no se puede eliminar y se mueve automáticamente al apuntador donde se esté trabajando. El comando anterior crea un apuntador nuevo pero no nos mueve a ese apuntador. Si quueres moverte a esa rama debes utilizar el siguiente comando:

$ git checkout prueba

De modo que si se realiza algún trabajo y se confirman los cambios después de hacer checkout a la rama prueba el repositorio local tendrá la siguiente estructura:

Estructura git 1

prueba avanza pero master permanece sin cambios. Si quieres volver a tener el proyecto en el estado en el que estaba master debes ejecutar el siguiente comando:

$ git checkout master

Este comando además de mover el apuntador HEAD de vuelta a la rama master modifica los ficheros del proyecto de modo que se quedan como estaban en el momento en que se tomó la instantánea confirmada para la rama master. Si, en este momento se realiza algún cambio en el proyecto y se confirma el estado del repositorio local tendrá una estructura como la siguiente:

Estructura git 2

De modo que el proyecto puede avanzar en dos direcciones distintas.

Con esto ya podemos ver un flujo de trabajo que se podría dar un día cualquiera.

Supón que estás trabajando en un proyecto web. Y te dispones a programar una funcionalidad genial. Para ello creas una rama a partir de la rama master llamada funcionalidad_genial con el siguiente comando:

$ git checkout -b funcionalidad_genial

Este comando crea la rama y salta directamente a ella. De modo que el repositorio tendrá la siguiente estructura:

Estructura git 3

Y como estás muy entusiasmado enseguida tienes una confirmación lista y hecha. De modo que el trabajo avanza de la manera siguiente:

Estructura git 4

Vas a seguir avanzando pero en ese momento te llega un reporte de un error en producción, y tu jefe te pide corregirlo antes de seguir con la funcionalidad. Tranquilo, el trabajo hecho en la nueva funcionalidad está a salvo en la rama funcionalidad_genial. Ahora lo que tienes que hacer es volver a master y resolver el fallo. Para ello vuelves a master con el siguiente comando:

$ git checkout master

Y creas una rama para corregir el fallo. Normalmente estas ramas son llamadas hotfix:

git checkout -b hotfix

Hoy está siendo un día muy productivo y ya has resuelto el fallo y confirmado el cambio, de modo que el repositorio tiene la estructura siguiente:

Estructura git 5

Como el fallo está resuelto, es hora de integrarlo en la rama principal del pryecto mater. Esto se hace yendo a la rama master y fusionando los cambios de la rama hotfix:

$ git checkout master
$ git merge hotfix

En este caso Git, al no haber conflictos entre las dos ramas y la rama desde donde se ha fusionado estaba directamente accesible desde master, avanza el apuntador de la rama master a la misma confirmación de la rama hotfix de modo que la estructura del pryecto se queda como la siguiente:

Estructura git 6

Como ya no se va a seguir utilizando la rama hotfix es hora de borrarla. En este caso no habrá ningún problema porque tanto master como hotfix apuntan a la misma confirmación. Para borrar la rama utilizamos el siguiente comando:

$ git branch -d hotfix

Y acto seguido vuelves al trabajo de la funcionalidad genial, realizando otra confirmación más:

Estructura git 7

Y como el trabajo ya está acabado hay que integrarlo en la rama master del proyecto. Se utiliza el mismo procedimiento que el utilizado para la rama hotfix:

$ git checkout master
$ git merge funcionalidad_genial

Pero en este caso Git tiene algo más de trabajo que hacer: busca una confirmación ancestro común a las dos ramas que se van a fusionar y crea a partir de esa confirmación y las dos confirmaciones a las que apuntan las dos ramas otra confirmación nueva con los cambios fusionados. De modo que el repositorio quedará como lo siguiente:

Estructura git 8

Y como la funcionalidad está acabada e integrada en el proyecto, la podemos eliminar la rama:

$ git branch -d funcionalidad_genial

Ahora bien, no todo en Git es un camino de rosas y se integra una rama a otra todos los días y ya está. En algunas ocasiones puede suceder que al realizar la fusión hayan conflictos que Git no puede resolver:

$ git merge funcionalidad_genial
Auto-merging archivo.html
CONFLICT (content): Merge conflict in archivo.html
Automatic merge failed: fix conflicts and then commit the result

En este caso no se crea la nueva confirmación de forma automática, para el proceso dejando que resuelvas el conflicto antes de continuar. Si quieres ver que ficheros están en conflicto se hace con el comando git status:

$ git status
On branch master
You have unmerges paths.
  (fix conflicts and run "git commmit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

  both modified:    archivo.html

no changes added to commit (use "git add" and/or "git commit -a")

De modo que si abrimos el fichero archivo.html encontraremos algo como lo siguiente:

<<<<<<< HEAD:archivo.html
<div id="contacto"> contacto: mail.mail@mail.com</div>
=======
contactanos en: mail.mail.@mail.com</div>
>>>>>>> funcionalidad_genial:archivo.html

Esto nos indica que lo que hay por encima de los símbolos ======= está en HEAD, en este caso la rama master que es donde queremos llevgar los cambios. Lo que está por debajo de los símbolos pertenece a la rama que queremos traer los cambios, en este caso funcionalidad_genial y Git no sabe resolver el conflicto y te deja a ti el trabajo de resolverlo. Puedes resolverlo dejando ese bloque en concreto como lo siguiente:

<div id="contacto"> contactanos en: mail.mail@mail.com</div>

Y ya puedes preparar el fichero y confirmar el cambio como de costumbre.

Hay varias formas de gestionar las ramas de Git. La más común es dejar en la rama master el trabajo que hay en producción, intentando dejar esta rama siempre en el estado más estable posible. La rama develop será más o menos estable, y contendrá los siguientes desarrollos que se van a implementar. Estas ramas son denominadas ramas de largo recorrido ya que son ramas que nunca se borran. Las demás ramas son ramas de corto recorrido, por ejemplo como la que hemos visto en el ejemplo de funcionalidad_genial, son ramas que tienen un periodo corto de vida, sirven para realizar nuevos desarrollos o corregir fallos, como en el caso de hotfix, y pueden o no acabar siendo integradas en las ramas de largo recorrido.

Además de tus ramas locales, también hay que tener en cuenta las ramas del repositorio remoto. Al igual que las ramas locales, las remotas son solamente una referencia al una confirmación en concreto y cada confirmación es el estado del repositorio en ese momento y su antecesor o antecesores. De modo que si estás realizando algún trabajo en la rama master y alguien al mismo tiempo también lo esta realizando sobre la misma rama y confirma los cambios antes que tu, cuando hagas git push Git se dará cuenta que no tienes tu repositorio local actualizado y rechará tu petición de incorporación de cambios. De modo que tendrás que actualizar tus referencias, fusionar los cambios y resolver los posibles conflictos antes de poder enviar cambios a la rama remota master. Para envia los cambios de tu repositorio local al repositorio remoto debes ejecutar el siguiente comando:

$ git push origin master

Siempre debemos especificar el nombre del repositorio remoto y la rama que queremos enviar.

Como hemos visto puede que este comando nos deniegue los cambios que hemos realizado. Para actualizar las referencias de tu repositorio local debes ejecutar el siguiente comando:

$ git fetch origin

Especificando el repositorio remoto. Git no obtiene las ramas remotas mediante este comando, solamente trae toda la información del repositorio remoto. Si quieres fusionar los cambios de lo obtenido del repositorio remoto debes ejecutar el siguiente comando después del comando anterior:

$ git merge

En el caso que no desees realizar dos comandos, lo puedes hacer solo con uno con el siguiente comando:

git pull origin

Si un compañero ha publicado algo en alguna rama y quieres descargar su contenido puedes hacerlo haciendo checkout a la rama remota publicada:

$ git checout -b otra_funcionalidad origin/otra_funcionalidad

Con esto ya tienes los conocimientos básicos para poder trabajar con los compañeros en un mismo proyecto y compartir vuestro trabajo.

Nos vemos en las próximas líneas.

Escrito el 05/06/2017