-
1. Inicio - Sobre el Control de Versiones
-
2. Fundamentos de Git
-
3. Ramificaciones en Git
-
4. Git en el Servidor
- 4.1 Los Protocolos
- 4.2 Configurando Git en un servidor
- 4.3 Generando tu clave pública SSH
- 4.4 Configurando el servidor
- 4.5 El demonio Git
- 4.6 HTTP Inteligente
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Git en un alojamiento externo
- 4.10 Resumen
-
5. Git en entornos distribuidos
-
6. GitHub
-
7. Herramientas de Git
- 7.1 Revisión por selección
- 7.2 Organización interactiva
- 7.3 Guardado rápido y Limpieza
- 7.4 Firmando tu trabajo
- 7.5 Buscando
- 7.6 Reescribiendo la Historia
- 7.7 Reiniciar Desmitificado
- 7.8 Fusión Avanzada
- 7.9 Rerere
- 7.10 Haciendo debug con Git
- 7.11 Submódulos
- 7.12 Agrupaciones
- 7.13 Replace
- 7.14 Almacenamiento de credenciales
- 7.15 Resumen
-
8. Personalización de Git
-
9. Git y Otros Sistemas
- 9.1 Git como Cliente
- 9.2 Migración a Git
- 9.3 Resumen
-
10. Los entresijos internos de Git
-
A1. Apéndice A: Git en otros entornos
- A1.1 Interfaces gráficas
- A1.2 Git en Visual Studio
- A1.3 Git en Eclipse
- A1.4 Git con Bash
- A1.5 Git en Zsh
- A1.6 Git en Powershell
- A1.7 Resumen
-
A2. Apéndice B: Integrando Git en tus Aplicaciones
- A2.1 Git mediante Línea de Comandos
- A2.2 Libgit2
- A2.3 JGit
-
A3. Apéndice C: Comandos de Git
- A3.1 Configuración
- A3.2 Obtener y Crear Proyectos
- A3.3 Seguimiento Básico
- A3.4 Ramificar y Fusionar
- A3.5 Compartir y Actualizar Proyectos
- A3.6 Inspección y Comparación
- A3.7 Depuración
- A3.8 Parcheo
- A3.9 Correo Electrónico
- A3.10 Sistemas Externos
- A3.11 Administración
- A3.12 Comandos de Fontanería
8.4 Personalización de Git - Un ejemplo de implantación de una determinada política en Git
Un ejemplo de implantación de una determinada política en Git
En esta sección, utilizarás lo aprendido para establecer un flujo de trabajo en Git que: compruebe si los mensajes de confirmación de cambios encajan en un determinado formato, obligue a realizar solo envíos de avance directo, y permita sólo a ciertos usuarios modificar ciertas carpetas del proyecto. Para ello, has de preparar los correspondientes scripts de cliente (para ayudar a los desarrolladores a saber de antemano si sus envíos van a ser rechazados o no), y los correspondientes scripts de servidor (para obligar a cumplir esas políticas).
Hemos usado Ruby para escribir los ejemplos, tanto porque es nuestro lenguaje preferido de scripting como porque creemos que es el más parecido a pseudocódigo; de tal forma que puedas ser capaz de seguir el código, incluso si no conoces Ruby. Pero, puede ser igualmente válido cualquier otro lenguaje. Todos los script de ejemplo que vienen de serie con Git están escritos en Perl o en Bash shell, por lo que tienes bastantes ejemplos en esos lenguajes de scripting.
Punto de enganche en el lado servidor
Todo el trabajo del lado servidor va en el script update
de la carpeta
hooks
. Dicho script se lanza cada vez que alguien sube algo a alguna rama,
y tiene tres argumentos:
-
El nombre de la referencia que se está subiendo
-
La vieja revisión de la rama que se está modificando
-
La nueva revisión que se está subiendo a la rama
También puedes tener acceso al usuario que los está enviando, si este los envía a
través de SSH. Si has permitido a cualquiera conectarse con un mismo usuario
(como "git", por ejemplo), has tenido que dar a dicho usuario una envoltura
(shell wraper) que te permite determinar cuál es el usuario que se conecta
según sea su clave pública, permitiéndote fijar una variable de entorno
especificando dicho usuario. Aquí, asumiremos que el usuario conectado queda
reflejado en la variable de entorno $USER
, de tal forma que el script
update
comienza recogiendo toda la información que necesitas:
#!/usr/bin/env ruby
$refname = ARGV[0]
$oldrev = ARGV[1]
$newrev = ARGV[2]
$user = ENV['USER']
puts "Enforcing Policies..."
puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"
Sí, estamos usando variables globales. No nos juzguen por ello, que es más sencillo mostrarlo de esta manera.
Obligando a utilizar un formato específico en el mensaje de commit
Tu primer desafío es asegurarte que todos y cada uno de los mensajes de confirmación de cambios se ajustan a un determinado formato. Simplemente, por fijar algo concreto, supongamos que cada mensaje ha de incluir un texto tal como "ref: 1234", porque quieres enlazar cada confirmación de cambios con una determinada entrada de trabajo en un sistema de control. Has de mirar en cada confirmación de cambios (commit) recibida, para ver si contiene ese texto; y, si no lo trae, salir con un código distinto de cero, de tal forma que el envío (push) sea rechazado.
Puedes obtener la lista de las claves SHA-1 de todos las confirmaciones de
cambios enviadas recogiendo los valores de $newrev
y de $oldrev
, y pasándolos
al comando de mantenimiento de Git llamado git rev-list
. Este comando es
básicamente el mismo que git log
, pero por defecto, imprime sólo los
valores SHA-1 y nada más. Con él, puedes obtener la lista de todas las claves
SHA que se han introducido entre una clave SHA y otra clave SHA dadas;
obtendrás algo así como esto:
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
Puedes recoger esta salida, establecer un bucle para recorrer cada una de esas confirmaciones de cambios, recoger el mensaje de cada una y comprobarlo contra una expresión regular de búsqueda del patrón deseado.
Tienes que imaginarte cómo puedes obtener el mensaje de cada una de esas
confirmaciones de cambios a comprobar. Para obtener los datos "en crudo" de
una confirmación de cambios, puedes utilizar otro comando de mantenimiento de
Git denominado git cat-file
. En [ch10-git-internals] volveremos en detalle
sobre estos comandos de mantenimiento; pero, por ahora, esto es lo que
obtienes con dicho comando:
$ git cat-file commit ca82a6
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
changed the version number
Una vía sencilla para obtener el mensaje, es la de ir hasta la primera línea
en blanco y luego tomar todo lo que siga a ésta. En los sistemas Unix, lo
puedes realizar con el comando sed
:
$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number
Puedes usar este "truco de magia" para recoger el mensaje de cada confirmación de cambios que se está enviando y salir si localizas algo que no cuadra en alguno de ellos. Para salir del script y rechazar el envío, recuerda que debes salir con un código distinto de cero. El método completo será algo así como:
$regex = /\[ref: (\d+)\]/
# enforced custom commit message format
def check_message_format
missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
missed_revs.each do |rev|
message = `git cat-file commit #{rev} | sed '1,/^$/d'`
if !$regex.match(message)
puts "[POLICY] Your message is not formatted correctly"
exit 1
end
end
end
check_message_format
Poniendo esto en tu script update
, serán rechazadas todas las actualizaciones
que contengan cambios con mensajes que no se ajusten a tus reglas.
Implementando un sistema de listas de control de acceso (ACL)
Imaginemos que deseas implementar un sistema de control de accesos (Access
Control List, ACL), para vigilar qué usuarios pueden enviar (push) cambios a
qué partes de tus proyectos. Algunas personas tendrán acceso completo, y
otras tan solo acceso a ciertas carpetas o a ciertos archivos. Para implementar
esto, has de escribir esas reglas de acceso en un archivo denominado acl
ubicado en tu repositorio git básico (bare) en el servidor. Y tienes que
preparar el enganche update
para hacerle consultar esas reglas, mirar los
archivos que están siendo subidos en las confirmaciones de cambio (commit)
enviadas (push), y determinar así si el usuario emisor del envío tiene o no
permiso para actualizar esos archivos.
El primer paso es escribir tu lista de control de accesos (ACL). Su formato es
muy parecido al del mecanismo ACL de CVS: utiliza una serie de líneas donde el
primer campo es avail
o unavail
(permitido o no permitido), el segundo
campo es una lista de usuarios separados por comas, y el último campo es la
ubicación (path) sobre la que aplicar la regla (dejarlo en blanco equivale a un
acceso abierto). Cada uno de esos campos se separan entre sí con el carácter
barra vertical (|).
Por ejemplo, si tienes un par de administradores, algunos redactores técnicos
con acceso a la carpeta doc
, y un desarrollador que únicamente accede a las
carpetas lib
y tests
, el archivo ACL resultante sería:
avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests
Para implementarlo, hemos de leer previamente estos datos en una estructura
que podamos emplear. En este caso, por razones de simplicidad, vamos a mostrar
únicamente la forma de implementar las directivas avail
(permitir). Este es
un método que te devuelve un array asociativo cuya clave es el nombre del
usuario y su valor es un array de ubicaciones (paths) donde ese usuario tiene
acceso de escritura:
def get_acl_access_data(acl_file)
# read in ACL data
acl_file = File.read(acl_file).split("\n").reject { |line| line == '' }
access = {}
acl_file.each do |line|
avail, users, path = line.split('|')
next unless avail == 'avail'
users.split(',').each do |user|
access[user] ||= []
access[user] << path
end
end
access
end
Si lo aplicamos sobre la lista ACL descrita anteriormente, este método
get_acl_access_data
devolverá una estructura de datos similar a esta:
{"defunkt"=>[nil],
"tpw"=>[nil],
"nickh"=>[nil],
"pjhyett"=>[nil],
"schacon"=>["lib", "tests"],
"cdickens"=>["doc"],
"usinclair"=>["doc"],
"ebronte"=>["doc"]}
Una vez tienes los permisos en orden, necesitas averiguar las ubicaciones modificadas por las confirmaciones de cambios enviadas; de tal forma que puedas asegurarte de que el usuario que las está enviando tiene realmente permiso para modificarlas.
Puedes comprobar fácilmente qué archivos han sido modificados en cada
confirmación de cambios, utilizando la opción --name-only
del comando
git log
(citado brevemente en el capítulo 2):
$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rb
Utilizando la estructura ACL devuelta por el método get_acl_access_data
y
comprobándola sobre la lista de archivos de cada confirmación de cambios,
puedes determinar si el usuario tiene o no permiso para enviar dichos cambios:
# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
access = get_acl_access_data('acl')
# see if anyone is trying to push something they can't
new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
new_commits.each do |rev|
files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n")
files_modified.each do |path|
next if path.size == 0
has_file_access = false
access[$user].each do |access_path|
if !access_path # user has access to everything
|| (path.start_with? access_path) # access to this path
has_file_access = true
end
end
if !has_file_access
puts "[POLICY] You do not have access to push to #{path}"
exit 1
end
end
end
end
check_directory_perms
Se puede obtener una lista de los nuevos commits a enviar con git rev-list
.
Para cada uno de ellos, puedes ver qué archivos se quieren modificar y
asegurarte que el usuario que está enviando los archivos tiene acceso a todos
ellos.
Desde este momento, los usuarios ya no podrán subir cambios con mensajes de confirmación que no cumplan las reglas, o cuando intenten modificar archivos a los que no tienen acceso.
Comprobación
Si lanzas chmod u+x .git/hooks/update
, siendo este el archivo en el que hemos
introducido el código anterior, y probamos a subir un commit
con un mensaje que
no cumple las reglas, verás algo como esto:
$ git push -f origin master
Counting objects: 5, done.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 323 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
Enforcing Policies...
(refs/heads/master) (8338c5) (c5b616)
[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
Hay un par de cosas interesantes aquí. Lo primero es que ves dónde empieza la ejecución del enganche.
Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)
Recuerda que imprimías esto al comienzo del script. Todo lo que el script
imprima sobre la salida estándar (stdout
) se enviará al cliente.
Lo siguiente que ves es el mensaje de error.
[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
La primera línea la imprime tu script, las otras dos son las que usa Git para decirte que el script finalizó con error (devuelve un valor distinto de cero) y que está rechazando tu envío. Por último, tienes esto:
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
Verás un mensaje de rechazo remoto para cada referencia que tu script ha rechazado, y te dice que fue rechazado explícitamente debido al fallo del script de enganche.
Y más aún, si alguien intenta realizar un envío, en el que haya confirmaciones
de cambio que afecten a archivos a los que ese usuario no tiene acceso, verá
algo similar. Por ejemplo, si un documentalista intenta tocar algo de la
carpeta lib
, verá esto:
[POLICY] You do not have access to push to lib/test.rb
Y eso es todo. De ahora en adelante, en tanto el script update
esté
presente y sea ejecutable, tu repositorio nunca se verá perjudicado, nunca
tendrá un mensaje de confirmación de cambios sin tu plantilla y tus usuarios
estarán controlados.
Puntos de enganche del lado cliente
Lo malo del sistema descrito en la sección anterior pueden ser los lamentos que inevitablemente se van a producir cuando los envíos de tus usuarios sean rechazados. Ver rechazado en el último minuto su tan cuidadosamente preparado trabajo, puede ser realmente frustrante. Y, aún peor, tener que reescribir su histórico para corregirlo puede ser un auténtico calvario.
La solución a este dilema es el proporcionarles algunos enganches (hook) del
lado cliente, para que les avisen cuando están trabajando en algo que el
servidor va a rechazarles. De esta forma, pueden corregir los problemas antes
de confirmar cambios y antes de que se conviertan en algo realmente complicado
de arreglar. Debido a que estos enganches no se transfieren junto con el
clonado de un proyecto, tendrás que distribuirlos de alguna otra manera. Y
luego pedir a tus usuarios que se los copien a sus carpetas .git/hooks
y
los hagan ejecutables. Puedes distribuir esos enganches dentro del mismo
proyecto o en un proyecto separado. Pero no hay modo de implementarlos
automáticamente.
Para empezar, se necesita chequear el mensaje de confirmación inmediatamente
antes de cada confirmación de cambios, para asegurarse de que el servidor no
los rechazará debido a un mensaje mal formateado. Para ello, se añade el
enganche commit-msg
. Comparando el mensaje del archivo pasado como primer
argumento con el mensaje patrón, puedes obligar a Git a abortar la confirmación
de cambios (commit) en caso de no coincidir ambos:
#!/usr/bin/env ruby
message_file = ARGV[0]
message = File.read(message_file)
$regex = /\[ref: (\d+)\]/
if !$regex.match(message)
puts "[POLICY] Your message is not formatted correctly"
exit 1
end
Si este script está en su sitio (el archivo .git/hooks/commit-msg
) y es
ejecutable, al confirmar cambios con un mensaje inapropiado, verás algo así
como:
$ git commit -am 'test'
[POLICY] Your message is not formatted correctly
Y la confirmación no se llevará a cabo. Sin embargo, si el mensaje está formateado adecuadamente, Git te permitirá confirmar cambios:
$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
1 file changed, 1 insertions(+), 0 deletions(-)
A continuación,los usuarios necesitan también asegurarse de no estar modificando
archivos fuera del alcance de tus permisos. Si la carpeta .git
de tu
proyecto contiene una copia del archivo de control de accesos (ACL) utilizada
previamente, este script pre-commit
podrá comprobar los límites:
#!/usr/bin/env ruby
$user = ENV['USER']
# [ insert acl_access_data method from above ]
# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
access = get_acl_access_data('.git/acl')
files_modified = `git diff-index --cached --name-only HEAD`.split("\n")
files_modified.each do |path|
next if path.size == 0
has_file_access = false
access[$user].each do |access_path|
if !access_path || (path.index(access_path) == 0)
has_file_access = true
end
if !has_file_access
puts "[POLICY] You do not have access to push to #{path}"
exit 1
end
end
end
check_directory_perms
Este es un script prácticamente igual al del lado servidor. Pero con dos importantes diferencias. La primera es que el archivo ACL está en otra ubicación, debido a que el script corre desde tu carpeta de trabajo y no desde la carpeta de Git. Esto obliga a cambiar la ubicación del archivo ACL de aquí:
access = get_acl_access_data('acl')
a este otro sitio:
access = get_acl_access_data('.git/acl')
La segunda diferencia es la forma de listar los archivos modificados. Debido a que el método del lado servidor utiliza el registro de confirmaciones de cambio, pero aquí sin embargo, la confirmación no se ha registrado aún, la lista de archivos se ha de obtener desde el área de preparación (staging area). En lugar de:
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
tenemos que utilizar:
files_modified = `git diff-index --cached --name-only HEAD`
Estas dos son las únicas diferencias; en todo lo demás, el script funciona de
la misma manera. Es necesario advertir de que se espera que trabajes localmente
con el mismo usuario con el que enviarás (push) a la máquina remota. Si no
fuera así, tendrás que ajustar manualmente la variable $user
.
El último aspecto a comprobar es el de no intentar enviar referencias que no sean de avance-rápido. Pero esto es algo más raro que suceda. Para tener una referencia que no sea de avance-rápido, tienes que haber reorganizado (rebase) una confirmación de cambios (commit) ya enviada anteriormente, o tienes que estar tratando de enviar una rama local distinta sobre la misma rama remota.
De todas formas, el único aspecto accidental que puede interesante capturar son los intentos de reorganizar confirmaciones de cambios ya enviadas. El servidor te avisará de que no puedes enviar ningún no-avance-rapido, y el enganche te impedirá cualquier envío forzado.
Este es un ejemplo de script previo a la reorganización que lo puede comprobar. Con la lista de confirmaciones de cambio que estás a punto de reescribir, las comprueba por si alguna de ellas existe en alguna de tus referencias remotas. Si encuentra alguna, aborta la reorganización:
#!/usr/bin/env ruby
base_branch = ARGV[0]
if ARGV[1]
topic_branch = ARGV[1]
else
topic_branch = "HEAD"
end
target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n")
remote_refs = `git branch -r`.split("\n").map { |r| r.strip }
target_shas.each do |sha|
remote_refs.each do |remote_ref|
shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
if shas_pushed.split("\n").include?(sha)
puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}"
exit 1
end
end
end
Este script utiliza una sintaxis no contemplada en la sección de Selección de Revisiones del capítulo 6. La lista de confirmaciones de cambio previamente enviadas, se comprueba con:
`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
La sintaxis SHA^@ recupera todos los padres de esa confirmación de cambios (commit). Estás mirando por cualquier confirmación que se pueda alcanzar desde la última en la parte remota, pero que no se pueda alcanzar desde ninguno de los padres de cualquiera de las claves SHA que estás intentando enviar. Es decir, confirmaciones de avance-rápido.
La mayor desventaja de este sistema es que puede llegar a ser muy lento; y muchas veces es innecesario, ya que el propio servidor te va a avisar y te impedirá el envío, siempre y cuando no intentes forzar dicho envío con la opción -f. De todas formas, es un ejercicio interesante. Y, en teoría al menos, pude ayudarte a evitar reorganizaciones que luego tengas de echar para atrás y arreglarlas.