Git
Chapters ▾ 2nd Edition

8.2 Personalización de Git - Git Attributes

Git Attributes

Algunos de los ajustes que hemos visto, pueden ser especificados para un camino (path) concreto, de tal forma que Git los aplicará únicamente para una carpeta o para un grupo de archivos determinado. Estos ajustes específicos relacionados con un camino, se denominan atributos en Git. Y se pueden fijar, bien mediante un archivo .gitattribute en uno de los directorios de tu proyecto (normalmente en la raíz del proyecto), o bien mediante el archivo git/info/attributes en el caso de no querer guardar el archivo de atributos dentro de tu proyecto.

Por medio de los atributos, puedes hacer cosas tales como indicar diferentes estrategias de fusión para archivos o carpetas concretas de tu proyecto, decirle a Git cómo comparar archivos no textuales, o indicar a Git que filtre ciertos contenidos antes de guardarlos o de extraerlos del repositorio Git. En esta sección, aprenderás acerca de algunos atributos que puedes asignar a ciertos caminos (paths) dentro de tu proyecto Git, viendo algunos ejemplos de cómo utilizar sus funcionalidades de manera práctica.

Archivos binarios

Un buen truco donde utilizar los atributos Git es para indicarle cuáles de los archivos son binarios (en los casos en que Git no podría llegar a determinarlo por sí mismo), dándole a Git instrucciones especiales sobre cómo tratar estos archivos. Por ejemplo, algunos archivos de texto se generan automáticamente y no tiene sentido compararlos; mientras que algunos archivos binarios sí que pueden ser comparados. Vamos a ver cómo indicar a Git cuál es cuál.

Identificación de archivos binarios

Algunos archivos aparentan ser textuales, pero a efectos prácticos merece más la pena tratarlos como binarios. Por ejemplo, los proyectos Xcode en un Mac contienen un archivo terminado en .pbxproj. Este archivo es básicamente una base de datos JSON (datos Javascript en formato de texto plano), escrita directamente por el IDE para almacenar aspectos tales como tus ajustes de compilación. Aunque técnicamente es un archivo de texto (ya que su contenido lo forman caracteres UTF-8). Realmente nunca lo tratarás como tal, porque en realidad es una base de datos ligera (y no puedes fusionar sus contenidos si dos personas lo cambian, porque las comparaciones no son de utilidad). Éstos son archivos destinados a ser tratados de forma automatizada. Y es preferible tratarlos como si fueran archivos binarios.

Para indicar a Git que trate todos los archivos pbxproj como binarios, puedes añadir esta línea a tu archivo .gitattributes:

*.pbxproj binary

A partir de ahora, Git no intentará convertir ni corregir problemas CRLF en los finales de línea; ni intentará hacer comparaciones ni mostar diferencias de este archivo cuando lances comandos git show o git diff en tu proyecto.

Comparación entre archivos binarios

Puedes utilizar los atributos Git para comparar archivos binarios. Se consigue diciéndole a Git la forma de convertir los datos binarios en texto, consiguiendo así que puedan ser comparados con la herramienta habitual de comparación textual.

En primer lugar, utilizarás esta técnica para resolver uno de los problemas más engorrosos conocidos por la humanidad: el control de versiones en documentos Word. Todo el mundo conoce el hecho de que Word es el editor más horroroso de cuantos hay; pero, desgraciadamente, todo el mundo lo usa. Si deseas controlar versiones en documentos Word, puedes añadirlos a un repositorio Git e ir realizando confirmaciones de cambio (commit) cada vez. Pero, ¿qué ganas con ello?. Si lanzas un comando git diff, lo único que verás será algo tal como:

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 88839c4..4afcb7c 100644
Binary files a/chapter1.docx and b/chapter1.docx differ

No puedes comparar directamente dos versiones, a no ser que extraigas ambas y las compares manualmente, ¿no?. Pero resulta que puedes hacerlo bastante mejor utilizando los atributos Git. Poniendo lo siguiente en tu archivo .gitattributes:

*.docx diff=word

