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

Posted: mayo 15th, 2011 | Author: | Filed under: General | Tags: , , , | 1 Comment »

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

Posted: mayo 1st, 2010 | Author: | Filed under: General | Tags: , , , , | 2 Comments »

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