Git
Chapters ▾ 2nd Edition

8.4 Prilagođavanje Gita - Primer polise sprovedene od strane Gita

Primer polise sprovedene od strane Gita

U ovom odeljku ćete iskoristiti ono što ste naučli da uspostavite tok rada sa Gitom koji proverava da li je ispunjen određeni format komit poruke, i dozvoljava samo određenim korisnicima da modifikuju određene poddirektorijume iz projekta. Napravićete klijentske skripte koje pomažu developeru da zna da li će podaci koje želi da gurne biti prihvaćeni, i serversku skriptu koja zapravo sprovodi ove polise.

Skripte koje ćemo predstaviti su pisane u Rubiju; delimično zbog naše intelektualne inercije, ali i zbog toga što se Rubi lako čita, iako ne mora da znači da umete da napišete nešto u njemu. Ipak, svaki jezik će uroditi plodom — sve skripte koje Git nudi kao primer su ili u Perlu ili u Bešu, tako da možete da vidite puno primera hukova i u ovim jezicima tako što ćete ih otvoriti.

Huk na serverskoj strani

Sav rad na serverskoj strani ide u datoteci update u direktorijumu hooks. Huk update se pokreće jednom po grani koja se gura i ima tri argumenta:

  • ime reference na koju se gura,

  • stara revizija gde se grana ranije nalazila i

  • nova revizija koja se gura.

Sem toga, imate pristup korisniku koji obavlja guranje ako se ono obavlja preko SSH-a. Ako ste dozvolili svakome da se poveže kao jedan korisnik (npr. "git") preko autentifikacije javnim ključem, možda ćete morati da tom korisniku date šel-omotač koji na osnovu javnog ključa određuje koji se korisnik povezao, i da, shodno tome, podesite promenljivu okruženja. Ovde ćemo pretpsotaviti da se korisnik koji se nakačio nalazi u promenljivoj okruženja $USER, tako da skripta update počinje prikupljanjem svih informacija koje su vam potrebne:

#!/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]})"

Da, to su globalne promenljive. Ne osuđujte nas — lakše je pokazati primer tako.

Podsticanje određenog formata za komit poruke

Prvi izazov jeste nametnuti polisu da svaka komit poruka mora da prati određeni format. Čisto da bismo imali neki cilj, pretpostavićemo da svaka poruka mora da sadrži string oblika ref: 1234, jer želite da svaki komit bude povezan sa nekim tiketom. Morate da pregledate svaki komit koji se gura, da ispitate da li se taj string nalazi u svakoj komit poruci, i, ako nedostaje u makar jednom komitu, vratite vrednost različitu od nule kako biste odbili guranje.

Možete da pribavite listu SHA-1 vrednosti svih komitova koji se guraju tako što ćete uzeti vrednosti $newref i $oldref i proslediti ih Gitovoj datavodnoj komandi git rev-list . Ovo je u suštini komanda git log, ali po podrazumevanim podešavanjima štampa samo SHA-1 vrednosti i nikakve druge informacije. Dakle, da biste dobili listu SHA-1 vrednosti iz komitova koji se javljaju između jednog SHA-1 i drugog, pokrenite nešto ovako:

$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475

Možete da uzmete taj izlaz, da prođete petljom kroz svaki od ovih SHA-1 vrednosti komita, uzmete poruku za njega, i testirate je spram regularne ekspresije koja traži šablon.

Morate da provalite kako da dobijete komit poruku za svaku od ovih komitova kako biste je testirali. Za sirove podatke o komitu, možete da koristite još jednu datavodnu komandu git cat-file. U Git iznutra ćemo detaljnije preći ove datavodne komande; zasad, evo šta vam ona vraća:

$ 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

Jednostavan način da dobijete komit poruku iz komita kada imate njegov SHA-1 jeset da odete na prvu praznu liniju i uzmete sve posle toga. To možete uraditi komandom sed na Juniks sistemima:

$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number

Možete da uzmete tu čarobnu reč da zgrabite komit poruku iz svakog komita koji treba da bude gurnut, i da izađete ako vidite nešto što se ne uklapa. Da izađete iz skripte i odbijete guranje, kao povratnu vrednost na izlazu vratite nešto što nije nula. Cela metoda izleda ovako:

$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

Ako stavite to u skriptu update, biće odbijeno guranje podataka koji sadrže komitove čije se poruke ne uklapaju u specificirani šablon.

Forsiranje ACL sistema baziranog na korisnicima

Recimo da želite da dodate mehanizam koji koristi liste za kontrolu pristupa (ACL) koja određuje kojim korisnicima je dozovljeno guranje promena kojim delovima projekta. Neki ljudi imaju potpun pristup, a drugi mogu da guranju promene samo kod određenih poddirektorijuma ili u određenim datotekama. Na biste primenili ovakvu polisu, morate da zapišete ta pravila u daoteci s imenom acl koju treba da smestite u golom Git repozitorijumu na serveru. Huk update će nadgledati ta pravila, videće koje datoteke imaju promene za sve komitove koji se guraju, i odrediće da li taj korisnik ima pristup ažuriranju tih datoteka.

