Pro Git, el libro oficial de Git

8.2. Migrando a Git

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-p4explicado anteriormente.