Chapters ▾ 2nd Edition

8.4 Customizing Git (سفارشی‌سازی Git) - An Example Git-Enforced Policy (یک مثال از سیاست‌های تحمیلی گیت)

An Example Git-Enforced Policy (یک مثال از سیاست‌های تحمیلی گیت)

در این بخش، شما از آنچه که آموخته‌اید برای ایجاد یک گردش‌کار Git استفاده خواهید کرد که قالب پیام کامیت سفارشی را بررسی می‌کند و تنها به برخی از کاربران اجازه می‌دهد که برخی زیرشاخه‌ها (subdirectories) در پروژه را ویرایش کنند. شما اسکریپت‌های کلاینتی خواهید ساخت که به توسعه‌دهنده اطلاع می‌دهد که آیا پوش او رد خواهد شد یا خیر و همچنین اسکریپت‌های سرور که در واقع این سیاست‌ها را اعمال می‌کنند.

اسکریپت‌هایی که در اینجا نشان داده می‌شود به زبان Ruby نوشته شده‌اند؛ بخشی به دلیل تنبلی فکری ما، ولی همچنین به این دلیل که Ruby خواندن آن آسان است، حتی اگر نتوانید حتماً آن را بنویسید. با این حال، هر زبان برنامه‌نویسی دیگری نیز کار خواهد کرد – تمام اسکریپت‌های هوک نمونه که با Git توزیع شده‌اند، به زبان‌های Perl یا Bash هستند، بنابراین شما می‌توانید نمونه‌های زیادی از هوک‌ها به این زبان‌ها را با نگاه کردن به نمونه‌ها ببینید.

Server-Side Hook (هوک سمت سرور)

تمام کارهای سمت سرور در فایل update داخل دایرکتوری hooks شما قرار خواهد گرفت. هوک update برای هر برنچی که در حال پوش است یکبار اجرا می‌شود و سه پارامتر دریافت می‌کند:

نام رفرنس که به آن پوش می‌شود *
بازنگری قدیمی که آن برنچ به آن اشاره می‌کرد *
بازنگری جدیدی که در حال پوش است *

همچنین اگر پوش از طریق SSH انجام شود، به کاربری که در حال پوش است دسترسی خواهید داشت. اگر به همه اجازه داده‌اید که با یک کاربر واحد (مثل git) از طریق احراز هویت کلید عمومی به سرور متصل شوند، ممکن است مجبور باشید به آن کاربر یک پوسته (shell) wrapper بدهید که تشخیص دهد کدام کاربر در حال اتصال است، بر اساس کلید عمومی، و سپس یک متغیر محیطی را مطابق آن تنظیم کند. در اینجا فرض می‌کنیم که کاربر متصل در متغیر محیطی $USER قرار دارد، بنابراین اسکریپت update شما با جمع‌آوری تمام اطلاعات مورد نیاز شروع می‌شود:

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

بله، این‌ها متغیرهای جهانی هستند. قضاوت نکنید – این روش آسان‌تر برای نشان دادن است.

Enforcing a Specific Commit-Message Format (تحمیل فرمت خاص پیام کامیت)

اولین چالش شما این است که اطمینان حاصل کنید که هر پیام کامیت با فرمت خاصی سازگار است. برای داشتن یک هدف مشخص، فرض کنید که هر پیام باید شامل رشته‌ای مانند ref: 1234 باشد، زیرا می‌خواهید هر کامیت به یک آیتم کاری در سیستم تیکتینگ شما لینک شود. شما باید هر کامیتی که در حال پوش است را بررسی کنید، ببینید آیا آن رشته در پیام کامیت وجود دارد یا خیر و اگر آن رشته از هرکدام از کامیت‌ها غایب بود، با خروجی غیر صفر (non-zero) خارج شوید تا پوش رد شود.

