Etiqueta: git

Migrar a GitLab manteniendo mirrors en GitHub

En iCarto habíamos planificado hace un tiempo migrar nuestra infraestructura de desarrollo (repositorios de código principalmente) a GitLab. Estos días hemos terminado la migración y he aprovechado para escribir un artículo de como se pueden mantener mirrors de los repos de gitlab en github a través de la propia plataforma o con un servidor intermedio.

Como funciona git reset y porqué no usar git-flow

Scott Chacon demistifica en el siguiente vídeo git reset, que es probablemente uno de los comandos más complejos de entender de git.

En su blog y en este gist hay alguna explicación adicional.

Hacia el final de la charla le preguntan que opina de git flow, lo que le permite explicar porque no le gusta y recomienda leer el workflow que siguen en github. Merece la pena leerlo.

Integrando las correcciones de extCAD en OpenCADTools

He estado trabajando en integrar todas las correcciones que se hicieron en la extensión oficial de CAD para gvSIG, desde el nacimiento de  OpenCADTools en las propias opencadtools. Este artículo describe:

  • Como se realizó el proceso de integración para ver de donde pueden venir errores futuros y la extraña apariencia que tiene el repositorio actual.
  • Contar el método seguido en sí por si resulta útil a gente que tenga que hacer algo parecido.

Debo agradecer al Cartolab el que me haya cedido horas de mi trabajo diario para poder escribir este artículo y realizar la integración en si misma. Además, debo agradecer a los organizadores del codesprint de Münich por financiar mi estancia allí y a los participantes por las aportaciones recibidas.

El problema

Tenemos tres repositorios svn con código parecido pero no igual:

  • gvSIG. Contiene entre otras muchas cosas el proyecto extCAD. Se trata del código base con commits desde 2005 hasta la actualidad
  • gisEIEL. A partir del año 2006 la Universidade da Coruña, a través de un convenio con la Diputación de A Coruña, realizó un fork de las herramientas de CAD de la versión 0.x de gvSIG. Este desarrollo fue asignado al Laboratorio de Bases de Datos de dicha universidad. En su repositorio actual sólo está el resultado final del fork, no los commits intermedios, porque lo que no se pueden comparar los logs para hacer una integración de código más ordenada.
  • OpenCADTools. A partir de 2009, con financiación de la Diputación de Pontevedra y en el marco de los trabajos de desarollo de gvSIG-EIEL Cartolab hizo una integración del código de gisEIEL con el de gvSIG adaptando todo ello primero a la versión 1.1.2 de gvSIG y luego a la 1.9. Se dispone de todo el historial desde el principio hasta la actualidad. Posteriormente Cartolab continuo el desarrollo de las OpenCADTools como un proyecto propio.

Lo que queremos ahora es:

  1. Obtener las mejoras realizadas en los tres proyectos desde que evolucionaron por separado para obtener una extensión de CAD mucho más potente. Nos concentraremos en el código de gvSIG y el de OpenCADTools por ser los que más desarrollo han tenido y estar ambos adaptados a la rama 1.9 de gvSIG, mientras que gisEIEL continua en versiones más antiguas. Emplearemos el proyecto de gisEIEL sólo para consultar dudas de autoría o Copyright.
  2. Es fundamental respetar lo máximo posible la historia de commits. Debemos saber cuando se integró un cambio, por qué, y quien lo hizo. Esto nos permitirá prevenir errores futuros.
  3. Es fundamental respetar lo máximo posible la autoría y el copyright de todas las modificaciones.
  4. Nuestra meta final es además que el código resultante pueda ser integrado en el proyecto gvSIG, por tanto nuestra base debe ser el código de extCAD, y las mejoras de OpenCADTools deben poder ser introducidas en forma de parches sobre extCAD.

Para ello necesitamos poder comparar de forma sencilla estos tres repositorios. Dada la cantidad de commits que ha habido y la divergencia existente resulta muy complicado comparar los logs o encontrar un punto común. Haremos por tanto un análisis inicial clase a clase que nos permita ver el grueso de las diferencias y tomar decisiones.

