Cómo: Implementar un servidor para Control de versiones con Subversion en CentOS 5.

Desde hace tiempo he tenido la intención de aprender a usar algún sistema de control de versiones para los proyectos y por fin decidí hacerlo. Escogí Subversion por ser el más actual y de más amplia utilización. Pero antes de poner manos a la obra veamos algo de teoría:

Introducción.

SubVersion es un sistema de control de versiones, cuyo cometido es administrar el acceso a un conjunto de ficheros, y mantener un historial de cambios realizados. El control de versiones es útil para guardar cualquier documento que cambie con frecuencia, como el código fuente de un programa.
Normalmente consiste en una copia maestra en un repositorio central, y un programa cliente con el que cada usuario sincroniza su copia local. Esto permite compartir los cambios sobre un mismo conjunto de ficheros. Además, el repositorio guarda registro de los cambios realizados por cada usuario, y permite volver a un estado anterior en caso de necesidad. Pero, ¿que hacer cuando dos usuarios intentan modificar el mismo fichero?. Existen dos estrategias:
  • Bloqueos: el usuario bloquea el fichero durante su edición, evitando el acceso concurrente de otros usuarios. Existen varios problemas: el usuario que acapara ficheros, el interbloqueo entre usuarios que necesitan varios ficheros, y la falta de concurrencia.
  • Merge (fusión de cambios): los ficheros se acceden concurrentemente. Los cambios realizados sobre un mismo fichero son fusionados inteligentemente por el sistema. El único problema es el intento de fusión de cambios incompatibles, que ha de solucionarse manualmente.

Características.

  • Actualiza ficheros modificados. El cliente recorre el código del servidor y sincroniza la copia local con el repositorio.
  • Copias de seguridad centralizadas. Solo el administrador debe preocuparse de realizar copias de seguridad en el repositorio.
  • Historial de cambios. El repositorio guarda registro de todos los cambios realizados. Es posible recuperar cualquiera de las versiones anteriores de cualquier fichero. Si alguien borra todos los ficheros, podemos volver atrás y recuperar su contenido.
  • SubVersion contiene dos métodos de acceso por red. Uno es una variante del protocolo HTTP llamado WedDAV/DeltaV, que proporciona mecanismos de autentificación, compresión y navegación a un repositorio. El otro método es un demonio que usa su propio protocolo el cual puede fácilmente ser encapsulado sobre ssh.
  • Seguridad. Es posible otorgar diferentes permisos sobre diferentes ramas del proyecto. Por ejemplo, estableciendo permiso universal de lectura, y permiso de escritura solo a ciertos usuarios.
  • Registra cambios en la estructura de directorios (permite mover y renombrar sin perder el historial). SubVersion usa un sistema virtual de ficheros versionado sobre una base de datos.
  • El uso de la base de datos Berkeley permite aislamiento, atomicidad, recuperación de datos, integridad, backups en caliente, y concurrencia sin necesidad de usar ficheros de lock. Con Berkeley DB no se pueden editar los ficheros a mano , pero eso tampoco es necesario porque el repositorio no se corrompe.
  • Commits atómicos, se realizan todos o ninguno. Las transacciones atómicas permiten identificar conjuntos de cambios. Cuando un desarrollador sube un conjunto de ficheros lo hace en una transacción atómica, de modo que todos los ficheros se etiquetan con un número de revisión en el repositorio. La atomicidad también impide que el repositorio quede en estado no compilable porque la red cae durante la subida de cambios.
  • Servidor y cliente intercambian diferencias entre versiones. Al enviar una nueva versión nunca es necesario transmitir ficheros enteros.
  • Permite operar directamente sobre el repositorio, sin copia local.
  • Posibilidad de trabajar con Apache versión 2 (se conecta al servidor como un modulo de extensión), esto hace que nos podamos aprovechar de toda la tecnología de este servidor Web. Es posible utilizar SSL (encriptación), trabajar remotamente utilizando el puerto estándar de los servidores Web (puerto 80).
  • Usa la biblioteca Apache Portable Runtime, que permite portar la capa de red a varios sistemas operativos.
  • Versiona todos los ficheros guardando comprimidas sus diferencias.
  • No es necesario duplicar el código en el repositorio para crear ramas. SubVersion usa copia perezosa, solo se crea un nuevo fichero cuando es modificado. Mientras tanto, el fichero de la nueva rama, esta implementado como un enlace al fichero original.
  • No es necesario conexión a red para ciertas operaciones: status, diff, revert. Esto se debe a que la copia local contiene una copia del fichero original presente en el repositorio. Este comportamiento ahorra ancho de banda a costa de mayor espacio en disco.
  • Los números de versiones son globales para todo el repositorio. No hay un número de versión por fichero (mirar apartado revisiones).

