Uso

Como puedo ...?

Bajar el proyecto (checkout)

Primero nos bajamos el proyecto que creamos antes, o algún otro que encontremos por ejemplo en http://svn.apache.org/repos/asf/. Yo bajé el que cree antes:

# creamos un directorio para albergar el proyecto 
mkdir /home/jano/awpool

# nos situamos en él
cd /home/jano/awpool

# y bajamos el proyecto
svn checkout http://127.0.0.1:80/repos/awpool/trunk .

Esta copia local contiene un subdirectorio svn en cada directorio, lo que permite que sea gestionada con el cliente de Subversion.

Actualizar la copia local (update)

Si queremos actualizar nuestra copia local con los cambios que otros usuarios hayan enviado al repositorio usamos update:

# por defecto es recursivo
svn update
At revision 4.

# para actualizar solo un directorio
svn -N update
At revision 4.

# para un fichero especifico
svn update GenericsTest.java
At revision 2.

Resolver conflictos

Acabo de modificar un fichero en local, y voy a actualizarlo para luego hacer commit (no quiero machacar los cambios de nadie). Pero al actualizar Subversion muestra una C de conflicto junto al fichero:

debian:/# svn update .
C  GenericsTest.java
Updated to revision 5.

Además han aparecido varias versiones del fichero en disco:

debian:/# ls
GenericsTest.java         # el fichero con mis cambios y marcas de conflicto
GenericsTest.java.mine    # el fichero con mis cambios
GenericsTest.java.r4      # la revisión antes de mis cambios
GenericsTest.java.r5      # la última revisión que acabo de actualizar

Todo esto ocurre porque desde la última vez que bajé este fichero, alguién más lo cambio, y al actualizarme con el servidor he recibido un trozo de código que entra en conflicto con mis cambios. O dicho de otra manera, un cambio que de haberse aplicado sobreescribiría mis propios cambios, y yo perdería información. Afortunadamente siempre podemos actualizar sin miedo, porque Subversion suspende la fusión de cambios y nos advierte del conflicto.

Vamos a abrir el fichero para ver que aspecto tienen las "marcas de conflicto":

   private static void print(Type t) {
      if (t instanceof TypeVariable) {
         print((TypeVariable)t);
<<<<<<< .mine
      } else if (t instanceof WildcardType) {
         print((ParameterizedType)t);
=======
      } else if (t instanceof WildcardType) {
         print((WildcardType)t);
>>>>>>> .r2
      } else {
         System.out.println(t);
      }
   }

Observa que hay un trozo <<< .mine mi-código === y un trozo === código-revisión-2 >>> .r2. Se corresponden a mi código y al código de la revisión 2 que acabo de bajarme.

Para solucionar el conflicto puedo:

  • Descartar mis cambios locales, o los cambios que baje del servidor. Esto se hace sobreescribiendo el fichero original GenericsTest.java por alguna de las copias que han aparecido.

  • Solucionar el conflicto editando a mano el fichero:

       private static void print(Type t) {
          if (t instanceof TypeVariable) {
             print((TypeVariable)t);
          } else if (t instanceof WildcardType) {
             print((WildcardType)t);
          } else {
             System.out.println(t);
          }
       }
    Oops! el cambio correcto no era el mío, asi que he dejado lo que ya había.

Tras solucionar el conflicto elimino los ficheros sobrantes y hago commit:

rm *.mine *.r4 *.r5
svn ci GenericsTest.java

Como ves, normalmente se soluciona intuitivamente editando el fichero a mano. Si quieres más detalles consulta el manual de Subversion: http://svnbook.red-bean.com/html-chunk/ch03s05.html#svn-ch-3-sect-4.4.

Enviar modificaciones (commit)

Al contrario que con CVS, los commit son atómicos. Es decir, cuando subes varios ficheros llegan en bloque (todos o ninguno), y si dos desarrolladores suben sus cambios a la vez primero se graba todo el bloque de uno, y luego el del otro. Los cambios nunca se mezclan.

Es buena práctica pensar por adelantado en el mensaje de log que vamos a usar, y hacer commits de cambios relacionados. De ese modo es más fácil revisar los cambios buscando errores. Conviene definir una variable SVN_EDITOR=kedit (o SVN_EDITOR=notepad) para usarlo como editor por defecto. De no hacerlo, tendremos que añadir el flag -m 'mensaje' para el obligado comentario que debe acompañar todo commmit.