Así le decimos a Git que sobre cualquier archivo coincidente con el patrón indicado, (.docx), ha de utilizar el filtro “word” cuando intente hacer una comparación con él. ¿Qué es el filtro “word”? Tienes que configurarlo tú mismo. Por ejemplo, puedes configurar Git para que utilice el programa docx2txt para convertir los documentos Word en archivos de texto plano, archivos sobre los que poder realizar comparaciones sin problemas.

En primer lugar, necesitas instalar docx2txt, obteniéndolo de: http://docx2txt.sourceforge.net. Sigue las instrucciones del archivo INSTALL e instálalo en algún sitio donde se pueda ejecutar desde la shell. A continuación, escribe un script que sirva para convertir el texto al formato que Git necesita, utilizando docx2txt:

#!/bin/bash
docx2txt.pl $1 -

No olvides poner los permisos de ejecución al script (chmod a+x). Finalmente, configura Git para usar el script:

$ git config diff.word.textconv docx2txt

Ahora Git ya sabrá que si intentas comparar dos archivos, y cualquiera de ellos finaliza en .docx, lo hará a través del filtro “word”, que se define con el programa docx2txt. Esto provoca la creación de versiones de texto de los archivos Word antes de intentar compararlos.

Por ejemplo, el capítulo 1 de este libro se convirtió a Word y se envió al repositorio Git. Cuando añadimos posteriormente un nuevo párrafo, el git diff muestra lo siguiente:

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 0b013ca..ba25db5 100644
--- a/chapter1.docx
+++ b/chapter1.docx
@@ -2,6 +2,7 @@
 This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so.
 1.1. About Version Control
 What is "version control", and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. For the examples in this book you will use software source code as the files being version controlled, though in reality you can do this with nearly any type of file on a computer.
+Testing: 1, 2, 3.
 If you are a graphic or web designer and want to keep every version of an image or layout (which you would most certainly want to), a Version Control System (VCS) is a very wise thing to use. It allows you to revert files back to a previous state, revert the entire project back to a previous state, compare changes over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also generally means that if you screw things up or lose files, you can easily recover. In addition, you get all this for very little overhead.
 1.1.1. Local Version Control Systems
 Many people's version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if they're clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory you're in and accidentally write to the wrong file or copy over files you don't mean to.

Git nos muestra que se añadió la línea “Testing: 1, 2, 3.”, lo cual es correcto. No es perfecto (los cambios de formato, por ejemplo, no los mostrará) pero sirve en la mayoría de los casos.

Otro problema donde puede ser útil esta técnica, es en la comparación de imágenes. Un camino puede ser pasar los archivos JPEG a través de un filtro para extraer su información EXIF (los metadatos que se graban dentro de la mayoría de los formatos gráficos). Si te descargas e instalas el programa exiftool, puedes utilizarlo para convertir tus imágenes a textos (metadatos), de tal forma que el comando “diff” podrá, al menos, mostrarte algo útil de cualquier cambio que se produzca:

$ echo '*.png diff=exif' >> .gitattributes
$ git config diff.exif.textconv exiftool

Si reemplazas cualquier imagen en el proyecto y ejecutas git diff, verás algo como:

diff --git a/image.png b/image.png
index 88839c4..4afcb7c 100644
--- a/image.png
+++ b/image.png
@@ -1,12 +1,12 @@
 ExifTool Version Number         : 7.74
-File Size                       : 70 kB
-File Modification Date/Time     : 2009:04:21 07:02:45-07:00
+File Size                       : 94 kB
+File Modification Date/Time     : 2009:04:21 07:02:43-07:00
 File Type                       : PNG
 MIME Type                       : image/png
-Image Width                     : 1058
-Image Height                    : 889
+Image Width                     : 1056
+Image Height                    : 827
 Bit Depth                       : 8
 Color Type                      : RGB with Alpha

Aquí se ve claramente que ha cambiado el tamaño del archivo y las dimensiones de la imagen.

Expansión de palabras clave

Algunos usuarios de sistemas SVN o CVS, echan de menos el disponer de expansiones de palabras clave al estilo de las que dichos sistemas tienen. El principal problema para hacerlo en Git reside en la imposibilidad de modificar los archivos con información relativa a la confirmación de cambios (commit). Debido a que Git calcula sus sumas de comprobación antes de las confirmaciones. De todas formas, es posible inyectar textos en un archivo cuando lo extraemos del repositorio (checkout) y quitarlos de nuevo antes de devolverlo al repositorio (commit). Los atributos Git admiten dos maneras de realizarlo.

