Git
Chapters ▾ 2nd Edition

8.4 Git’i Fərdiləşdirmək - Git-Enforced Siyasət Nümunəsi

Git-Enforced Siyasət Nümunəsi

Bu hissədə, xüsusi bir commit mesajı formatını yoxlayan və yalnız müəyyən istifadəçilərin bir layihədəki müəyyən alt qovluğu dəyişdirməsinə imkan verən bir Git workflow qurmaq üçün öyrəndiklərinizi istifadə edəcəksiniz. Developer-in push-nunun rədd ediləcəyini və siyasətləri həqiqətən tətbiq edən server skriptlərini bilməsinə kömək edən müştəri skriptləri quracaqsınız.

Göstərəcəyimiz ssenarilər Ruby ilə yazılmışdır; qismən intellektual hərəkətsizliyimizə görə, həm də Ruby-ni mütləq yaza bilməsəniz də oxumaq asandır. Bununla birlikdə, hər hansı bir dil işləyəcəkdir - Git ilə paylanan bütün nümunə hook skriptləri ya Perl ya da Bash-dadır, buna görə nümunələrə baxaraq bu dillərdə çox sayda hook nümunəsi görə bilərsiniz.

Server Tərəf Hook’u

Bütün server tərəfli işlər hooks qovluğunuzdakı update faylına daxil olacaq. update hook-u hər bir push üçün bir dəfə işləyir və üç arqument götürür:

  • push olunan istinadın adı

  • Bu branch-ın olduğu köhnə düzəliş

  • push olunan yeni versiya

Push SSH üzərində işləyirsə push edən istifadəçiyə də girişiniz var. Hər kəsin ümumi key identifikasiyası yolu ilə tək bir istifadəçi ilə (“git” kimi) əlaqə qurmasına icazə verdiyiniz təqdirdə, bu istifadəçiyə açıq key-ə əsasən hansı istifadəçinin birləşdiyini təyin edən shell wrapper verməlisiniz və müvafiq olaraq bu mühit dəyişəndir. Burada əlaqə istifadəçisinin $USER mühit dəyişənində olduğunu düşünəcəyik, buna görə də yeniləmə skriptiniz lazım olan bütün məlumatları toplamaqla başlayır:

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

Bəli, bunlar qlobal dəyişənlərdir. Mühakimə etməyin - bu şəkildə nümayiş etdirmək daha asandır.

Xüsusi bir Commit-Mesaj Formatının Tətbiq Edilməsi

İlk probleminiz hər bir commit mesajının müəyyən bir formata riayət etməsini təmin etməkdir. Yalnız bir hədəfə sahib olmaq üçün hər bir mesajın “ref: 1234” kimi görünən bir simli daxil etdiyini fərz edin, çünki hər bir commit-in bilet sisteminizdəki bir iş elementi ilə əlaqələndirilməsini istəyirsiniz. Hər bir push-un yuxarı qaldırıldığına baxmalısınız, bu sətirin commit mesajında olub olmadığını görməlisiniz və əgər sətir heç bir commit-də yoxdursa, non-zero-dan çıxın, beləliklə push rədd edilir.

$newrev$oldrev dəyərlərini götürərək git rev-list adlı Git plumbing əmrinə ötürərək push edilən bütün commit-lərin SHA-1 dəyərlərinin siyahısını əldə edə bilərsiniz. Bu, əsasən git log əmridir, lakin standart olaraq yalnız SHA-1 dəyərlərini yazdırır və başqa heç bir məlumat yoxdur. Beləliklə, bir SHA-1 əmri ilə digər biri arasında tətbiq olunan bütün SHA-1-lərin siyahısını almaq üçün belə bir şey edə bilərsiniz:

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

Bu nəticəni götürə bilər, SHA-1-ləri işləyənlərin hər birindən keçə bilər, bunun üçün mesajı götürə və bu mesajı nümunə axtaran normal bir ifadəyə qarşı test edə bilərsiniz.

