Si tienes un proyecto que utiliza otro sistema de control de versiones pero has decidido empezar a utilizar Git, debes migrar el código fuente del proyecto al nuevo repositorio. Esta sección muestra alguna de las herramientas de importación que incluye Git y después explica cómo crear tu propio importador.
8.2.1. Importando
Esta sección explica cómo importar información de los dos gestores de código fuente más utilizados en el ámbito empresarial: Subversion y Perforce. La razón es que estas herramientas son las que utilizan la mayoría de usuarios que se están pasando a Git y además, que Git incluye herramientas de importación muy buenas para ambos sistemas.
8.2.2. Subversion
Si has leído la sección anterior sobre cómo utilizar git svn
, puedes seguir esas instrucciones para clonar un repositorio con git svn clone
. Después, deja de utilizar el servidor Subversion, sube los cambios al repositorio de Git y continúa trabajando en el nuevo repositorio. Si necesitas mantener el historial de cambios, solo tienes que bajarlo del repositorio Subversion (y esperar un buen rato).
El problema es que esta importación no es perfecta y tarda mucho tiempo. En primer lugar, hay problemas con la información de los autores de cada cambio. En Subversion, cada programador tiene asociado un usuario del sistema y esa información se incluye en el commit. Los ejemplos de las secciones anteriores muestran por ejemplo el nombre de usuario schacon
, como por ejemplo en el resultado del comando blame
y de git svn log
. Si quieres mantener esta información, tienes que asociar o mapear los usuarios de Subversion con los usuarios de Git. Crea un archivo llamado users.txt
con el siguiente formato para mapear los usuarios:
schacon = Scott Chacon <[email protected]> selse = Someo Nelse <[email protected]>
Para obtener la lista completa de los nombres de los autores de Subversion, ejecuta el siguiente script en la consola:
$ svn log --xml | grep author | sort -u | perl -pe 's/.>(.?)<./$1 = /'
Este script obtiene el log en formato XML, busca a los autores, crea una lista única y elimina el contenido XML sobrante. Obviamente este sript solo funciona si en tu máquina tienes instalado grep
, sort
, y perl
. Redirige la salida de ese script al archivo users.txt
y después añade los usuarios de Git que corresponden a cada usuario de Subversion.
Para mapear la información de manera más precisa, pasa ese archivo al comando git svn
. También puedes añadir a git svn
la opción --no-metadata
en los comandos clone
e init
para que Subversion no incluya los metadatos que normalmente importa. De esta forma, el comando import
a utilizar sería como el siguiente:
$ git-svn clone http://my-project.googlecode.com/svn/ \ --authors-file=users.txt --no-metadata -s my_project
La importación de Subversion realizada en el directorio my_project
será así mejor. En lugar de commits como el siguiente:
commit 37efa680e8473b615de980fa935944215428a35a Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029> Date: Sun May 3 00:12:22 2009 +0000
fixed install - go to trunk
git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de- be05-5f7a86268029
Ahora tendrán el siguiente aspecto:
commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2 Author: Scott Chacon <[email protected]> Date: Sun May 3 00:12:22 2009 +0000
fixed install - go to trunk
No solo la información del autor es más clara, sino que ya no se incluye el valor git-svn-id
.
Después de la importación, ahora te toca hacer algo de limpieza. Primero, borra todas las referencias extrañas que ha creado git svn
. Para ello, convierte las etiquetas importadas como ramas en etiquetas de Git. Después, convierte las otras ramas en ramas locales.
Para convertir las etiquetas importadas en verdaderas etiquetas de Git, ejecuta:
$ cp -Rf .git/refs/remotes/tags/* .git/refs/tags/ $ rm -Rf .git/refs/remotes/tags
Este comando convierte las referencias que antes eran ramas remotas que empezaban por tag/
y las convierte en etiquetas reales de Git.
Después, convierte las referencias creadas bajo refs/remotes
en ramas locales:
$ cp -Rf .git/refs/remotes/* .git/refs/heads/ $ rm -Rf .git/refs/remotes
Ahora todas las viejas ramas de Subversion son ramas de Git y todas las viejas etiquetas son etiquetas reales de Git. Por último, añade el nuevo servidor Git como referencia remota y sube (push) los cambios. Como quieres subir todas las ramas y etiquetas, ejecuta lo siguiente:
$ git push origin --all
Ahora todas las ramas y etiquetas deberían estar en tu servidor de Git y la limpieza se habrá realizad con éxito y absoluta limpieza.
8.2.3. Perforce
El siguiente sistema del que vamos a importar es Perforce. Git también incluye un importador de Perforce, pero solamente en la sección contrib
del código fuente de Git, por lo que no está disponible por defecto como git svn
. Para ejecutarlo, descarga el código fuente de Git, que puedes obtener de git.kernel.org
:
$ git clone git://git.kernel.org/pub/scm/git/git.git $ cd git/contrib/fast-import
En este directorio fast-import
encontrarás un script ejecutable de Python llamado git-p4
. Para que funcione la importanción debes tener tanto Python como la herramienta p4
instalada en tu máquina. El siguiente ejemplo importa el proyecto Jam
del repositorio público de Perforce. Primero crear una variable de entorno llamada P4PORT
y que apunte al repositorio de Perforce:
$ export P4PORT=public.perforce.com:1666
Ejecuta el comando git-p4 clone
para importar el proyecto Jam
del servidor Perforce, indicando tanto las rutas del repositorio y proyecto como la ruta en la que quieres importar el proyecto:
$ git-p4 clone //public/jam/src@all /opt/p4import Importing from //public/jam/src@all into /opt/p4import Reinitialized existing Git repository in /opt/p4import/.git/ Import destination: refs/remotes/p4/master Importing revision 4409 (100%)
Si accedes al directorio /opt/p4import
y ejecutas el comando git log
, verás el resultado de la importación:
$ git log -2 commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2 Author: Perforce staff <[email protected]> Date: Thu Aug 19 10:18:45 2004 -0800
Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into the main part of the document. Built new tar/zip balls.
Only 16 months later.
[git-p4: depot-paths = "//public/jam/src/": change = 4409]
commit ca8870db541a23ed867f38847eda65bf4363371d Author: Richard Geiger <[email protected]> Date: Tue Apr 22 20:51:34 2003 -0800
Update derived jamgram.c
[git-p4: depot-paths = "//public/jam/src/": change = 3108]
Observa que cada commit incluye el identificador de git-p4
. No está mal mantener ese identificador por si necesitas más adelante el identificador del cambio de Perforce. No obstante, si quieres eliminar ese identificador, lo mejor es hacerlo ahora mismo, antes de empezar a trabajar en el nuevo repositorio. Ejecuta el comando git filter-branch
para eliminar este identificador en todos los cambios:
$ git filter-branch --msg-filter ' sed -e "/^\[git-p4:/d" ' Rewrite 1fd4ec126171790efd2db83548b85b1bbbc07dc2 (123/123) Ref 'refs/heads/master' was rewritten
Ejecuta de nuevo git log
, y verás que las sumas de comprobación SHA-1 de todos los commits han cambiado, pero las cadenas relacionadas con git-p4
ya no se incluyen en los mensajes de los commits:
$ git log -2 commit 10a16d60cffca14d454a15c6164378f4082bc5b0 Author: Perforce staff <[email protected]> Date: Thu Aug 19 10:18:45 2004 -0800
Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into the main part of the document. Built new tar/zip balls.
Only 16 months later.
commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2 Author: Richard Geiger <[email protected]> Date: Tue Apr 22 20:51:34 2003 -0800
Update derived jamgram.c
La importación ya ha funalizado y ya puedes empezar a subir cambios al nuevo repositorio de Git.
8.2.4. Importador personalizado
Si tu proyecto no utiliza ni Subversion ni Perforce, lo mejor es que busques en Internet algún importador para tu sistema. Existen importadores muy buenos para sistemas como CVS, Clear Case, Visual Source Safe e incluso para directorios simples de archivos.
Si ninguno de estos importadores te convence o si utilizas un sistema muy extraño o si requieres un control muy preciso de la importación, tendrás que utilizar git fast-import
. A este comando se le pasan instrucciones sencillas que utiliza para escribir información en el repositorio Git. Esta es una forma mucho más sencilla de crear objetos Git, en vez de ejecutar directamente los comandos de Git para crear esos objetos (tal y como se explica en el capítulo 9). De esta forma puedes crear un script de importación que obtenga la información necesaria del sistema que utilices actualmente y genere las instrucciones necesarias para crear los objetos Git. Después puedes utilizar ese script junto con el comando git fast-import
.
Vamos a escribir un importador simple para demostrar rápidamente cómo se hace. Imagina que quieres importar a Git un sistema de gestión de código propio que se basa en hacer de vez en cuando copias de seguridad en directorios cuyo nombre indica la fecha en la que se realizó la copia de seguridad (formato: back_YYYY_MM_DD
). La estructura de directorios actual podría tener el siguiente aspecto (la carpeta current
contiene el código fuente actual):
$ ls /opt/import_from back_2009_01_02 back_2009_01_04 back_2009_01_14 back_2009_02_03 current
Para importar esta información a un repositorio Git, debes conocer cómo almacena su información Git. Seguramente recuerdes que Git es simplemente una lista enlazada de objetos que apunta a un estado determinado del contenido. Así que a fast-import
solo tienes que decirle cuáles son esos estados, que contenidos apuntan a cada uno y el orden correcto. La estrategia será recorrer los estados del código uno a uno y crear los commits con los contenidos de cada directorio, enlazando cada commit con el anterior.
Al igual que en la sección Un ejemplo de implantación de una determinada política en Git del capítulo 7, vamos a utilizar Ruby para escribir este script porque es lo que más utilizo y suele generar código fácil de leer. Utiliza en vez de Ruby cualquier otra tecnología que domines y en la que te sientas cómodo. Lo único importante es que tu script genere la misma información. Si utilizas Windows, ten mucho cuidado de no insertar retornos de carro al final de cada línea, ya que el comando git fast-import
es un poco especial y solo admite caracteres LF
(line feeds) y no los habituales CRLF
(carriage return line feeds) que suele utilizar Windows.
Comienza accediendo al directorio del proyecto y localizando todos los subdirectorios, siendo cada uno de ellos un estado diferente del proyecto y que vas a importar como commit. Accede a cada subdirectorio y genera los comandos necesarios para exportar sus contenidos. El núcleo de ese script es algo así como lo siguiente:
last_mark = nil
# loop through the directories Dir.chdir(ARGV[0]) do Dir.glob("*").each do |dir| next if File.file?(dir)
# move into the target directory Dir.chdir(dir) do last_mark = print_export(dir, last_mark) end end end
Dentro de cada directorio se ejecuta print_export
, que espera el manifest y el mark del estado anterior y devuelve los del estado actual, de forma que sea sencillo enlazarlos. El "mark" es el término utilizado por fast-import
para referirse al identificador de cada commit. Al crear un commit se le añade un mark que se puede utilizar para enlazarlo con otros commits. Así que lo primero que se hace con el método print_export
es generar el mark a partir del nombre del directorio:
mark = convert_dir_to_mark(dir)
Se crea un array de directorios y se emplea el valor de su índice como mark, ya que el valor del mark debe ser un número entero. El método completo es el siguiente:
$marks = [] def convert_dir_to_mark(dir) if !$marks.include?(dir) $marks << dir end ($marks.index(dir) + 1).to_s end
Una vez calculado el número entero que representa al commit, se necesita una fecha para los metadatos del commit. Como la fecha se encuentra en el propio nombre del directorio, solo hay que procesar ese nombre. La siguiente línea del archivo print_export
es:
date = convert_dir_to_date(dir)
donde la función convert_dir_to_date
se define como:
def convert_dir_to_date(dir) if dir == 'current' return Time.now().to_i else dir = dir.gsub('back_', '') (year, month, day) = dir.split('_') return Time.local(year, month, day).to_i end end
El código anterior devuelve un número entero con la fecha de cada directorio. La última información necesaria para los metadatos del commit es precisamente la del propio autor del commit. Esta información se puede escribir directamente en una variable global:
$author = 'Scott Chacon <[email protected]>'
Ahora si que ya está todo listo para empezar a generar las instrucciones del importador. La información inicial indica que se está definiendo un objeto de tipo commit y la rama en la que se encuentra, seguido del mark generado, la información de la persona que hace el commit y el mensaje asociado al commit. Si existe, también se indica el anterior commit. El código resultante es el siguiente:
# print the import information puts 'commit refs/heads/master' puts 'mark :' + mark puts "committer #{$author} #{date} -0700" export_data('imported from ' + dir) puts 'from :' + last_mark if last_mark
La zona horaria asociada con la fecha se escribe directamente en el script (-0700
) porque así es mucho más fácil. Si estás importando desde otro sistema, debes especificar la zona horaria en forma de offset desde la hora GMT o UTC.
El mensaje del commit se debe indicar de una manera especial:
data (size)\n(contents)
La sintaxis que sigue es: palabra data
, seguida de un espacio en blanco y el tamaño de datos que vienen a continuación. Después se añade un carácter de nueva línea (\n
) y es incluye el mensaje. Como más adelante se utiliza este mismo formato para indicar los contenidos de los archivos, es mejor que te crees un método reutilizable llamado export_data
:
def export_data(string) print "data #{string.size}\n#{string}" end
Lo único que falta es indicar los contenidos de los archivos en cada estado del proyecto. Como cada estado tiene su propio directorio, esto es bastante fácil. Utiliza el comando deleteall
seguido de los contenidos de cada archivo del directorio. Git guardará así cada estado del proyecto correctamente:
puts 'deleteall' Dir.glob("**/*").each do |file| next if !File.file?(file) inline_data(file) end
NOTA: como muchos sistemas consideran sus revisiones como cambios de un commit a otro, fast-import
también admite comandos para cada commit, de forma que se especifique qué archivos se han creado, eliminado o modificado y qué contenidos son nuevos. Se podrían determinar las diferencias entre cada estado del proyecto e indicar solamente esa información, aunque es algo bastante más complejo. También puedes proporcionar a Git toda la información y que sea este el que se las apañe. Si crees que hacerlo así es interesante para tu importación, consulta la documentación de fast-import
para obtener más detalles sobre cómo hacerlo.
El formato para indicar que los contenidos son nuevos o para especificar que son contenidos modificados es el siguiente:
M 644 inline path/to/file data (size) (file contents)
El número 644
indica los permisos del archivo (si tienes archivos ejecutables, cambia este valor por 755
), y la palabra inline
indica que los contenidos del archivo se incluyen inmediatamente después de esta línea. El método inline_data
tendría el siguiente aspecto:
def inline_data(file, code = 'M', mode = '644') content = File.read(file) puts "#{code} #{mode} inline #{file}" export_data(content) end
Este código reutiliza el método export_data
definido anteriormente, ya que la lógica necesaria es la misma que para los mensajes de los commits.
Por último, devuelve el valor del mark actual para que se pueda utilizar en la siguiente importación:
return mark
NOTA: si utilizas Windows, asegúrate de añadir un paso más. Como se mencionó anteriormente, Windows usa CRLF
como caracteres de nueva línea, mientras que fast-import
solo funciona si se utiliza LF
. Para solucionar este problema, dile a Ruby que utilice LF
en vez de CRLF
:
$stdout.binmode
Y ya está. Si ejectuas el script completa, obtendrás algo parecido a lo siguiente:
$ ruby import.rb /opt/import_from commit refs/heads/master mark :1 committer Scott Chacon <[email protected]> 1230883200 -0700 data 29 imported from back_2009_01_02deleteall M 644 inline file.rb data 12 version two commit refs/heads/master mark :2 committer Scott Chacon <[email protected]> 1231056000 -0700 data 29 imported from back_2009_01_04from :1 deleteall M 644 inline file.rb data 14 version three M 644 inline new.rb data 16 new version one (...)
Para ejecutar el importador, pasa los comandos anteriores directamente a git fast-import
estando dentro del directorio Git donde quieres importar la información. Si lo prefieres, crea un nuevo directorio, ejecuta git init
en el y después ejecuta el script:
$ git init Initialized empty Git repository in /opt/import_to/.git/ $ ruby import.rb /opt/import_from | git fast-import ## git-fast-import statistics:Alloc'd objects: 5000 Total objects: 18 ( 1 duplicates ) blobs : 7 ( 1 duplicates 0 deltas) trees : 6 ( 0 duplicates 1 deltas) commits: 5 ( 0 duplicates 0 deltas) tags : 0 ( 0 duplicates 0 deltas) Total branches: 1 ( 1 loads ) marks: 1024 ( 5 unique ) atoms: 3 Memory total: 2255 KiB pools: 2098 KiB ## objects: 156 KiBpack_report: getpagesize() = 4096 pack_report: core.packedGitWindowSize = 33554432 pack_report: core.packedGitLimit = 268435456 pack_report: pack_used_ctr = 9 pack_report: pack_mmap_calls = 5 pack_report: pack_open_windows = 1 / 1 pack_report: pack_mapped = 1356 / 1356 ---------------------------------------------------------------------
Como se puede observar, cuando finaliza correctamente, el script proporciona varias estadísticas útiles sobre el trabajo realizado. En este caso, se han importado 18 objetos para 5 commits en una única rama. Si ejecutas el comando git log
, verás el historial de cambios:
$ git log -2 commit 10bfe7d22ce15ee25b60a824c8982157ca593d41 Author: Scott Chacon <[email protected]> Date: Sun May 3 12:57:39 2009 -0700
imported from current
commit 7e519590de754d079dd73b44d695a42c9d2df452 Author: Scott Chacon <[email protected]> Date: Tue Feb 3 01:00:00 2009 -0700
imported from back_2009_02_03
Gracias al importador has conseguido tener un repositorio Git completo y limpio. No obstante, todavía no se ha descargado (checkout) ningún contenido, así que no tienes ningún archivo en tu directorio de trabajo. Para descargarlos, situa tu rama donde se encuentre ahora mismo la rama master
:
$ ls $ git reset --hard master HEAD is now at 10bfe7d imported from current $ ls file.rb lib
La herramienta fast-import
permite hacer muchas más cosas, como gestionar diferentes permisos, tratar con datos binarios, manejar varias ramas y fusiones, etiquetas, barras de progreso y más. El directorio contrib/fast-import
del código fuente de Git contiene muchos ejemplos de escenarios más complejos. Uno de los mejores ejemplos es precisamente el script git-p4
explicado anteriormente.