برای به‌دست آوردن فهرستی از مقادیر SHA-1 تمام کامیت‌هایی که در حال پوش شدن هستند، می‌توانید مقادیر $newrev و $oldrev را گرفته و آن‌ها را به یک دستور Git به نام git rev-list ارسال کنید. این در واقع همان دستور git log است، اما به طور پیش‌فرض تنها مقادیر SHA-1 را چاپ می‌کند و هیچ اطلاعات دیگری را نمایش نمی‌دهد. پس برای دریافت فهرستی از تمام SHA-1های کامیت‌هایی که بین یک SHA-1 کامیت و دیگری معرفی شده‌اند، می‌توانید چیزی شبیه به این را اجرا کنید:

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

شما می‌توانید آن خروجی را بگیرید، برای هر کدام از SHA-1های کامیت‌ها حلقه بزنید، پیام مربوط به آن‌ها را بگیرید و سپس آن پیام را با یک عبارت منظم (regular expression) که به دنبال یک الگو می‌گردد تست کنید.

شما باید راهی پیدا کنید که چگونه پیام کامیت هر کدام از این کامیت‌ها را برای تست بدست آورید. برای دریافت داده‌های خام کامیت، می‌توانید از دستور دیگری به نام git cat-file استفاده کنید. ما تمام این دستورات پلاومبینگ (plumbing) را به طور دقیق در [ch10-git-internals] بررسی خواهیم کرد؛ اما برای حالا، در اینجا چیزی است که آن دستور به شما می‌دهد:

$ 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

تغییر شماره نسخه

یک روش ساده برای دریافت پیام کامیت از یک کامیت زمانی که SHA-1 آن را دارید این است که به اولین خط خالی بروید و تمام چیزی که بعد از آن آمده را بگیرید. شما می‌توانید این کار را با دستور sed در سیستم‌های یونیکس انجام دهید:

$ git cat-file commit ca82a6 | sed '1,/^$/d'
تغییر شماره نسخه

شما می‌توانید از آن روش جادویی برای دریافت پیام کامیت از هر کامیتی که در حال تلاش برای ارسال است استفاده کنید و اگر چیزی را ببینید که با آن مطابقت ندارد، از اسکریپت خارج شوید. برای خروج از اسکریپت و رد کردن ارسال، باید با خروجی غیرصفر (non-zero) خارج شوید. کل روش به این صورت است:

$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

قرار دادن این در اسکریپت update شما، به‌روزرسانی‌هایی را که شامل کامیت‌هایی هستند با پیام‌هایی که با قانون شما هم‌خوانی ندارند، رد خواهد کرد.

Enforcing a User-Based ACL System (اجرای یک سیستم کنترل دسترسی مبتنی بر کاربر (User-Based ACL System))

فرض کنید می‌خواهید یک مکانیسم اضافه کنید که از یک فهرست کنترل دسترسی (ACL) استفاده کند و مشخص کند که کدام کاربران مجاز به ارسال تغییرات به کدام بخش‌های پروژه شما هستند. برخی افراد دسترسی کامل دارند و دیگران تنها می‌توانند تغییرات را به زیرشاخه‌ها یا فایل‌های خاصی ارسال کنند. برای اعمال این محدودیت‌ها، شما این قوانین را در فایلی به نام acl که در مخزن bare Git شما روی سرور قرار دارد، می‌نویسید. هوک update شما این قوانین را بررسی می‌کند، مشاهده می‌کند که چه فایل‌هایی برای تمام کامیت‌هایی که در حال ارسال هستند معرفی شده‌اند و تعیین می‌کند که آیا کاربری که در حال ارسال است دسترسی به به‌روزرسانی تمام آن فایل‌ها را دارد یا خیر.

اولین کاری که باید انجام دهید نوشتن فهرست ACL است. در اینجا از فرمت مشابهی با مکانیسم ACL در CVS استفاده خواهید کرد: این فرمت شامل یک سری خط است، جایی که اولین فیلد avail یا unavail است، فیلد بعدی یک لیست از کاربران است که قانون به آن‌ها اعمال می‌شود (که با ویرگول از هم جدا شده‌اند)، و آخرین فیلد مسیر مربوطه است (که اگر خالی باشد یعنی دسترسی باز است). تمام این فیلدها با یک کاراکتر خط عمودی (|) از هم جدا شده‌اند.