Análisis clase a clase

  • Montamos un repositorio local de git que contenga en ramas separadas el código de cada proyecto.
  • Obtenemos un fichero de texto all_files.txt con todas los ficheros que hay (sin repetir) estén en una rama u otra. Haremos el listado de ficheros de cada rama mediante tree, y compararemos los ficheros con meld para incluír rapidamente los ficheros que están en una y no en otra en el fichero all_files.txt

    git checkout heads/extcad
    tree -fi --noreport > extcad_files.txt
    git checkout heads/opencadtools
    tree -fi --noreport > opencadtools_files.txt
    cp opencadtools_files.txt all_files.txt
  • Hacemos un script muy sencillo que mete un carácter = antes de cada línea de un fichero checked_files.txt para todos aquellos ficheros que no hayan variado.
  • Sobre el fichero checked_files.txt marcamos con una x aquellos ficheros que sabemos que no es necesario que comprobemos, por ejemplo el directorio install de OpenCADTools que con el nuevo sistema de paquetes de gvSIG está obsoleta. Algunos de estos ficheros serán eliminados a posteriori
  • Los ficheros distintos los revisaremos uno a uno con algo como esto:

    fileToCheck=./src/com/iver/cit/gvsig/CADExtension.java
    git diff master:$fileToCheck refs/heads/opencadtools:$fileToCheck

    La variable fileToCheck la rellenamos con un copy&paste desde checked_files.txt. Y vamos marcando en el fichero de checked_files.txt con una m aquellos en las que la «versión más correcta» sea la del trunk de gvSIG. A veces el cambio es simplemente una cabecera, indentado, …

Este proceso aunque algo tedioso nos permite hacernos una imagen mental del estado de los repositorios lo que aumentará nuestra capacidad para tomar decisiones.

Primera fase de la de integración

En el caso de OpenCADTools fue sencillo encontrar el punto en que se completo la migración de las herramientas desde la versión 1.1.2 de gvSIG a la 1.9. Perder el histórico de la migración no era grave y nos permitía simplificar el proceso, así que lo que se decidió fue escoger el commit en que esa migración se consideraba finalizaday volcar todo el repo de OpenCADTools en ese punto sobre el de extCAD.


# En un repo con sólo OpenCADTools, hacemos
$ git checkout -b ultimoMigracion
# 0f517d66 = commit de interés, del 14 de Septiembre con mensaje "Updated StartEditing from gvSIG 1.9"
$ git reset --hard 0f517d66
# y sobre nuestro repo de trabajo:
cp -r $repoOpenCadTools .

A continuación vamos descartando todos los ficheros en los que sepamos que el contenido del master es más válido, los marcados como m en la fase anterior:

git checkout -- los_ficheros_donde_prefiramos_el_master

Nuestros lectores más hábiles en el uso de git se habrán dado cuenta, de que podríamos hacer el análisis de los ficheros tipo m directamente en este paso, pero como ya comentamos, hacerlo del otro modo nos permite afianzar nuestro conocimiento de los repositorios.

Segunda fase de la integración

Ahora tendremos en nuestro repo ficheros nuevos y ficheros modificados.

Los ficheros nuevos son todos ellos nuevas herramientas cuyas clases pueden ser agrupadas. Así que vamos comiteándolos aprovechando para revisar las cabeceras y las autorías. Resulta sencillo identificar si una de estas nuevas clases fue desarrollada inicialmente por el Laboratorio de Bases de Datos y adaptada con posterioridad por Cartolab, o fue creada directamente por Cartolab. Basta comprobar si existe o no en el repositorio de gisEIEL. En general los autores concretos de cada clase o quienes la adaptaron están también idendificados en el código fuente, cuando esto no es así lo que se ha hecho ha sido poner de forma genérica «Laboratorio de Bases de Datos» y/o «Cartolab». La autoría se ha reflejado mediante anotaciones @author en el código fuente de las clases

De este modo vamos haciendo commits para las herramientas y dado que es git empleamos la opción –author para reflejar quien hizo el commit original en el repo de OpenCADTools (al menos de forma aproximada)

Los ficheros modificados en este caso son pocos, por lo que permite tomar decisiones de forma individual.

Tercera fase de la integración

Probablemete la fase más complicada. Nos toca integrar los 143 commits que hubo en OpenCADTools desde el que tomamos como referencia hasta el último commit previo a la integración.

Tras varias pruebas y sin encontrar una forma rápida y segura de integrar esos commits se optó por generar parches e ir integrándolos uno. En algunos casos hubo que retocar los parches o los ficheros a mano puesto que no se integraban correctamente.

Cuarta fase de la integración

Toca limar algunas aspectos, por ejemplo:

  • El nombre de los commiters, para lo que se emplea este script
  • Comprobar que podemos exportar lo que tenemos a un repositorio vacio de git (pasos 7 a 9)

Además se hace testeo funcional de las herramientas de CAD para encontrar posibles problemas en la integración y resolver los bugs que se deriven de ella.

Conclusiones

Dada la disparidad de la evolución de los repositorios el proceso fue tedioso. Pero ahora tenemos un repositorio git actualizado a los últimos cambios de las herramientas por defecto de gvSIG y de las opencadtools. Esto nos permite trazar el origen de algunos bugs, y realizar integración de parches en ambos sentidos, así que merece la pena.

