Ramas de proveedores

En el caso especial del desarrollo de software, los datos que mantiene bajo control de versiones están a menudo relacionados con, o quizás dependan de, los datos de terceros. Generalmente, las necesidades de su proyecto dictarán que debe permanecer lo más actualizado posible con los datos proporcionados por esa entidad externa sin sacrificar la estabilidad de su propio proyecto. Este escenario ocurre constantemente—siempre que la información generada por un grupo de personas tiene un efecto directo en aquello generado por otro grupo.

Por ejemplo, desarrolladores de software podrían estar trabajando en una aplicación que hace uso de una librería externa. Subversion tiene justamente este tipo de relación con la Apache Portable Runtime library (vea “La librería Apache Portable Runtime”). El código fuente de Subversion depende de la librería APR para sus necesidades de portabilidad. En fases iniciales del desarrollo de Subversion, el proyecto seguía de cerca los cambios de la API de la APR, siempre manteniéndose en la última versión recién salida del horno. Ahora que tanto APR como Subversion han madurado, Subversion sólo intenta sincronizarse con la API de APR en momentos bien verificados, versiones estables públicas.

Ahora, si su proyecto depende de la información de otros, hay varios modos que podría intentar para sincronizar esa información con la suya. La más dolorosa, sería ordenar verbalmente, o por escrito, instrucciones a todos los contribuyentes de su proyecto, indicándoles que se aseguren de tener las versiones específicas de esa información externa que su proyecto necesita. Si la información externa se mantiene en un repositorio Subversion, podría usar las definiciones externas de Subversion para fijar de forma efectiva versiones específicas de esa información en alguno de los directorios de su copia local (vea “Repositorios externos”).

Pero a veces necesita mantener modificaciones propias a los datos de terceros en su propio sistema de control de versiones. Volviendo al ejemplo de desarrollo de software, los programadores podrían necesitar hacer modificaciones a esa librería externa para sus propios propósitos. Estas modificaciones podrían incluir nueva funcionalidad o correcciones de fallos, mantenidos internamente sólo hasta que se conviertan en parte de un lanzamiento oficial de esa librería externa. O los cambios quizás nunca sean enviados a los autores de la librería, y existen solamente con el propósito de satisfacer las necesidades de sus desarrolladores como modificaciones personalizadas.

Ahora se encuentra en una situación interesante. Su proyecto podría almacenar sus modificaciones propias a los datos de terceros de algún modo disjunto, como usando ficheros parche o completas versiones alternativas de ficheros y directorios. Pero estos métodos se convierten rápidamente en problemas de mantenimiento, requiriendo de algún mecanismo que aplique sus cambios personales a los datos de terceros, sin olvidar la necesidad de regenerar esos cambios con cada versión sucesiva de los datos de terceros a los que sigue la pista.

La solución a este problema es usar ramas de proveedor. Una rama de proveedor es un árbol de directorios en su propio sistema de control de versiones que contiene información proporcionada por una entidad externa, o proveedor. Cada versión de los datos del proveedor que decide absorber en su proyecto es llamada hito de proveedor.

Las ramas de proveedor proporcionan dos beneficios clave. Primero, al almacenar el hito de proveedor actualmente soportado en su propio sistema de control de versiones, los miembros de su proyecto nunca tendrán que preguntarse si poseen la versión correcta de los datos del proveedor. Simplemente obtienen la versión correcta como parte de su actualización habitual de sus copias locales de trabajo. Segundo, ya que los datos viven en su propio repositorio Subversion, puede almacenar cambios personalizados directamente en el mismo lugar—ya no tiene necesidad de crear un método automático (o peor aún, manual) para proporcionar sus modificaciones.

Procedimiento general de gestión de ramas de proveedor

Gestionar ramas de vendedor generalmente funciona de esta manera. Primero crea un directorio en la raíz de su jerarquía (como por ejemplo /vendor) para almacenar las ramas de proveedor. Entonces importa el código de terceros en un subdirectorio de ese directorio principal. Luego copia ese subdirectorio en su rama principal de desarrollo (por ejemplo, /trunk) en un lugar apropiado. Siempre realiza cambios locales en la rama de desarrollo principal. Con cada nueva versión del código que está siguiendo, lo incluye en la rama de proveedor y fusiona los cambios en /trunk, resolviendo cualquier conflicto entre sus cambios locales y los cambios oficiales.

Quizás un ejemplo ayude a aclarar este algoritmo. Usaremos un escenario donde su equipo de desarrollo está creando un programa calculador que enlaza contra una librería externa de aritmética de números complejos, libcomplex. Comenzaremos con la creación inicial de la rama de proveedor, y la importación del primer hito de proveedor. Llamaremos al directorio de nuestra rama de proveedor libcomplex, y nuestros hitos de código irán en un subdirectorio de nuestra rama de proveedor llamado current. Y dado que svn import crea todos los directorios padre intermedios que necesita, en realidad podemos realizar ambos pasos con un único comando.

