Git Hack, imgen ornamentalEste es un tema interesante y muy poco documentado, especialmente en Windows, y como me he tenido que pelear con ello recientemente, aprovecho y lo cuento por si le puede resultar útil a alguien más.

Imagina la siguiente situación: tienes un repositorio que sigue las reglas de Git-Flow, por lo que tienes una rama master para las versiones de tu software que irán a producción, una rama develop a partir de la que desarrollas creando subramas por cada tarea, y una serie de pequeñas subramas para release y bugs. Vamos, lo que viene siendo el clásico Git-Flow "de toda la vida":

Esquema de ramas en Git-Flow

De hecho ni siquiera tiene por qué ser este flujo, sino que vale uno cualquiera en el que debas mezclar ramas relacionadas.

Bien, el problema en cuestión surge porque la aplicación tiene un archivo de configuración (o varios, pero por simplificar, digamos uno solo) que es diferente en producción y en desarrollo. De modo que lo que quieres es que, cuando estés desarrollando, el archivo config.json (o como sea: web.config, params.xml, etc.) tenga unos ciertos valores adecuados para tu máquina y las de tus compañeros, pero cuando mezcles la rama de desarrollo hacia producción (hacia master) los valores que hay en el config.json de la rama master no se modifiquen, ya que, por ejemplo, quieres hacer despliegue continuo y se van a mover a producción tal cual están.

Es decir, quieres que un determinado archivo nunca se vea afectado por las mezclas de ramas de Git, y puedan cambiarse los valores que ya tiene.

¿Cómo puedes conseguirlo?

Sobrescribiendo los métodos de resolución de conflictos para archivos concretos

Cuando haces la mezcla de dos ramas en Git, si en la rama que estás mezclando (develop) un parámetro tiene el valor x, y en la rama que recibe la mezcla (master) ese mismo parámetro del mismo archivo tiene el valor y, lo que va a ocurrir es que se produce un conflicto. Sin embargo si lo que se hace en develop es añadir nuevos parámetros y no afectar a los existentes, cuando se haga la mezcla no ocurrirá conflicto alguno y por tanto se incorporarán los nuevos parámetros sin problema, lo cual es fenomenal porque no perderemos nada de lo nuevo. Sólo queremos que no afecte a lo preexistente. Es decir, sólo queremos que actúe cuando hay un conflicto.

En caso de conflicto lo que debes hacer para conservar los valores existentes es simplemente no aceptar los cambios entrantes y decirle, durante la resolución del conflicto, que quieres quedarte con los valores actuales (con tus valores, que son los de la rama en la que estás). De este modo conservas lo que querías.

Bien. El problema de esto es que tienes que hacerlo cada vez que se haga una mezcla. Y eso es tedioso y propenso a fallos.

Lo que pretendemos con la técnica que propongo, es que esa resolución de conflicto sea automática: que se quede con lo de la rama actual siempre, sin que tengas que decirle nada ni hacer nada manualmente.

Para conseguirlo, lo que vamos a hacer son dos cosas:

1.- Definir un nuevo driver de resolución de conflictos

 Git es muy extensible. No solo gracias a los git-hooks sino también mediante su correcta configuración. Una de las cuestiones que nos permite configurar son los drivers para mezclas. Un driver de mezcla no es más que un script o un ejecutable que se ejecuta al producirse un conflicto al mezclar archivos. Aparte de los drivers de este tipo que ofrece por defecto, podemos crear los nuestros propios, y para el caso que no ocupa es extremadamente sencillo.

La definición de un driver de mezcla se hace introduciendo una definición sencilla en la configuración de Git. Puede hacerse de manera global o local a nuestro proyecto. El comando necesario es el siguiente en Linux o macOS:

git config merge.actual.driver true

Esto lo creará en el repositorio local. Si lo queremos para todos los repositorios del equipo, o sea, un driver de mezcla global, sólo tenemos que añadir el modificador --global justo después del config.

Lo que hacemos es definir un driver de mezcla de nombre actual (porque lo que hará será quedarse con la versión actual del archivo conflictivo, pero le puedes llamar como quieras), que lo único que hará será lanzar un comando de Bash (true) que lo único que hace es devolver un código de éxito y no hacer nada. Es decir, lo que hará cuando se ejecute es nada de nada, y por lo tanto dejará el archivo original intacto. Ahora lo veremos.

En Windows, dado que no tenemos un comando true, (salvo que estemos usando el subsistema de Linux, WSL, pero entonces es lo mismo que un Linux) podemos utilizar una táctica alternativa equivalente, con la que me ha levado un buen rato dar, y que consiste en hacer esto:

git config merge.actual.driver "exit 0" %O %A %B

En este caso llamamos al comando de la línea de comandos llamado exit, de modo que devuelva un código de error 0 (o sea, sin error). Son importantes las comillas dobles. Además le pasamos los 3 parámetros que puede tomar un driver de mezcla de Git para trabajar, a saber: 

  • %O: la versión del archivo anterior a la actual
  • %A: la versión del archivo en la rama actual
  • %B: la versión del archivo en la rama que estamos mezclando y que da conflicto

En este caso no hace nada con ellos pero los meto por completitud (y porque ha sido la manera en la que me ha funcionado el "truco"). La info completa la tienes en la documentación de los atributos de Git.

Bien, de esta forma tenemos definido el driver de mezcla. De hecho podemos verlo en el archivo .git/config de nuestro repositorio:

El contenido del archivo .git/config

Como podemos observar la definición no es más que un poco de texto. De hecho, podemos copiar ese texto y pegarlo en el archivo config de otro repositorio local que tengamos y funcionará también, sin recurrir a la línea de comandos.

2.- Cambiar los atributos del archivo para que use el nuevo driver

 Vale. Ya tenemos definido el driver de mezcla llamado "actual" que deja la versión de la rama actual de los archivos a los que se lo apliquemos.

Ahora sólo nos queda decidir a qué archivos se lo aplicamos. Para ello vamos a sacar partido al fichero de atributos de Git. Debemos crear un archivo en la raíz de nuestro repositorio local (valdría también en una subcarpeta si solo queremos afectar a ésta) con el nombre .gitattributes. Este es un archivo de texto plano que, al igual que .gitignore nos permite especificar archivos o patrones de archivos (usando Globs de UNIX) y los atributos específicos que queremos usar con ellos.

Así, por ejemplo, si queremos que nuestro archivo de ejemplo config.json use el nuevo driver que hemos creado sólo tenemos que escribir dentro del archivo lo siguiente:

config.json merge=actual

en donde le estamos indicando que para ese archivo en concreto (suponiendo que este en la raíz) deberá usar el driver de mezcla "actual" que hemos definido.

Nota: como se pueden usar globs, al igual que en .gitignore, si por ejemplo hubiese varios archivos de configuración dentro de la carpeta "configuracion" y sus subcarpetas, podríamos haber usado: /configuracion/**/*.json merge=actual y afectaríamos a todos ellos. Es bastante flexible. 

Vale, pues ya está. A partir de ahora si nos movemos a la rama master y mezclamos las nuevas características desde la rama develop, se producirá el conflicto igualmente pero ya estará aceptado el valor local como resultado del mismo, por lo que no nos tendremos que preocupar de que algo haya cambiado. Si hay cosas nuevas añadidas al mismo, las incorporará, como en cualquier mezcla, pero no nos afectará a las preexistentes que entrarían en conflicto.

Por ello, más allá de que recibimos un aviso de conflicto, lo único que tendremos que hacer es el commit para cerrar la mezcla y listo. Un problema menos 😊

Inconvenientes

Para mi la mayor "pega" que tiene esto es que debes definir el driver en cada uno de los repositorios que lo vayan a usar. Es decir, que si hay varias personas desarrollando es muy importante que se le diga a todas ellas que cada vez que clonen el repositorio deben añadir el driver al mismo. Para ello se les puede dejar el comando en un archivo .bat en Windows o .sh en Linux/macOS y listo. Pero deben ser disciplinados o les saltarán los conflictos sin resolver (aunque la primera vez que les salten seguro que se acuerdan de añadirlo en cualquier caso).

Los atributos del archivo o archivos que queremos afectar no es necesario hacer nada porque, como van en el archivo .gitattributes y éste va dentro del repositorio (y por tanto lo reciben todos), ya está incluida la configuración en el propio repositorio.

Cabría pensar que sería posible usar un archivo .gitconfig en la raíz del proyecto para definir este driver para mezcla y así poder distribuirlo a todos los miembros del equipo. Y de hecho es lo primero que intenté, pero no funciona. Por lo que he podido averiguar es una medida básica de seguridad para evitar que cosas potencialmente peligrosas pudieran transferirse de este modo, por lo que no quedará más remedio que hacerlo en cada repositorio.

Por lo demás es un buen método para conseguir lo que necesitamos y se puede extrapolar a otras aplicaciones, como por ejemplo mantener ramas "máster" específicas de ciertos clientes con su configuración personalizada.

¡Espero que te sea útil!

Escrito por un humano, no por una IA