- Entender el concepto de fontanería y loza.
- Entender el concepto de hooks o puntos de enganche.
- Entender las órdenes menos usuales de git usadas desde los hooks.
- Saber adaptar hooks para una labor determinada.
Cuando se crea por primera vez un repositorio veremos que aparecen misteriosamente una serie de ficheros con esta estructura dentro del directorio .git
.
branches
lo dejamos de lado porque ya no se usa (aunque por alguna razón se sigue creando). config
, HEAD
, refs
y objects
son ficheros o directorios que almacenan información dinámica, por ejemplo config
almacena las variables de configuración (que se han visto anteriormente). El resto de los ficheros y directorios se copian de una plantilla; esta plantilla se instala con git
y se usa cada vez que hacemos clone
o init
, y contiene hooks
, description
, branches
e info
y los ficheros que se encuentran dentro de ellos.
Esta plantilla la podemos modificar y cambiar. La plantilla que se usa por omisión se encuentra en /usr/share/git-core/templates/
y contiene una serie de ficheros junto con ejemplos (samples) para ganchos. Sin embargo, podemos personalizar nuestra plantilla editando (con permiso de superusuario) estos ficheros o bien usando la opción --template <nombre de directorio>
de clone
o init
. En ese caso, en vez de copiar los ficheros por omisión, copiará los contenidos en ese directorio.
Por ejemplo, se puede usar esta plantilla que elimina los ficheros de ejemplo, sustituye por otro y traduce los contenidos de los otros ficheros al castellano; también mete en los patrones ignorados (sin necesidad de usar .gitignore
) los ficheros que terminan en ~
, producidos por Emacs como copia de seguridad.
Estos ficheros forman parte de las cañerías de git
y podemos cambiar su comportamiento editando config
como ya se ha visto en el capítulo de uso básico; de hecho, existe también un fichero de configuración a nivel global, .gitconfig
que sigue el mismo formato y que ya hemos visto
[alias]
ci = commit
st = status
[core]
editor = emacs
Estos ficheros de configuración siguen un formato similar al de los ficheros .ini
, es decir, bloques definidos entre corchetes y variables con valor, dentro de ese bloque, a las que se le asigna usando =
. En este caso definimos dos alias y un editor o, mejor dicho, el editor. Esto podemos hacerlo tanto en el fichero global como en el local si queremos que afecte solo a nuestro repositorio.
Otro fichero dentro de este directorio que se puede modificar es .git/info/exclude
; es similar a .gitignore
, salvo que en este caso afectará solamente a nuestra copia local del repositorio y no a todas las copias del mismo. Por ejemplo, podemos editarlo de esta forma:
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
*~
para excluir los ficheros de copia de seguridad de Emacs (que hemos definido antes como editor) que nos interesa evitar a nosotros, pero que puede que tengan un significado especial para otro usuario del repo y que por tanto no quiera evitar.
Por supuesto, el tema principal de este capítulo está en el otro directorio, hooks, cuyo contenido tendremos que cambiar si queremos añadir ganchos al repositorio. Pero para usarlo necesitamos también conocer algunos conceptos más de git, empezando por cómo se accede a más cañerías.
Si miramos en el directorio .git/objects
encontraremos una serie de
directorios con nombres de dos letras, dentro de los cuales están los
objetos de git, que tienen otras 38 letras para componer las 40 letras
que componen el nombre único de cada objeto; este nombre se genera a
partir del contenido usando
SHA1.
git
almacena toda la información en ese directorio y de hecho está
organizado para acceder a la información almacenada por contenido. Por
eso tenemos que imaginarlo como un sistema de ficheros normal, con una
raíz (que es HEAD, el punto en el que se encuentra el repositorio en
este momento) y una serie de ramas que apuntan a ficheros y a
diferentes versiones de los mismos.
git, entonces, procede de la forma siguiente:
- Crea un SHA1 a partir del contenido del fichero cambiado o añadido. Este fichero se almacena en la zona temporal en forma de blob.
- El nombre del fichero se almacena en un árbol, junto con el nombre
de otros ficheros que estén, en ese momento, en la zona temporal. Un
árbol almacena los nombres de varios ficheros y apunta al contenido de
los mismos, almacenado como blob. De este objeto, que tiene un
formato fijo, se calcula también el SHA1 y se almacena en
.git/objects
. Un árbol, a su vez, puede apuntar a otros árboles creados de la misma forma. - Cuando se hace commit, se crea un tercer tipo de objeto con ese nombre. Un commit contiene enlaces a un árbol (el de más alto nivel) y metadatos adicionales: quién lo ha hecho, cuándo y por supuesto el mensaje de commit.
Veremos más adelante cómo se listan ficheros de todos estos tipos, pero por lo pronto la idea es que un comando de git de alto nivel involucra varias órdenes de bajo nivel que, eventualmente, van a parar a información que se almacena en un directorio determinado con nombres de fichero que se calculan usando SHA1, aparte de que se puede actuar a diferentes niveles, desde el más bajo de almacenar un objeto directamente en un árbol o crear un commit "a mano" hasta el más alto (que es el que estamos acostumbrados). Este sistema, además, asegura que no se pierda ninguna información y que podamos acceder al contenido de un fichero determinado hecho en un momento determinado de forma fácil y eficiente. Pero para poder hacerlo debe haber una forma única y también compacta de referirse a un elemento determinado dentro de ese repositorio. Es lo que explicaremos a continuación.
Como ya hemos visto antes, todos los objetos (sean blobs, árboles o
commits) están representados por un SHA1. Si conocemos el SHA1, se
puede usar show
, por ejemplo, para visualizarlo. Haciendo git log
veremos, por ejemplo, los últimos commits y si hacemos show
sobre
uno de ellos,
git show fe88e5eefff7f3b7ea95be510c6dcb87054bbcb
commit fe88e5eefff7f3b7ea95be510c6dcb87054bbcb0
Author: JJ Merelo <[email protected]>
Date: Thu Apr 17 18:29:11 2014 +0200
Añade layout
diff --git a/views/layout.jade b/views/layout.jade
new file mode 100644
index 0000000..36cc059
--- /dev/null
+++ b/views/layout.jade
@@ -0,0 +1,6 @@
[....]
El mismo resultado que obtendríamos si hacemos git show HEAD
, que
recordemos que es una referencia que apunta al último commit. También
obtendremos lo mismo si hacemos git show master
. En cualquiera de
los casos, lo que está mostrando es un objeto de tipo commit, el
último realizado.
Pero veremos como
funciona este último ejemplo. Al lado del directorio objects
está el
directorio refs
, que almacena referencias y que es como git
sabe a
qué commit corresponde cada cosa. Este comando:
~/repos-git/repo-ejemplo<master>$ tree .git/refs/
.git/refs/
├── heads
│ ├── img-dir
│ └── master
├── remotes
│ ├── heroku
│ │ └── master
│ └── origin
│ ├── HEAD
│ ├── img-dir
│ └── master
└── tags
├── v0.0.1
├── v0.0.2
├── v0.0.2.1
└── v0.0.3
5 directories, 10 files
muestra todo lo que hay almacenado en este directorio: referencia a
las ramas locales en heads
y a las remotas en remotes
. Si
mostramos el contenido de los ficheros:
~/repos-git/repo-ejemplo<master>$ cat .git/refs/heads/master
fe88e5eefff7f3b7ea95be510c6dcb87054bbcb0
Que muestra que, efectivamente, el hash del commit es el que corresponde.
Podemos mirar en .git/objects/fe a ver si efectivamente se encuentra; puedes hacerlo sobre tu copia del repositorio
repo-ejemplo
, ya que los hash son iguales en todos lados.
Como hemos visto anteriormente, un commit apunta a un árbol. Podemos
indicarle a show
que nos muestre este árbol de esta forma:
~/repos-git/repo-ejemplo<master>$ git show master^{tree}
tree master^{tree}
.aspell.es.pws
.gitignore
.gitmodules
.travis.yml
LICENSE
Procfile
README.md
curso
package.json
shippable.yml
test/
views/
web.js
En este caso el formato es rama (circunflejo o caret) {tree}
; el
circunflejo se usa en la selección de referencias de git
para
cualificar lo que se encuentra antes de ella, pero no hay muchas más
opciones aparte de tree
, aunque sí podemos acceder a versiones
anteriores del repositorio y a sus ficheros:
~/repos-git/repo-ejemplo<master>$ git show master~1
commit 5be23bb2a610260da013fcea807be872a4bd6981
Author: JJ Merelo <[email protected]>
Date: Thu Apr 17 17:42:39 2014 +0200
Aclara una frase
[...]
La virgulilla o palito ~
indica un ancestro, es decir, el padre del commit
anterior, que, como vemos,
corresponde al commit 5be23bb.
La lista completa de opciones para especificar revisiones está, curiosamente, en la página de referencia del comando
rev-parse
. Hay un número excesivo de ellas, pero si en algún momento no se entiende qué es lo que se está usando, conviene ir ahí.
Podemos
ir más allá hasta que nos aburramos: ~2
accederá al padre de este y
así sucesivamente. Y, por supuesto, podemos cualificarlo con
^{tree}
para que nos muestre el árbol en el estado que estaba en
ese commit. Y también para que nos muestre un fichero sin necesidad de
sacarlo del repositorio:
~/repos-git/repo-ejemplo<master>$ git show master~2:README.md
repo-ejemplo
============
Ejemplo de repositorio para trabajar en el
[curso de `git`](http://cevug.ugr.es/git) el contenido del cual
[...]
En esta sección hemos usado
show
para mostrar las capacidades de los diferentes selectores degit
, pero se pueden usar con cualquier otra orden, comocheckout
o cualquiera que admita, en general, una referencia a un objeto.
Para entendernos, todas las órdenes que hemos usado hasta ahora son loza. Es decir, es la interfaz del usuario de toda la instalación de fontanería que lleva a cabo realmente la labor de quitar de en medio lo que uno deposita en las instalaciones sanitarias. Pero por debajo de la loza y pegado a ella, están las cañerías y toda la instalación de fontanería.
Los comandos de git
se dividen en dos tipos: fontanería o cañería, que son comandos que generalmente no ve el usuario, y loza, que son los que ve y los que usa. Sin embargo, este capítulo trata realmente de esa fontanería, porque van a ser una serie de órdenes que se van a llevar a cabo después de que se ejecuten las órdenes de loza, o quizás dentro de esas órdenes de loza.
Pero antes de usar esas órdenes de fontanería tenemos que entender cómo son las cañerías. Una parte se ha visto anteriormente: el index o índice que contiene todos los objetos a los que git
debe prestarles atención a la hora de hacer un commit. Pero existen además los objetos y las referencias.
Los objetos son, en general, información que está almacenada en el repositorio. Incluyen, por supuesto, los ficheros que almacenamos en el mismo, pero también los mensajes de commit, las etiquetas y los árboles. Los ficheros almacenados están divididos: el contenido del fichero se almacena en un blob y el nombre del fichero se almacena en el árbol. Hay, pues, cuatro tipos de objetos: blob, tree, commit y tag.
La orden ls-tree
nos permite ver qué tipos de objetos tenemos almacenados y sus códigos SHA1, que son los nombres de ficheros calculados a partir del contenido del mismo. Aunque todos están almacenados en el directorio .git/objects
, esta orden nos permite ver también de qué tipo son:
~/repos-git/repo-ejemplo<master>$ git ls-tree HEAD
100644 blob a6f69e4284566cd84272c6a4e4996f64643afbea .aspell.es.pws
100644 blob a72b52ebe897796e4a289cf95ff6270e04637aad .gitignore
100644 blob cc5411b5557f43c7ba2f37ad31f8dc34cccda075 .gitmodules
100644 blob 4e7b6c1b5a6cb3a962ea05874d10c943c1923f39 .travis.yml
100644 blob d5445e7ac8422305d107420de4ab8e1ee6227cca LICENSE
100644 blob d1913ebe4d9e457be617ee0e786fc8c30a237902 Procfile
100644 blob c5badda0c484c989e958ea4e27dfe11d69f3c8ef README.md
160000 commit fa8b7521968bddf235285347775b21dd121b5c11 curso
100644 blob f8c35adaf57066d4329737c8f6ec7ce6179cc221 package.json
100644 blob 08827778af94ea4c0ddbc28194ded3081e7b0f87 shippable.yml
040000 tree 39da6b155c821af1e6a304daca9b66efb1ac651f test
100644 blob 94f151d9ef9340c81989b0c3fa8c517c068e1864 web.js
En este caso tenemos objetos de tres tipos: blob, commit y
tree. A ls-tree
se le pasa un tree-ish, es decir, algo que
apunte a donde esté almacenado un árbol pero, para no preocuparnos de
qué se trata esto, usaremos simplemente HEAD, que apunta como sabéis a
la punta de la rama en la que nos encontramos ahora mismo. También
nos da el SHA1 de 40 caracteres que representa cada uno de los
ficheros. Si queremos que se expandan los tree
para mostrar los
ficheros que hay dentro también usamos la opción -r
:
~/repos-git/repo-ejemplo<master>$ git ls-tree -r HEAD
100644 blob a6f69e4284566cd84272c6a4e4996f64643afbea .aspell.es.pws
100644 blob 4e7b6c1b5a6cb3a962ea05874d10c943c1923f39 .travis.yml
100644 blob d5445e7ac8422305d107420de4ab8e1ee6227cca LICENSE
100644 blob d1913ebe4d9e457be617ee0e786fc8c30a237902 Procfile
[...]
100644 blob c5badda0c484c989e958ea4e27dfe11d69f3c8ef README.md
que muestra solo los objetos de tipo blob
(y un commit
) con el camino completo que llega hasta ellos.
Si editamos un fichero tal como el README.md, el repositorio tendrá esta apariencia tras hacer el commit:
~/repos-git/repo-ejemplo<master>$ git ls-tree HEAD
[...]
100644 blob da5b5121adb42e990b9e990c3edb962ef99cb76a README.md
Como vemos, ha cambiado el SHA1 (comienza con da5
, comparadlo con el
listado más arriba en el que comienza con c5b
). Pero ls-tree
va más
allá y te puede mostrar también cuál es el estado del repositorio hace
varios commits. Por ejemplo, podemos usar HEAD^
para referirnos al
commit anterior y git ls-tree HEAD^
nos devolvería exactamente el
mismo estado en el que estaba antes de hacer la modificación a
README.md. De hecho, podemos usar también la abreviatura del commit de
esta forma git ls-tree 5be23bb
, siendo este último una parte del
SHA1 (o hash) del último commit; nos devolvería el último resultado.
Pero podemos ir todavía a una profundidad mayor dentro de las
tuberías. ls-tree
solo lista los objetos que ya forman parte del
árbol, del principal o de alguno de los secundarios. Puede que
necesitemos acceder a aquellos objetos que se han añadido al índice,
pero todavía no han pasado a ningún árbol. Para eso usamos
ls-files
. Tras añadir un fichero que está en un subdirectorio
views
con add
, podemos hacer:
git ls-files --stage
[...]
100644 a72b52ebe897796e4a289cf95ff6270e04637aad 0 .gitignore
100644 cc5411b5557f43c7ba2f37ad31f8dc34cccda075 0 .gitmodules
100644 4e7b6c1b5a6cb3a962ea05874d10c943c1923f39 0 .travis.yml
100644 d5445e7ac8422305d107420de4ab8e1ee6227cca 0 LICENSE
100644 d1913ebe4d9e457be617ee0e786fc8c30a237902 0 Procfile
100644 da5b5121adb42e990b9e990c3edb962ef99cb76a 0 README.md
160000 fa8b7521968bddf235285347775b21dd121b5c11 0 curso
100644 f8c35adaf57066d4329737c8f6ec7ce6179cc221 0 package.json
[...]
100644 94f151d9ef9340c81989b0c3fa8c517c068e1864 0 web.js
que nos devuelve, en penúltimo lugar, un fichero que todavía no ha pasado al árbol. Evidentemente, tras el commit:
~/repos-git/repo-ejemplo<master>$ git ls-tree HEAD
[...]
040000 tree fd3846c0d6089437598004131184c61aea2b6514 views
este listado nos muestra el nuevo objeto de tipo tree
que se ha creado y nos da su SHA1, que podemos usar para examinarlo con ls-tree
:
ls-files
, por otro lado, nos mostrará un listado más parecido al que
se puede obtener con la orden ls
de Linux.
~/repos-git/repo-ejemplo<master>$ git ls-files views
views/layout.jade
Hay un tercer comando relacionado con el examen de directorios y ficheros locales, cat-file
, que muestra el contenido de un objeto, en general. Por ejemplo, en este caso, para listar el contenido de un objeto de tipo tree
:
~/repos-git/repo-ejemplo<master>$ git cat-file -p fd3846c
100644 blob 36cc059186e7cb247eaf7bfd6a318be6cffb9ea3 layout.jade
nos muestra que ese objeto contiene un solo fichero, layout.jade
, y sus características. Pero más curioso aún es cuando se usa sobre objetos de tipo commit (no sobre el objeto commit que aparece arriba, que se trata de un commit de otro repositorio al contener el directorio curso
un submódulo de git). Por ejemplo, podemos hacer:
~/repos-git/repo-ejemplo<master>$ git cat-file -p HEAD
tree 1c40899a32c2b5ec7f930bd943e5dbb98562d373
parent 5be23bb2a610260da013fcea807be872a4bd6981
author JJ Merelo <[email protected]> 1397752151 +0200
committer JJ Merelo <[email protected]> 1397752151 +0200
Añade layout
que, dado que HEAD
apunta al último commit, nos muestra en modo pretty-print toda la información sobre el último commit y muestra el árbol de ficheros correspondiente, que podemos listar con
~/repos-git/repo-ejemplo<master>$ git cat-file -p 1c40899a
[...]
100644 blob d5445e7ac8422305d107420de4ab8e1ee6227cca LICENSE
100644 blob d1913ebe4d9e457be617ee0e786fc8c30a237902 Procfile
[...]
160000 commit fa8b7521968bddf235285347775b21dd121b5c11 curso
[...]
040000 tree 39da6b155c821af1e6a304daca9b66efb1ac651f test
040000 tree fd3846c0d6089437598004131184c61aea2b6514 views
100644 blob 97c32024cda29e0fb6abebf48d3f6740f0acb9e2 web.js
En general, si queremos ahondar en las entrañas de un punto determinado en la historia del repositorio, trabajar con ls-files
, cat-file
y ls-tree
permite obtener toda la información contenida en el mismo. Esto nos va a resultar útil un poco más adelante.
En muchos casos para procesar los cambios dentro de un gancho
necesitaremos saber cuál es la diferencia con versiones anteriores del
fichero. Hay que tener en cuenta que esas diferencias, dependiendo del
estado en el que estemos, estarán en el árbol o en el índice
preparadas para ser enviadas al repositorio. En general, son una
serie de órdenes con diff
en ellas. La más simple, git diff
, nos
mostrará la diferencia entre los archivos en el índice y el último
commit:
~/repos-git/repo-ejemplo<master>$ git diff
diff --git a/views/layout.jade b/views/layout.jade
index 36cc059..2a66d58 100644
--- a/views/layout.jade
+++ b/views/layout.jade
@@ -1,6 +1,11 @@
-!!! 5
+doctype html
+html(lang="en")
-body
+html
+ head
+ title #{title}
+ body
-#wrapper
- block content
\ No newline at end of file
+ h1 Curso de git
+
+ block content
\ No newline at end of file
diff --git a/web.js b/web.js
index 97c3202..93b6255 100644
--- a/web.js
+++ b/web.js
@@ -27,6 +27,12 @@ app.get('/', function(req, res) {
res.send(routes['README']);
});
+app.get('/curso/:ruta', function(req, res) {
+ var ruta = "curso/texto/"+req.params.ruta;
+ console.log("Request "+req.params.ruta + " doc " //...
+ res.render('doc', { content: routes[ruta], title://...
+})
+
app.get('/curso/texto/:ruta', function(req, res) {
// console.log("Request "+req.params.ruta);
var ruta_toda = "curso/texto/"+req.params.ruta;
En esta vista de
diff
,
las diferencias siguen el formato habitual en
la utilidad diff
, que permite
generar parches para aplicarlos a conjuntos de ficheros. En concreto,
muestra qué ficheros se están comparando (pueden ser diferentes
ficheros si se ha cambiado el nombre) los SHA1 de los contenidos
correspondientes, y luego un +
o -
delante de cada una de las
líneas que hay de diferencia. Este fichero se podría usar directamente
con la utilidad diff
de Linux, pero realmente no nos va a ser de
mucha utilidad a la hora de saber, por ejemplo, qué ficheros se han
modificado. Para hacer esto, simplemente:
~/repo-ejemplo<master>$ git diff --name-only
views/layout.jade
web.js
que se puede hacer un poco más completa con --name-status
:
~/repo-ejemplo<master>$ git diff --name-status
M views/layout.jade
M web.js
~/repo-ejemplo<master>$ git diff --name-status --cached
A views/doc.jade
que nos muestra, usando una sola letra, qué tipo de cambio han sufrido. En
el primer caso nos muestra que han sido Modificados, y en el segundo
caso, además usamos otra opción, --cached
que, en este caso, nos
muestra los ficheros que han sido preparados para el commit; es decir,
la
diferencia que hay entre la cabeza y el índice;
en este caso, "A" indica que se trata de un fichero añadido. Podemos
ver todo junto con
~/repo-ejemplo<master>$ git diff --name-status HEAD
A views/doc.jade
M views/layout.jade
M web.js
que nos muestra la diferencia entre el commit más moderno (HEAD) y el
área de trabajo ahora mismo: hemos cambiado dos ficheros y añadido
uno. Un resultado similar obtendremos con diff-index
, cuya principal
diferencia es que compara siempre el índice con algún árbol, sin
tener ningún valor por omisión:
~/repo-ejemplo<master>$ git diff-index HEAD
Además del estado muestra el hash inicial y final de cada uno de los
ficheros. En este caso, como todavía no le hemos hecho commit, muestra
0. dif-tree
, sin embargo, sí muestra el SHA1 puesto que trabaja con
el árbol:
~/repo-ejemplo<master>$ git diff-tree HEAD
Aunque en este caso muestra un árbol, views
, que ha sido cambiado
porque se le ha añadido un fichero nuevo, views/doc.jade
. En el
momento que se haga el commit y pase por tanto del índice a la zona de
staging, los hash ya están calculados y cambia la salida:
~/repo-ejemplo<master>$ git diff-tree HEAD
diff-index
, sin embargo, no devolverá nada puesto que todos los
cambios que se habían hecho han pasado al árbol.
En general, lo que más nos va a interesar a la hora de hacer un gancho
es qué ficheros han cambiado. Pero conviene conocer toda la gama de
posibilidades que ofrece git
, sobre todo para poder entender su
estructura interna.
No todo el contenido que hay en el repositorio son los ficheros que
forman parte del mismo. Hay una parte importante de la fontanería que
son los metadatos del repositorio. Hay dos órdenes importantes, var
y config
. Con -l
nos listan todas las variables o variables de
configuración disponibles:
~/repos-git/curso-git/texto<master>$ git var -l
[email protected]
user.name=JJ Merelo
filter.obj-add.smudge=cat
push.default=simple
rerere.enabled=true
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
[email protected]:oslugr/curso-git.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
[...]
GIT_EDITOR=editor
GIT_PAGER=pager
Todas excepto las cuatro últimas variables son variables de
configuración que, por tanto, se pueden obtener también con git config -l
. Por sí solo, config
o var
listan el valor de una
variable:
~/repos-git/curso-git/texto<master>$ git config user.name
JJ Merelo
La mayoría de estos valores están disponibles o como variables de entorno o en ficheros; sin embargo estas órdenes dan una interfaz común para todos los sistemas operativos.
Todavía nos hacen falta una serie de órdenes para tomar decisiones sobre ficheros y sobre dónde estamos en el repositorio. Las veremos a continuación.
La
tersa descripción del comando rev-parse
, "recoge y procesa parámetros",
esconde la complejidad del mismo y su potencia, que va desde el
procesamiento de parámetros hasta la especificación de objetos, pasando
por la búsqueda de diferentes directorios dentro del repositorio git.
Por ejemplo, se puede usar para verificar si un objeto existe o no:
~/repo-ejemplo<master>$ git rev-parse --verify HEAD
637c2820013188f1c4951aef0c21de20440a6fbb
Nos muestra el SHA1 de la cabeza actual del repositorio de ejemplo, verificando que actualmente existe. No lo hará si acabamos de crear el repositorio, por ejemplo,
/tmp/pepe<>$ git init
Initialized empty Git repository in /tmp/pepe/.git/
/tmp/pepe<>$ git rev-parse --verify HEAD
fatal: Needed a single revision
De hecho, con él podemos encontrar todo tipo de objetos usando la notación que permite especificar revisiones
~/repo-ejemplo<master>$ git rev-parse HEAD@{1.month}
61253ecba351921c96a1553f6c5b7f9910f286f3
que correspondería a
este commit del 9 de marzo
y que podemos listar usando show
, describe
o cualquiera de los
otros comandos que se pueden aplicar a objetos. En general, para esto
se usará dentro de los garfios: para poder acceder a un objeto
determinado o a sus metadatos a la hora de ver las diferencias con el
objeto actual.
Un hook literalmente garfio o gancho es un programa que se ejecuta cuando sucede un evento determinado en el repositorio. Los webhooks de GitHub, por ejemplo, son un ejemplo: cuando se lleva a cabo un push, se envía información al sitio configurado para que ejecute un programa determinado: pase unos tests, publique un tweet, o lleve a cabo una serie de comprobaciones.
Los ganchos no son estrictamente necesarios en todo tipo de instalaciones; se puede trabajar con un repositorio sin tener la necesidad de usarlos. Sin embargo, son tremendamente útiles para automatizar una serie de tareas (como los tests que se usan en integración continua), implementar una serie de políticas para todos los usuarios de un repositorio (formato de los mensajes de commit, por ejemplo) y añadir información al repositorio de forma automática.
Los hooks son, por tanto, programas ejecutables. Cualquier programa que se pueda lanzar puede servir, pero generalmente se usa o guiones del shell (si uno es suficientemente masoquista) o lenguajes de scripting tales como Perl, Python, Ruby, Javascript o, si uno es realmente masoquista, PHP. En realidad a git le da igual qué lenguaje se use.
Los hooks van en su propio directorio, .git/hooks
que se crea
automáticamente y que tiene, siempre, una serie de scripts ejemplo,
ninguno de ellos activados. Solo se admite un hook por evento, y ese
hook tendrá el nombre del evento asociado; es decir, un programa
llamado post-merge
en ese directorio se ejecutará siempre cada vez
que se termine un merge con éxito. Como generalmente uno quiere que
los scripts tengan un nombre razonable, la estrategia más general es
usar un enlace simbólico de esta forma:
ln -s nombre-real-del-script.sh post-merge
y, en todo caso, no se debe olvidar
chmod +x nombre-real-del-script.sh
para hacerlo ejecutable.
Windows seguramente tendrá su forma particular de hacer lo mismo, o ninguna. Por cualquiera de esas dos razones, nunca recomendamos Windows como una plataforma para desarrollo. Úsala para la declaración de la renta o para jugar al Unreal, pero para desarrollar usa una plataforma para desarrolladores: Linux (o Mac, que tiene un núcleo Unix por debajo).
Los hooks se activarán cuando se ejecute un comando determinado y
recibirán una serie de parámetros como argumento o en algún caso como
entrada estándar. Este
cuadro
resume cuándo se ejecutan y también qué reciben como parámetro. En
general, también tendrán influencia en si tiene éxito o no el comando
determinado: salir con un valor no nulo, en algunos casos, parará la
ejecución del comando con un mensaje de error. Por ejemplo, un hook
applypatch-msg, que se aplica desde el comando git am
antes de que
se ejecute, parará la aplicación del parche si se sale con un valor 1.
De todos los hooks posibles solo veremos los que se refieren al
commit. Son los que se pueden usar en local; los referidos a push
solo se programan en remoto, y los que se aplican a git am
o git gc
quedan fueran de los temas de este libro. Hay únicamente cuatro de
estos, que veremos a continuación.
En general, un hook hará lo siguiente:
- Examinar el entorno y los parámetros de entrada.
- Hacer cambios en el entorno, los ficheros o la salida.
- Salir con un mensaje si hay algún error, o ninguno.
No son diferentes de ningún otro programa, en realidad, salvo que los parámetros de entrada y cómo se debe salir está establecido de antemano.
Miremos un script simple, que actúa como gancho para preparación de un
mensaje de commit (prepare-commit-msg
, incluido en
la plantilla de repositorio usado en este curso
#!/bin/sh
SOB=$(git config github.user)
grep -qs "^$SOB" "$1" || echo ". Cambio por @$SOB" >> "$1"
Este script tiene toda la simplicidad de estar en dos líneas y toda la complicación de estar escrito para el shell. Esta introducción venerable te puede ayudar a empezar a trabajar con él, pero en este capítulo no pretendemos que aprendas a programar, solo que tengas las nociones básicas para echar a andar y posiblemente modificar ligeramente un gancho.
Empecemos por la primera línea: es común a todos los guiones del
shell. Simplemente indica el camino en el que se encuentra el mismo,
con #!
indicando que se trata de un fichero ejecutable (junto con el
chmod +x
, que se lo indica el sistema de ficheros).
La siguiente línea define una variable, SOB, que no es acrónimo de
nada, cuidadito. Esa variable usa el formato de ejecución de un
comando del shell $()
para asignar la salida de dicho comando a la
variable.
La tercera línea, en resumen, comprueba si el nombre del usuario ya
está en el mensaje y si no lo está le añade una "firma" que lo
incluye. Lo hace de la forma siguiente: grep -qs "^$SOB" "$1"
comprueba si el valor de la variable no (de ahí el caret ^
) está ya
en el mensaje (cuyo nombre de fichero está en el primer argumento,
$1
, según vemos en la
chuleta de ganchos de git. Si
no lo está (de ahí el ||
, es decir, O, se escribe (echo
) el
valor de la variable añadiéndose (>>
) al final del fichero cuyo
nombre está en la variable $1
.
La variable github.user
no tiene por qué estar definida siempre. Se
puede sustituir por user.email, por ejemplo, o por user.name
, que sí
se suele definir siempre cuando se crea un repositorio.
Este otro ejemplo es todavía más minimalista, y también añade información al commit (que, como en el caso anterior, se puede eliminar si se edita el fichero de commit):
STATS=$(git diff --cached --shortstat)
echo ". Cambios en este commit\n ${STATS}" >> "$1"
Es interesante notar, en este caso, que se usa diff --cached
ya que
en este caso los cambios estarán ya staged o cached y las
diferencias (que suelen aparecer de todas formas en el mensaje que da
el comando) serán entre HEAD y lo que hay ya almacenado en el área de
preparación de los ficheros, no entre HEAD y lo que hay en el sistema
de ficheros; esto es así porque ya estamos dentro del commit y los
ficheros están ya preparados para ser procesados en las tuberías de
git
por sus comandos-tubería
. En resumen, esta orden da una
mini-estadística que dice el número de ficheros y líneas cambiadas,
produciendo
resultados sobre este mismo fichero tales como este:
Mini-corrección
. Cambios en este commit
1 file changed, 1 insertion(+)
Otro hook puede servir para comprobar que los mensajes de commit son correctos. Como se ha visto anteriormente, una buena práctica es usar una primera línea de 50 caracteres (que aparecerán como título) seguida por una línea vacía y el resto del mensaje. Esto se puede aplicar mediante programas en cualquier lenguaje, como este en Ruby, o el siguiente en Node, una implementación de Javascript:
#!/usr/bin/env node
var fs = require('fs');
var msg_file = process.argv[2];
fs.readFile( msg_file, 'utf8', function( err, data ) {
if ( err ) throw err;
var lines = data.split("\n");
if ( lines[0].length > 50 ) {
console.log("[FORMATO] Primera línea > 50 caracteres");
process.exit(1);
}
})
La primera línea es común a los scripts, y a continuación se carga el
módulo necesario para usar el sistema de ficheros (fs
) y se lee el
argumento que se pasa por línea de órdenes, el nombre del fichero que
contiene el mensaje del commit (este mensaje estará ahí aunque lo
hayamos pasado con -m
desde la línea de órdenes).
El resto, usando el modo asíncrono que es común en Node, lee el
fichero (creando una excepción si hay algún error), lo divide en
líneas usando split
y a continuación comprueba si la primera línea
(lines[0]
) tiene más de 50 caracteres, en cuyo caso sale del proceso
con un código de error (1), de esta forma:
~/repos-git/curso-git<master>$ git commit\
-am "Añade muchas cosas y comenta con un montón de caracteres"
[FORMATO] Primera línea > 50 caracteres
Si no es así, simplemente deja pasar el mensaje. En este hook, curiosamente, no se usa más comando de git que el mensaje almacenado en el fichero; realmente, no es imprescindible usarlo, solo cuando vayamos a usar información de git en el mismo.
El programa anterior es un ejemplo de cómo se pueden implementar
políticas de formato o de cualquier otro tipo sobre un repositorio;
sin embargo, la capacidad que tienen es limitada, ya que se aplican
solo sobre los mensajes. En realidad, el que actúen de esa forma es
convencional, porque los programas que ejecutan los ganchos se
diferencian solamente en el momento en el que actúan, no en lo que
pueden hacer. Sin embargo, si queremos implementar una política sobre
los nombres de ficheros o el contenido de los
mismos, hay que usar un gancho que actúe cuando se añadan al repositorio como
el siguiente gancho pre-commit
escrito en Perl:
#!/usr/bin/env perl
my $is_head = `git rev-parse --verify HEAD`;
my $last_commit =
$is_head?"HEAD":"4b825dc642cb6eb9a060e54bf8d69288fbee4904";
my @changes = `git diff-index --name-only $last_commit`;
my %policies = ( no_underscore => qr/_/ );
for my $f (@changes) {
for my $p ( keys %policies ) {
if ($f =~ /$policies{$p}/) {
die "[FORMATO]: $p ";
}
}
}
Este programa es similar a los anteriores, pero para empezar usa un
comando cañería que se encuentra mucho: rev-parse
. Las dos
primeras órdenes comprueban si nos encontramos en el primer commit
de un repositorio (en cuyo caso HEAD
no existe y tendremos que usar
el commit primigenio que es igual en todos los repositorios de
git
; lo que pretenden esas dos líneas es encontrar el "último
commit" para ver cuál es la diferencia con respecto a él. Como vamos a
tratar con ficheros que acaban de ser añadidos al índice, usamos
diff-index
en la siguiente línea con un formato que nos devolverá
solamente los ficheros cambiados desde el último commit (o desde el
primero) sin que nos interese nada más sobre ellos.
La siguiente línea define un hash con un nombre y una expresión regular que será la que se tiene que implementar. Si no conoces el lenguaje no te preocupes, pero si lo conoces (ese u otro) es relativamente fácil añadir políticas nuevas, como por ejemplo que no se permitan archivos .pdf, simplemente añadiéndole una línea.
El bucle for
posterior es el que va recorriendo cada uno de los
cambios y cada una de las políticas (aunque en este caso habrá una
sola) y saldrá con die
si alguno de los ficheros tiene un nombre
incorrecto.
~/repo-plantilla<master>$ git commit -am "A ver si me deja"
[FORMATO]: no_underscore at .git/hooks/pre-commit line 13.
~/repo-plantilla<master>$ git status
# En la rama master
# Cambios para hacer commit:
# (use «git reset HEAD <archivo>...«para eliminar stage)
#
# archivo nuevo: uno_que_no
Lo que se puede hacer ahora no es tan trivial: puedes cambiar el
fichero de nombre a pelo, pero ya está en el índice, por eso es mejor
hacer algo como git mv
(o git rm --force
).
Hay múltiples posts en blogs que
publican y explican diferentes ejemplos de hooks
y lo que se puede hacer con ellos. Por ejemplo, en
este conjunto
usa programas externos y su código de salida ($?
) para comprobar si
un programa es correcto o no; también usa extensivamente git stash
para almacenar todos los ficheros que se hayan modificado y luego los
recupera con git stash pop
. Este, por ejemplo,
crea una plantilla para usarla en los mensajes de commit, Pero
este gist incluye algunos
hooks útiles que vamos a ver. Por ejemplo,
el siguiente
comprueba si se encuentra la palabra debugger
en el fichero (es
decir, comentarios de depuración):
if git-rev-parse --verify HEAD >/dev/null 2>&1; then
against=HEAD
else
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
for FILE in
`git diff-index --check --name-status $against -- | cut -c3-` ;
do
# Check if the file contains 'debugger'
if [ "grep 'debugger' $FILE" ]
then
echo $FILE ' contains debugger!'
exit 1
fi
done
Lo más complicado que tiene este fichero es el uso de filtros del
shell (algo que, por otro lado, debería aprenderse); esos filtros
hacen algo similar a lo que se ha hecho antes con Perl: recorre el
nombre de los ficheros y le aplica el buscador de cadenas, grep
,
buscando la palabra debugger
; si la encuentra, sale. Quizás está
mejor explicado en
la historia original.
En general, vas a necesitar usar un lenguaje de programación o
scripting como Perl, Python o Ruby para escribir ganchos realmente
útiles; en algunos casos, los ganchos trabajarán con interfaces de
programación de aplicaciones o bases de datos para hacer
comprobaciones o llevar a cabo tareas de contabilidad. En todos los
casos anteriores también se contará con módulos o bibliotecas que
ofrecerán un interfaz relativamente simple, en algunos casos dirigido
a objetos, para trabajar con los repositorios. Algunos lenguajes van
más allá, ofreciendo un marco de desarrollo de ganchos que permite
configurar los más habituales tales como comprobación de ortografía o
políticas de codificación locales simplemente con un fichero de
configuración; Git::Hooks
.
Este artículo, por ejemplo,
te explica como usar ganchos en
Python; este otro, en Perl,
llegando a incluir cosas como que un mensaje de commit
tiene que
referirse a un issue válido. Si usas cualquier otro lenguaje de
programación, solo tienes que buscar un poco y encontrarás multitud de
scripts y módulos que te ayudarán a escribir tus propios ganchos.