En aquesta secció, utilitzareu el que heu après per establir un flux de treball de Git que verifica un format de missatge de commit personalitzat i permet només a certs usuaris modificar certs subdirectoris en un projecte. Construireu scripts del costat del client que ajudin al desenvolupador a saber si el seu push serà rebutjat i scripts del costat del servidor que apliquin realment les polítiques.
Els scripts que mostrarem estan escrits en Ruby; en part a causa de la nostra inèrcia intel·lectual, però també perquè Ruby és fàcil de llegir, fins i tot si no podeu necessàriament escriure’l. No obstant això, qualsevol llenguatge funcionarà: tots els scripts de ganxo de mostra distribuïts amb Git estan en Perl o Bash, així que també podeu veure molts exemples de ganxos en aquests llenguatges mirant les mostres.
Tot el treball del costat del servidor anirà al fitxer update al vostre directori hooks. El ganxo update s’executa una vegada per cada branca que s’està enviant i pren tres arguments:
-
El nom de la referència a la qual s’està enviant.
-
L’antiga revisió on estava aquella branca.
-
La nova revisió que s’està enviant.
També teniu accés a l’usuari que està fent el push si el push s’està executant sobre SSH. Si heu permès que tots es connectin amb un únic usuari (com “git”) mitjançant autenticació de clau pública, potser haureu de donar a aquest usuari un wrapper de shell que determini quin usuari es connecta basant-se en la clau pública i estableixi una variable d’entorn en conseqüència. Aquí suposarem que l’usuari connectat està a la variable d’entorn $USER, així que el vostre script d’actualització comença recollint tota la informació que necessiteu:
#!/usr/bin/env ruby
$refname = ARGV[0]
$oldrev = ARGV[1]
$newrev = ARGV[2]
$user = ENV['USER']
puts "Aplicant Polítiques..."
puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"Sí, aquestes són variables globals. No jutgeu: és més fàcil demostrar-ho així.
El vostre primer desafiament és fer complir que cada missatge de commit segueixi un format particular. Només per tenir un objectiu, suposem que cada missatge ha d’incloure una cadena que sembli “ref: 1234” perquè voleu que cada commit estigui vinculat a un element de treball al vostre sistema de tickets. Heu de mirar cada commit que s’està pujant, veure si aquesta cadena està al missatge del commit i, si la cadena està absent en qualsevol dels commits, sortir amb un codi diferent de zero perquè el push sigui rebutjat.
Podeu obtenir una llista dels valors SHA-1 de tots els commits que s’estan pujant prenent els valors $newrev i $oldrev i passant-los a una comanda de plomería de Git anomenada git rev-list. Això és bàsicament la comanda git log, però per defecte imprimeix només els valors SHA-1 i cap altra informació. Així que, per obtenir una llista de tots els SHA-1 dels commits introduïts entre un SHA-1 de commit i un altre, podeu executar alguna cosa com això:
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475Podeu prendre aquesta sortida, fer un bucle a través de cada un d’aquests SHA-1 de commit, agafar el missatge per a ell i provar aquest missatge contra una expressió regular que busqui un patró.
Heu de descobrir com obtenir el missatge de commit de cadascun d’aquests commits per provar-lo. Per obtenir les dades brutes del commit, podeu utilitzar una altra comanda de plomería anomenada git cat-file. Veurem totes aquestes comandes de plomería en detall a ch10-git-internals.asc; però per ara, aquí teniu el que us dona aquesta comanda:
$ 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
Change the version numberUna manera senzilla d’obtenir el missatge de commit d’un commit quan teniu el valor SHA-1 és anar a la primera línia en blanc i prendre tot el que hi ha després. Podeu fer-ho amb la comanda sed en sistemes Unix:
$ git cat-file commit ca82a6 | sed '1,/^$/d'
Change the version numberPodeu utilitzar aquesta incantació per agafar el missatge de commit de cada commit que està intentant ser pujat i sortir si veieu alguna cosa que no coincideix. Per sortir de l’script i rebutjar el push, sortiu amb un codi diferent de zero. Tot el mètode sembla així:
$regex = /\[ref: (\d+)\]/
# format de missatge de commit personalitzat aplicat
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 "[POLÍTICA] El vostre missatge no està formatat correctament"
exit 1
end
end
end
check_message_formatPosar això al vostre script update rebutjarà les actualitzacions que continguin commits que tinguin missatges que no segueixin la vostra regla.
Suposem que voleu afegir un mecanisme que utilitzi una llista de control d’accés (ACL) que especifiqui quins usuaris tenen permís per pujar canvis a quines parts dels vostres projectes. Algunes persones tenen accés complet, i altres només poden pujar canvis a certs subdirectoris o fitxers específics. Per aplicar això, escriureu aquestes regles en un fitxer anomenat acl que es troba al vostre repositori Git nu al servidor. Fareu que el ganxo update miri aquestes regles, vegi quins fitxers s’estan introduint per a tots els commits que s’estan pujant i determini si l’usuari que està fent el push té accés per actualitzar tots aquests fitxers.
El primer que fareu és escriure la vostra ACL. Aquí utilitzareu un format molt semblant al mecanisme ACL de CVS: utilitza una sèrie de línies, on el primer camp és avail o unavail, el següent camp és una llista delimitada per comes dels usuaris als quals s’aplica la regla, i l’últim camp és la ruta a la qual s’aplica la regla (en blanc significa accés obert). Tots aquests camps estan delimitats per un caràcter de barra vertical (|).
En aquest cas, teniu un parell d’administradors, alguns escriptors de documentació amb accés al directori doc, i un desenvolupador que només té accés als directoris lib i tests, i el vostre fitxer ACL sembla així:
avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|testsComenceu llegint aquestes dades en una estructura que pugueu utilitzar. En aquest cas, per mantenir l’exemple simple, només aplicareu les directives avail. Aquí teniu un mètode que us dona un array associatiu on la clau és el nom d’usuari i el valor és un array de camins als quals l’usuari té accés d’escriptura:
def get_acl_access_data(acl_file)
# llegir dades ACL
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
endAl fitxer ACL que heu vist anteriorment, aquest mètode get_acl_access_data retorna una estructura de dades que sembla així:
{"defunkt"=>[nil],
"tpw"=>[nil],
"nickh"=>[nil],
"pjhyett"=>[nil],
"schacon"=>["lib", "tests"],
"cdickens"=>["doc"],
"usinclair"=>["doc"],
"ebronte"=>["doc"]}Ara que teniu els permisos ordenats, heu de determinar quins camins han modificat els commits que s’estan pujant, per assegurar-vos que l’usuari que està pujant té accés a tots ells.
Podeu veure fàcilment quins fitxers han estat modificats en un únic commit amb l’opció --name-only de la comanda git log (esmentada breument a ch02-git-basics-chapter.asc):
$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rbSi utilitzeu l’estructura ACL retornada pel mètode get_acl_access_data i la compareu amb els fitxers llistats en cadascun dels commits, podeu determinar si l’usuari té accés per pujar tots els seus commits:
# només permet a certs usuaris modificar certs subdirectoris en un projecte
def check_directory_perms
access = get_acl_access_data('acl')
# veure si algú està intentant pujar alguna cosa que no pot
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 # l'usuari té accés a tot
|| (path.start_with? access_path) # accés a aquest camí
has_file_access = true
end
end
if !has_file_access
puts "[POLÍTICA] No teniu accés per pujar a #{path}"
exit 1
end
end
end
end
check_directory_permsObteniu una llista dels nous commits que s’estan pujant al vostre servidor amb git rev-list. Llavors, per a cadascun d’aquests commits, trobeu quins fitxers estan modificats i assegureu-vos que l’usuari que està pujant té accés a tots els camins que estan sent modificats.
Ara els vostres usuaris no poden pujar cap commit amb missatges mal formatats o amb fitxers modificats fora dels seus camins designats.
Si executeu chmod u+x .git/hooks/update, que és el fitxer on hauríeu d’haver posat tot aquest codi, i després intenteu pujar un commit amb un missatge no compliant, obteniu alguna cosa com això:
$ 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.
Aplicant Polítiques...
(refs/heads/master) (8338c5) (c5b616)
[POLÍTICA] El vostre missatge no està formatat correctament
error: hooks/update va sortir amb codi d'error 1
error: el ganxo va declinar actualitzar refs/heads/master
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: no es van poder pujar algunes referències a 'git@gitserver:project.git'Hi ha un parell de coses interessants aquí. Primer, veieu això on el ganxo comença a executar-se.
Aplicant Polítiques...
(refs/heads/master) (fb8c72) (c56860)Recordeu que vau imprimir això al principi del vostre script d’actualització. Tot el que el vostre script ecoï a stdout serà transferit al client.
El següent que notareu és el missatge d’error.
[POLÍTICA] El vostre missatge no està formatat correctament
error: hooks/update va sortir amb codi d'error 1
error: el ganxo va declinar actualitzar refs/heads/masterLa primera línia va ser impresa per vosaltres, les altres dues van ser Git dient-vos que el script d’actualització va sortir amb un codi diferent de zero i que això és el que està rebutjant el vostre push. Finalment, teniu això:
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: no es van poder pujar algunes referències a 'git@gitserver:project.git'Veureu un missatge de referència remota rebutjada per a cada referència que el vostre ganxo va declinar, i us diu que va ser rebutjada específicament a causa d’una fallada del ganxo.
A més, si algú intenta editar un fitxer al qual no té accés i puja un commit que el conté, veuran alguna cosa similar. Per exemple, si un autor de documentació intenta pujar un commit modificant alguna cosa al directori lib, veuran:
[POLÍTICA] No teniu accés per pujar a lib/test.rbA partir d’ara, sempre que aquest script update estigui allà i sigui executable, el vostre repositori mai no tindrà un missatge de commit sense el vostre patró i els vostres usuaris estaran en una caixa de sorra.
L’inconvenient d’aquest enfocament és el llaç que inevitablement resultarà quan els pushes dels commits dels vostres usuaris siguin rebutjats. Que el seu treball acuradament elaborat sigui rebutjat en l’últim moment pot ser extremadament frustrant i confús; i a més, hauran d’editar el seu historial per corregir-lo, cosa que no sempre és per a coratjosos.
La resposta a aquest dilema és proporcionar alguns ganxos del costat del client que els usuaris puguin executar per notificar-los quan estiguin fent alguna cosa que el servidor probablement rebutjarà. D’aquesta manera, poden corregir qualsevol problema abans de fer el commit i abans que aquests problemes es facin més difícils de solucionar. Com que els ganxos no es transfereixen amb un clon d’un projecte, heu de distribuir aquests scripts d’una altra manera i després fer que els vostres usuaris els copiïn al seu directori .git/hooks i els facin executables. Podeu distribuir aquests ganxos dins del projecte o en un projecte separat, però Git no els configurarà automàticament.
Per començar, hauríeu de verificar el vostre missatge de commit just abans que es registri cada commit, per saber que el servidor no rebutjarà els vostres canvis a causa de missatges de commit mal formatats. Per fer això, podeu afegir el ganxo commit-msg. Si el feu llegir el missatge del fitxer passat com a primer argument i compareu això amb el patró, podeu forçar Git a avortar el commit si no hi ha coincidència:
#!/usr/bin/env ruby
message_file = ARGV[0]
message = File.read(message_file)
$regex = /\[ref: (\d+)\]/
if !$regex.match(message)
puts "[POLÍTICA] El vostre missatge no està formatat correctament"
exit 1
endSi aquest script està en el seu lloc (a .git/hooks/commit-msg) i és executable, i feu un commit amb un missatge que no està correctament formatat, veureu això:
$ git commit -am 'Test'
[POLÍTICA] El vostre missatge no està formatat correctamentNo es va completar cap commit en aquesta instància. No obstant això, si el vostre missatge conté el patró adequat, Git us permet fer el commit:
$ git commit -am 'Test [ref: 132]'
[master e05c914] Test [ref: 132]
1 file changed, 1 insertions(+), 0 deletions(-)A continuació, voleu assegurar-vos que no esteu modificant fitxers que estan fora de l’abast del vostre ACL. Si el directori .git del vostre projecte conté una còpia del fitxer ACL que vau utilitzar anteriorment, llavors el següent script pre-commit us farà complir aquestes restriccions:
#!/usr/bin/env ruby
$user = ENV['USER']
# [ inserir mètode acl_access_data de més amunt ]
# només permet a certs usuaris modificar certs subdirectoris en un projecte
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 "[POLÍTICA] No teniu accés per pujar a #{path}"
exit 1
end
end
end
end
check_directory_permsAixò és més o menys el mateix script que la part del costat del servidor, però amb dues diferències importants. Primer, el fitxer ACL està en un lloc diferent, perquè aquest script s’executa des del vostre directori de treball, no des del vostre directori .git. Heu de canviar el camí al fitxer ACL d’aquest:
access = get_acl_access_data('acl')a això:
access = get_acl_access_data('.git/acl')L’altra diferència important és la manera com obteniu una llista dels fitxers que han estat canviats. Com que el mètode del costat del servidor mira el registre de commits, i, en aquest punt, el commit encara no s’ha registrat, heu d’obtenir la vostra llista de fitxers de l’àrea d’staging. En lloc de:
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`heu d’utilitzar:
files_modified = `git diff-index --cached --name-only HEAD`Però aquestes són les úniques dues diferències: d’una altra manera, l’script funciona de la mateixa manera. Un advertiment és que espera que esteu executant localment com el mateix usuari que pugeu al servidor remot. Si això és diferent, heu d’establir la variable $user manualment.
Una altra cosa que podem fer aquí és assegurar-nos que l’usuari no puja referències no fast-forward. Per obtenir una referència que no és fast-forward, o bé heu de fer un rebase després d’un commit que ja heu pujat o intentar pujar una branca local diferent a la mateixa branca remota.
Suposadament, el servidor ja està configurat amb receive.denyDeletes i receive.denyNonFastForwards per fer complir aquesta política, així que l’única cosa accidental que podeu intentar atrapar és el rebase de commits que ja han estat pujats.
Aquí teniu un exemple de script pre-rebase que comprova això. Obté una llista de tots els commits que esteu a punt de reescriure i comprova si existeixen en alguna de les vostres referències remotes. Si veu un que és accessible des d’una de les vostres referències remotes, avorta el rebase.
#!/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 "[POLÍTICA] El commit #{sha} ja ha estat pujat a #{remote_ref}"
exit 1
end
end
endAquest script utilitza una sintaxi que no es va cobrir a ch07-git-tools.asc. Obteniu una llista de commits que ja han estat pujats executant això:
`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`La sintaxi SHA^@ es resol a tots els pares d’aquell commit. Esteu buscant qualsevol commit que sigui accessible des de l’últim commit al remot i que no sigui accessible des de cap pare de qualsevol dels SHA-1 que esteu intentant pujar, el que significa que és un fast-forward.
L’inconvenient principal d’aquest enfocament és que pot ser molt lent i sovint és innecessari: si no intenteu forçar el push amb -f, el servidor us advertirà i no acceptarà el push. No obstant això, és un exercici interessant i en teoria us pot ajudar a evitar un rebase que posteriorment hàgiu de tornar enrere i corregir.