Git, branching y otros comandos no tan básicos

Estándar

Continuamos un poco más allá de dónde lo *dejamos* el otro día con Git. Esta vez vamos a tratar otros temas intersantes como crear y manejar ramas, tanto en local como en nuestro servidor remoto, ignorar ciertos ficheros y el uso de algunas GUI’s para el control de nuestros repositorios.

Ignorando ficheros

Cuando desarrollamos generamos numerosos ficheros que se crean automáticamente y que (normalmente) no queremos que se almacenen en un servidor de control de versiones, tales como ficheros compilados. Para evitar que Git nos recuerde que tenemos esos ficheros sin añadir al repositorio podemos hacer uso del fichero de configuración .gitignore (que lleva un punto delante porque es un fichero oculto), en el cual añadiremos dichos ficheros y que también acepta comodines para quitarnos una carpeta completa de encima de un plumazo. Cada carpeta puede tener su propio .gitignore para evitar tener que poner rutas enormes en el fichero de la raíz del proyecto. Si queremos evitar la carpeta target de nuestro proyecto maven en el que se generan las clases compiladas (archivos .class) y los paquetes comprimidos tendremos que añadir al .gitignore del proyecto lo siguiente:

target/*

Con esto nos evitamos la carpeta completa y adios problema. Pero, ¿y si ya hemos añadido estos ficheros con anterioridad al repositorio? Para librarnos de estos ficheros no nos bastará con añadir una línea a nuestro .gitignore, sino que además tenemos que ejecutar (como vimos en el post anterior) el comando git rm –cached para eliminarlos del repositorio, aunque no de nuestro disco duro, y dejar que nuestro fichero de ignores se encargue de hacer su trabajo.

Usando GUI’s

No todo en la vida es la consola de comandos y por tanto tenemos disponibles una gran cantidad de GUI’s para controlar y gestionar nuestros repositorios. Git nos brinda una herramienta visual que podemos usar lanzando el comando gitk (en Linux hay que tener instalado el paquete tk). Esta herramienta nos sirve para ver de forma más visual nuestra línea temporal de commits y a dónde apunta cada rama de nuestro repositorio y, aunque no tiene todos los comandos que podemos hacer desde la terminal, puede ser una buena opción si sólo queremos una herramienta puntualmente.

A decir verdad en Linux no he encontrado herramientas visuales que merezcan demasiado la pena, o son muy simples, o son muy rebuscadas. Algunas de las que encontré en su momento fueron:

  • SmartGit: una herramienta escrita en Java gratuita para usos no comerciales. Bastante pesada por otro lado.
  • git-cola: Licenciado como GPL y escrito en Python es una versión un poco mejorada que la que trae Git por defecto.
  • Giggle: La herramienta de gestion de Git de Gnome, muy integrada con el gestor de ventanas si es el que usamos.
  • tig: Si lo que nos va son las interfaces gráficas en texto (ncurses), esta es nuestra herramienta.

Para los que uséis Os X si que existen más variantes de interfaces gráficas, tales como:

  • Tower: Tiene bastante buena interfaz y parace llena de funcionalidades, la única pega es que vale 50€ una licencia (ouch -.-‘)
  • SourceTree: Personalmente es la que uso por el momento, bastante buena interfaz y todo muy condensado.
  • GitX, GitX (brotherbard fork) y GitX (L): Las dos últimas son forks del desarrollo de la primera y al verlas me han entrado ganas de probarlas, y lo haré en breve

Las GUI’s están bien, pero siempre debemos saber qué hay por debajo de ellas para poder solucionar correctamente los problemas que nos puedan surgir, escoged vuestra arma y a luchar.

Branching

Una característica que hace a Git muy útil y efectivo es su manera de crear y manejar ramas. Como vimos en el anterior post sobre Git, este control de versiones no guarda diferencias entre ficheros en los commits, sino que guarda referencias a ficheros completos creando así un mini sistema de ficheros. Cada commit tiene a su vez una referencia a su commit anterior para seguir una línea temporal. Lo que llamamos ramas en Git son referencias a commits específicos y dado que únicamente son eso, referencias, resulta muy rápido el cambio entre ellas y, en general, trabajar con ellas.

Cuando iniciamos un repositorio o clonamos uno en el que no se ha creado ninguna rama, tenemos la rama por defecto llamada master. Podemos usar Git y ni siquiera enterarnos de que existen las ramas ya que el trabajo con la rama master es automático. Cuando creamos una rama nueva lo que hacemos es crear una referencia nueva, que de momento apuntará al mismo commit que apunta la rama master. Git sabe en qué rama nos situamos actualmente debido a que guarda internamente otra referencia llamada HEAD que apunta al commit que apunta la rama en la que estamos actualmente.

Pero lo interesante de las ramas en Git es que podemos trabajar con ellas de manera local y no informar al servidor de que hay ciertos cambios en otra rama de la cual no tiene ni idea. Cuando terminamos de trabajar en una rama lo más común es fusionarla (merge) con la rama master y eliminarla ya que ya no es necesaria. Esa operación mezclará los cambios en ambas ramas, si ambas siguen la misma línea temporal, pero una es antecesora de otra, al fusionarlas Git tendrá en cuenta esto y realizará un fast-forward merge (o merge rápido) que consiste básicamente en avanzar el puntero de la rama más atrasada hacia donde se encuentra la más adelantada. Vamos con los comandos más usados para el manejo de ramas.

En un principio, como hemos dicho, sólo tenemos la rama master en nuestro repositorio, como podemos ver con el comando:

git branch

El estado del repositorio es, por tanto, el siguiente:
Estado inicial del repositorio

Este comando nos marca además la rama en la que estamos con un asterisco, pero como sólo tenemos no nos cambia nada. Digamos que queremos implementar una nueva funcionalidad en nuestra aplicación, pero no queremos que el código estable se tambalee por culpa de nuestro código nuevo. Crearemos una nueva rama, a la cual llamaremos newFeature, y cambiaremos a ella para continuar programando.

#creamos la rama y cambiamos a ella
git branch newFeature && git checkout newFeature;

Yendo por partes, el primer comando (el de creación de la rama) nos sitúa en el siguiente estado
Creada la rama newFeature

y al cambiar a la nueva rama, las referencias quedarían de la siguiente manera
Cambio a la rama newFeature

Como vemos en el comando, el concepto de checkout en Git cambia con respecto a otros VCS’s. En Git checkout cambia nuestra referencia HEAD a la referencia de la rama que le indiquemos.

Si vamos a crear la rama e instantáneamente después de crearla vamos a cambiar a ella podemos utilizar este otro comando que nos acorta las cosas.

git branch -b newFeature;

Después de unos cuantos commits en nuestra nueva rama el estado de nuestro repositorio sería el siguiente
Unos commits en la rama newFeature

Decidimos que hemos terminado de implementar nuestra nueva funcionalidad y que los cambios están preparados para ser incluidos en nuestra rama master. Para esto cambiamos a nuestra rama master y realizamos un merge con la rama de la nueva funcionalidad.

git checkout master;
git merge newFeature;

En este caso, como vemos en la imagen siguiente, el commit al que apunta la rama master es antecesor del commit al que apunta la rama newFeature, lo que nos lleva al caso de un fast-forward merge.
Cambio a la rama master mostrando las versiones antecesoras

Fast merge master into newFeature

Pero, ¿y si tenemos que hacer unos cambios en la rama master mientras que estamos implementando nuestra nueva funcionalidad? Entonces sucederá que nuestras dos ramas divergerán de la línea temporal en la que estaban y seguirán caminos distintos, como vemos en al siguiente imagen. Esto nos complica ligeramente al tener que hacer un merge de las dos ramas. Bueno… más que a nosotros, a Git.
Divergencia entre ramas master y newFeature

Si estamos en esta situacion y queremos que master tenga todos los cambios de newFeature también tendremos que hacer un merge, pero la situación resultante es completamente distinta. Git realizará un three way merge (o merge de tres vías). Este tipo de merge se denomina así porque Git escoge tres snapshots (o commits) en las que fijarse para realizar la mezcla de cambios. El commit común más nuevo, el commit en el que está master y el commit en el que está newFeature.
Antes del three way merge

Con estos tres commits Git mezcla los cambios y realiza un nuevo commit generado automáticamente, llamado merge commit (o commit de merge), al que ahora apunta master. Este commit es especial en el sentido que es el único commit que tiene dos commits inmediatamente antecesores.
Merge commit

Este tipo de commits son la razón por la cual cuando tenemos nuestro propios commits en nuestro ordenador y la copia del servidor ha cambiado con los commits de algún compañero de proyecto no debemos hacer un git pull, porque como recordaréis lo que intenta hacer es bajarse los cambios y mezclarlos con nuestra rama, lo que generará un commit de merge y ensuciará la línea temporal del proyecto, cuando no es neceario. En estos casos funciona mucho mejor el git rebase con el que ponemos nuestro cambios encima de los cambios que se han producido en la otra rama, siempre y cuando no haya conflictos entre los ficheros modificador por ambas ramificaciones.

Una vez mezclados los cambios entre nuestra rama de la nueva funcionalidad con la rama master del proyecto, la rama no nos sirve para nada más y debemos eliminarla ejecutando la siguiente orden

git checkout -d newFeature

Existe otra opción en lugar de -d que es -D, la cual no comprueba si los cambios de la rama a eliminar están mezclados en la rama configurada como upstream (por defecto master). Es decir, nos deshacemos de los cambios que existen en esa rama.

Remote branching

Todo esto está muy bien, pero si la rama va a pasar mucho tiempo en vuestro desarrollo, lo más seguro es tener una copia de la misma en el servidor, lo que nos sirve también para que nuestro compañeros pueda bajarsela y colaborar en sus cambios. Hasta el momento sólo habíamos creado las ramas en nuestro repositorio local, ¿qué tal si creamos una rama en el servidor?

Inicialmente decíamos que teníamos una rama master y que trabajabamos sobre ella. Al igual, en el servidor existe una rama master que se sincroniza con la nuestra cada vez que subimos los cambios al servidor y la nuestra se actualiza con respecto a la del servidor. Esta rama se denomina orig/master, y tiene como precedente la localización de la rama (el servidor se configura de forma predeterminada como nuestro origen, orig para abreviar).

En realidad la palabra clave orig es un remote que se añade a nuestro repositorio local para poder ser usado de manera rápida. Podemos definir tantos remotes como queramos para exprimir al máximo las capacidades distribuidas de Git. Cuando realizamos en nuestra copia local un git fetch origin lo que estamos realizando es actualizar los commits remotos y además las referencias de las ramas remotas que tenemos en origin.

Cuando trabajamos en local con una rama creada por nosotros y queremos que esta rama sea accesible por otros en el servidor, es decir que se la puedan bajar a su propia copia local, debemos subirla al servidor, pero nuestro comando git push no funcionará esta vez de esta manera tan sencilla, tendremos que ampliarlo un poco más. Nuestro Git no sabe a dónde mandar esa rama, tendremos que decirselo.

git push origin newFeature #teniendo en cuenta que la rama se llama como en nuestro anterior ejemplo

Esto es un atajo que nos permite hacer Git y que se expande sustituyendo nuestro origin por sus referencias internas al remote y añade una referencia para la nueva rama en el servidor. No nos vamos a parar mucho en esto ya que no nos interesa demasiado para nuestros propósitos.

Una vez que el servidor tenga esta rama, cuando los demás colaboradores del repositorio actualicen su copia local bajarán la referencia a la nueva rama, pero estas nuevas ramas no crean automáticamente ramas en el repositorio local de cada miembro, porque puede que no todos estén interesados en ellas por lo que requiere un comando extra.

git branch -b newFeature orig/newFeature

Para saber el nombre de la referencia a la nueva rama en el servidor usaremos el comando git branch -a que nos muestra todas las referencias a ramas que tenemos en nuestra copia local del repositorio, tanto remotas como locales.

Cuando ejecutamos el comando anterior para crear una rama local a partir de una remota automáticamente la rama se crea como tracking branch, es decir que cuando hagamos un git push o un git pull Git sabrá exactamente a dónde envíar los cambios o de dónde traerlos. Si no queremos este comportamiento tendremos que añadir la opción –no-track al comando anterior.

Con esto me despido por esta vez, si tenéis alguna duda o pregunta preguntadla en los comentarios e intentaré ser lo más útil posible.

Anuncios

4 comentarios en “Git, branching y otros comandos no tan básicos

  1. Pregunta: Se supone que git es un control de versiones distribuidos, y hasta ahora todo al final queda centralizado, o en local o en el repositorio.

    Imaginate que tenemos una rama Master en el repositorio, y en el servidor de cara el público tiene la rama deploy. ¿Hay alguna forma de hacer un pull de mi ordenador directamente a la rama deploy del servidor sin tener que pasar por el repositorio?

    Gracias 🙂

    • No entiendo muy bien la diferencia que haces entre servidor y repositorio (para mi es lo mismo), pero si entiendo bien lo que quieres hacer, sí; Se pueden configurar nuevas remotes de las cuales coger cambios de ramas que no existen en el repositorio central, el cual no es más que un remote añadido por defecto cuando clonamos el repositorio por primera vez.

      Podríamos añadir un nuevo remote llamado server con el comando git remote add server [url del repositorio en el server]. Una vez añadido ese remote podemos bajar las definiciones de las ramas (y los commits) de ese remote con el comando git fetch server y crear la rama deploy (que también existiría en el remote server) con el comando git branch -b deploy server/deploy. Este comando nos creará la rama de tipo track lo que nos permitirá poder hacer git pull cuando estemos en esa rama y bajarnos los cambios que se haya hecho en ese remote en esa rama concretamente.

      Espero haber aclarado un poco tu duda y en cuanto a las capacidades distribuidas de Git comentarte que puede llegar a ser un auténtico lío tener muchas remotes en tu repositorio local y administrarlas todas, por lo que al final se aprovechan otro tipo de características ventajosas que tiene el control de versiones y sólo en algunas ocasiones sus capacidades distribuidas.

      Un saludo!

  2. >> Si vamos a crear la rama e instantáneamente después de crearla vamos a cambiar a ella
    >> podemos utilizar este otro comando que nos acorta las cosas.
    >> git branch -b newFeature

    bug? debiera decir: git checkout -b newFeature

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s