Prva stvar koju treba da uradite jeste da napišete ACL. Ovde ćemo koristiti format koji ej veoma sličan CVS ACL mehanizam: koristi niz linija, gde je prvo polje avail ili unavail, a sledeće polje je lista korisnika koji za koje važi pravilo, razgraničena zarezima, dok je poslednje polje putanja za koju važi pravilo (pri čemu prazno znači otvoreni pristup). Ova polja su ograničena cevkom (|).

U ovom slučaju, imate nekoliko administratora, neke ljude zadužene za dokumentaciju koji imaju pristup direktorijumu doc, i jednog developera koji ima pristup samo direktorijumima lib i tests — ACL onda izgleda ovako:

avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests

Prvo čitate ove podatke u strukturu koje možete da iskoristite. U ovom slučaju, da bi primer bio jednostavan, koristećemo samo direktorive avail. Evo metode koja vam daje asocijativan niz gde je klju korisničko ime a vrednost je niz putanja u kojima korisnik ima dozvolu za pisanje.

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

Kada se metoda get_acl_access_data pozove nad ACL datoteku koju ste videli ranije, struktura podataka koja se dobija izleda ovako:

{"defunkt"=>[nil],
 "tpw"=>[nil],
 "nickh"=>[nil],
 "pjhyett"=>[nil],
 "schacon"=>["lib", "tests"],
 "cdickens"=>["doc"],
 "usinclair"=>["doc"],
 "ebronte"=>["doc"]}

Sada je sređena stvar oko permisija, treba da odredite koje to putanje modifikuju gurnuti komitovi, kako biste bili sigurni da korisnik ima pristup svima.

Prilično lako možete da vidite koje datoteke su modifikovane u jednom komitu koristeći opciju --name-only uz komandu git log (kratko opisanu u drugom poglavlju).

$ git log -1 --name-only --pretty=format:'' 9f585d

README
lib/test.rb

Ako uzmete ACL strukturu koju je vratila metoda get_acl_access_data i uporedite je sa datotekama koje su izlistane u svakom od komitova, možete da odredite da li korisnik ima pravo da gurne sve komitove:

# dozvoljava samo određenim korisnicima da modifikuju doređene poddirektorijume
def check_directory_perms
  access = get_acl_access_data('acl')

  # proveri da li neko pokušava da gurne promene koje ne sme
  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  # pristup svemu
           || (path.start_with? access_path) # pristup ovoj putanji
          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

Dobijate listu novih komitova koji se guraju na server komandom git rev-list. Onda, za svaki od ovih komitova, tražite koje datoteke se modifikuju i postarate se da korisnik koji gura ima pristup svim putanjama koje se modifikuju.

Sada korisnici ne mogu da guraju komitove sa loše formatiranim porukama ili sa promenama koje modifikuju datoteke van putanja koje su im dodeljene.

Testiranje

Ako pokrenete chmod u+x .git/hooks/update, a to je datoteka u koju treba da stavite sav ovaj kod, i onda probate da gurnete komit sa porukom koja se ne uklapa u polisu, dobićete nešto ovako:

$ 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'

Ovde ima nekoliko zanimljivih stvari. Prvo, vidite gde se tačno huk pokreće.

Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)

Setite se da se to odštampali na samom početku skripte update. Sve što vaša skripta odštampa na stdout-u će se preneti klijentu.

Sledeća stvar koju možete da primetite jesu poruke o greškama.