$ svn import /path/to/libcomplex-1.0 \
             http://svn.example.com/repos/vendor/libcomplex/current \
             -m 'importing initial 1.0 vendor drop'
…

Tenemos ahora la versión actual del código fuente de libcomplex en /vendor/libcomplex/current. Ahora, etiquetamos esa versión (vea “Etiquetas”) y luego la copiamos en la rama principal de desarrollo. Nuestra copia creará un nuevo directorio llamado libcomplex en nuestro directorio existente de proyecto calc. Es en esta versión copiada de los datos del proveedor que realizaremos nuestras personalizaciones.

$ svn copy http://svn.example.com/repos/vendor/libcomplex/current  \
           http://svn.example.com/repos/vendor/libcomplex/1.0      \
           -m 'tagging libcomplex-1.0'
…
$ svn copy http://svn.example.com/repos/vendor/libcomplex/1.0  \
           http://svn.example.com/repos/calc/libcomplex        \
           -m 'bringing libcomplex-1.0 into the main branch'
…

Ahora obtenemos una copia local de la rama principal de nuestro proyecto—que ahora incluye una copia del primer hito de proveedor—y comenzamos a personalizar el código de libcomplex. Antes de darnos cuenta, nuestra versión modificada de libcomplex está completamente integrada en nuestro programa calculador. [41]

Unas pocas semanas después, los desarrolladores de libcomplex lanzan una nueva versión de su librería—la versión 1.1—la cual contiene algunas características y funcionalidades que realmente deseamos. Nos gustaría actualizarnos a esta nueva versión, pero sin perder las personalizaciones que realizamos sobre la versión anterior. Lo que esencialmente nos gustaría hacer, es reemplazar nuestra versión de línea base de libcomplex 1.0 con una copia de libcomplex 1.1, y entonces reaplicar las modificaciones propias que anteriormente hicimos sobre la librería antes de la nueva versión. Pero en realidad nos acercaremos al problema desde otra dirección, aplicando los cambios realizados sobre libcomplex entre las versiones 1.0 y 1.1 sobre nuestra propia copia modificada.

Para realizar esta actualización, obtenemos una copia local de nuestra rama de proveedor, y reemplazamos el código del directorio current con el nuevo código fuente de libcomplex 1.1. Literalmente copiamos nuevos ficheros sobre los existentes, quizás expandiendo el fichero comprimido del distribución de libcomplex 1.1 sobre nuestros ficheros y directorios. El objetivo aquí es que el directorio current solamente contenta el código de libcomplex 1.1, y asegurarnos que todo ese código fuente está bajo control de versiones. Oh, y queremos realizar esto con la menor perturbación posible sobre el historial del control de versiones.

Tras reemplazar el código de la versión 1.0 con el de la 1.1, svn status mostrará los ficheros con modificaciones locales, junto con quizás algunos ficheros no versionados o ausentes. Si realizamos lo que debíamos realizar, los ficheros no versionados son aquellos ficheros nuevos introducidos en la versión 1.1 de libcomplex—ejecutamos svn add sobre ellos para ponerlos bajo control de versiones. Los ficheros ausentes son ficheros que existían en la 1.0 pero no en la 1.1, y sobre éstos usamos svn remove. Finalmente, una vez que nuestra copia local de current contiene únicamente el código de libcomplex 1.1, enviamos al repositorio los cambios realizados para que tenga este aspecto.

Nuestra rama current ahora contiene el hito de proveedor. Etiquetamos la nueva versión (del mismo modo que etiquetamos el anterior hito de proveedor como la versión 1.0), y entonces fusionamos las diferencias entre las etiquetas de la versión anterior y la actual en nuestra rama principal de desarrollo.

$ cd working-copies/calc
$ svn merge http://svn.example.com/repos/vendor/libcomplex/1.0      \
            http://svn.example.com/repos/vendor/libcomplex/current  \
            libcomplex
… # resolve all the conflicts between their changes and our changes
$ svn commit -m 'merging libcomplex-1.1 into the main branch'
…

En el caso de uso trivial, la nueva versión de nuestra herramienta de terceros sería, desde un punto de vista de ficheros y directorios, justo igual que nuestra versión anterior. Ninguno de los ficheros de código fuente de libcomplex habría sido borrado, renombrado o movido a una ubicación diferente—la nueva versión solamente tendría modificaciones textuales contra la anterior. En un mundo perfecto, nuestras modificaciones serían aplicadas limpiamente sobre la nueva versión de la librería, absolutamente sin complicaciones o conflictos.

Pero las cosas no son siempre tan simples, y de hecho es bastante habitual que ficheros de código fuente sean desplazados entre versiones de un software. Esto complica el proceso de asegurarnos que nuestras modificaciones son todavía válidas para la nueva versión del código, y puede degenerar rápidamente en la situación donde tenemos que recrear manualmente nuestras personalizaciones para la nueva versión. Una vez Subversion conoce la historia de un fichero de código fuente determinado—incluyendo todas sus ubicaciones previas—el proceso de fusionar la nueva versión de la librería es bastante simple. Pero somos responsables de indicar a Subversion cómo ha cambiado la estructura de ficheros de un hito de proveedor a otro.

