Git Gui: Usando Git desde su espartana interfaz gráfica

En la fecha en la que nos encontramos, a nadie sorprende ya el hecho de que Git se haya convertido, por méritos propios, en el estándar de facto en lo que al control de versiones se refiere. De hecho su uso se ha extendido hasta llegar a profesionales que no necesariamente lo usan para versionar código fuente.

Aunque gran parte de los usuarios de Git tienden a utilizarlo a través de línea de comandos, que es su entorno nativo, se puede llegar echar de menos una interfaz gráfica y para ello vamos a analizar el uso de la interfaz gráfica que trae por defecto la herramienta, una interfaz espartana y sobria pero efectiva para la mayoría de usuarios.

Introducción a git gui y gitk

Como hemos comentado anteriormente, la interfaz nativa de git es la línea de comandos, pero en la instalación de la herramienta existen dos interfaces gráficas que son en las que nos vamos a centrar en este artículo: git gui y gitk.

Git gui es una interfaz que no destaca por ser intuitiva ni agradable a la vista, pero tampoco lo pretende, simplemente busca aportar las funcionalidades básicas para la mayoría de usuarios.

Git gui, interfaz gráfica por defecto de git

Gitk es un visor del historial de git, permite ver el histórico de commits del repositorio. En la parte superior de la ventana se observa un árbol con las diferentes ramas, los commits con los datos del autor, fecha y hora. En la parte inferior podemos ver a la derecha los ficheros implicados en el commit y en la izquierda los cambios sobre la versión anterior con información adicional sobre las ramas, autor, procedencia etc.

Gitk, el visor de histórico de commits de Git

Inicializando repositorio con Git gui

Para comenzar vamos a crear un repositorio con git gui, para lo cual vamos a iniciar la herramienta. En linux/mac lo haremos mediante el comando git gui. En windows simplemente ejecutaremos el acceso directo a Git gui.

Una vez iniciada la aplicación, pulsaremos sobre la opción Create New Repository.

A continuación vamos a indicarle dónde queremos crear el repositorio.

Una vez inicializado el repositorio, nos aparecerá la pantalla principal de la aplicación, con la cual trabajaremos la mayor parte del tiempo.

Haciendo commits

Para comenzar con nuestro cometido, vamos a crear en el directorio elegido como repositorio un archivo Person.java con el siguiente contenido:

public class Person {
  private String name;

  public String getName() {
    return name;
  }

  public void setName(String newName) {
    this.name = newName;
  }
}

Ahora tenemos un nuevo fichero que versionar en nuestro repositorio, por lo que vamos a añadirlo y a hacer commit. Para ello primero pulsaremos el botón Rescan.

Tras pulsar Rescan, veremos cómo aparece nuestro nuevo fichero en la sección Unstaged Changed, disponible para ser seleccionado para el siguiente commit. El botón Rescan es recomendable que lo pulsemos siempre antes de cualquier operación para sincronizar la interfaz con el estado actual del repositorio.

En la sección Unstaged Changes (1) vemos el fichero cambiado (en este caso es nuevo) disponible para ser incluido en el commit. Para que sea incluido tenemos dos opciones, bien pulsar sobre el icono del fichero (2) o bien pulsar el botón Stage Changed el cual incluirá TODOS los ficheros que están en Unstaged Changes.

Una vez tenemos los ficheros deseados en la sección Staged Changed (1), estos son los que se incluirán en el commit, a continuación bastará escribir el mensaje de commit (2) y pulsar Commit (3).

Vamos a añadir un nuevo cambio al fichero Person.java, quedando el fichero con el siguiente contenido:

public class Person {
  private String name;

  public String getName() {
    return name;
  }

  public void setName(String newName) {
    this.name = newName;
  }
  
  public String toString(){
      return "[ name= "+this.name+" ]";
  }
}

Hemos incluido un método toString() en la clase y ahora vamos a hacer commit de ese cambio. Pulsamos Rescan y añadimos el cambio como hemos hecho anteriormente.

En la imagen anterior vemos los ficheros que han sido cambiados (1), las diferencias que hay respecto al anterior commit (2), incluiremos el texto del commit (3) y pulsaremos el botón Stage Changes para incluir todos los cambios en el commit y luego Commit (4).

Jugando con ramas

Si algo demuestra la potencia de Git es su manejo de las ramas (branches). La creación de ramas nos permite avanzar en paralelo en nuestro desarrollo sin que afecte a otras ramas hasta que lleguemos al estado en que consideremos que podemos aplicar todos los cambios sobre la rama de la que partimos inicialmente.

Es muy habitual crear una rama nueva simplemente para hacer alguna prueba, sin miedo a romper nada, y en caso de tener que descartar estos cambios, simplemente nos posicionaremos (checkout) sobre la rama inicial y borraremos la rama de prueba, volviendo al estado de partida. Veamos un ejemplo.

Abrimos Git gui en el repositorio que veníamos utilizando, y en la pantalla principal podemos ver la rama actual.

