miércoles, 24 de julio de 2013

Git: fundamentos para trabajo en grupo


En algún post anterior hemos hablado de qué es GIT, cómo instalarlo, cómo integrarlo en eclipse,... pero siempre, para trabajar de forma autónoma, es decir, un equipo de desarrollo de un único programador.

En este artículo voy a comentar los comandos básicos cuando trabajamos en un equipo multi-programador y pondré sobre la mesa situaciones típicas como:
  • descargar el proyecto completo del repositorio central
  • actualizar mi copia local con los últimos cambios del repositorio central
  • subir mis cambios locales al repositorio central
  • resolver conflictos de mi copia local frente al repositorio central

Veamos cómo hacerlo

1. Puesta en situación: Rita y Braulio

Para que me resulte más fácil de explicar, voy a utilizar a un equipo de desarrollo formado por dos personajes ficticios: Rita y Braulio.

Estos programadores trabajarán sobre un mismo proyecto que estará en el respositorio testrep alojado en el servidor con dirección rita_braulio.dinaserver.com.

Para simularlo en mi equipo, he creado una carpeta gitRita y otra gitBraulio en la que estará el código de Rita y el código de Braulio respectivamente.

Al inicio, ambas carpetas están vacías:



2. Descargar el proyecto completo del repositorio central [clone]

El primer paso será descargar el proyecto.

Esta operación se realiza mediante el comando clone, que ásicamente consiste en tomar todo el código del repositorio que está alojado en nuestro servidor y descargarlo a nuestro propio equipo, obteniendo así una copia local del proyecto.

Desde línea de comandos, tanto Rita como Braulio harían:
git clone git@dws22.dinaserver.com:testrep


Como vemos en la imagen anteior, al hacer el clone, el servidor nos pedirá la clave del usuario git, y a continuación, nos descarga el código del proyecto.

Como vemos, cuando termina tenemos una nueva carpeta que es testrep (el nombre del repositorio).

Entramos en la carpeta testrep y aquí está tooooodo el proyecto. En este ejemplo sencillo, nuestro proyecto es un simple fichero hola.txt:



3. Subir mis cambios al repositorio central [commit + push]

Como es lógico, después de descargar el proyecto, Rita se pone a trabajar. Ha modificado el fichero hola.txt y además ha creado un nuevo archivo, prueba.txt:


Para subir esos cambios al repositorio central, y que Braulio pueda descargarlos, ejecutará un commit y luego un push:


4. Origin sirve como alias de tu servidor

Una vez que has descargado una copia del proyecto en tu equipo local, es decir, después de haber hecho clone, justo en ese momento, te puedes ahorrar escribir el nombre larguísimo de git@mi_servidor.com:testrep.

En lugar de escribir todo eso, basta con poner origin.

A partir de este momento, no volverá a poner el nombre completo del servidor + repositorio, sino que usaré siempre origin. Ya verás que funciona perfectamente.


5. Actualizar mi copia local con los cambios que se hayan subido al repositorio central [pull]

Los cambios que ha introducido Rita no están en el código de Braulio. Si Braulio quiere descargar las novedades, los cambios que ha introducido Rita, deberá hacer un pull:


Fíjate que para el pull he usado origin, el alias del servidor remoto (este alias lo crea automáticamente git), en lugar del nombre completo.

Como vemos, el fichero hola.txt ahora tiene la línea goodbye que añadió Rita.

Pero ha habido un fallo, no llegó el fichero prueba.txt. Esto es así porque Rita no añadió ese fichero al repositorio (comando add). Vamos a hacerlo y volvemos a hacer un pull con Braulio.

Rita añade el fichero prueba.txt al repositorio:



Braulio vuelve a hacer un pull y ahora sí que tiene el fichero prueba.txt:


6. Cuando dos programadores modifican el mismo fichero y la misma línea, ¿y ahora?

Los conflictos se producen cuando dos desarrolladores editan la misma línea del mismo fichero.
En esa situación, GIT no es capaz de hacer el auto-merge, porque,

  • ¿con qué línea se queda? 
  • ¿Elimina la de Braulio y toma como buena la de Rita?
  • ¿Elimina la línea de Rita y se queda con la de Braulio? 
  • ¿se queda con las dos y coloca primero la de Rita y luego la de Braulio?
  • ...
En estos casos, mejor que lo resuelva un programador que es el que sabe cómo debe quedar su código.