La primera, es inyectando automáticamente la suma de comprobación SHA-1 de un gran objeto binario (blob) en un campo $Id$ dentro del archivo. Si colocas este atributo en un archivo o conjunto de archivos, Git lo sustituirá por la suma de comprobación SHA-1 la próxima vez que lo/s extraiga/s. Es importante destacar que no se trata de la suma SHA de la confirmación de cambios (commit), sino del propio objeto binario (blob):

$ echo '*.txt ident' >> .gitattributes
$ echo '$Id$' > test.txt

La próxima vez que extraigas el archivo, Git le habrá inyectado el SHA-1 del objeto binario (blob):

$ rm test.txt
$ git checkout -- test.txt
$ cat test.txt
$Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

Pero esto tiene un uso bastante limitado. Si has utilizado alguna vez las sustituciones de CVS o de Subversion, sabrás que pueden incluir una marca de fecha (la suma de comprobación SHA no es igual de útil, ya que, por ser bastante aleatoria, es imposible deducir si una suma SHA es anterior o posterior a otra).

Aunque resulta que también puedes escribir tus propios filtros para realizar sustituciones en los archivos al guardar o recuperar (commit/checkout). Se trata de los filtros “clean” y “smudge”. En el archivo ‘.gitattibutes’ puedes indicar filtros para carpetas o archivos determinados y luego preparar tus propios scripts para procesarlos justo antes de confirmar cambios en ellos (“clean”, ver Ejecución de filtro “smudge” en el checkout.), o justo antes de recuperarlos (“smudge”, ver Ejecución de filtro “clean” antes de confirmar el cambio.). Estos filtros pueden utilizarse para realizar todo tipo de acciones útiles.

Ejecución de filtro ``smudge'' en el checkout.
Figura 144. Ejecución de filtro “smudge” en el checkout.
Ejecución de filtro ``clean'' antes de confirmar el cambio.
Figura 145. Ejecución de filtro “clean” antes de confirmar el cambio.

El mensaje de confirmación para esta funcionalidad nos da un ejemplo simple: el de pasar todo tu código fuente C por el programa indent antes de almacenarlo. Puedes hacerlo poniendo los atributos adecuados en tu archivo .gitattributes, para filtrar los archivos *.c a través de “indent”:

*.c filter=indent

E indicando después que el filtro “indent” actuará al manchar (smudge) y al limpiar (clean):

$ git config --global filter.indent.clean indent
$ git config --global filter.indent.smudge cat

En este ejemplo, cuando confirmes cambios (commit) en archivos con extensión *.c, Git los pasará previamente a través del programa indent antes de confirmarlos, y los pasará a través del programa cat antes de extraerlos de vuelta al disco. El programa cat es básicamente transparente: de él salen los mismos datos que entran. El efecto final de esta combinación es el de filtrar todo el código fuente C a través de indent antes de confirmar cambios en él.

Otro ejemplo interesante es el de poder conseguir una expansión de la clave $Date$ del estilo de RCS. Para hacerlo, necesitas un pequeño script que coja el nombre de un archivo, localice la fecha de la última confirmación de cambios en el proyecto, e inserte dicha información en el archivo. Este podria ser un pequeño script Ruby para hacerlo:

#! /usr/bin/env ruby
data = STDIN.read
last_date = `git log --pretty=format:"%ad" -1`
puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')

Simplemente, utiliza el comando git log para obtener la fecha de la última confirmación de cambios, y sustituye con ella todas las cadenas $Date$ que encuentre en el flujo de entrada “stdin”; imprimiendo luego los resultados. Debería de ser sencillo de implementarlo en cualquier otro lenguaje que domines.

Puedes llamar expanddate a este archivo y ponerlo en el path de ejecución. Tras ello, has de poner un filtro en Git (podemos llamarle dater), e indicarle que use el filtro expanddate para manchar (smudge) los archivos al extraerlos (checkout). Puedes utilizar una expresión Perl para limpiarlos (clean) al almacenarlos (commit):