Para ver el historial de commits y las ramas implicadas pulsaremos en esta misma ventana sobre Repository -> Visualize All Branch History. Se nos abrirá la herramienta gitk que vimos al principio.

En esta ventana, en la sección superior izquierda podemos ver el histórico de commits, a su derecha el autor de cada uno y la fecha y hora del cambio. En la mitad inferior vemos a la derecha los ficheros involucrados en el cambio y a la izquierda los cambios aplicados sobre el fichero respecto al commit inmediatamente anterior.

Bien, fijémonos de nuevo en la sección superior izquierda, ahí podemos ver en color negro sobre verda la rama master. Es la rama por defecto y sobre la que hemos venido trabajando, sólo existe esta rama en estos momentos. A continuación vamos a crear otra para ver cómo se visualiza.

Vamos a volver a la ventana de Git gui (podemos cerrar esta de momento), y pulsamos en la barra superior sobre la opción Branch -> Create. Aparecerá una ventana como la siguiente.

En esta ventana, simplemente necesitamos establecer el nombre, el resto de opciones las vamos a dejar por defecto, así que en campo Name vamos a poner rama_1, y a continuación pulsaremos sobre el botón Create. Esto hará que se cree una rama nueva partiendo de la rama en la que nos encontremos (master en nuestro caso) y nos posicionaremos en la rama nueva. Muy importante tener en cuenta que cuando se cree una nueva rama o se pretenda cambiar de rama, no podemos tener ningún cambio pendiente (Unstaged Changes).

Ahora volvemos a gitk para ver qué ha pasado al crear la nueva rama. Si ya teníamos abierto gitk, tendremos que refrescar desde Archivo -> Reload o Shift+F5), en caso contrario volvemos a lanzarlo desde Repository -> Visualize All Branch History.

Ahora en la sección superior izquierda vemos dos ramas, master y rama_1. Y vemos que rama_1 aparece en negrita, debido a que es la rama sobre la que estamos posicionados actualmente.

Encontrándonos en la rama rama_1, vamos a hacer cambios en el fichero Person.java de modo que el fichero quede con el siguiente contenido:

public class Person {
  private String name;
  private String secondName;

  public String getName() {
    return name;
  }

  public void setName(String newName) {
    this.name = newName;
  }
  
  public String getSecondName() {
    return this.secondName;
  }

  public void setSecondName(String newSecondName) {
    this.secondName = newSecondName;
  }
  
  public String toString(){
      return "[ name= "+this.name+", secondName= "+this.secondName+" ]";
  }
}

Lo que hemos hecho es añadir el atributo secondName. Vamos a hacer commit en la rama rama_1. Para ello, primero pulsaremos Rescan, aparecerá el fichero modificado en Unstaged Changes, y lo pasaremos a Staged Changes como ya sabemos. Rellenamos un mensaje de commit y pulsamos Commit.

Ahora volvemos a gitk para ver qué ha pasado en nuestro histórico de cambios.

Podemos ver como la rama rama_1 está ahora en un estado posterior a la rama master, ya que incluye unos cambios que no están presentes en master.

Ahora vamos a volver a la rama master y haremos cambios en ella para ver cómo son representados en gitk. Para ello volvamos a la herramienta Git gui.

Para cambiar de rama, pulsaremos sobre Branch -> Checkout y aparecerá la siguiente ventana.

En esta ventana seleccionamos la rama master y pulsamos sobre Checkout para realizar el cambio de rama. Vuelvo a repetir, siempre al cambiar de rama o crear una rama nueva tenemos que tener claro que no haya ningún cambio pendiente en Unstaged Changes.

Una vez realizado el cambio de rama, podemos ver el contenido del fichero Person.java, el cual no tiene el atributo creado anteriormente (secondName) debido a que los cambios se versionaron en la otra rama.

A continuación vamos a cambiar el contenido del fichero con el siguiente código:

public class Person {
  private String name;
  private Integer age;

  public String getName() {
    return name;
  }

  public void setName(String newName) {
    this.name = newName;
  }
  
  public Integer getAge(){
      return this.age;
  }
  
  public void setAge(Integer newAge){
      this.age = newAge;
  }
  
  public String toString(){
      return "[ name= "+this.name+", age="+this.age+" ]";
  }
}

En este caso hemos añadido el atributo age a la clase. A continuación vamos a hacer commit como ya sabemos y nos dirigimos a gitk para ver cómo se representan estos cambios.

Aquí podemos ver como la rama master y la rama rama_1 se han bifurcado. Esto es debido a que parten de un commit común en el que se encontraban en el mismo estado ambas ramas, y cada una de las ramas ha seguido su curso con cambios diferentes e independientes.

Merge y conflictos entre ramas

Para terminar nuestro primer paseo por el entorno gráfico de Git, vamos a tratar de resolver un conflicto con la interfaz gráfica. En primer lugar debemos saber qué es un merge, y qué es un conflicto.

Merge es la operación que se realiza con el objetivo de fusionar dos ramas. Para realizar un merge nos posicionamos en la rama de destino del merge y hacemos merge de la rama de origen hacia la de destino. Esta operación en la mayoría de casos es automática y Git aplica los cambios sin problemas.