Vamos a provocar a la fuerza esta situación, que es más común de lo que puedas pensar, sobre todo en proyectos grandes (p.e. en los ficheros .css de una solución web).

Pues bien, Rita abre el fichero hola.txt y añade una línea al final, buenas noches mundo!!! y Braulio también edita el fichero hola.txt y añade una línea al final, Good night world!!!

Veamos cómo quedan los ficheros de cada uno al terminar de editar, justo antes de subir sus cambios al servidor:


Como vemos, la última línea de cada uno es diferente.





6.1. Un programador sube sus cambios al servidor después de que ambos hayan modificado el mismo fichero y la misma línea: sin problemas (commit + push)

Imaginemos que Rita se adelanta, y sube sus cambios. Ella no va a tener ningún problema:




6.2. El otro programador intenta subir sus cambios sin antes actualizar (commit + push + error + pull + push)

Ahora, cuando Braulio intenta subir, se encuentra con un problema, y es que han habido cambios en el servidor (los cambios que acaba de subir Rita).


Siempre, antes de hacer un push es bueno hacer un pull, de modo que, si han habido cambios desde nuestro último pull, se reflejen en nuestra copia local.


6.3. El otro programador, al descargar los cambios que han habido en el servidor, se encuentra con el conflicto (pull + CONFLICT)

Va a ser Braulio el que se encuentre con el problema. La última línea del fichero hola.txt fue modificada por él y por Rita, y eso es lo que le está diciendo git:



6.4. Realizamos la mezcla/unión manualmente (editar fichero/s en conflicto)

Y ésta es la potencia del Git, y su gran utilidad como control de versiones en proyectos informáticos. El código fuente de un programa es texto, ASCII.

Así que simplemente abrimos el fichero que tiene el conflicto, hola.txt, y buscamos las apariciones del texto <<<<<<<<  y >>>>>>>>> que lo introduce git para encuadrar el error:


En este caso, lo que decide Braulio es que va a dejar las dos líneas, tanto la suya como la de Rita, y que va a poner antes su línea y luego la de Rita. De modo, que simplemente eliimina las marcas <<<<<< que introduce git y guarda:




6.5. Marcamos el conflicto como resuelto (add)

En este momento, si Braulio intentara hacer un commit, git le daría un mensaje de error, indicando que hay conflictos no resueltos.

Falta aún decirle a git que sí, que ya resolvimos el conflicto que había con el fichero hola.txt.
Para, simplemente añadimos el fichero modificado al repositorio:

git add "hola.txt"

6.6. Con el conflicto resuelto, subimos cambios al servidor central (push)

Ahora que Braulio ha resuelto el conflicto, puede subir los cambios al repositorio central simplemente con:
git push origin



7. ¿Qué ficheros he modificado? [status]

El comando status de git nos dice qué ficheros hemos modificado, cuáles hemos borrado, cuáles son nuevos, qué ficheros tienen conflicto,...

Veamos algunos ejemplos:



7.1. Inicialmente no figura ningún cambio

Inicialmente, si preguntamos por el estado de nuestro repositorio local, git nos indica que no hay nada que commitear, que no hay ningún cambio:





7.2. Modificamos un fichero y status nos dice que está modificado

Braulio edita el fichero hola.txt, añade una nueva línea, y luego consulta el status:



Efectivamente git nos muestra que hola.txt ha sido modificado.


7.3. Al subir las modificaciones status dice que no hay cambios

Después de modificar hola.txt, Braulio hace commit, es decir, guarda sus cambios en el repositorio local. 
Siempre que hacemos un commit, el contador de cambios se resetea, indicando que no hay cambios pendientes por subir:




7.4. Al agregar un nuevo fichero, o al eliminar uno ya existente, status también lo refleja

Ahora Braulio crea el archivo prueba2.txt y elimina prueba.txt.
Justo cuando elimina prueba.txt, pregunta por el stado del repositorio, y efectivamente git le indica que el fichero prueba.txt ha sido eliminado:



A continuación, todavía sin haber hecho commit, crea el fichero prueba2.txt. Al preguntar por el estado a git, le sigue mostrando que prueba.txt ha sido eliminado y además le indica que se ha creado un nuevo fichero, prueba2.txt:




8. Descartar los cambios que he hecho en local, volver a la versión que descargué del servidor [checkout]

Hay ocaciones, que te pones a picar teclas, y cuando te vienes a dar cuenta, has montado un desaguisado en el código terrible, y eres incapaz de volver al estado anterior, donde al menos compilaba.