Ahora sí, ya que hemos conocido a breves rasgos el subversion, implementémoslo en un equipo con CentOS 5.

Implementación.

Lo primero será instalar los paquetes necesarios:

[root@dv4-1413 ~]# yum install mod_dav_svn subversion

mod_dav_svn es el móduo de apache para permitirle trabajar con SVN, subversion es el paquete que instalará el SVN. Si no se tuviera el Apache (httpd) ya instalado, el comando yum indicado lo agregará a la lista de paquetes a descargar/instalar, así como a cualquier otra dependencia necesaria.

Bien, el siguiente paso será verificar que el servidor Apache arranque. Normalmente no es necesario hacer modificaciones a la configuración de apache, así que procederemos con:

[root@dv4-1413 ~]# service httpd start
[root@dv4-1413 ~]# chkconfig httpd on

Si se desea personalizar cualquier cosa en el Apache, se podrá hacer con toda libertad y luego reiniciar el servidor Apache.

Ahora configuremos el módulo de apache para trabajar con subversion, de tal forma que puedan trabajar juntos adecuadamente. Editaremos entonces el archivo de configuración de ejemplo que viene con el módulo:

[root@dv4-1413 ~]# cd /etc/httpd/conf.d/
[root@dv4-1413 conf.d]# vi subversion.conf
# Asegúrate de descomentar estas dos líneas para garantizar la carga de los módulos
LoadModule dav_svn_module     modules/mod_dav_svn.so
LoadModule authz_svn_module   modules/mod_authz_svn.so

# Agrega una sección similar a ésta para tu repositorio SVN
<Location /svn/repo>
   DAV svn
   SVNPath /var/www/svn/repo

   AuthType Basic
   AuthName "Subversion :: Proyecto Repo"
   AuthUserFile /var/www/.htpasswd
   Require valid-user
</Location>

Como vemos, la configuración incluye un sistema básico de autenticación, así que deberemos crear el archivo de claves así:

[root@dv4-1413 ~]# htpasswd -cm /var/www/.htpasswd pbernal

La clave para el usuario pbernal se solicitará 2 veces en la consola y luego se almacenará en el archivo indicado. El siguiente paso será crear la estructura de directorios necesaria para el repositorio:

[root@dv4-1413 ~]# cd /var/www
[root@dv4-1413 ~]# mkdir svn
[root@dv4-1413 ~]# cd svn
[root@dv4-1413 ~]# svnadmin create repo
[root@dv4-1413 ~]# chown -R apache.apache repo
[root@dv4-1413 ~]# service httpd restart

Ahora probemos que se puede acceder al repositorio a través de un navegador: http://laIpDeTuMaquina/svn/repo. Se debería obtener un popup solicitando el usuario y la clave de acceso al repositorio SVN. De ser así ingresar las credenciales (indicadas en el comnado htpasswd antes) y se debería mostrar una página indicando:

De ser así, eso es todo, ya hemos configurado nuestro primer repositorio SVN. Si se desearan múltiples repositorios, basta con agregar secciones <Location> en la configuración indicando las particularidades de cada una.

A continuación podremos empezar a utilizar el repositorio recién configurado con cualquier cliente, IDE, etc., que soporte SVN para el control de versiones.

Uso de Subversion.

Como referencia veamos un ejemplo de proyecto que agregaremos al repositorio repo.

Empecemos por crear el esquema de directorios de nuestro proyecto. La documentación recomienda que se creen al menos los directorios: branches, tags y trunk en el directorio raíz del esquema, siendo trunk el directorio que contendrá tus archivos.

[pbernal@dv4-1413 ~]$ cd /tmp/
[pbernal@dv4-1413 tmp]$ mkdir proy1
[pbernal@dv4-1413 tmp]$ cd proy1
[pbernal@dv4-1413 proy1]$ mkdir branches
[pbernal@dv4-1413 proy1]$ mkdir tags
[pbernal@dv4-1413 proy1]$ mkdir trunk
[pbernal@dv4-1413 trunk]$ vi config.php
[pbernal@dv4-1413 trunk]$ vi trunk/index.php

Hay que notar que si el proyecto no se tratara de una aplicación, sino quizá un simple método de almacenar/versionar archivos de configuración, este esquema no sería necesario y podríamos colocar los archivos directamente en la raíz del esquema, o en carpetas con un nombre cualquiera. Existe entonces total libertad sobre la disposición y contenido del directorio de trabajo.