Pero, ¿qué ocurre cuando se han realizado cambios sobre el mismo fichero en diferentes ramas? ¿cual de los cambios es más digno de conservar? Está claro que a estas respuestas Git no puede responder, es el usuario quien debe decidir qué es lo que se va a conservar y qué no, y el orden en el cual se fusionarían los cambios en caso de querer conservar ambos cambios en la rama.

Meld como herramienta de merge

Para tratar con esta situación (que no problema) vamos a utilizar la herramienta Meld, herramienta disponible en Linux y Windows (para OS X no hay soporte oficial pero hay builds que se pueden instalar). Una vez instalada la aplicación, vamos a configurar Git para utilizar como herramienta de merge Meld.

Para configurar Git abriremos una consola (git bash en windows) y escribiremos lo siguiente (ejemplo para windows, para linux/mac la ruta que corresponda):

git config --global merge.tool meld
git config --global mergetool.meld.path "C:\Program Files (x86)\Meld\Meld.exe"

Resolviendo conflictos con Git gui y Meld

Bien, vamos a provocar y resolver el conflicto. El escenario es el siguiente, tenemos una clase Person.java donde en la rama master se ha añadido el atributo age con todos los cambios derivados de este. Por otro lado tenemos la rama rama_1 en la cual hemos añadido el atributo secondName.

¿Cual es el resultado que buscamos? Esto hay que tenerlo claro, no podemos resolver un conflicto sin saber qué resultado queremos. En nuestro caso se busca un fichero que incluya los cambios de las dos ramas, conservando los atributos creados en ambas.

En primer lugar nos dirigimos a la ventana de git gui, y nos aseguramos que nos encontramos en la rama master. Una vez hecho esto pulsamos sobre la opción Merge -> Local Merge.

En esta ventana seleccionamos rama_1 que será la rama desde la cual vamos a traernos los cambios hacia master. A continuación pulsamos Merge.

El resultado ya lo veíamos venir, Git no ha podido fusionar automáticamente las dos ramas, así que tendremos que decidir nosotros.

En la ventana de Git gui vemos qué ha pasado, se muestras los conflictos pero no podemos editar nada, aquí entra en juego Meld. Para lanzar nuestra herramienta de merge pulsamos con el botón derecho sobre la sección superior derecha y pulsamos «Run Merge Tool«.

Meld nos muestra tres columnas, la izquierda (LOCAL) es la correspondiente a la rama en la cual nos encontrábamos en nuestro caso master, la derecha es la rama desde la cual estábamos integrando los cambios, y la central que es el resultado de la operación.

(Hay veces que aparece marcada toda la columna como cambio, por lo que nos impide ver realmente las diferencias. Lo que yo hago en estos casos es, dejar en la columna central mis cambios locales (desde <<<<HEAD hasta antes de ====) de modo que aplico los cambios de la columna de la derecha a mis cambios.)

Para añadir cambios desde una columna a otra, pulsaremos las flechas que permiten mover cambios (si pulsamos control y shift tendremos la opciones de cambiar las flechas por añadir arriba/abajo y borrar el trozo de código).

En primer lugar vamos a aplicar el cambio de la columna de la izquierda hacia la central pulsando la flecha apropiada. Luego pulsamos control + clic en la flecha que va desde el atributo secondName de la columna de la derecha hacia la columna central, de modo que copiemos los cambios arriba del atributo age.

Con los métodos get y set actuaremos de la misma forma. Con el último método tenemos un problema, y es que al haberse modificado parcialmente por cada una de las ramas, no podemos decidir que bloque es el elegido para pasar a la versión final del fichero. Tampoco es posible incluir los dos bloques de código a la vez, ya que son dos returns. La solución sería editarlo a mano, para lo cual editaremos en el bloque central hasta obtener el resultado deseado.

Por último guardaremos los cambios con Control + S, cerramos Meld y ya tenemos nuestro conflicto resuelto. Meld creará un backup del fichero original con extensión .orig, el cual podemos borrar una vez comprobado que todo está bien.

Ahora añadiremos el cambio a Staged Changes haciendo clic en el icono del fichero y hacemos commit (podemos editar el mensaje de commit si lo deseamos).

En gitk podemos ver cómo las ramas master y rama_1 se encuentran ahora unidas, nuestro merge realizado correctamente y todo a través de la interfaz gráfica de Git y con la ayuda de Meld para resolver conflictos.

Conclusiones

Hemos visto que para la mayor parte de operaciones es más que suficiente el uso de esta interfaz gráfica. No es muy intuitiva pero cumple con las necesidades que podamos tener, y añadiendo Meld nos aporta una herramienta de merge bastante solvente.

Esta es la primera aproximación al uso de git gui, pero no será la última, espero que os haya servido de ayuda y volveré a escribir con algún tip interesante sobre el uso de una herramienta que puede dar más de sí.

Espero que os haya gustado 😉