در این مثال، شما چند مدیر، برخی نویسندگان مستندات با دسترسی به دایرکتوری doc و یک توسعه‌دهنده که تنها دسترسی به دایرکتوری‌های lib و tests دارد، دارید. بنابراین فایل ACL شما به این شکل خواهد بود:

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

شما با خواندن این داده‌ها به یک ساختار که می‌توانید از آن استفاده کنید، شروع می‌کنید. در این مثال، برای ساده نگه داشتن موضوع، تنها دستورات avail را اعمال خواهید کرد. در اینجا یک متد آورده شده که یک آرایه انجمنی (associative array) به شما می‌دهد که در آن کلید، نام کاربر و مقدار، یک آرایه از مسیرهایی است که کاربر به آن‌ها دسترسی نوشتن دارد:

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

در فایل ACL که پیش‌تر مشاهده کردید، این متد get_acl_access_data یک ساختار داده‌ای را باز می‌گرداند که به این شکل است:

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

حالا که مجوزها را مرتب کرده‌اید، باید تعیین کنید که کامیت‌های در حال ارسال چه مسیرهایی را تغییر داده‌اند تا مطمئن شوید که کاربری که در حال ارسال است به تمام آن‌ها دسترسی دارد.

شما به راحتی می‌توانید ببینید که چه فایل‌هایی در یک کامیت تغییر کرده‌اند با استفاده از گزینه --name-only در دستور git log (که به طور مختصر در مقدمات گیت ذکر شده است):

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

README
lib/test.rb

اگر از ساختار ACL که از متد get_acl_access_data باز می‌گردد استفاده کنید و آن را با فایل‌های فهرست شده در هر یک از کامیت‌ها مقایسه کنید، می‌توانید تعیین کنید که آیا کاربر دسترسی به ارسال تمام کامیت‌های خود را دارد یا خیر:

# فقط اجازه می‌دهد کاربران خاصی زیرپوشه‌های خاصی را در یک پروژه تغییر دهند
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

شما با استفاده از دستور git rev-list فهرستی از کامیت‌های جدیدی که به سرور شما ارسال می‌شوند، دریافت می‌کنید. سپس برای هر یک از این کامیت‌ها، فایل‌های تغییر یافته را پیدا کرده و مطمئن می‌شوید که کاربری که در حال ارسال است، به تمام مسیرهایی که در حال تغییر هستند، دسترسی دارد.

حالا کاربران شما نمی‌توانند کامیت‌هایی با پیام‌های نادرست یا فایل‌هایی که خارج از مسیرهای مجازشان تغییر کرده‌اند، ارسال کنند.

Testing It Out آزمایش آن

اگر دستور chmod u+x .git/hooks/update را اجرا کنید (که فایلی است که باید تمام این کدها را در آن قرار دهید) و سپس سعی کنید یک کامیت با پیامی غیرمنطبق ارسال کنید، چیزی شبیه به این دریافت خواهید کرد:

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

چند نکته جالب در اینجا وجود دارد. اولاً، شما این را مشاهده می‌کنید که اسکریپت هوک شروع به اجرا می‌کند.

[source,console]

تحمیل سیاست‌ها…​ (refs/heads/master) (fb8c72) (c56860)

یادآوری کنید که شما این را در ابتدای اسکریپت `update` خود چاپ کردید.
هر چیزی که اسکریپت شما به `stdout` ارسال کند، به کلاینت منتقل خواهد شد.

چیز بعدی که متوجه خواهید شد، پیام خطاست.

[source,console]

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

خط اول توسط شما چاپ شده است، دو خط دیگر از طرف Git است که به شما می‌گوید اسکریپت update با کد غیر صفر خارج شده و این باعث رد شدن ارسال شما شده است.
در نهایت، شما این را مشاهده می‌کنید:

[source,console]

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

برای هر مرجعی که هوک شما آن را رد کرده است، پیامی مبنی بر رد از طرف ریموت خواهید دید و به شما گفته می‌شود که دقیقاً به دلیل شکست هوک این ارسال رد شده است.