Un remedio habitual es descartar todos los últimos cambios que has hecho en un determinado fichero.

Volvamos a nuestro ejemplo. Así está la situación en este momento: status indica que no hay cambios y el fichero hola.txt tiene una serie de líneas como hola mundo, ciao mundo, hello world, goodbye world,...



Ahora Braulio hace un ls y la salida la vuelca sobre hola.txt y efectivamente git status indica que el fichero hola.txt ha cambiado:



Realmente este cambio sin git no lo podríamos descartar fácilmente. Aquí no tenemos ctrl+z, ya que un volcado a disco desde la consola, no se puede revertir. Así que regenerar un fichero a mano puede ser un auténtico infierno, sobre todo si no es un fichero de 7 líneas como es hola.txt, sino un fichero .php o .cpp de 1000, 2000, 3000,... líneas.

Sin embargo, con nuestro código bajo control de versiones, basta con hacer git checkout hola.txt, y devolvemos el fichero a su estado anterior:




9. Creando etiquetas en nuestro árbol de versiones

Como cualquier otro gestor de versiones, Git permite la etiquetar versiones. A medida que vas haciendo commit en tu repositorio, se va creando un historial de versiones, donde cada commit nos lleva a un nuevo número de versión. Para terminar de complicarlo, git no lleva un contador lineal del número de versión, sino que para Git una versión es un Guid (un chorizo de número y letras).

A cualquier equipo de desarrollo le interesa poder responder a preguntas como las siguientes:
  • Déjame la versión de código que está ahora mismo en explotación
  • ¿Qué versión de código exactamente se llevó a tal/cual presentación?
  • ¿Qué versión de código es la que tenemos en el market de Android?
  • ...
Es por eso que, etiquetar una versión no es más que eso, ponerle un nombre a uno de los cientos de commit que irás haciendo.

Veamos entonces cómo crear etiquetas.



9.1. Crear una etiqueta [tag]

Volviendo a nuestro ejemplo, supongamos que el código de Rita y Braulio, tal y como está ahora está listo para publicar, y quieren marcarlo como que es la versión 1.0.

Braulio crea la etiqueta v1.0 para la versión de código actual:



Fíjate que cuando ejecuta git tag se le muestran todas las etiquetas que existen, en este caso, solo una etiqueta, la de la versión que acaba de crear.


9.2. Subir las etiquetas al servidor central [push --tags]

Aunque Braulio haya creado una etiqueta, esa etiqueta NO es visible por el resto del equipo de desarrollo. Fíjate lo que se le muestra a Rita cuando le pregunta a git por las etiquetas:



Entonces, Braulio indica a git que suba las etiquetas al servidor central, git push origin --tags:



En realidad, en este momento, con el próximo pull que hiciera Rita se descargaría el código nuevo y también las etiquetas, salvo que NO haya código nuevo, sino solo etiquetas nuevas. En ese caso, git no descarga nada, salvo que le forcemos a descargar con --tags:





10. Deshacer todos los cambios que he hecho [reset]

Hay veces que te pones a trabajar, modificas un fichero, modificas otro, y otro, y otro,... y no consigues hacer que una nueva funcionalidad camine, y sigues modificando código, aquí y allá,... y nada, el nuevo requisito que no va ni para atrás.

Pero hay algo que es peor, y es que ahora hay funcionalidades que antes sí que iban bien y ahora también dan "pepino". 

No son raras las veces que he preferido dejar las cosas como estaban antes de empezar a reparar el código que he roto, más aún cuando las nuevas funcionalidades que intentaba implementar tampoco funcionan.

Haciendo checkout uno a uno sobre los ficheros que he modificado podría ser un plan B para resolver esta situación, pero puede resultar tedioso, si son muchos los ficheros modificados, en diferentes carpetas,... Mucho más rápido, más sencillo, y con una única instrucción, es haciendo un hard reset sobre mis cambios.

Para ello, nos colocamos en la carpeta raíz del repositorio local, y escribimos:
git reset --hard

Y automáticamente nuestro repositorio local vuelve a la situación anterior a los cambios: nuestro último commit.
Es como hacer un checkout recursivo, que se mete por todas las carpetas y subcarpetas de nuestro repositorio deshaciendo los cambios en todos los ficheros.

Volviendo a nuestro ejemplo sencillo, veamos cómo Braulio modifica tanto el fichero hola.txt como prueba.txt, creando nuevas líneas, borrando otras,...:



Y a continuación, con un simple hard reset, devuelve todo al estado anterior: