lunes, 7 de abril de 2014

Curso de git - Parte 1

Como están?

Llevo un año utilizando git de forma "seria" y de vez en cuando me sorprendo teniendo que explicarle a personas qué es o cuales son las diferencias que hay entre git y subversion (o algún otro controlador de versiones). Para tratar de dejar una guía sencilla para los interesados, he decidido crear una secuencia de capítulos en mi blog donde voy a explicar lo que es git y como utilizarlo.


Sin más que agregar, aquí arranca mi "curso".

Qué es git?

Git es, entre otras cosas y en pocas palabras, un controlador de versiones distribuido.

Qué es un controlador de versiones distribuido?

Esta tenía que ser la siguiente pregunta, cierto?

Un controlador de versiones distribuido es aquel en donde no existe un solo repositorio centralizado (desde el punto de vista de diseño de la aplicación, quiero decir) sino que se pueden tener tantos repositorios como se considere necesarios (en equipos locales o remotos).

Para qué se quiere tener más de un repositorio?

Esto puede parecer trivial pero en realidad es una de las grandes ventajas del uso de DVCS (Distributed Version Control Systems). Imaginemos las siguientes situaciones:

- Queremos trabajar en un "feature" totalmente experimental y queremos tener control de versiones.... pero no queremos hacerlo público.... o por lo menos, no hacerlo público hasta que sea estable o por lo menos presentable.

- No tenemos acceso al servidor centralizado (se cayó la red, estoy en mitad de un vuelo al pacífico).

- Queremos movernos hacia otra revisión del proyecto (esto puede tomar algo de tiempo utilizando un CVCS).... en realidad este punto no solo tiene que ver con movernos sino con el hecho de hacer casi cualquier operación que requiera control de versiones como comparar revisiones, o ver anotaciones de algún archivo, todas ellas van a requerir acceso (on-line) al servidor y todas ellas entonces van a depender entonces de la latencia de los recursos involucrados (velocidad de la red, velocidad del servidor por discos/cpu/carga, etc).

- Lo casi peor que puede pasar: Se jodió el servidor.

- Lo peor que puede pasar: Lo anterior pero agréguenle que no hay backups.

Junten todas esas razones y me parecen más que suficientes para querer manejar versiones de forma distribuida. Continuemos.

Qué hay en un repositorio git?

En un repositorio git hay básicamente una cosa:
- Commits (acometidas, changesets, revisiones, como lo quieran llamar) que indican el estado de los directorios/archivos en el mismo. Son "revisiones" de un proyecto en un momento dado y que se apuntan unos a otros (los hijos apuntan a sus padres, básicamente). Eso es _casi_ todo lo que hay.

En serio......Pero EN SERIO? Y las ramas? Aquí es donde la versatilidad de git se comienza a poner de manifiesto. Las ramas también están.... pero, y esta es una de las características más poderosas de git, no son más que "apuntadores" a revisiones. Apuntadores que pueden ser creados, movidos y eliminados (y recreados si fuera necesario) a discreción del dueño de un repositorio.

Y las etiquetas? Son exactamente igual que las ramas, apuntadores a revisiones que pueden ser creados, movidos, eliminados (y también recreados si fuera necesario) a discreción del dueño de un repositorio.

Entonces cual es la diferencia entre rama y etiqueta? Que al hacer una nueva revisión sobre una rama, el apuntador se mueve a la nueva revisión, pero al crear una nueva revisión sobre una etiqueta, la etiqueta no se mueve a la nueva revisión.



Pongamos en práctica estos pocos conocimientos que tenemos hasta el momento.

Primero que todo, debemos indicarle a git quienes somos. Esto se hace con dos sencillas instrucciones (ajústenlas a sus propias identidades):

$ git config --global user.name "Edmundo Carmona"
$ git config --global user.email eantoranz@gmail.com

Lo interesante de usar --global en este caso es para que esto quede configurado  para la "globalidad" de git _pero_ si al trabajar en un proyecto en el futuro necesitan utilizar una identidad particular, pueden indicar en el repositorio de dicho proyecto una identidad diferente ejecutando estos mismos comandos sin el --global para que dicha identidad se mantenga solo en dicho repositirio manteniendo la identidad global en el resto de los repositorios.

Hecho esto, inicialicemos un "repositorio". Las instrucciones que voy a dictar en este punto son dadas sobre un equipo con gnu/linux sobre un terminal pero se pueden llevar a otros ambientes (por ejemplo gráficos o con un IDE) y SOs.


Digamos que creo un directorio para tener el proyecto curso-git (que se llama de la misma forma). Nada más sencillo:

$ git init curso-gitInitialized empty Git repository in /home/antoranz/curso-git/.git/


Habiendo hecho eso, git creó el directorio "vacío" en mi equipo que se llama curso-git (sobre el home que es donde estaba parado al momento de correr git init).