Como usar GIT tras no haber seguido el flujo de trabajo idóneo

GIT es una herramienta genial cuando eres un desarrollador disciplinado y sigues el flujo de trabajo recomendado:


$ git checkout -b nuevaFuncionalidad
... programar
$ git add -u # añadimos automaticamente todos los ficheros indexados que han sido modificados
$ git add fichero # añadimos los no indexados
$ git commit -m "Mensaje del commit"
... más commits
$ git checkout master
$ git pull --rebase
$ git checkout nuevaFuncionalidad
$ git rebase master
... (solucionar posibles conflictos)
$ git checkout master
$ git rebase nuevaFuncionalidad
$ git push

Pero si hay algo que me gusta, y aquí se nota que es una herramienta hecha por desarrolladores para desarrolladores, es lo bien que se comporta cuando no eres tan disciplinado o has metido la pata. Vayamos con un ejemplo (casi) real que se inicia con un viaje en tren Coruña – Vigo.

El estado del repositorio al empezar era este:


# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: config/text_en.properties
# modified: config/text_es.properties
# modified: config/text_gl.properties
# new file: src/es/udc/cartolab/gvsig/tocextra/ReloadVectorialDBLayerTocMenuEntry.java
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# bin/

Se trata de la extensión para gvSIG, extTOCextra desarrollada inicialmente por Javi y liberada por Cartolab como parte del proyecto gvSIG-EIEL. Como vemos en algún momento del pasado se hizo un commit directamente sobre master que no se subió al repositorio y tenemos cuatro ficheros preparados para hacer un commit que todavía no se ha hecho.

En el viaje de vuelta Vigo – Coruña, nuestro desarrollador se pone a acabar lo que había empezado a la ida, hace un git status y se encuentra que el estado del repositorio es este:

# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: config/text_en.properties
# modified: config/text_es.properties
# modified: config/text_gl.properties
# new file: src/es/udc/cartolab/gvsig/tocextra/ReloadVectorialDBLayerTocMenuEntry.java
#
# Changed but not updated:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: .classpath
# modified: config/text_es.properties
# modified: src/es/udc/cartolab/gvsig/tocextra/ShowActivesTocMenuEntry.java
# modified: src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# bin/
# src/es/udc/cartolab/gvsig/tocextra/preferences/

A base de memoria y git diff sabemos que:

  • El commit ya realizado y los archivos preparados para hacer commit constituyen una nueva funcionalidad (funcionalidad 1) que no nos interesa subir al repo por ahora
  • La modificación del .classpath y parte de lo modificado en TocExtraExtension son una pequeña refactorización que tiene sentido en si misma y que nos interesa subir como un commit separado del resto
  • El nuevo paqueta es.udc.cartolab.gvsig.tocextra.preferences, y los cambios ShowActivesTocMenuEntry, text_es.properties, y parte de los de TocExtraExtension son parte de una nueva funcionalidad (funcionalidad 2) que se empezó a programar y que todavía no está terminada. Así que nos interesa tenerla en una rama disinta de master para poder seguir trabajando sobre ella.

A la vista de esto nuestro objetivo será:

  • Tener una nueva rama con la funcionalidad 1 conservando la diferencia entre el commit ya realizado y el que tenemos preparado.
  • Una rama nueva con la refactorización en un sólo commit para luego traérnosla a master (tras haber sincronizado master con el repo) y subirla
  • Una rama nueva con la funcionalidad 2 en tantos commits como queramos para seguir trabajando.

Para conseguirlo tenemos muchos caminos alternativos, provemos uno en el que usemos distintos comandos que nos permitan ver la potencialidad de git.

Acabamos el commit que tenemos preparado

$ git commit -m "i18n for ReloadVectorialDBLayerTocMenuEntry"

Ocultamos temporalmente los cambios que nos quedan para que no nos molesten, creamos una nueva rama para la funcionalidad 1, y limpiamos el master.


$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 2 commits.
#
# Changed but not updated:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: .classpath
# modified: config/text_es.properties
# modified: src/es/udc/cartolab/gvsig/tocextra/ShowActivesTocMenuEntry.java
# modified: src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# bin/
# src/es/udc/cartolab/gvsig/tocextra/preferences/
$ git stash
# On branch master
# Your branch is ahead of 'origin/master' by 2 commits.
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# bin/
# src/es/udc/cartolab/gvsig/tocextra/preferences/

$ git checkout -b reloadDBLayers
$ git checkout master
$ git reset -hard HEAD^^ # Devolvemos la rama master al estado que tenía hace dos commits, es decir, eliminamos los cambios en local

Creamos una nueva rama para la refactorización y deshacemos la ocultación


$ git checkout -b refactor
Switched to a new branch 'refactor'
$ git stash apply
Auto-merging .classpath
CONFLICT (content): Merge conflict in .classpath
Auto-merging config/text_es.properties
CONFLICT (content): Merge conflict in config/text_es.properties
$ git st
# On branch refactor
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: src/es/udc/cartolab/gvsig/tocextra/ShowActivesTocMenuEntry.java
# modified: src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
#
# Unmerged paths:
# (use "git reset HEAD ..." to unstage)
# (use "git add/rm ..." as appropriate to mark resolution)
#
# both modified: config/text_es.properties
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# bin/
# src/es/udc/cartolab/gvsig/tocextra/preferences/

Ups, el stash apply nos ha provocado un conflicto. ¿Por qué?. Tómate 15 sg para pensarlo y luego sigue leyendo…

En el commit que teniamos sin hacer había modificaciones sobre text_es.properties, cuando lo comiteamos dejamos algunas modificaciones sobre ese archivo que estaban basadas en los cambios comiteados. Como la rama «refactor» la creamos a partir de un master limpio, sin esas modificaciones cuando ejecutamos stash apply ese fichero se encuentra en un estado distinto al esperado y se produce un conflicto que no es capaz de resolver automáticamente. Si hubieramos creado la rama refactor a partir de la rama reloadDBLayers el conflicto no se hubiera producido, pero en esa rama hay cambios que no nos interesan por lo que no podemos hacer esto.

Tras la solución del conflicto el estado de repo es este:

# On branch refactor
# Changed but not updated:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: .classpath
# modified: config/text_es.properties
# modified: src/es/udc/cartolab/gvsig/tocextra/ShowActivesTocMenuEntry.java
# modified: src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# bin/
# src/es/udc/cartolab/gvsig/tocextra/preferences/
# no changes added to commit (use "git add" and/or "git commit -a")

Separar los cambios realizados sobre TocExtraExtension.java

Como los cambios de la refactorización para el archivo TocExtraExtension.java están mezclados con los de la funcionalidad 2, usaremos la opción –patch de git add para separarlos separarlos. Esto nos permitirá escoger que cambios (hunks) de un archivo queremos preparar para comitear en lugar de añadir todo el archivo.

$ git add -i src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
$ git status
# On branch refactor
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
#
# Changed but not updated:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: .classpath
# modified: config/text_es.properties
# modified: src/es/udc/cartolab/gvsig/tocextra/ShowActivesTocMenuEntry.java
# modified: src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# bin/
# src/es/udc/cartolab/gvsig/tocextra/preferences/
$ git add .classpath
$ git commit -m "Small refactor"
[refactor 715b4da] Small refactor
1 files changed, 18 insertions(+), 14 deletions(-)

Como se puede ver, hemos hecho commit de sólo una parte de los cambios realizados en TocExtraExtension. Los cambios que quedan son todos los de la funcionalidad 2, así que los comiteamos a una nueva rama

$ git checkout -b newFeature
M config/text_es.properties
M src/es/udc/cartolab/gvsig/tocextra/ShowActivesTocMenuEntry.java
M src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
Switched to a new branch 'newFeature'
$ git add -u
$ git add src/es/udc/cartolab/gvsig/tocextra/preferences/
$ git status
# On branch newFeature
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: config/text_es.properties
# modified: src/es/udc/cartolab/gvsig/tocextra/ShowActivesTocMenuEntry.java
# modified: src/es/udc/cartolab/gvsig/tocextra/TocExtraExtension.java
# new file: src/es/udc/cartolab/gvsig/tocextra/preferences/TOCExtraPreferencesPage.java
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# bin/
$ git commit -m "WIP. new feature"
[newFeature eea0ced] WIP. new feature
4 files changed, 137 insertions(+), 13 deletions(-)
create mode 100644 src/es/udc/cartolab/gvsig/tocextra/preferences/TOCExtraPreferencesPage.java

Subimos al repositorio la refactorización


$ git checkout master
$ git pull --rebase
$ git checkout refactor
$ git rebase master
$ git checkout master
$ git merge refactor
$ git push
$ git branch -D refactor
$ git checkout newFeature # para seguir trabajando

Parece complicado, pero en realidad es más difícil de leer que de escribir una vez coges un poco de costumbre.

Primeros pasos para configurar git

Git es el sistema de control de versiones (SCV) que se está imponiendo en el mundo del software libre. Todo programador que se precie debe aprender a usar un SCV y dado que git puede operar también sobre repositorios svn te recomiendo encarecidamente que lo pruebes. Una de las ventajas de git es que es increíblemente configurable. En este artículo encontraras como realizar esa primera configuración para que sea más cómodo trabajar con git.

La configuración global de git se guarda en el home del usuario en archivo llamado .gitconfig. Además dentro de cada repositorio existe un archivo config dentro del directorio .git donde podemos usar unas configuraciones distintas a las globales o añadir parámetros nuevos. La configuración que deseemos la podemos escribir directamente en esos archivos o usar el comando git config.

Indicamos a git quien somos

git config --global user.name "Francisco Puga"
git config --global user.email "fran.puga@gmail.com"

Esto generará en el fichero .gitconfig dos nuevas líneas como las que siguen:

[user]
email = fran.puga@gmail.com
name = Francisco Puga

Si quisiéramos configurar nuestro correo electrónico para un repositorio concreto ejecutaríamos git config sin la opción de –global dentro del directorio que contiene el repositorio.

Colorear la salida

A continuación añadiremos unas líneas al fichero de configuración global (o mediante ordenes como git config –global color.ui auto) para que nos coloree la salida por pantalla

[color]
ui = auto
diff = auto
status = auto
branch = auto

Con añadir esas líneas empezaríamos a usar los colores por defecto pero estos son personalizables como se puede ver en estos enlaces.

Si tras activar los colores ves algo de basura en la salida añade a ~.bashrc la línea:
LESS=-R

Ignorar ciertos ficheros

Cuando trabajamos en un proyecto puede haber ciertos directorios, ficheros resultantes de la compilación, … que queremos que git no indexe y no nos aparezcan al hacer un estatus. Para ignorar ciertos ficheros tenemos varios métodos:

Si lo que queremos es una configuración global para todos nuestros repositorios añadiremos en el .gitconfig la directiva excludesfile con la ruta completa a un archivo de texto donde definiremos las rutas a ignorar. Por ejemplo en mi caso:

[core]
excludesfile = "/home/fpuga/.gitignore"

Y el contenido de ~.gitignore es

*.[oa]
*.lo
*.la
*.gmo
semantic.cache
*~
*.pyc

Si lo que queremos es definir un patrón de exclusión para un repositorio concreto lo haremos en el fichero .git/info/exclude.

La tercera opción es crear un fichero .gitignore dentro algún directorio de nuestro repositorios. Este fichero debería usarse no para las configuraciones personales si no para las de todo el grupo de trabajo. Es decir .gitignore es un fichero que podría subirse al repositorio de modo que todo el equipo de desarrollo comparta esa configuración. La particularidad de .gitignore es que no tiene que colocarse en la raíz del repositorio si no que puede colocarse en algún subdirectorio, así, si en el mismo repositorio tenemos varios proyectos, cada uno en un directorio podemos aplicar configuraciones distintas a cada proyecto.

Para teclear menos

Los comando de git suelen ser nombres bastante largos y en algunos hay que incluir ademas varios parámetros. Para que tengamos que teclear menos git permite configurar alias. La sección alias de mi .gitconfig es la siguiente:

[alias]
unstage = reset HEAD
st = status
rma = ls-files --deleted | xargs git rm
co = checkout
com = checkout master

5.- Programas por defecto que se usan. Se puede configurar el programa que se empleará para editar el mensaje de commit (por ejemplo emacs en lugar de vi) y el paginador que se usa para ver el log (por ejemplo most en lugar de less)

Si bien en el caso del editor es más lógico configurar la variable de entorno global EDITOR=emacs en nuestro .bash_profile, también podemos configurarlo en exlusiva para git con algo como esto

[core]
editor=emacs
pager=moss

Evitar meter las claves

Si a los repositorios git que tenemos se accede mediante ssh, cosa bastante habitual, tendremos que teclear nuestra contraseña cada vez que bajemos o subamos algo al repo. Para evitarlo podemos copiar nuestra clave pública al repositorio remoto. Haciendo esto, cuando queramos trabajar contra el repo, este automáticamente se encargará que nuestra clave privada se corresponde a la clave pública que hemos subido y nos dará acceso.

El proceso es tan sencillo como, crear una nueva clave si todavía no lo hemos hecho:

ssh-keygen -b 4096 -t rsa

Si usamos una passphrase nos la preguntarán sólo la primera vez de la sesión que la clave sea usada. Se trata de una contraseña para permitir al sistema acceder a nuestra clave privada, no tiene nada que ver con el servidor remoto.

Una vez tengamos nuestra clave ssh, debemos copiarla al servidor, para ello existe un comando especial que hace todo por nosotros

ssh-copy-id @servidor