Para enviar modificaciones locales al servidor:

# envía las modificaciones encontradas en todos los subdirectorios
$svn commit

# actualiza todos los *.txt del directorio actual y lo que haya en el directorio src
$svn commit *.txt src
El cliente svn lanzará el editor configurado, escribimos el mensaje de log, grabamos, cerramos el editor, y el commit se realiza. Un ejemplo de la salida en Windows:
$svn commit *.txt src
Sending        STATUS.txt
Sending        src/java.head/overview.html
Transmitting file data ..
Committed revision 4.

Si alguno de los ficheros que enviamos ha sido modificado en el repositorio, es obligado que actualicemos dichos cambios (usando update) antes de hacer commit. Esto evita que machaquemos los cambios de otros usuarios.

Examinar cambios (status)

El comando status muestra las modificaciones que hemos hecho en la copia local desde el último checkout o commit. Es útil hacerlo antes de un commit para hacernos idea de lo que poner en el log. Por defecto, el cliente no accede a la red durante la ejecución de este comando.

svn status

Este ejemplo muestra los posibles estados devueltos:

$ svn status
  L    ./abc.c               # fichero bloqueado
M      ./bar.c               # modificado localmente
 M     ./baz.c               # modificado localmente en sus propiedades, no en su contenido
?      ./foo.o               # fichero ignorado
!      ./foo.c               # fichero borrado por el usuario u otro programa
~      ./qux                 # versionado como fichero pero es directorio (o viceversa)
A  +   ./moved_dir           # añadido con historial de procedencia
M  +   ./moved_dir/README    # añadido con historial y tiene modificaciones locales
D      ./stuff/fish.c        # preparado para borrado
A      ./stuff/loot/bloo.h   # preparado para añadir
C      ./stuff/loot/lump.c   # conflictos con un update
    S  ./stuff/other_dir     # directorio cambiado a rama
G      ./foo.c               # modificado localmente e incorporados cambios remotos

Recuperar una revisión (update)

Al hacer un update/commit, nos indica un número de versión. Para machacar una copia local con una revisión anterior hacemos:

$svn update -r3
U  STATUS.txt
U  src\java.head\overview.html
Updated to revision 3.

# y de vuelta a la revisión 4
$svn update -r4
U  STATUS.txt
U  src\java.head\overview.html
Updated to revision 4.
Hemos deshecho los cambios que se hicieron tras la revisión 3. Observa que ahora he colocado una opción a la derecha del comando en vez de a la izquierda. En Subversion no importa donde colocar las opciones.

Mover un fichero o directorio (move)

Es posible mover o renombrar ficheros o directorios sin perder el historial de cambios. El comando move puede usarse para renombrar o para mover de un directorio a otro.

# primero se marca para su renombrado
svn move LEEME.txt README.txt
A         README.tx
D         LEEME.txt

# comprobamos los cambios pendientes
svn status
D      LEEME.txt
A  +   README.txt

# y ejecutamos el cambio
svn commit -m "Fichero renombrado"
Deleting       LEEME.txt
Adding         README.txt
Committed revision 5.

También se puede operar directamente en el servidor usando HTTP:

$svn move http://localhost/repos/awpool/trunk/README.txt 
          http://localhost/repos/awpool/trunk/README

Ver logs (log)

Con SVN es posible obtener mensajes de log:

# para todas las revisiones:
svn log

# idem con información adicional
svn log --verbose

# para la revisión 4
svn log -r 4

# para un fichero concreto (deberemos estar en 
# el directorio del fichero o especificar el path completo)
svn log README.txt

También podemos mostrar log operando directamente sobre el servidor, sin necesidad de habernos bajado nada:

$svn log http://localhost/repos/awpool

Ver el autor de cada cambio en un fichero (blame)

Si con log veíamos el historial de revisiones de un fichero, con blame vemos el fichero entero y el autor de cada cambio sobre cada parte del fichero:

svn blame http://ruta/al/fichero

Crear un changelog (log)

# entro en el proyecto
cd /home/jano/work/trunk

# Creo un fichero de nombre arbitrario Changelog con todos los mensajes de log 
# desde la revisión 1 (-r1) hasta la última revisión (HEAD).
svn log -r1:HEAD >> ChangeLog

# Hago add porque es un fichero nuevo
svn add ChangeLog