Qué hay dentro de este repositorio? No mucho, la verdad:

$ cd curso-git/
$ ls -l
total 0
$ git status# On branch master
#
# Initial commit
#
nothing to commit (create/copy files and use "git add" to track)


En el mensaje de creación del repositorio (si se fijaron bien) git creó un directorio "escondido" que es donde el guarda toda su magia:

$ ls -latotal 12
drwxr-xr-x  3 antoranz antoranz 4096 Apr  7 20:15 .
drwxr-xr-x 62 antoranz antoranz 4096 Apr  7 20:15 ..
drwxr-xr-x  7 antoranz antoranz 4096 Apr  7 20:16 .git


En el directorio .git es donde git guarda toda la información del repositorio y es el único directorio que "embasurará" el proyecto (hay otros detalles como el archivo .gitignore que trataremos en su momento).

Ahora procedamos a crear algunas revisiones de este proyecto. Creen un par de archivos de texto dentro del protecto o traiganlos de alguna otra parte.

$ ls -ltotal 8
-rw-r--r-- 1 antoranz antoranz 47 Apr  7 20:20 archivo1.txt
-rw-r--r-- 1 antoranz antoranz 48 Apr  7 20:20 archivo2.txt



Veamos qué nos dice git acerca de estos dos archivos:

$ git status# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add ..." to include in what will be committed)
#
#       archivo1.txt
#       archivo2.txt
nothing added to commit but untracked files present (use "git add" to track)



Lo que git nos está diciendo es que de esos archivos git no tiene _ni idea_ de donde salieron y que hasta el momento no le hemos indicado a git qué hacer con ellos.

Procedamos a "agregarlos" y los acometemos tal cual están en este momento.

$ git add archivo*.txt
$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached ..." to unstage)
#
#       new file:   archivo1.txt
#       new file:   archivo2.txt
#
$ git commit -m "Revision inicial del proyecto del curso de git"[master (root-commit) a8d7d41] Revision inicial del proyecto del curso de git
 2 files changed, 3 insertions(+)
 create mode 100644 archivo1.txt
 create mode 100644 archivo2.txt


En este punto hay varias cosas muy interesantes que no son tan obvias a primera vista.

Primero: Se crea una "revisión" del proyecto (esta sí es obvia, no?). Las revisiones en git no son incrementales (esta no era obvia, ven?), las revisiones tienen un id sha-1 que las identifica de forma unívoca. No hay dos revisiones que tengan el mismo identificador sha-1. Cual es el identificador sha-1 de esta revisión? Veamos:

$ git show --summary HEADcommit a8d7d412b058916ed75a12fa88285739b655b275
Author: Edmundo Carmona Antoranz
Date:   Mon Apr 7 20:27:39 2014 -0500

    Revision inicial del proyecto del curso de git

 create mode 100644 archivo1.txt
 create mode 100644 archivo2.txt


El identificador está en la primera línea de salida del comando (a8d7d412b058916ed75a12fa88285739b655b275).

Segundo: La rama "por defecto" en la que se trabaja en un repositorio con git se llama "master" y acaba de ser "creada" y está apuntando a la revisión a8d7d412b058916ed75a12fa88285739b655b275 (que es la primera revisión de nuestro proyecto).


Ahora borremos un archivo, modifiquemos el otro archivo y creemos otro archivo.

$ git status# On branch master
# Changes not staged for commit:
#   (use "git add/rm ..." to update what will be committed)
#   (use "git checkout -- ..." to discard changes in working directory)
#
#       deleted:    archivo1.txt
#       modified:   archivo2.txt
#
# Untracked files:
#   (use "git add ..." to include in what will be committed)
#
#       archivo3.txt
no changes added to commit (use "git add" and/or "git commit -a")



Muy interesante. Git se da cuenta del archivo que modificamos, del que borramos y del nuevo archivo del que git no tiene ni idea de donde salió.

Ahora procedamos a incluir _todos_ los cambios en la siguiente acometida del proyecto. Para ello debemos "agregar" el archivo nuevo y el modificado e indicarle a git que el otro archivo fue borrado (si no le indicamos a git que guarde todos los cambios, el procedería a acometer solo los cambios que le indiquemos expresamente y dejaría otros cambios "flotando" en el proyecto para acometidas posteriores).

$ git add archivo2.txt archivo3.txt
$ git rm archivo1.txt
rm 'archivo1.txt'
$ git status# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#       deleted:    archivo1.txt
#       modified:   archivo2.txt
#       new file:   archivo3.txt
#
$ git commit -m "Segunda revision del proyecto del curso"[master 37662b9] Segunda revision del proyecto del curso
 3 files changed, 3 insertions(+), 2 deletions(-)
 delete mode 100644 archivo1.txt
 create mode 100644 archivo3.txt


Ahora se creó la segunda revisión del proyecto. Veamos la información del sumario de la revisión:

$ git show --summary HEADcommit 37662b930de0742c38a2cd63db4368ed80e2c1a9
Author: Edmundo Carmona Antoranz
Date:   Mon Apr 7 20:37:50 2014 -0500

    Segunda revision del proyecto del curso

 delete mode 100644 archivo1.txt
 create mode 100644 archivo3.txt



De nuevo podemos ver el sha-1 de la revisión (como les dije, no son revisiones incrementales) y vemos los archivos nuevos/borrados. Los archivos modificados no salen en el sumario aunque efectivamente forman parte del cambio:

$ git show --stat   HEADcommit 37662b930de0742c38a2cd63db4368ed80e2c1a9
Author: Edmundo Carmona Antoranz
Date:   Mon Apr 7 20:37:50 2014 -0500

    Segunda revision del proyecto del curso

 archivo1.txt | 2 --
 archivo2.txt | 2 ++
 archivo3.txt | 1 +
 3 files changed, 3 insertions(+), 2 deletions(-)



La rama en este punto se ve asi:



Tenemos la rama master que tiene dos revisiones lineales en su historia. Ahora juguemos un poco con el concepto de los apuntadores de los que hablaba anteriormente.

Digamos que vamos a crear una nueva rama a partir de la primera revisión del proyecto. Como lo podemos hacer? Muy sencillo: Le indicamos a git que coloque una nueva rama sobre la primera revisión del proyecto y eso es todo. Recordemos que para git las ramas son solo apuntadores así que para él se trata solo de crear un nuevo apuntador a la revisión deseada. Digamos que la rama se va a llamar "rama1" y para indicarle la revisión pueden indicar el sha-1 de dicha revisión (en realidad se puede indicar menos que el sha-1 completo pero eso lo dejaré para otra ocasión):

$ git branch rama1 a8d7d412b058916ed75a12fa88285739b655b275

Podemos ver que ahora tenemos una nueva rama que se llama "rama1" y que está justo donde lo esperábamos:



Ahora saquemos esta rama del repositorio a nuestro árbol de trabajo y veamos qué sucede con la historia de nuestra rama:


$ git checkout rama1Switched to branch 'rama1'


Como podemos ver, es como si la segunda revisión que hicimos hubiera desaparecido.... efectivamente desapareció, pero solo de nuestro radar. La rama "master" sigue existiendo y sigue apuntando a la misma revisión donde la dejaron en caso de que la necesiten.


Veamos el contenido del proyecto:

$ ls -ltotal 8
-rw-r--r-- 1 antoranz antoranz 47 Apr  7 20:51 archivo1.txt
-rw-r--r-- 1 antoranz antoranz 48 Apr  7 20:51 archivo2.txt
$ git status# On branch rama1
nothing to commit, working directory clean




Justo lo que esperábamos y miren cual es la rama que git dice que estamos trabajando.

Ahora modifiquemos alguno de los archivos y acometamos:

$ git status# On branch rama1
# Changes not staged for commit:
#   (use "git add ..." to update what will be committed)
#   (use "git checkout -- ..." to discard changes in working directory)
#
#       modified:   archivo2.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
$ git add archivo2.txt
$ git commit -m "Una modificacion que va a quedar en la rama 1"
[rama1 d1580e8] Una modificacion que va a quedar en la rama 1
 1 file changed, 2 insertions(+)


Ahora veamos la historia de ambas ramas juntas:



Podemos ver ambas ramas y como se bifurcaron desde la primera revisión del proyecto:

Ahora regresemos a la rama "master" y, para terminar de demostrar el concepto de las ramas como apuntadores, juguemos a "borrar" la rama rama1:

$ git checkout masterSwitched to branch 'master'
$ git branch -D rama1Deleted branch rama1 (was d1580e8).

$ git checkout rama1error: pathspec 'rama1' did not match any file(s) known to git.

Oops! Borré la rama!!!! No tengan miedo, como había explicado antes, las ramas no son más que apuntadores y mientras git tenga conocimiento de la revisión dentro del repositorio (que es al fin y al cabo casi que lo único que hay en un repositirio git), una rama siempre es recuperable siempre y cuando se tenga el ID de la revisión en cuestión (en este caso, git cuando borró la rama fue tan amable de indicarme la parte "significativa" del identificador sha-1 necesaria para indicar la revisión a la que estaba apuntando la rama previamente, lo ven?):

$ git branch rama1 d1580e8
$ git checkout rama1
Switched to branch 'rama1'
$ git show --summary HEADcommit d1580e8b7f28ff9a33e6e3569b33e67a9b56415f
Author: Edmundo Carmona Antoranz
Date:   Mon Apr 7 20:55:33 2014 -0500

    Una modificacion que va a quedar en la rama 1




Y con esto doy por terminada la primera lección esperando que haya sido suficientemente clara.

Hasta la próxima.