El siguiente paso será importar al repositorio los archivos de mi directorio local de trabajo. Para ello usaremos el subcomando import del comando svn:

[pbernal@dv4-1413 ~]$ svn import /tmp/proy1/ http://192.168.7.7/svn/repo/proy1 -m "Esquema inicial de repositorio para proy1"
Reino de autentificación: <http: //192.168.7.7:80> DonPool :: Subversion repos
Clave de 'pbernal':
Añadiendo      /tmp/proy1/trunk
Añadiendo      /tmp/proy1/trunk/index.php
Añadiendo      /tmp/proy1/branches
Añadiendo      /tmp/proy1/config.php
Añadiendo      /tmp/proy1/tags
Commit de la revisión 1.

Así habremos importado hacia el repositorio todo el contenido de mi directorio de trabajao /tmp/proy1. De ahora en adelante podrá ser accesado local ó remotamente con cualquier cliente SVN.

Veamos ahora cómo hacer checkout (obtener) de los archivos desde algún otro equipo:

[pbernal@aspire ~]$ svn co http://192.168.7.7/svn/repo/proy1
A    proy1/trunk
A    proy1/trunk/index.php
A    proy1/branches
A    proy1/config.php
A    proy1/tags
Revisión obtenida: 1

Ahora, dado que ya hemos obtenido la revisión 1, podremos editarla a voluntad y luego de ello, enviar los cambios hacia el repositorio:

[pbernal@aspire ~]$ cd proy1/
[pbernal@aspire proy1]$ svn move config.php trunk/config.php
A         trunk/config.php
D         config.php
[pbernal@aspire proy1]$ svn commit -m "Movido el archivo config.php hacia dentro del trunk y editado para agregar otra variable."
Eliminando     config.php
Añadiendo      trunk/config.php
Transmitiendo contenido de archivos .
Commit de la revisión 2.

Lo bonito de esto es que se puede borrar todos los directorios que acabamos de obtener en tu equipo, sin afectar en lo más mínimo lo almacenado en el repositorio. La única razón por la que se obtuvieron (checked out) fue la de editarlos y luego enviarlos de vuelta en línea. Puedes revisar los cambios aplicados navegando en el repositorio.

Veamos unos ejemplos de cómo agregar o eliminar archivos al repositorio:

[pbernal@aspire proy1]$ cp ~/bin/arreglaPermisos.php trunk/
[pbernal@aspire proy1]$ svn add trunk/arreglaPermisos.php
A         trunk/arreglaPermisos.php
[pbernal@aspire proy1]$ svn commit -m "Agregado al trunk script de corrección de permisos"
Añadiendo      trunk/arreglaPermisos.php
Transmitiendo contenido de archivos .
Commit de la revisión 3.
[pbernal@aspire proy1]$ svn delete trunk/arreglaPermisos.php
D         trunk/arreglaPermisos.php
[pbernal@aspire proy1]$ svn commit -m "Eliminado del trunk el script de corrección de permisos"
Eliminando     trunk/arreglaPermisos.php
Commit de la revisión 4.

En los pasos anteriores hemos agregado el archivo trunk/arreglaPermisos.php (revisión 3) y lo eliminamos (revisión 4). Pensemos que deseáramos regresar a un estado previo del proyecto, por ejemplo si ha pasado un tiempo desde la última vez que revisamos el proyecto, siempre es buena idea echar un vistazo a la bitácora del repositrio:

[pbernal@aspire proy1]$ svn log http://192.168.7.7/svn/repo/proy1
------------------------------------------------------------------------
r4 | pbernal | 2010-07-25 15:27:49 -0500 (dom 25 de jul de 2010) | 1 line

Eliminado del trunk el script de corrección de permisos
------------------------------------------------------------------------
r3 | pbernal | 2010-07-25 14:33:29 -0500 (dom 25 de jul de 2010) | 1 line

Agregado al trunk script de corrección de permisos
------------------------------------------------------------------------
r2 | pbernal | 2010-07-25 14:10:03 -0500 (dom 25 de jul de 2010) | 1 line

Movido el archivo config.php hacia dentro del trunk y editado para agregar otra variable.
------------------------------------------------------------------------
r1 | pbernal | 2010-07-25 13:53:21 -0500 (dom 25 de jul de 2010) | 1 line

Esquema inicial de repositorio para proy1
------------------------------------------------------------------------