Bu commit-lərin hər birindən test etmək üçün commit mesajını necə alacağınızı bilməlisiniz. Raw commit məlumatlarını almaq üçün, git cat-file adlı başqa bir plumbing əmrindən istifadə edə bilərsiniz. Plumbing əmrlərinə daha detallı Git’in Daxili İşləri-dan baxacağıq; ancaq hələlik bu əmrin sizə verdiyi budur:

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

SHA-1 dəyərinə sahib olduqda commit mesajını commit-dən əldə etməyin sadə bir yolu ilk boş sətirə keçmək və bundan sonra hər şeyi götürməkdir. Bunu Unix sistemlərindəki sed əmri ilə edə bilərsiniz:

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

Uyğun olmayan bir şey görsəniz, push etməyə və çıxmağa çalışan hər bir commit-dən commit mesajını almaq üçün bu tilsimi istifadə edə bilərsiniz. Ssenaridən çıxmaq və push-u rədd etmək üçün non-zero’dan çıxın. Bütün metod belə görünür:

$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

Bunu update skriptinə qoymağınız qaydanıza əməl etməyən mesajları olan commit-ləri olan yeniləmələri rədd edəcəkdir.

User-Based ACL Sisteminin Tətbiqi

Fərz edək ki, hansı istifadəçilərə layihələrinizin hansı hissələrində dəyişikliklər etməyə icazə verildiyini göstərən bir giriş nəzarəti siyahısı (ACL) istifadə edən bir mexanizm əlavə etmək istəyirsiniz. Bəzi insanlar tam giriş hüququna malikdirlər, bəziləri isə yalnız müəyyən alt qovluqlara və ya müəyyən fayllara dəyişiklik edə bilər. Bunu tətbiq etmək üçün bu qaydaları serverdəki boş Git deposunda yaşayan acl adlı bir fayla yazacaqsınız. Bu qaydalara baxaraq update hook-na baxacaqsınız, push etdiyiniz bütün commit-lər üçün hansı faylların təqdim olunduğuna baxın və push edən istifadəçinin bütün bu faylları yeniləməyə giriş imkanının olub olmadığını müəyyənləşdirəcəksiniz.

Edəcəyiniz ilk şey ACL yazmaqdır. Burada CVS ACL mexanizmi kimi bir formatdan çox istifadə edəcəksiniz: birinci sahənin avail və ya unavail olduğu bir sıra sətirlərdən istifadə edir, növbəti sahə istifadəçilərin vergüllə ayrılmış siyahısıdır,sonra qayda tətbiq olunur və son sahə qaydanın tətbiq olunduğu path-dir (blank açıq giriş deməkdir). Bu sahələrin hamısı boru (|) işarəsi ilə ayrılmışdır.

Bu vəziyyətdə, bir neçə idarəçiniz var, bəzi sənəd yazarlarına doc qovluğuna giriş imkanı var və yalnız libtests qovluqlarına çıxışı olan bir developer var və ACL dosyanız belə görünür:

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

Bu məlumatları istifadə edə biləcəyiniz bir quruluşu oxumaqla başlayırsınız. Bu vəziyyətdə, nümunəni sadə saxlamaq üçün yalnız avail direktivlərini yerinə yetirəcəksiniz. Burada key-in istifadəçi adı olduğu və dəyərin istifadəçinin yazma girişinə sahib olduğu bir sıranın olduğu assosiativ bir sıra verən metod var:

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

Daha əvvəl baxdığınız ACL faylında bu get_acl_access_data metodu aşağıdakı kimi bir məlumat strukturunu qaytarır:

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

Artıq icazələrinızı çeşidlədikdən sonra, push etdiyiniz commit-lərin hansı path-ların dəyişdirildiyini təyin etməlisiniz, beləliklə push edən istifadəçinin hər yerə girişinə əmin ola bilərsiniz.

git log əmrinə --name-only seçimi ilə təkcə bir işdə hansı faylların dəyişdirildiyini olduqca asanlıqla görə bilərsiniz ( Git’in Əsasları-də qısaca qeyd olunur):

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

README
lib/test.rb

get_acl_access_data metodundan qaytarılan ACL quruluşundan istifadə edirsinizsə və hər bir siyahıda sadalanan fayllarla müqayisə etsəniz, istifadəçinin bütün commit-lərini push etmək imkanının olub olmadığını müəyyən edə bilərsiniz:

# 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

Serverinizə git rev-list ilə göndərilən yeni commit-lərin siyahısını alırsınız. Sonra, bu commit-lərin hər biri üçün hansı faylların dəyişdirildiyini tapır və push edən istifadəçinin dəyişdirilən bütün path-lara girişinə əmin olun.

Artıq istifadəçiləriniz pis qurulmuş mesajlarla və ya təyin olunmuş path-ların xaricində dəyişdirilmiş fayllarla heç bir commit götürə bilməzlər.

Testdən Keçirmək

Bütün bu kodu qoymağınız lazım olan fayl olan chmod u+x .git/hooks/update işlədirsinizsə və uyğun olmayan bir mesajla bir commit götürməyə çalışsanız, buna bənzər bir şey əldə edirsiniz.

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

Burada bir-iki maraqlı şey var. Birincisi, bunu hook işləməyə başladığı yerdə görürsən.

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

Yeniləmə skriptinizin əvvəlində onu çap etdiyinizi unutmayın. Ssenarinizin stdout ilə əks olunduğu hər hansı bir şey müştəriyə ötürüləcəkdir.

Növbəti şey error mesajıdır.

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

Birinci sətir sizin tərəfinizdən çap olundu, digər ikisi Git, yeniləmə skriptinin sıfırdan çıxdığını və bununla push etmənizin azaldığını söylədi. Son olaraq:

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

Hook-un rədd etdiyi hər bir müraciət üçün remote rədd edilmiş bir mesaj görəcəksiniz və o, bunun hook çatışmazlığı səbəbindən xüsusi olaraq rədd edildiyini bildirir. Bundan əlavə, kimsə girişi olmayan bir faylı düzəltməyə və içərisində olan bir commit-i push etməyə çalışsa, oxşar bir şey görəcəkdir. Məsələn, bir fayl müəllifi lib qovluğunda bir şey dəyişdirərək bir commit-i push etməyə çalışırsa, belə görürür:

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

Bundan sonra, o update skripti olduğu və icra edilə biləcəyi müddətdə, deponuzda heç vaxt sizin nümunəniz olmayan bir commit mesajı olmayacaq və istifadəçiləriniz sandbox altında qalacaq.

Müştəri Tərəf Hook-lar

Bu yanaşmanın mənfi tərəfi, istifadəçilərinizin push-ları rədd edildikdə qaçılmaz olaraq nəticələnəcək. Diqqətlə hazırlanmış işlərinin son anda rədd edilməsi son dərəcə məyus və qarışıq ola bilər; və bundan əlavə tarixlərini düzəltmək üçün edit etməli olacaqlar ki, bu da həmişə ürək qırıqlığıdır.

Bu çıxılmaz vəziyyətin cavabı, istifadəçilərin serverin rədd edə biləcəyi bir şey etdikləri zaman xəbərdar etmək üçün istifadə edə biləcəyi bəzi müştəri tərəfindəki hook-ları təmin etməkdir. Beləliklə, hər hansı bir problemi commit etməzdən əvvəl və bu problemləri həll etmək çətinləşməmişdən əvvəl düzəldə bilərlər. Hook-lar bir layihənin klonu ilə köçürülmədiyi üçün bu skriptləri başqa bir şəkildə paylamalı və sonra istifadəçilərinizdən bunları .git/hooks qovluğuna kopyalayıb icraya hazır etməlisiniz. Bu hook-ları proyekt daxilində və ya ayrı bir layihədə paylaya bilərsiniz, lakin Git onları avtomatik olaraq qurmayacaqdır. Başlamaq üçün, hər bir commit qeydə alınmazdan əvvəl commit mesajınızı yoxlamalısınız, belə ki, serverin pis formatlanmış commit mesajlarına görə dəyişikliklərinizi rədd etməyəcəyini bilirsiniz. Bunu etmək üçün commit-msg hook-u əlavə edə bilərsiniz. Birinci arqument kimi ötürülən fayldan mesajı oxudunuzsa və bunu nümunə ilə müqayisə etsəniz, uyğunlaşma olmadığı təqdirdə Git-i commit-i ləğv etməyə məcbur edə bilərsiniz:

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