علاوه بر این، اگر کسی سعی کند فایلی را که به آن دسترسی ندارد ویرایش کرده و یک کامیت شامل آن ارسال کند، پیامی مشابه مشاهده خواهد کرد.
برای مثال، اگر یک نویسنده مستندات سعی کند یک کامیت ارسال کند که در آن فایلی در دایرکتوری lib تغییر کرده باشد، او این را مشاهده می‌کند:

[source,console]

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

از این به بعد، تا زمانی که اسکریپت `update` در آن‌جا باشد و قابل اجرا باقی بماند، مخزن شما هرگز کامیتی نخواهد داشت که پیام آن الگوی مورد نظر شما را نداشته باشد، و کاربران شما در محدوده دسترسی مشخص‌شده خود (sandboxed) باقی خواهند ماند.

==== Client-Side Hooks (هوک‌های سمت کلاینت)

یکی از معایب این روش، نارضایتی و غرولندی است که اجتناب‌ناپذیر خواهد بود، زمانی که پوش‌های کاربران به دلیل رد شدن کامیت‌ها پذیرفته نمی‌شوند.
رد شدن کارِ با دقت و زحمت انجام‌شده‌ی آن‌ها در لحظه‌ی آخر می‌تواند بسیار ناامیدکننده و گیج‌کننده باشد؛ و علاوه بر این، آن‌ها مجبور خواهند شد تاریخچه‌ی خود را برای اصلاح تغییر دهند، که این کار همیشه برای همه آسان نیست.

راه‌حل این مشکل، فراهم کردن چند هوک سمت کلاینت (Client-side hooks) است که کاربران بتوانند با اجرای آن‌ها، از احتمال رد شدن تغییرات‌شان توسط سرور مطلع شوند.
به این ترتیب، آن‌ها می‌توانند مشکلات را قبل از کامیت کردن اصلاح کنند و این‌طوری حل آن‌ها ساده‌تر خواهد بود.

از آن‌جا که هوک‌ها به‌طور خودکار همراه با کلون یک پروژه منتقل نمی‌شوند، باید این اسکریپت‌ها را از طریقی دیگر منتشر کرده و از کاربران بخواهید آن‌ها را در مسیر `.git/hooks/` کپی کرده و اجرایی (executable) کنند.
می‌توانید این هوک‌ها را درون خود پروژه یا در پروژه‌ای مجزا منتشر کنید، اما گیت به‌صورت خودکار آن‌ها را تنظیم نمی‌کند.

برای شروع، باید پیام کامیت را درست قبل از ثبت هر کامیت بررسی کنید، تا مطمئن شوید که سرور به‌خاطر فرمت نادرست پیام آن را رد نخواهد کرد.
برای این کار می‌توانید از هوک `commit-msg` استفاده کنید.
اگر این هوک، پیام کامیت را از فایلی که به‌عنوان آرگومان اول دریافت می‌کند بخواند و آن را با الگوی موردنظر مطابقت دهد، می‌توانید با عدم تطابق، گیت را مجبور به توقف فرآیند کامیت کنید:

[source,ruby]

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

اگر این اسکریپت در مسیر `.git/hooks/commit-msg` قرار گرفته باشد و قابل اجرا (executable) باشد، و شما سعی کنید با پیامی که فرمت درستی ندارد کامیت بزنید، با چنین چیزی مواجه خواهید شد:

[source,console]

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

در آن حالت هیچ کامیتی انجام نشد.
        با این حال، اگر پیام شما شامل الگوی مناسب باشد، گیت به شما اجازه می‌دهد که کامیت کنید:

[source,console]

$ git commit -am Test [ref: 132]

1 file changed, 1 insertions(+), 0 deletions(-)
در مرحله‌ی بعد، باید مطمئن شوید که فایل‌هایی خارج از محدوده‌ی دسترسی ACL خود را تغییر نمی‌دهید.
اگر دایرکتوری `.git` پروژه‌تان شامل یک نسخه از فایل ACL باشد که قبلاً از آن استفاده کرده‌اید، اسکریپت `pre-commit` زیر این محدودیت‌ها را برایتان اعمال خواهد کرد:


[source,ruby]

#!/usr/bin/env ruby

$user = ENV[USER]

scroll-to-top