# subo el fichero para añadirlo a la distribución
svn commit ChangeLog -m 'Actualizo ChangeLog'

Eliminar un fichero o directorio (rm, remove, del, delete)

# Se hace como en CVS, primero se marca para eliminarlo
svn rm fichero

# y luego se hace commit para eliminarlo en el repositorio.
# Si es un directorio, la eliminación se realizará recursivamente por defecto.
svn commit

# del, delete, remove, y rm, son seudonimos del mismo comando

Si ejecutamos el comando sobre una URL la eliminación será inmediata. Después de ejecutar rm y commit, los ficheros/directorios, habrán desaparecido de la copia local y el HEAD del repositorio, pero permanecerán sus revisiones anteriores.

Añadir un fichero o directorio (add)

# añadir un directorio recursivamente (por defecto no se añaden subdirectorios)
svn add directorio -R
svn commit

# añadir un fichero
svn add fichero
svn commit

Deshacer cambios (revert)

# no funciona recursivamente a menos que usemos -R 
svn revert

Deshace los cambios locales, incluyendo modificaciones, cambios de propiedades, y planificados de cambios (add, rm, para los que aun no hayamos hecho commit).

Crear diffs (diff)

Los diffs (diferencias) son los datos diferentes entre dos versiones de un fichero (o conjuntos de ficheros), de modo que aplicando esa diferencia es posible pasar de un fichero a otro. Un diff se aplica usando el comando GNU patch. SVN usa el formato unificado diff.

# Obtener los diffs de las modificaciones de la copia local
svn diff

# diff entre las revisiones 14 y 18 para ficheros *.java y *.xml
svn diff -r14:18 *.java *.xml > /home/jano/tmp/r14-a-r18.patch

# diff entre un fichero local "foo.c" y su última revisión
svn diff --revision HEAD foo.c

# lo mismo que el anterior pero sin tener en cuenta nuestras modificaciones locales
svn diff --revision BASE:HEAD foo.c

El parametro --revision también acepta fechas, no solo números de revisión.

Crear una rama

Una buena práctica de la ingeniería del software es mantener los proyectos en estado compilable y funcional en todo momento. Pero cuando necesitamos introducir un cambio extenso en un proyecto, corremos el riesgo de "dejar en obras" gran parte de la aplicación. Esto suele traducirse en la interrupción de funcionalidades críticas y molestias en el trabajo de otras personas.

Para evitar problemas conviene crear una rama (branch). Una rama es una copia del código principal que evoluciona por separado, pero que tiene un pasado común respecto al origen a partir del cual se creó. En otras palabras, un proyecto propio con el cual experimentar. Más adelante, cuando hayamos completado nuestro trabajo, fundiremos nuestros cambios con la rama principal del proyecto. SVN proporciona un comando merge para integrar una rama en otra.

Nota

Suele hablarse de ramas principal y secundaria, pero internamente no existe esa distinción, son simplemente dos directorios del repositorio. De hecho, para Subversion no existe el concepto de rama, es solo un nombre que nosotros empleamos.

Un tema problemático con la fusión de cambios entre ramas, es que mientras trabajamos en una rama, el resto del equipo sigue añadiendo cambios a la principal. Sería deseable que la rama donde trabajamos integrase automáticamente los cambios producidos en el código principal para evitar discrepancias al integrar los cambios en el código principal. Esta funcionalidad esta prevista en alguna de las versiones posteriores a la 1.0, por ahora el soporte de merge es similar al del CVS.

En el siguiente ejemplo tenemos la rama principal en /cygdrive/d/desarrollo/awpool/trunk y crearemos una rama en /cygdrive/d/desarrollo/awpool/branch2-awpool2. Por cierto, estas rutas tan raras son de mi instalación de Cygwin en Windows. Podría haber usado d:\desarrollo\... o cualquiera ruta válida en mi disco duro.

cd /cygdrive/d/desarrollo/awpool
svn copy trunk branch-awpool2
svn commit -m "creo la rama branch-awpool2"

La copia no duplica el código en disco, tal como hace CVS, sino que crea enlaces apuntando al código existente. Lo único que ocupará espacio en disco serán los cambios que introduzcamos a partir de aquí.

Crear una etiqueta

En los sistemas de control de versiones, un tag (etiqueta) es una etiqueta que marca un punto en la evolución del código. Para Subversion un tag es un nombre que damos a una determinada revisión de un directorio del repositorio.

Para crear una etiqueta también se usa copy:

cd /cygdrive/d/desarrollo/awpool
svn copy trunk release-1.0
svn commit -m "marco el proyecto con la etiqueta release-1.0"

Aunque pueda sorprender a los veteranos del CVS, esto es coherente con lo que veíamos en el apartado "Branches". El comando copy crea un enlace a una revisión del código. Si no añadimos cambios es como si hubiéramos etiquetado una versión del código. Si añadimos cambios es como si tuviéramos una rama. Internamente, Subversion no conoce los conceptos etiqueta y rama, solo entiende de directorios del repositorio.

Crear un release en tar.gz (export)

Vamos a crear un fichero trunk.tar.gz que contenga un directorio trunk con todo el proyecto:

cd /home/jano/tmp
svn export http://localhost/repos/awpool/trunk
tar cf trunk.tar trunk
gzip -9 trunk.tar

Copia de seguridad (dump)

Vamos a crear una copia de seguridad del repositorio y todas sus versiones en un fichero .gz. Ocupará lo que ocupa el release que hicimos antes, más lo que ocupe la información de las revisiones.

svnadmin dump /home/jano/proyectos/svn | gzip -9 > dump.gz
* Dumped revision 0.
* Dumped revision 1.
* Dumped revision 2.
* Dumped revision 3.
* Dumped revision 4.
* Dumped revision 5.
Usando el parametro -r podemos volcar a partir de una revisión concreta.

Si luego queremos restaurar la copia hacemos:

gunzip -c dump.gz | svnadmin load /home/jano/proyectos/svn
o en Windows:
svnadmin load d:\ruta\al\repositorio < "fichero.descomprimido.dump"

Obtener ayuda (help)

# muestra los comandos disponibles
svn help

# muestra ayuda sobre un comando concreto
svn help comando

Desbloquear la copia de trabajo (cleanup)

Si estamos realizando cambios a la copia de trabajo e interrumpimos con Ctrl+C o se produce un cuelgue de todo el sistema, es posible que la próxima vez que accedamos nos diga que parte de nuestra copia de trabajo está bloqueada (locked). Esto se selecciona ejecutando svn cleanup, que terminará las operaciones interrumpidas y eliminará los bloqueos.

Eliminar un repositorio (obliterate)