[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master

Prvu liniju ste vi odštampali, a ostale sve su od strane Gita koji vam govori da je skripta update vratila vrednost različitu od nule i da je to ono zbog čega guranje nije uspelo. Sem toga, tu je i ovo:

To git@gitserver:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'

Videćete poruku remote rejected za svaku referencu koju je huk odbio, što vam govori da je vaš zahtev odbijen zbog huka.

Štaviše, ako neko pokuša da promeni datoteku kojoj nemaju pristup i onda gurnu komit koji je sadrži, videćete nešto slično. Na primer, ako autor dokumentacije pokuša da gurne komit koji menja nešto iz direktorijuma lib, videće sledeće:

[POLICY] You do not have access to push to lib/test.rb

Odsad pa nadalje, sve dok je skripta update izvršna i nalazi se na mestu, repozitorijum nikada neće imati komit poruku bez navedenog šablona, a korisnici će imati ograničen pristup datotekema.

Hukovi na strani klijenta

Loša strana ovog pristupa jesu očajnički krici korisnika kao rezultat činjenice da im je komit odbijen. Kad rad u koji je uloženo puno truda bude odbijen u poslednjem trenutku, to zna da bude dosta frustrirajuće i zbunjujuće; štaviše, moraće da promene svoju istoriju kako bi ispravili problem, a to nije posao za one sa slabim srcem.

Odgovor ovoj dilemi je postavljanje hukova na strani klijenta koju korisnici mogu da pokrenu i da na vreme budu obašteni o tome da rade nešto što će server verovatno odbiti. Na taj način mogu da reše problem pre nego što komituju i pre nego što ti problemi budu teži za rešavanje. Pošto se hukovi ne prenose zajedno sa klonom projekta, moraćete da distribuirate te skripte na neki drugi način i onda objasnite korisnicima da treba da ih smeste u direktorijum .git/hooks i da ih učine izvršnim. Možete da distribuirate ove hukove u okviru projekta ili kao poseban projekat, ali ih Git neće automatski postaviti.

Za početak, treba da proverite komit poruku pre nego što se svaki komit zaebleži, kako biste znali da vam server neće odbiti promene zbog loše formatiranih komit poruka. Da biste uradili ovo, možete dodati huk commit-msg. Ako ga podesite tako čita poruke iz datoteke koja se prosledi kao prvi agrument i onda uporedite to sa šablonom, možete da forsirate Git da obustavi kreiranje komita ako nse ne pronađe poklapanje:

#!/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

Ako se ta skripta namesti gde treba (u .git/hooks/commit-msg) i podesi kao izvršna datoteka, a korisnik napravi komit sa porukom koja nije ispravno formatiarna, videćete ovo:

$ git commit -am 'test'
[POLICY] Your message is not formatted correctly

Komit nije završen u tom slučaju. Ipak, ako komit sadrži odgovarajući šablon, Git vam dozvoljava da kreirate komit:

$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
 1 file changed, 1 insertions(+), 0 deletions(-)

Sada želite da se osigurate da ne modifikujete datoteke koje vam nisu dodeljene ACL-om. Ako direktorijum .git u vašem projektu sadrži kopiju ACL datoteke koju ste ranije iskoristili, onda će sledeća pre-commit skripta obezbediti ovu polisu:

#!/usr/bin/env ruby

$user    = ENV['USER']

# (ovde ubacite metodu acl_access_data odozgo)

# dozvoli samo određenim korisnicima da modifikuju određene poddirektorijume
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

Ovo je oprilike ista skripta kao i ona na serverskoj strani, ali uz dve ključne razlike. Prvo, ACL datoteka se nalazi na drugom mestu, jer se ova skripta pokreće iz radnog direktorijuma, a ne iz direktorijuma .git. Morate da promenite putanju do ACL datoteke od:

access = get_acl_access_data('acl')

na ovo:

access = get_acl_access_data('.git/acl')

Još jedna važna razlika je način na koji dobijate listu svih datoteka koje su promenjene. Pošto metoda na serverskoj strani gleda log komitova, a komit još nije zabeležen u ovom trenutku, morate da pribavite listu datoteka iz stejdža. Umesto:

files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`

morate da koristite:

files_modified = `git diff-index --cached --name-only HEAD`

Ali ovo su jedine dve razlike — sem toga, skripta radi na isti način. Jedina začkoljica je u tome što skripta očekuje da se pokrene lokalno od strane istog korisnika kao onaj koji će obaviti guranje na udaljenu mašinu. Ako se ova dva korisnika razlikuju, morate da ručno pdosite promenljivu $user.

Još jedna stvar koju ovde možemo da uradimo jeste da se postaramo da korisnik ne sme da gurne refence koje nisu premotane unapred. Da biste dobili referencu koja nije motanje unapred, morate ili da rebazirate komit koji ste već gurnuli ili da pokušavate da gurnete drugačiju lokalnu granu na istu udaljenu.

Server je verovatno neć konfigurisan sa receive.denyDeletes i receive.denyNonFastForwards koji se staraju o ovoj polisi, tako da je jedina slučajna stvar koju možete da dodate jeste metoda kojom ćete se postarati da niko ne rebazira komitove koji su već gurnuti.

Evo primer pre-base skripte koja radi upravo to. Preuzima listu svih komitova koje biste prebrisali i proverava da li postoje na nekim od vaših udaljenih referenci. Ako vidite bar jedan takav, ne dozvoljava vam da rebazirate.

#!/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

Ova skripta koristi sintaksu koja nije obrađena u poglavlju "Izbor revizije" u sedmom poglavlju. Dobijate listu komitova koji su već gurnuti ovako:

`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`

Sintaksa SHA^@ se obraća svim roditeljima tog komita. Tražite komit koji nije moguće dosegnuti od poslednjeg komita na udaljenoj referenci, ni od bilo kog roditelja bilo kog SHA-1 koji pokušavate da gurnete — što znači da se radi o motanju unapred.

Glavni nedostatak ovoga jeste da ume da bude spor i često nepoterban — ako ne pokušate da isforsirate guranje zastavicom -f, server će vas upozoriti i neće prihvatiti gurnute komitove. Ipak, ovo je zanimljiva vežba i teoretski vam može pomoći da izbegnete rebaziranje za kojim bi kasnije morali da počistite.