svn_load_dirs.pl

Los hitos de proveedor que conllevan algo más que algunos borrados, adiciones o movimientos de ficheros complican el proceso de actualizarse a cada versión sucesiva de los datos de terceros. Por lo que Subversion proporciona el script svn_load_dirs.pl para asistirle en este proceso. Este script automatiza los procesos de importado anteriormente mencionados en el proceso de gestión de la rama del proveedor para minimizar el número posible de errores. Todavía será responsable de usar los comandos de fusionado para fusionar las nuevas versiones de los datos de terceros en su rama de desarrollo principal, pero svn_load_dirs.pl puede ayudarle a llegar a esta fase más rápido y con facilidad.

En resumen, svn_load_dirs.pl es una mejora a svn import que tiene ciertas características importantes:

  • Puede ser ejecutado en cualquier momento para transformar un directorio existente del repositorio para que sea una réplica de un directorio externo, realizando todas las operaciones de adición y borrado necesarias, y también opcionalmente haciendo algunas operaciones de renombrado.

  • Se encarga de una serie de operaciones complicadas entre las cuales Subversion requiere como paso intermedio enviar cambios al repositorio— como antes de renombrar un fichero o directorio dos veces.

  • Opcionalmente etiquetará el nuevo directorio importado.

  • Opcionalmente añadirá propiedades arbitrarias a ficheros y directorios que coincidan con una expresión regular.

svn_load_dirs.pl recibe tres parámetros obligatorios. El primer parámetro es la URL al directorio base de Subversion en el que se realizará el trabajo. Este parámetro es seguido por la URL—relativa respecto al primer argumento—en la cual se importará el hito de proveedor actual. Finalmente, el tercer parámetro es el directorio local que desea importar. Usando nuestro ejemplo anterior, una ejecución típica de svn_load_dirs.pl podría ser:

$ svn_load_dirs.pl http://svn.example.com/repos/vendor/libcomplex \
                   current                                        \
                   /path/to/libcomplex-1.1
…

Puede indicar que desearía que svn_load_dirs.pl etiquete el nuevo hito de proveedor pasando la línea de comando -t e indicando un nombre de etiqueta. Esta etiqueta es otra URL relativa al primer argumento pasado al programa.

$ svn_load_dirs.pl -t libcomplex-1.1                              \
                   http://svn.example.com/repos/vendor/libcomplex \
                   current                                        \
                   /path/to/libcomplex-1.1
…

Cuando ejecuta svn_load_dirs.pl, examina el contenido de su hito de proveedor actual, y lo compara con el hito de proveedor propuesto. En el caso trivial, no habrá ficheros que existan en una versión pero no en la otra, así que el script realizará la nueva importación sin incidentes. No obstante, si hay discrepancias en la estructura de ficheros entre las versiones, svn_load_dirs.pl le preguntará cómo desea resolver esas diferencias. Por ejemplo, tendrá la oportunidad de decirle al script que sabe que el fichero math.c en la versión 1.0 de libcomplex fue renombrado como arithmetic.c en libcomplex 1.1. Cualquier discrepancia no explicada como renombrado será tratada como una adición o borrado normal.

El script también acepta un fichero de configuración separado para ajustar propiedades sobre ficheros y directorios que coincidan con una expresión regular y que vayan a ser añadidos al repositorio. Este fichero de configuración se le indica a svn_load_dirs.pl usando la opción de línea de comando -p. Cada línea del fichero de configuración es un conjunto de dos o cuatro valores separados por espacios en blanco: una expresión regular estilo Perl que debe coincidir con una ruta, una palabra de control (ya sea break o cont), y opcionalmente, un nombre de propiedad y su valor.

\.png$              break   svn:mime-type   image/png
\.jpe?g$            break   svn:mime-type   image/jpeg
\.m3u$              cont    svn:mime-type   audio/x-mpegurl
\.m3u$              break   svn:eol-style   LF
.*                  break   svn:eol-style   native

Para cada ruta añadida, se aplicarán en orden los cambios de propiedades configurados para las rutas que coincidan con las expresiones regulares, a no ser que la especificación de control sea break (lo cual significa que no deben aplicarse más cambios de propiedades a esa ruta). Si la especificación de control es cont—una abreviación de continue—entonces se seguirá buscando coincidencias con el patrón de la siguiente línea del fichero de configuración.

Cualquier carácter de espaciado en la expresión regular, nombre de la propiedad, o valor de la propiedad deberá ser rodeado por caracteres de comillas simples o dobles. Puede escapar los caracteres de comillas que no son usados para envolver caracteres de espaciado precediéndolos con un carácter de contrabarra (\). Las contrabarras sólo escapan las comillas cuando se procesa el fichero de configuración, así que no proteja ningún otro carácter excepto lo necesario para la expresión regular.



[41] ¡Y por su puesto, completamente libre de fallos!