Por ahora no se puede :-(. Esta capacidad se llamará "obliterate" y esta planeada para después de la versión 1.0. Lo máximo que puedes hacer ahora es

  • eliminar todos los ficheros y dejar vacío el repositorio (el historial permanecerá),

  • o eliminar físicamente todo el repositorio, crear uno nuevo, y restaurar las copias de seguridad.

Compartir el repositorio entre sistemas operativos

Compartir el repositorio entre sistemas operativos es posible, pero no una práctica oficialmente soportada.

Al intentar acceder a un mismo repositorio Windows en FAT32 desde linux aparece este mensaje:

svn: Unable to open an ra_local session to URL 
svn: Unable to open repository 'file:///mnt/win5/repositorio' 
svn: Berkeley DB error while opening environment for filesystem 
/mnt/win5/repositorio: 
Invalid argument 

El problema es que la base de datos de Berkeley usa shared memory (un método de comunicar dos procesos usando una misma región de memoria), y almacena los punteros en ficheros cuyo formato es diferente entre windows y linux.

La solución es eliminar los ficheros repositorio/db/__db.00* y ejecutar svnadmin recover /ruta/al/repositorio.

En el futuro otra opción será usar un repositorio fsfs (svnadmin create --fs-type /ruta/al/repositorio). Se trata de un repositorio que usa el sistema de ficheros en vez de la base de datos de Berkeley. Es una funcionalidad de Subversion que a día de hoy (agosto de 2004) aun no es estable. Consulta la lista de desarrollo de Subversion para más información.

Propiedades

Subversion permite adjuntar pares arbitrarios nombre/valor a ficheros y directorios. Estos metadatos se llaman propiedades en Subversion. Las propiedades son versioneadas, es decir, se almacenan sus sucesivos valores igual que se hace con los ficheros.

Las propiedades son modificaciones locales que no serán permanentes hasta que ejecutemos commit. Como cualquier modificación, las propiedades pueden verse con diff, status, revert.

Establecer una propiedad (propset)

Para establecer una propiedad se usa svn propset nombre valor fichero. Ejemplo:

svn propset autor jano EstoNoMeEstaPasandoAMiException.java
svn propset proposito "ilustrar el uso de propiedades" DeQueVasException.c

También se pueden adjuntar ficheros binarios como propiedades:

svn propset diagrama -F uml.png Command.java

Leer una propiedad (propget)

Para leer una propiedad usa svn propget nombre fichero. Ejemplo:

svn propget color Command.java
svn propget height Command.java

Listar propiedades (proplist)

# ver nombres de propiedades
svn proplist Command.java

# ver nombres y valores de propiedades
svn proplist Command.java --verbose

Borrar una propiedad (propdel)

Para borrar una propiedad usa svn propdel nombre fichero.

Editar propiedades (propedit)

Las propiedades se editan ejecutando svn propedit nombre fichero. Esto hará que se abran en el editor de texto que hayamos definido. Es útil sobretodo para introducir propiedades con retornos de carro.

svn:executable

Solo para ficheros. Si esta propiedad existe, significa que el fichero es ejecutable. El valor de esta propiedad es irrelevante.

svn:mime-type

Sirve para indicar el tipo mime. Normalmente no es necesario porque los comandos add e import, incorporan un algoritmo de detección.

Si establecemos el tipo mime a algo que no comience por text/, Subversion supone que es un fichero binario, si no, supone que es de texto simple.

Cuando recuperamos un fichero usando el navegador, el módulo mod_dav_svn lo envía con el valor del mime-type en la cabecera Content-type.

svn:ignore

Se aplica a un directorio y permite establecer patrones de ficheros que serán ignorados por SVN. Si queremos crearla, es conveniente usar el comando propget. Esto es similar al .cvsignore del CVS.

svn propedit svn:ignore .
*.bak
*~

Si nos da un error como este:

svn: Working copy is not up-to-date
svn: Commit failed (details follow):
svn: Cannot commit propchanges for directory '/home/jano/...

Debemos actualizar el directorio: svn update directorio antes de hacer commit a ese directorio.

svn:eol-style

Sirve para forzar los códigos de control de los saltos de línea los ficheros. Si desarrollamos desde varios sistemas operativos, podemos emplear esta propiedad para hacer siempre checkout en el salto de línea nativo del sistema que usemos.

Por defecto, los ficheros en el repositorio tienen los mismos saltos de línea que en la copia de trabajo. Los valores posibles son:

  • LF

  • CR

  • CRLF

  • native: los ficheros aparecen siempre con los saltos de línea propios del sistema operativo local. En el repositorio los ficheros se guardan con LF.

svn:keywords

Subversion sustituye cinco palabras clave, aquí se listan junto con su sinónimo:

  • LastChangedDate, Date Fecha del último cambio, por ejemplo: 2002-07-22 21:42:37 -0700 (Mon, 22 Jul 2002)

  • LastChangedRevision, Rev Última revisión en que fue cambiado el fichero. Por ejemplo: 18

  • LastChangedBy, Author Nombre del último usuario que la cambio.

  • HeadURL, URL de la última versión de este fichero

  • Id Sumario de las palabras anteriores. Por ejemplo: bar 148 2002-07-28 21:30:43 epg, que significa que el fichero bar cambio en la revisión 148, por el usuario epg, en la fecha 2002-07-28 21:30:43.

Para activar las palabras clave en un fichero se usa la propiedad svn:keywords.

svn propset svn:keywords "palabra1, palabra2, ..." fichero

Ejemplo:

# Esto hace que en el fichero foo.c se inserte el valor de dichas palabras, 
# allá donde aparezcan las cadenas $Date$, $LastChangedDate$, $Author$, y $LastChangedBy$.
svn propset svn:keywords "Date Author" foo.c

Para obtener la propiedad svn:keywords se usa

svn propget svn:keywords fichero

Lo mínimo que necesita saber un cliente

# 1. checkout
svn checkout http://svn.example.com/repos/proyecto/trunk

# 2. actualizar cambios hechos por otros
svn update

# 3. ¿que cambios voy a enviar?
svn status

# 4. enviar mis cambios
svn commit –m "cambie tal y tal cosa"

# 5. poner ficheros y directorios bajo el control de subversion
svn add fichero1 fichero2 subdirectorio
svn commit –m "añado dos ficheros y un directorio"

# 6. borrar ficheros y directorios
svn delete fichero1 fichero2 subdirectorio
svn commit –m "borre lo de antes porque no me molaba"