$ git config filter.dater.smudge expand_date
$ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'

Esta expresión Perl extrae cualquier cosa que vea dentro de una cadena $Date$, para devolverla a como era en un principio. Una vez preparado el filtro, puedes comprobar su funcionamiento preparando un archivo que contenga la clave $Date$ e indicando a Git cual es el atributo para reconocer ese tipo de archivo:

$ echo '# $Date$' > date_test.txt
$ echo 'date*.txt filter=dater' >> .gitattributes

Al confirmar cambios (commit) y luego extraer (checkout) el archivo de vuelta, verás la clave sustituida:

$ git add date_test.txt .gitattributes
$ git commit -m "Testing date expansion in Git"
$ rm date_test.txt
$ git checkout date_test.txt
$ cat date_test.txt
# $Date: Tue Apr 21 07:26:52 2009 -0700$

Esta es una muestra de lo poderosa que puede resultar esta técnica para aplicaciones personalizadas. No obstante, debes de ser cuidadoso, ya que el archivo .gitattibutes se almacena y se transmite junto con el proyecto; pero no así el propio filtro, (en este caso, dater), sin el cual no puede funcionar. Cuando diseñes este tipo de filtros, han de estar pensados para que el proyecto continúe funcionando correctamente incluso cuando fallen.

Exportación del repositorio

Los atributos de Git permiten realizar algunas cosas interesantes cuando exportas un archivo de tu proyecto.

export-ignore

Puedes indicar a Git que ignore y no exporte ciertos archivos o carpetas cuando genera un archivo de almacenamiento. Cuando tienes alguna carpeta o archivo que no deseas incluir en tus registros, pero quieras tener controlado en tu proyecto, puedes marcarlos a través del atributo export-ignore.

Por ejemplo, supongamos que tienes algunos archivos de pruebas en la carpeta test/, y que no tiene sentido incluirlos en los archivos comprimidos (tarball) al exportar tu proyecto. Puedes añadir la siguiente línea al archivo de atributos de Git:

test/ export-ignore

A partir de ese momento, cada vez que lances el comando git archive para crear un archivo comprimido de tu proyecto, esa carpeta no se incluirá en él.

export-subst

Otra cosa que puedes realizar sobre tus archivos es algún tipo de sustitución simple de claves. Git te permite poner la cadena $Format:$ en cualquier archivo, con cualquiera de las claves de formateo de --pretty=format que vimos en el capítulo 2. Por ejemplo, si deseas incluir un archivo llamado LAST COMMIT en tu proyecto, y poner en él automáticamente la fecha de la última confirmación de cambios cada vez que lances el comando git archive:

$ echo 'Last commit date: $Format:%cd$' > LAST_COMMIT
$ echo "LAST_COMMIT export-subst" >> .gitattributes
$ git add LAST_COMMIT .gitattributes
$ git commit -am 'adding LAST_COMMIT file for archives'

Cuando lances la orden git archive, lo que la gente verá en ese archivo cuando lo abra será:

$ cat LAST_COMMIT
Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$

Estrategias de fusión (merge)

También puedes utilizar los atributos Git para indicar distintas estrategias de fusión para archivos específicos de tu proyecto. Una opción muy útil es la que nos permite indicar a Git que no intente fusionar ciertos archivos concretos cuando tengan conflictos, manteniendo en su lugar tus archivos sobre los de cualquier otro.

Puede ser interesante si una rama de tu proyecto es divergente o esta especializada, pero deseas seguir siendo capaz de fusionar cambios de vuelta desde ella, e ignorar ciertos archivos. Digamos que tienes un archivo de datos denominado database.xml, distinto en las dos ramas, y que deseas fusionar en la otra rama sin perturbarlo. Puedes ajustar un atributo tal como:

database.xml merge=ours

Y luego definir una estrategia ours con:

$ git config --global merge.ours.driver true

Si fusionas en la otra rama, en lugar de tener conflictos de fusión con el archivo database.xml, verás algo como:

$ git merge topic
Auto-merging database.xml
Merge made by recursive.

Y el archivo database.xml permanecerá inalterado en cualquiera que fuera la versión que tenias originalmente.

scroll-to-top