Esta es la razón por la que es buena idea agregar un comentario con el modificador -m cada vez que hacemos un commit al repositorio. Adicionalmente, para éste y otros comandos, se puede especificar una revisión con el parámetro -r, por ejemplo, para obtener el log de la revisión 2:

[pbernal@aspire proy1]$ svn log -r2 http://192.168.7.7/svn/repo/proy1
------------------------------------------------------------------------
r2 | pbernal | 2010-07-25 14:10:03 -0500 (dom 25 de jul de 2010) | 1 line

Movido el archivo config.php hacia dentro del trunk y editado para agregar otra variable.
------------------------------------------------------------------------

E incluso un rango de revisiones, por ejemplo el log de las revisiones 2 -> 3:

[pbernal@aspire proy1]$ svn log -r2:3 http://192.168.7.7/svn/repo/proy1
------------------------------------------------------------------------
r2 | pbernal | 2010-07-25 14:10:03 -0500 (dom 25 de jul de 2010) | 1 line

Movido el archivo config.php hacia dentro del trunk y editado para agregar otra variable.
------------------------------------------------------------------------
r3 | pbernal | 2010-07-25 14:33:29 -0500 (dom 25 de jul de 2010) | 1 line

Agregado al trunk script de corrección de permisos
------------------------------------------------------------------------

Bien, ahora que tenemos claras las modificaciones realizadas en cada revisión, podremos revertir los cambios en la copia local hacia el estado anterior que se desee, simplemente obteniendo (checkout) dicha revisión específica:

[pbernal@aspire proy1]$ svn co -r3 http://192.168.7.7/svn/repo/proy1
A    proy1/trunk/arreglaPermisos.php
Revisión obtenida: 3

Por otro lado, una de las características, que yo personalmente como desarrollador veo más útil, es la posibilidad de obtener diffs entre las revisiones, lo cual me permite generar archivos de parche para actualizar fácilmente entre versiones en los sistemas ya en producción. Veamos:

[pbernal@aspire proy1]$ svn diff -r1:2 http://192.168.7.7/svn/repo/proy1
Index: config.php
===================================================================
--- config.php	(revisión: 1)
+++ config.php	(revisión: 2)
@@ -1,2 +0,0 @@
-<?php
-$var = 1
Index: trunk/config.php
===================================================================
--- trunk/config.php	(revisión: 0)
+++ trunk/config.php	(revisión: 2)
@@ -0,0 +1,3 @@
+<?php
+$var = 1;
+$var2 = 2;

La salida del comando se puede almacenar en un archivo (con un redirect > por ejemplo) que podrá ser usado directamente con el comando patch para migrar/actualizar cualquier directorio de trabajo que tenga la revisión 1 y transformarla así a la revisión 2.

Por último, veamos cómo controlar o discriminar los accesos hacia el repositorio a través de ACLs. Las ACLs pueden ser habilitadas con la opción AuthzSVNAccessFile que toma un nombre de archivo como parámetro y que se puede agregar a cualquier sección <Location>:

<Location /svn/repo>
   DAV svn
   SVNPath /var/www/svn/repo
   AuthzSVNAccessFile /etc/svn-acl-conf
   AuthType Basic
   AuthName "DonPool :: Subversion repo"
   AuthUserFile /var/www/.htpasswd
   Require valid-user
</Location>

Ahora podemos crear el archivo /etc/svn-acl-conf que consta de secciones de la forma siguiente:

[nombrerepo:rutarepo]
usuario = acceso

Donde acceso puede ser r (lectura), rw (lectura, escritura) ó vacío (ningún acceso). La ACL por defecto es no dar ningún acceso a ningún usuario. Supongamos que necesitamos ofrecer acceso de lectura y escritura a pbernal y de sólo lectura a nawes:

[repo:/]
nawes =  r
pbernal = rw

También es posible crear grupos en una sección groups. De esta manera los grupos pueden ser usados en las ACLs anteponiendo el signo @ delante del nombre:

[groups]
desarrollo = donpool, epe, david

[repo:/]
nawes =  r
@desarrollo = rw

Si se deseara hacer a todos los repositorios legibles por cualquiera, se puede agregar una sección para el raíz de todo repositorio:

[/]
* =  r

Conclusiones.

Esta es sólo una pequeña guía de la instalación y uso básicos de SVN y sólo cubre los aspectos más relevantes o los más comunmente utilizados, sin embargo no he sino rascado la superficie de esta poderosísima herramienta de control de versiones, es así que siempre será recomendable profundizar revisando la documentación oficial y también a este excelente libro libre, que fueron las principales fuentes de éste artículo; además de este PDF que me proporcionó la parte teórica del mismo.