DevOps, CI/CD y contenedores
De Subversion a Git: conceptos, migración y hábitos de equipo
Intermedio12 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Sacar un proyecto de Subversion supone dos trabajos a la vez: convertir el historial con fidelidad y reeducar a un equipo cuyo modelo mental da por hecho un único servidor central. Esta guía compara ambos modelos con honestidad, traduce el vocabulario comando a comando, recorre una conversión completa con git svn que incluye ramas, tags y autores, y propone un plan de corte que no pierde trabajo.
Lo que aprenderás
- Contrastar los modelos centralizado y distribuido y explicar qué optimiza cada uno
- Traducir los comandos, las convenciones de estructura y las propiedades de Subversion a sus equivalentes en Git
- Convertir un repositorio con git svn, incluidos un archivo de autores y estructuras no estándar
- Transformar los directorios de ramas y tags de SVN importados en ramas reales de Git y tags anotados
- Planificar el corte, verificar el resultado y cambiar los hábitos que el equipo arrastra
Subversion todavía sostiene una gran cantidad de código en producción, y los proyectos que lo usan rara vez son los pequeños. Una migración, por tanto, cae sobre equipos con una década de revisiones, una estructura de directorios que se ha alejado de las convenciones, scripts de build que interpretan números de revisión y desarrolladores que nunca han trabajado sin un servidor central que arbitre. La mecánica de convertir el historial es la mitad fácil; la mitad difícil es que Git te pide pensar de otra manera sobre qué es un commit y cuándo pasa a ser visible para los demás.
Esta guía cubre ambas mitades. Empieza por la diferencia conceptual, porque de ella nace la mayor parte de la fricción de una migración; después traduce el vocabulario y recorre una conversión real con git svn, incluidas las partes en las que todo el mundo tropieza: el archivo de autores, las estructuras no estándar y la transformación de los directorios importados en ramas y tags auténticos. Termina con el corte y los hábitos que conviene cambiar desde el primer día.
Qué distingue de verdad a los dos modelos
Subversion tiene un único repositorio. Vive en un servidor, guarda el historial completo y asigna a cada commit un número de revisión secuencial que crece para todo el repositorio. Tu working copy es un checkout de una ruta en una revisión concreta y no contiene historial alguno. Hacer commit implica contactar con el servidor; si no está accesible, o no tienes permiso de escritura, no puedes hacer commit.
Git da a cada desarrollador un repositorio completo. Clonar copia toda la base de objetos, de modo que el historial, las ramas y los tags son locales. Hacer commit escribe en tu propio repositorio y no involucra a nadie más. Compartir es un acto aparte y explícito —push— y recibir es otro, fetch. Como un commit se identifica por el hash de su contenido y de toda su ascendencia, no hay un contador global que incrementar y, por tanto, no hace falta un coordinador.
Ese único cambio estructural produce la mayoría de las diferencias prácticas:
- Crear ramas es barato. En Subversion, una rama es una copia de un directorio en el servidor; en Git, es un archivo que contiene un hash de commit. Los equipos que creaban una rama por release pasan a crear una por cambio.
- El historial es un grafo, no una línea. Los números de revisión de Subversion implican un orden total. Los commits de Git tienen padres, los merges tienen dos, y «qué fue primero» solo tiene sentido a lo largo de un camino del grafo.
- La mayoría de las operaciones son offline e instantáneas.
log,diff,blame,checkoutycommitleen datos locales. En un repositorio grande la diferencia es enorme. - Los checkouts parciales no son lo normal. Subversion te deja hacer checkout de un solo subdirectorio. Git lo clona todo, y por eso los monorepos enormes con binarios exigen pensarlo bien (sparse checkout, partial clone, Git LFS).
- El historial se puede reescribir. Amend, rebase y squash no tienen equivalente en Subversion. Son potentes antes de compartir y disruptivos después, lo cual es una regla nueva que el equipo tiene que aprender.
Ningún modelo es mejor en todos los casos. La autoridad central de Subversion hace sencillo el control de acceso por directorio y ofrece bloqueo para archivos que no se pueden fusionar. Git renuncia a eso a cambio de velocidad, trabajo sin conexión y un modelo de ramas que hizo viables la revisión de código moderna y la integración continua.
Traducir el vocabulario
Casi toda la correspondencia del día a día se vuelve mecánica en cuanto aceptas que un comando de SVN suele convertirse en dos comandos de Git, porque los pasos local y remoto están separados.
| Subversion | Git | Nota |
|---|---|---|
svn checkout URL | git clone URL | El clon trae todo el historial, no una sola revisión. |
svn update | git fetch y luego git merge (o git pull) | Fetch nunca toca tus archivos. |
svn commit | git commit y luego git push | El commit es local hasta que haces push. |
svn status | git status | Git también muestra lo que está en staging. |
svn diff | git diff / git diff --staged | Dos diffs porque Git tiene un índice. |
svn add / svn rm | git add / git rm | git add también añade modificaciones al staging. |
svn revert FILE | git restore FILE | Descarta los cambios sin commitear. |
svn merge -c 4711 | git cherry-pick <hash> | Aplica un cambio en otro sitio. |
svn copy ^/trunk ^/branches/x | git switch -c x | Sin ida y vuelta al servidor. |
svn switch ^/branches/x | git switch x | Cambiar de rama es una operación local sobre archivos. |
svn log -v | git log --stat | Añade --graph para ver los merges. |
svn blame | git blame | La misma idea, ejecutada en local. |
propiedad svn:ignore | archivo .gitignore | Un archivo versionado, no metadatos. |
svn:externals | submodule, subtree o dependencia de paquetes | Sin conversión automática. |
svn lock | bloqueo de archivos de Git LFS, o nada | Git no tiene bloqueo nativo. |
número de revisión r4711 | hash de commit a1b2c3d | Usa git describe para obtener nombres legibles. |
Dos convenciones merecen un comentario. Los directorios trunk, branches y tags de Subversion son pura convención: el servidor solo ve directorios normales, y por eso una copia es una rama. Git tiene refs de rama y de tag de primera clase, así que tras la conversión esos directorios desaparecen y se convierten en refs. Además, los tags de Subversion son directorios modificables en los que a veces se hace commit; los tags de Git están pensados como punteros permanentes, así que un «tag» que recibió commits después de crearse debe convertirse en una rama.
Elegir la vía de conversión
Tres herramientas cubren casi todos los casos.
git svn viene con Git, no requiere instalación adicional y hace una importación fiel, revisión a revisión. Es la opción por defecto adecuada para un repositorio único de hasta unas decenas de miles de revisiones, y además puede actuar como puente bidireccional durante un periodo de transición.
svn2git es un envoltorio ligero sobre git svn que automatiza la conversión de ramas y tags descrita más abajo. Es cómodo, pero entiende lo que hace: tendrás que inspeccionar el resultado igualmente. Otra herramienta en C++ con el mismo nombre, desarrollada para grandes migraciones de proyectos open source, lee directamente un archivo de volcado de Subversion y es mucho más rápida con repositorios muy grandes, a cambio de un archivo de reglas que tienes que escribir.
reposurgeon es la opción pesada: carga el historial en una forma editable para que puedas reescribir autores, corregir mensajes, eliminar rutas o dividir repositorios. Recurre a ella cuando el historial está realmente desordenado.
Preparar el archivo de autores
Subversion registra un nombre de usuario; Git registra un nombre y una dirección de correo en cada commit. Sin una correspondencia, todos los commits históricos se atribuyen a jdoe <jdoe@localhost>, lo que rompe git shortlog, el filtrado de blame y cualquier herramienta que vincule commits con cuentas. Construye la correspondencia antes de empezar.
# Every distinct committer in the repository's history
svn log --quiet https://svn.example.com/shop \
| awk '/^r[0-9]+ \| /{print $3}' \
| sort -u > authors-raw.txt
Convierte esa lista en authors.txt, con una línea por usuario:
jdoe = Jane Doe <[email protected]>
amartin = Alex Martin <[email protected]>
build = Build Service <[email protected]>
(no author) = Unknown <[email protected]>
Incluye la línea (no author): las revisiones creadas por el administrador del repositorio o por las primeras importaciones a menudo no tienen autor, y git svn se detiene en cuanto encuentra un nombre de usuario que no está en el archivo. Pide a cada persona que revise su propia línea: una dirección que no coincida con su identidad de Git hará que sus commits históricos y los nuevos parezcan de dos personas distintas.
Ejecutar la conversión
Para un repositorio que sigue la estructura estándar:
git svn clone https://svn.example.com/shop \
--stdlayout \
--authors-file=authors.txt \
--no-metadata \
--prefix=svn/ \
shop-git
--stdlayout es la forma abreviada de --trunk=trunk --branches=branches --tags=tags. --no-metadata omite la línea git-svn-id: que, de lo contrario, se añadiría a cada mensaje de commit; no la uses si piensas seguir haciendo commits de vuelta a Subversion a través del puente, porque el puente necesita esa línea. --prefix=svn/ agrupa las refs importadas bajo refs/remotes/svn/, lo que hace inequívoco el paso de limpieza de más abajo.
Las estructuras no estándar son habituales. Indica las rutas explícitamente y repite la opción cuando las ramas estén en varios sitios:
git svn clone https://svn.example.com/legacy \
--trunk=main/src \
--branches=branches --branches=feature-branches \
--tags=releases \
--authors-file=authors.txt \
--ignore-paths='^(vendor/binaries|docs/generated)/' \
legacy-git
--ignore-paths recibe una expresión regular y es la forma más sencilla de dejar fuera del nuevo historial artefactos generados, binarios de terceros o un volcado de base de datos commiteado por accidente. Para importar solo el historial reciente, añade -r 40000:HEAD: obtienes una conversión rápida cuyo primer commit es un snapshot, y el servidor antiguo se mantiene en solo lectura para todo lo anterior.
Si la importación se detiene —un fallo de red, un autor que falta—, corrige la causa y reanúdala con git svn fetch dentro del repositorio convertido a medias. Continúa desde la última revisión convertida en lugar de empezar de nuevo.
Convertir las refs importadas en ramas y tags
Tras el clon, git branch -a muestra entradas como remotes/svn/trunk, remotes/svn/release-2.4 y remotes/svn/tags/v2.4.0. Son remote-tracking refs, no ramas reales, y hacer push de ellas a un servidor nuevo no sirve de nada. Conviértelas:
cd shop-git
# SVN tag directories become annotated Git tags
git for-each-ref --format='%(refname:short) %(objectname)' refs/remotes/svn/tags \
| while read -r ref sha; do
name="${ref#svn/tags/}"
git tag -a "$name" -m "Imported from Subversion tag $name" "$sha"
git branch -r -d "$ref"
done
# The remaining SVN branches become local branches
git for-each-ref --format='%(refname:short) %(objectname)' refs/remotes/svn \
| while read -r ref sha; do
name="${ref#svn/}"
[ "$name" = trunk ] && continue
git branch "$name" "$sha"
git branch -r -d "$ref"
done
git branch -m master main # if trunk was checked out as master
Antes de hacer push, haz la limpieza que el repositorio antiguo nunca permitió. Convierte las propiedades de ignore de Subversion en un archivo real y añade las entradas que antes gestionaba el servidor:
git svn show-ignore >> .gitignore
git add .gitignore && git commit -m "Add .gitignore from svn:ignore properties"
Después, busca lo que engorda el repositorio. git count-objects -vH informa del tamaño del repositorio, y este pipeline lista los blobs más grandes que se hayan commiteado jamás:
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1=="blob" {print $3, $4}' | sort -nr | head -20
Si lo primero de esa lista son archivos de release o jars de terceros, elimínalos con git filter-repo --path <dir> --invert-paths antes del primer push. Reescribir el historial después invalida todos los clones.
Verificar y hacer el corte
Nunca te fíes de una conversión que no hayas comprobado. Tres verificaciones poco costosas detectan casi todo:
Comparar las puntas
Exporta la punta de trunk desde Subversion y la demaindesde Git en dos directorios y ejecutadiff -r, ignorando.svny.git. Las únicas diferencias deberían ser los archivos que excluiste deliberadamente.Comparar los recuentos
svn log --quiet | grep -c '^r'frente agit rev-list --count --all. No coincidirán exactamente —los merges y las rutas excluidas cambian la forma—, pero una diferencia de un orden de magnitud significa que las opciones de estructura eran incorrectas.Compilar cada tag
Haz checkout de dos o tres tags de release y ejecuta el build. Una release que no puedes reconstruir desde el nuevo repositorio es una migración que falló sin hacer ruido.
Para el corte en sí, congela las escrituras en lugar de intentar mantener ambos sistemas en paralelo indefinidamente. Anuncia una fecha, pon el repositorio de Subversion en solo lectura con un hook pre-commit, ejecuta un último git svn fetch para recoger las revisiones finales, repite la conversión de ramas y tags y, después, haz push:
git remote add origin [email protected]:acme/shop.git
git push --all origin
git push --tags origin
Un periodo de puente breve es razonable si algunas personas no pueden cambiar de inmediato. Con los metadatos conservados (sin --no-metadata), git svn rebase trae las nuevas revisiones de SVN al clon de Git y git svn dcommit reproduce los commits locales de Git como revisiones de SVN. Trátalo como una concesión de dos semanas, no como una arquitectura: el puente solo funciona desde un repositorio cuyo historial no se ha reescrito, y escribir en los dos sitios invita a la divergencia.
Actualiza todo lo que depende del repositorio en la misma ventana. Los jobs de build que hacían referencia a ${SVN_REVISION} necesitan un nuevo esquema de versiones: git describe --tags o el hash corto. Los enlaces a r4711 en el gestor de incidencias se rompen. Los scripts de release que hacían svn copy para crear un tag ahora hacen git tag -a y git push origin <tag>. La guía del pipeline de entrega explica estrategias de versionado que funcionan con hashes en lugar de contadores.
Lo que el equipo tiene que desaprender
Tres hábitos provocan casi toda la confusión inicial, y nombrarlos de antemano ahorra dos semanas de preguntas de soporte.
«Commit» ya no significa «compartir». Los desarrolladores acostumbrados a Subversion o bien acumulan cambios durante días porque hacer commit era público, o bien hacen commit y se preguntan por qué nadie ve su trabajo. Dilo explícitamente: haz commit pronto y a menudo en local, y haz push cuando el trabajo sea coherente.
«Actualizar» ya no reescribe tus archivos a tus espaldas. git fetch es seguro y se puede ejecutar constantemente; el merge o el rebase es el paso que toca el working tree.
Las ramas son para los cambios, no para las releases. El instinto heredado de Subversion es que una rama es un acontecimiento pesado que requiere aprobación. Elige un modelo y déjalo por escrito: la guía de workflows de ramas compara las opciones realistas, y el trunk-based development suele ser el cambio más pequeño respecto a los hábitos de un equipo de Subversion.
Apoya la transición de forma práctica: una hoja con la traducción de comandos colgada en la pared y una persona designada para responder preguntas sobre Git durante el primer mes. Asegúrate de que todo el mundo lea la parte de los fundamentos de Git sobre cómo deshacer cambios, porque si no, «¿cómo recupero mi archivo?» será la pregunta de cada día.
Trampas que conviene prever
- Un autor que falta aborta la importación tras horas de trabajo. Genera la lista primero e incluye
(no author). - Tags en los que se hizo commit. Se convierten en tags que apuntan al último estado y pierden en silencio las revisiones intermedias. Busca con
svn log ^/tagslas rutas de tags con más de una revisión y conviértelas en ramas. - Los directorios vacíos desaparecen. Git versiona archivos, no directorios. Si el build necesita un directorio vacío, añade un archivo
.gitkeep. - Finales de línea y propiedades de archivo.
svn:eol-stylese convierte en una entrada de.gitattributes(* text=auto), ysvn:executablese traduce en el modo del archivo. Define.gitattributesen el primer commit tras la conversión en lugar de descubrir el problema más adelante en una máquina Windows. - Nadie se hace cargo del servidor antiguo. Decide quién mantiene en marcha la instancia de Subversion en solo lectura, durante cuánto tiempo y dónde están sus copias de seguridad. «Lo daremos de baja el próximo trimestre» suele significar que desaparecerá sin previo aviso.
Puntos clave
- Subversion centraliza el historial en un servidor y numera las revisiones de forma global; Git da a cada desarrollador el historial completo, y por eso crear ramas, consultar el log y hacer commit son operaciones locales e instantáneas.
- La mayoría de los comandos pasan de uno a dos: un commit de SVN se convierte en un commit de Git más un push, y un update de SVN en un fetch más un merge.
- Prepara el archivo de autores antes de convertir, incluye
(no author)y crea un mirror local del repositorio si el servidor es lento. git svn clone --stdlayoutresuelve los repositorios corrientes;--trunk,--branches,--tagse--ignore-pathsexplícitos resuelven el resto, y la importación se puede reanudar congit svn fetch.- Las refs importadas no son ramas reales: convierte los directorios de tags en tags anotados y los de ramas en ramas, añade un
.gitignorey un.gitattributes, y elimina los binarios grandes antes del primer push. - Verifica comparando las puntas, cotejando el número de commits y recompilando los tags publicados; después congela Subversion, haz push una sola vez y mantén corto el periodo de puente.
- Dedica tanta atención al cambio de hábitos —commit no es compartir, fetch es seguro, las ramas son baratas— como a la propia conversión.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Conservamos todo el historial de Subversion o empezamos de cero?
- Ambas opciones son defendibles. Una conversión completa conserva blame, bisect y la arqueología del código, algo que importa en código de negocio de larga vida, pero lleva más tiempo y arrastra cada binario grande que se haya commiteado. Empezar con un repositorio vacío y un único commit de importación es rápido y limpio, y funciona si el servidor antiguo sigue en línea en solo lectura para consultar el pasado. Decide preguntándote con qué frecuencia alguien lee de verdad historial de más de dos años.
- ¿Cuánto tarda una conversión con git svn?
- Recorre el historial de Subversion revisión a revisión a través de la red, así que cuenta con entre una y unas pocas revisiones por segundo. Un repositorio de 50.000 revisiones puede tardar fácilmente toda la noche. Ejecútala en una máquina cercana al servidor SVN, usa un mirror local creado con svnsync si puedes y nunca des por hecho que terminará dentro de una ventana de mantenimiento prevista para una tarde.
- ¿Qué pasa con svn:externals?
- Nada automático: git svn los ignora. Un external que apunta a otro proyecto suele convertirse en una dependencia resuelta por Maven, npm o el gestor de paquetes de tu lenguaje, que casi siempre es la mejor respuesta. Si de verdad necesitas un checkout anidado, un submodule de Git o un subtree merge reproduce el comportamiento, pero revisa cada external por separado en lugar de convertirlos mecánicamente.
- ¿Seguimos necesitando bloquear archivos después de pasar a Git?
- Git no tiene equivalente a svn lock, porque su modelo de merge presupone texto que se puede combinar. Los equipos que bloqueaban archivos binarios —diseños, hojas de cálculo, documentos generados— deberían llevarlos a Git LFS, que ofrece un workflow de bloqueo de archivos, o mantenerlos en un sistema pensado para binarios. Bloquear código fuente solía ser un hábito que convenía perder, no un requisito que haya que reproducir.