Əgər həmin skript yerindədirsə (.git/hooks/commit-msg-də) və işlədilə bilərsə və düzgün hazırlanmamış bir mesajla məşğul olursunuzsa, bunu görürsünüz:

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

Bu vəziyyətdə heç bir commit tamamlanmamışdır. Bununla birlikdə, mesajınız uyğun bir pattern-i ehtiva edirsə, Git sizə imkan verir:

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

Sonra ACL əhatənizdən kənar faylları dəyişdirmədiyinizə əmin olmaq istəyirsiniz. Layihənizin .git qovluğunda əvvəllər istifadə etdiyiniz ACL sənədinin bir nüsxəsi varsa, aşağıdakı pre-commit ssenarisi sizin üçün bu məhdudiyyətləri tətbiq edəcəkdir:

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

Bu, server tərəfindəki hissə ilə təxminən eyni skriptdir, lakin iki mühüm fərq var. Birincisi, ACL faylı başqa bir yerdədir, çünki bu ssenari .git qovluğundan deyil, iş qovluğundan işləyir. ACL faylının yolunu buradan dəyişdirməlisiniz:

access = get_acl_access_data('acl')

to this:

access = get_acl_access_data('.git/acl')

Digər vacib fərq, dəyişdirilmiş faylların siyahısını əldə etmək üsuludur. Server tərəfindəki metod, tapşırıqların jurnalına baxdığından və bu anda commit hələ qeyd olunmadığından, bunun əvəzinə fayl siyahısını səhnələşdirmə sahəsindən almalısınız. Bunun əvəzinə:

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

you have to use:

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

Lakin, sadəcə iki fərq bunlardır - əks halda, ssenari eyni şəkildə işləyir. Bir xəbərdarlıq budur ki, remote machine-a push etdiyiniz eyni istifadəçi ilə yerli olaraq işləməyinizi gözləyir. Fərqlidirsə, $user dəyişənini manual olaraq təyin etməlisiniz.

Burada edə biləcəyimiz başqa bir şey də istifadəçinin sürətli göndərilməyən istinadları push etməsindən əmin olmaqdır. Sürətli olmayan bir istinad almaq üçün ya əvvəldən push etdiyiniz bir commit-i geri qaytarmalısınız, ya da fərqli bir yerli branch-ı eyni remote branch-a push etməyə çalışmalısınız.

Çox güman ki, server bu siyasəti tətbiq etmək üçün artıq receive.denyDeletesreceive.denyNonFastForwards ilə konfiqurasiya olunmuşdur, buna görə tutmağa çalışa biləcəyiniz tək təsadüfi şey, artıq push olunmuş commit-lərin geri qaytarılmasıdır.

Budur bunu yoxlayan bir pre-rebase skriptinin nümunəsi. Yenidən yazmaq istədiyiniz bütün commit-lərin siyahısını alır və remote istinadlarınızdan birində olub olmadığını yoxlayır. Remote istinadlarınızdan birinin əlçatan olduğunu görsə, geri qaytarmanı ləğv edir.

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

Bu skript Reviziya Seçimi-də əhatə olunmayan bir sintaksisdən istifadə edir. Bunu işə salmaqla əvvəlcədən push edilmiş commit-lərin siyahısını alırsınız:

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

SHA^@ sintaksisi, bu vəzifəni yerinə yetirən bütün valideynləri həll edir. Remote-dakı son işdən əldə edilə bilən və push etməyə çalışdığınız SHA-1-lərin hər hansı bir valideynindən əlçatmaz olan hər hansı bir commit axtarırsınız - yəni bu irəliləyişdir.

Bu yanaşmanın əsas çatışmazlığı çox yavaş ola biləcəyi və çox vaxt lazımsız olmasıdır - itələməni -f ilə məcbur etməyə çalışmasanız, server sizi xəbərdar edəcək və push qəbul etməyəcək. Bununla birlikdə, bu maraqlı bir məşqdir və nəzəri cəhətdən daha sonra geri qayıtmaq və düzəltmək məcburiyyətində qalmağınızın qarşısını almağa kömək edə bilər.

scroll-to-top