Subversion es un sistema de control de versiones, es decir, sirve para tener un control sobre nuestro proyecto y permitirnos realizar cambios a los distintos componentes del mismo manteniendo un histórico de estos cambios y permitiéndonos en cualquier momento deshacer los cambios hechos en un momento dado, actualizar el proyecto a la última versión de los programadores, ver históricos de cambios y comentarios, y unir nuestros cambios con los que otros hayan podido hacer sobre los mismos ficheros. De hecho uno de los motivos por los que estos sistemas se utilicen tanto en el mundo del software libre se debe a que permite, de forma cómoda, coordinar el trabajo de varios profesionales a través de Internet cómodamente.
Sin duda el sistema de control de versiones (SCV de aquí en adelante) más utilizado actualmente por los programadores de software libre es el viejo y probado CVS. Este sistema tiene ya bastantes años detrás y aunque sirve bastante bien para sus propósitos tiene una serie de limitaciones que han hecho que poco a poco otros SCV estén ocupando su (todavía predominante) cuota de mercado.
Entre estos nuevos sistemas podemos destacar tres: Arch, BitKeeper y Subversion. De los tres, en mi opinión, uno de los más interesantes para los que conozcan y hayan utilizado CVS con anterioridad es Subversion porque es el que más se le parece dado que sus programadores lo que han intentado es hacer una evolución natural de CVS superando sus limitaciones y mejorándolo allí donde estaba claro que se podía mejorar. Arch sigue una filosofía bastante distinta de CVS y BitKeeper, aún siendo probablemente el más avanzado de los tres, no es libre (sólo de uso gratuito si se va a utilizar en proyectos libres).
Así que en este artículo vamos a explicar como trabajar con Subversion, aunque dada la similitud con CVS lo aprendido aquí es fácilmente aplicable a ese otro SCV.
svn co http://www.supermail.org/repositorio/svn/tronco supermail
Esto nos copiaría desde la red a un directorio 'supermail' en nuestro sistema de archivos todo el código fuente del proyecto hospedado en el repositorio Subversion del proyecto. 'svn' es el comando que vamos a utilizar casi siempre mientras trabajamos con Subversion, indicándolo subcomandos que especifican la operación a realizar; 'co' es la abreviatura de 'checkout' que es la operación que creará una copia del repositorio en nuestro sistema de archivos local sobre la que podamos trabajar y a continuación vemos una URL que indica la dirección de donde obtener las fuentes (Podemos observar que la es una dirección HTTP: Subversion utiliza Apache+WebDav para los repositorios de acceso por Internet; si nuestro repositorio va a ser privado no necesitaremos Apache). Por último, la palabra 'supermail' indica el nombre del directorio local donde se copiará el contenido del anterior repositorio (de no existir se crearía) y es necesario porque en este caso (como sucede con bastantes repositorios de Subversion) el repositorio del proyecto no se ha denominado 'supermail' sino 'tronco' (o más habitualmente su equivalente en inglés 'trunk'). Esto suele hacerse así porque en ocasiones puede convenir que en un mismo repositorio coexista más de un proyecto.
Una vez hemos ejecutado el comando vemos que empieza a aparecer una salida en la que se va enumerando cada fichero y directorio contenido en el proyecto con una letra 'A' antepuesta. Esta letra indica que el fichero se ha añadido nuevo, más adelante veremos que existen otras letras que indican otras operaciones sobre nuestra copia del repositorio.
Cuando los ficheros se han terminado de descargar ya podemos meternos en el directorio supermail/ y empezar a hacer los cambios que queramos (programación, documentación, etc), teniendo solamente en cuenta que cuando creemos un fichero nuevo tenemos que utilizar el comando 'svn add fichero' (si el fichero no existe en el directorio especificado lo creará vacío) para informar a Subversion de los ficheros nuevos que añadamos, 'svn rm fichero' para informarle de los ficheros que eliminemos, y 'svn mv fichero' y 'svn cp fichero' para mover y copiar, respectivamente, los ficheros. Es decir, si dividiéramos los tipos de cambios en 'cambios a ficheros' y 'cambios al árbol de directorios' los primeros no necesitarían de ningún comando especial (tan sólo necesitamos hacer los cambios como los haríamos si no estuviera Subversion, es decir, con nuestro editor favorito en el caso de ficheros de texto) mientras que los cambios al árbol de directorios del proyecto si necesitan utilizar los comandos que hemos visto para indicarle a Subversion estos cambios (svn [add|rm|cp|mv], normalmente).
Una vez hemos quedado satisfechos con los cambios realizados, es hora de comprobar el conjunto de cambios que hemos realizado sobre la copia original que descargamos al principio.
Esto se realiza mediante el comando 'svn status' que producirá una salida con un lista de los ficheros que han cambiado con respecto al original y una letra indicando el tipo de cambio. el significado para algunas de estas letras es:
Como podemos ver por el significado de cada carácter, este comando es muy útil para asegurarnos de que no nos ha olvidado añadir o eliminar ficheros o de que no hemos realizado modificaciones inesperadas a ficheros que no pretendíamos modificar (en este caso podríamos deshacer las modificaciones inesperadas haciendo 'svn revert FICHERO').
Por defecto svn status ignora los ficheros que concuerdan terminan con .o, .lo, .la, y algunos más, pues casi siempre estos ficheros son ficheros temporales creados por la compilación de archivos. La expresión regular que contiene los ficheros a ignorar es la propiedad svn:ignore del directorio padre (veremos más sobre propiedades un poco más adelante).
También, como con casi todos los comandos de Subversion, podemos obtener información de estado sobre un sólo fichero o directorio indicando su ruta:
svn status supermail/README
Además, si ejecutamos svn status con el parámetro -v aparecerán dos nuevas columnas indicando cual fue la última revisión en la que se cambió el fichero y quien lo hizo.
El último paso antes de hacer efectivos nuestros cambios con el repositorio global es actualizar nuestra copia local para que se reflejen esos cambios, y poder resolver los conflictos que hayan podido surgir.
Para ello utilizamos 'svn up' (de update) que nos irá mostrando una lista de los ficheros que han cambiado con, de nuevo, una letra indicando el tipo de cambio. El significado de esta letra es el mismo que hemos explicado con 'svn status' pero además pueden darse:
Si se produce un conflicto en algún fichero debemos resolverlo manualmente (Subversion aún no sabe programar) para lo cual abrimos el fichero, observamos las partes que Subversion nos ha marcado indicando los cambios locales y los del repositorio global y resolvemos el conflicto. Una vez lo hemos hecho ejecutamos tenemos que ejecutar 'svn resolve FICHERO' para indicar a Subversion que el conflicto está resuelto.
Una vez hemos comprobado (mediante svn status y svn update) que nuestros cambios son correctos y que no son conflictivos con lo que actualmente existe en el repositorio global (y si no lo fueran, lo resolvemos y ejecutamos de nuevo svn status y svn update para asegurarnos) llega el momento de enviar nuestros cambios a dicho repositorio.
Para ello se utiliza el commando 'svn commit' al que normalmente le vamos a pasar el parámetro -m con un mensaje de texto indicando los cambios que hemos realizado (luego la gente podrá ver ese mensaje cuando consulte los históricos). Si no le pasamos el parámetro -m utilizará la variable de entorno $EDITOR para abrir un editor de texto en el que introduzcamos un mensaje.
Por ejemplo, el comando podría quedar de la siguiente forma:
svn commit -m "He mejorado mucho la ritratransfusión de los megabixeloides"
svn commit fallará si se han realizado nuevos cambios en el repositorio global; en este caso tendremos que actualizar (svn update) y volver al punto 4.
En el caso de que no tuviéramos permiso de escritura, Subversion nos permite crear un parche, que podamos enviar por email a los autores, de forma muy sencilla; Sólo tenemos que ejecutar desde el directorio superior del proyecto:
cvs diff > parche.diff
Y ya tendríamos nuestro parche en el archivo parche.diff, que podríamos mandar al autor o autores para que lo añadieran utilizando el comando patch < parche.diff desde el mismo directorio.
Y ya está, en realidad, salvo casos muy concretos, no vamos a salirnos casi nunca de los comandos especificados en este apartado. Una vez hemos terminado de trabajar, y nos hemos asegurado de que nuestros cambios se han enviado correctamente al repositorio global podemos o bien borrar nuestro directorio de trabajo y volver a hacer el checkout la próxima vez que queramos trabajar en el, o no borrarlo y hacer el update en su lugar.
Una vez que hemos visto el manejo fundamental de Subversion, vamos a recorrer otras operaciones habituales que podemos realizar con el mismo, y algunas 'bases teóricas' fundamentales.
El sistema de versiones de Subversion es distinto al de CVS. En el segundo, cada fichero tiene su propia versión, y los directorios no tienen versiones pues CVS los considera simplemente como 'contenedores de ficheros', lo que causa muchos dolores de cabeza a sus usuarios a la hora de reorganizar el árbol de un proyecto. En Subversion las versiones se aplican a todo el árbol de nuestro proyecto (incrementándose en uno en cada commit), y por lo tanto todos los ficheros tendrán la misma versión en una misma 'captura' del árbol, incluyendo los directorios.
Por ejemplo, si tuviéramos un sencillo proyecto que contuviera los siguientes ficheros:
hola/README hola/INSTALL hola/holamundo.c
En la primera versión todos los ficheros tendrían la versión '1'. Si después hiciéramos un cambio al fichero INSTALL y ejecutáramos el commit, las versión del árbol, y por lo tanto la de todos sus archivos, cambiaría a '2'.
Crear un repositorio es tan sencillo como utilizar los comando:
mkdir /home/usuario/svn
svnadmin create /home/usuario/svn
Con esto se nos crearía un repositorio básico (vacío) en el cual podríamos empezar a crear ficheros y directorios. Pero normalmente nos interesará importar algún arbol de código existente, para lo cual tendríamos que hacer a continuación:
svn import file:///home/usuario/svn /home/usuario/HolaMundo holamundo
Analicemos. El comando para importar un código existente es 'svn import'. A continuación le hemos pasado la URL a nuestro repositorio (que como resulta que está en nuestro sistema de archivos tiene la forma file://[ruta_completa], hay que recordar que svn siempre trabaja con URLs cuando se refiere a repositorios globales, no así svnadmin), después le indicamos donde está el árbol existente que queremos importar (en este caso /home/usuario/HolaMundo) y por último el subdirectorio dentro del repositorio en el que queremos que se importe, de modo que si quisiéramos hacer un checkout para crear una copia local y comprobar que se ha importado correctamente tendríamos que hacer:
svn co file:///home/usuario/svn/holamundo copia_local
Lo que nos crearía en el directorio actual un subdirectorio 'copia_local' conteniendo el árbol (Subversionizado) de nuestro proyecto 'HolaMundo'.
Sin embargo normalmente nos va a interesar crear más de un subdirectorio dentro de nuestro repositorio de 'HolaMundo' de forma que en uno de ellos contengamos el árbol de la versión en desarrollo o inestable, en otro las distintas ramas (veremos algo sobre ramas más adelante) y en otro las distintas versiones estables. Por ello nos conviene utilizar el subcomando mkdir para crear los distintos subdirectorios dentro del repositorio de nuestro proyecto 'holamundo':
mkdir /home/usuario/svn
svnadmin create /home/usuario/svn
svn mkdir file:///home/usuario/svn/holamundo -m "Directorio base para HolaMundo"
svn import file:///home/usuario/svn/holamundo /home/usuario/HolaMundo head -m "Rama en desarrollo"
svn mkdir file:///home/usuario/svn/holamundo/ramas -m "Directorio de ramas"
svn mkdir file:///home/usuario/svn/holamundo/finales -m "Directorio de versiones finales"
Tras esto nos quedaría un repositorio conteniendo a su vez tres directorios (que podríamos considerar 'subrepositorios') que serían 'head', con código ya añadido, y que contendrá las versiones en continuo desarrollo, "ramas" que podría contener en el futuro distintas ramas del desarrollo y "finales" que contendría imágenes de las versiones finales estables de nuestro programa.
Existen dos formas de deshacer un cambio, dependiendo de en que fase del desarrollo
lo hayamos hecho. Si el cambio se ha producido sólo localmente, y queremos volver en
determinado archivo a la versión del repositorio global, tan sólo tenemos que hacer
svn revert fichero
o svn revert
a secas desde el directorio raíz si quisiéramos
deshacer todos los cambios.
El segundo caso es que queramos deshacer uno o más cambios ya enviados al repositorio global en un commit anterior. En este caso tendremos que utilizar el subcomando 'merge' indicando en primer lugar la revisión desde la que queremos volver atrás y en segundo lugar la revisión a la que queremos volver. Por ejemplo, si en nuestro proyecto (revisión 5) un día que estábamos borrachos hicimos dos horrendos commits (revisión 6 y revisión 7) y al día siguiente comprobáramos con resacoso horror el estropicio realizado, podríamos volver tranquilamente a nuestra revisión pre-pedal con:
svn merge -r 7:5
Aquí podemos ver que hemos utilizado el subcomando merge con un parámetro '-r' que nos permite indicar dos revisiones sobre las que efectuar el cambio, y hemos indicado en primer lugar la última revisión que hicimos tolingas seguido de dos puntos y la última revisión que hicimos sobrios. Como con casi todos los comandos, además, podemos especificar como parámetro uno o varios archivos o directorios para que los cambios se realicen sólamente sobre ellos.
'merge' tiene otros usos que veremos más adelante.
'svn log' permite obtener los mensajes que el autor de cada revisión escribió. Si lo ejecutamos en el directorio raíz y sin parámetros nos mostrará todos los mensajes de todas las revisiones, pero también podemos ejecutarlo sobre un archivo o directorio en cuyo caso sólo nos mostrará los mensajes de las revisiones que modificaran ese archivo. También puede especificarse un conjunto de revisiones con el parámetro -r (como vimos anteriormente con svn merge) si sólo queremos ver las revisiones comprendidas en ese rango.
'svn diff' ejecutado sin parámetros nos permite, como vimos anteriormente, ver las diferencias
entre nuestra copia de trabajo y el fichero original en el repositorio global, pero, como era de esperar,
también admite el parámetro -r para especificar un rango de revisiones entre las que queremos que se nos
muestren los cambios a los ficheros, por ejemplo svn diff -r 2:4
ejecutado desde el
directorio raíz de la copia de trabajo mostraría todos los cambios realizados a los ficheros
que fueran modificados entre la segunda y la cuarta revisión. 'svn diff' también admite ficheros o
conjuntos de ficheros como parámetro para mostrar sólo los cambios producidos en ellos.
El concepto de rama es uno de los más útiles a la hora de trabajar con sistemas de control de versiones. Se explica mejor con un ejemplo; imaginamos que nuestro afamado programa 'HolaMundo' ha llegado a un punto en el que tiene la estabilidad y características necesarias para poder considerarlo una versión 'Beta', pero tenemos otras características en mente que quisiéramos añadir (como colorear el HolaMundo, efectos de fuegos artificiales que forman las palabras, etc) pero a una versión posterior a esta beta. Una solución sería hacer un paquete con el código fuente de la versión actual, publicarlo como 'HolaMundo 1.0-Beta' y seguir trabajando en las nuevas características en nuestro repositorio Subversion. El problema obvio es que si, como es natural, a nuestra versión beta le salen fallos, ya no podemos incorporarlos al repositorio sin incorporar al mismo tiempo a esa versión las nuevas (e inestables) características que hemos desarrollado, con lo cual podríamos introducir nuevos fallos. Tendríamos que, o bien seguir desarrollando el código de esa beta por separado sin ningún repositorio, o bien crear un nuevo repositorio sólo para ella hasta que se convierta en 'versión estable'. En cualquier caso si arregláramos un fallo en la versión beta, y quisiéramos incorporar ese arreglo también a la versión en desarrollo, tendríamos que hacerlo 'a mano'.
Las ramas nos proporcionan una solución elegante a todo este embrollo. Las ramas ('branches') nos permiten seguir desarrollando una versión paralela del proyecto, surgida a partir de una determinada última revisión común, y nos permitirán seguir intercambiando cambios entre ambas cuando queramos.
Para crear una rama a partir de una revisión existente se utiliza el subcomando 'cp'. En este caso vamos a suponer que la revisión 173 de nuestro proyecto 'HolaMundo' es lo bastante estable como para poder considerarla una beta, por lo que decidimos crear una rama del proyecto llamada 'holamundo-beta'. Suponiendo que hubiéramos utilizado la estructura explicada en el apartado de creación de un repositorio haríamos:
svn cp file:///home/usuario/svn/holamundo/head file:///home/usuario/svn/holamundo/ramas/holamundo-beta
Ya tenemos nuestra rama lista para ser modificada independientemente de la principal. Es importante destacar que la nueva rama no ocupará nada de espacio en disco mientras no modifiquemos nada, pues sus ficheros en realidad serán punteros a los mismos ficheros de la revisión en la que salga en la rama inestable pero según vayamos modificando ficheros se irán creando copias con las modificaciones en esta rama. Como vemos, ya podemos arreglar fallos tranquilamente en la rama holamundo-beta mientras seguimos añadiendo felizmente características inútiles y llenas de bugs a la rama head.
Aún nos queda el segundo problema que teníamos ¿cómo hacemos que las soluciones a los bugs de la versión beta se apliquen también a la versión inestable? Para ello se utiliza el comando svn merge, que ya conocíamos anteriormente aplicado a deshacer una revisión o parte de ella. En este caso, suponiendo que la solución al fallo se haya hecho en la revisión 177 de la rama holamundo-beta, el comando a utilizar sería (suponiendo que estuviéramos en el directorio raíz de la versión inestable):
svn merge -r 176:177 file:///home/usuario/svn/holamundo/ramas/holamundo-beta .
Con esto (si no hay conflictos) ya deberíamos tener también la solución al fallo incorporada en la versión inestable. Por supuesto para que este comando sólo incorpore la solución, esta debe de haber el único cambio incorporado entre la revisión 176 y la 177, es decir, no se deben de haber realizado otros cambios en ese mismo commit pues sino esos otros cambios también se aplicarán en nuestra versión inestable (salvo que especifiquemos ficheros pero, de nuevo, sólo suponiendo que la solución al fallo sea la única fuente de modificación de esos ficheros). Por ello (entre otros muchos motivos como por ejemplo la conveniencia de poder deshacer un cambio)es más que conveniente intentar que los commits sean pequeños y que nunca incorporen cambios que no estén relacionados.
¿Y qué son los tags? Los tags en terminología CVS no son más que etiquetas que le dan un nombre a una determinada revisión del árbol, por ejemplo llamar '1.0Final' a la revisión 345. Subversion no tiene tags pero es fácil conseguir el mismo resultado simplemente copiando la revisión que interese con 'svn cp' al directorio 'finales' con el nombre de la versión real (que podríamos haber llamado igualmente 'tags') y ajustando la propiedad del directorio a sólo-lectura (veremos más adelante como ajustar las propiedades de un fichero o directorio):
svn cp -r 345 file:///home/usuario/svn/holamundo/ramas/holamundo-beta \
file:///home/usuario/svn/holamundo/finales/1_0
svn propset svn:read-only 1 file:///home/usuario/svn/holamundo/finales/1_0
Una vez que hemos terminado de trabajar con una rama, y creado una nueva versión (como hemos visto antes, con svn cp al directorio de tags) o incorporado sus cambios experimentales en nuestra rama principal, podemos eliminarla con 'svm rm'.
Las propiedades son valores textuales que podemos asociar con cada fichero o directorio de un repositorio. Por ejemplo en el apartado anterior hemos puesto la propiedad 'svn:read-only' de un directorio del repositorio a uno. svn:read-only en realidad es una propiedad especial con un significado concreto, pero nosotros podemos poner propiedades con los nombre y los valores que queramos simplemente teniendo cuidado de que sus nombres no empiecen por 'svn:' pues este prefijo se ha reservado para las propiedades con un significado especial para Subversion.
Entre estas 'propiedades especiales', destacaremos:
Pero ¿cómo cambiar y obtener las propiedades de un fichero o directorio? Muy sencillo, simplemente utilizamos 'svn propget propiedad fichero' para obtener las propiedades y 'svn propset propiedad valor fichero' para ponerlas. En el caso de que queramos editar una propiedad con nuestro editor de texto (algo interesante para propiedades que puedan tener valores de varias líneas como svn:ignore) utilizaremos 'svn propedit propiedad fichero'.
Esta utilidad sirve para analizar los cambios que se han ido
produciendo a un repositorio a lo largo del tiempo, sin cambiarlo.
La forma más obvia de utilizarlo es con svnlook
/home/usuario/svn
(siendo por supuesto la ruta dependiente de
donde esté nuestro repositorio). Esto nos mostrará por pantalla el árbol
de directorios y ficheros correspondiente a ese repositorio en la
revisión actual. Además podemos especificar una revisión concreta con
svnlook /home/usuario/svn rev número
lo que nos dará el
árbol del repositorio para la revisión 'número'. svnlook [ruta] rev
[numero] puede tener además subcomandos adicionales que especifican el
tipo de información que queremos obtener para esa revisión determinada,
que irán al final del comando:
Ya hemos visto anteriormente que podemos utilizar svnadmin para crear nuevo repositorios mediante su subcomando 'create'. Pero svnadmin tiene otros subcomandos bastante interesantes, entre los que están:
En algunos ejemplos anteriores hemos visto que cuando accedemos a un repositorio en Internet con Subversion especificamos una URL de protocolo HTTP (http://www.midominio,org/mirepositorio, por ejemplo). Esto es así porque para publicar repositorios por Internet Subversion utiliza Apache con el módulo WebDav, que es un protocolo que permite acceder y modificar ficheros de el servidor Apache. No nos vamos a extender en como instalar Apache (por cierto, necesita la versión 2.0) y el módulo WebDav, pero indicaré los pasos a seguir:
Los pasos uno a tres suelen poderse conseguir fácilmente si utilizamos una distribución moderna de Linux, pues casi todas incluyen ya entre sus paquetes Apache 2.0, el módulo WebDav para el mismo, y en algunas ocasiones hasta el plugin para el módulo webDav.
El último paso (configurar el fichero http.conf) consiste en añadir al final del fichero http.conf lo siguiente:
<Location /repos/mirepo> DAV svn SVNPath /home/usuario/svn </Location>
Cambiando '/home/usuario/svn' por la ruta real de nuestro repositorio y 'repos/mirepo' por la parte final de la URL que queramos que represente nuestro repositorio a través de Internet, por ejemplo, si nuestro dominio es http://www.midominio.com y queremos que nuestro repositorio, situado en /home/usuario/repositorio se vea a través de internet como http://www.midominio.com/repositorio/HolaMundo tendríamos que añadir al http.conf:
<Location /repositorio/HolaMundo> DAV svn SVNPath /home/usuario/repositorio </Location>
Ya sólo nos queda restringir los permisos de acceso para que sólo puedan leer y hacer commits a nuestro repositorio las personas que nosotros consideremos oportuno. Para el caso más habitual de un acceso de lectura público y un acceso de escritura por usuario creamos un fichero que contenga nombres de usuarios y claves cifradas con el comando crypt con el siguiente formato:
usuario1:clave_cifrada1 usuario2:clave_cifrada2 ...
Después dentro del grupo 'Location' que acabamos de definir en el archivo http.conf añadimos las siguientes opciones:
AuthType Basic AuthName "Repositorio Subversion" AuthUserFile /ruta/al/fichero/de/usuarios <LimitExcept GET PROPFIND OPTIONS REPORT> Require valid-user </LimitExcept>
Sustituyendo "Repositorio Subversion" por el texto que queramos que aparezca en la cadena de autenticación y "/ruta/al/fichero/de/usuarios" por la ruta real del fichero que hemos creado anteriormente con el listado de usuarios.