8.4 自訂 Git-一個示例 git 強制策略
An Example Git-Enforced Policy
在本節中, 您將使用所學知識來建立一個 Git 工作流, 該工作流檢查自訂提交消息格式, 並只允許某些使用者修改專案中的某些子目錄。您將構建用戶端腳本, 以説明開發人員知道他們的推送是否會被拒絕, 以及實際強制實施策略的伺服器腳本。
我們將展示的腳本是用 Ruby 編寫的。部分原因是我們的智力慣性, 但也因為 Ruby 易於閱讀, 即使你不一定能寫它。但是, 任何語言都可以工作--與 Git 一起分發的所有示例鉤子腳本都在 Perl 或 Bash 中, 因此您還可以通過查看示例來查看這些語言中的鉤子示例。
伺服器端掛鉤
所有伺服器端工作都將進入
hooks目錄中的update檔。update鉤子在每個被推送的分支中運行一次, 並採用三個參數:- 要推送到的引用的名稱
- 舊的修訂, 該分支
- 正在推動的新修訂
如果推送是在 SSH 上運行, 您還可以訪問執行推送的使用者。如果您允許每個人通過公開金鑰身份驗證與單個使用者 (如 "git") 連接, 則可能必須為該使用者提供一個 shell 包裝器, 以確定基於公開金鑰連接的使用者, 並相應地設置環境變數。在這裡, 我們假設連接使用者位於 $USER 環境變數中, 因此更新腳本從
$USER所需的所有資訊開始:#!/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]})"
是的, 這些是全域變數。不要評判--這樣證明更容易。
強制執行特定的提交消息格式
您面臨的第一個挑戰是強制要求每個提交消息都遵循特定的格式。只是為了有一個目標, 假設每條消息都必須包含一個類似于 "ref:1234" 的字串, 因為您希望每個提交都連結到票務系統中的工作項。您必須查看每個被推上的提交, 查看該字串是否在提交消息中, 如果該字串不在任何提交中, 則退出非零, 以便拒絕推送。
您可以通過獲取 $newrev 並
$oldrev$newrev REV-LIST 的 git 管道命令來獲取正在推送的所有提交的 sha-1 值的git rev-list清單。這基本上是git log命令, 但預設情況下, 它只列印 SHA-1 值, 而不列印其他資訊。因此, 要獲取在一個提交 SHA-1 和另一個提交 SHA-1 之間引入的所有提交 Sha-1 的清單, 您可以運行如下所示的操作:$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
您可以獲取該輸出, 遍歷每個提交 Sha-1 的輸出, 獲取該消息, 並使用查找模式的正則運算式測試該消息。
$ 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
當您具有 SHA-1 值時, 從提交中獲取提交消息的一個簡單方法是轉到第一個空行, 並在該值之後獲取所有內容。您可以使用 Unix 系統上的
sed命令執行此操作:$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number
如果您看到任何不匹配的內容, 則可以使用該咒語從試圖推送和退出的每個提交中獲取提交消息。若要退出腳本並拒絕推送, 請退出非零。整個方法如下所示:
$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腳本中將拒絕包含包含不符合您的規則的消息的提交的更新。實施基於使用者的 ACL 系統
假設您要添加一個機制, 該機制使用存取控制清單 (ACL), 指定允許哪些使用者將更改推送到專案的哪些部分。有些人具有完全存取權限, 而另一些人只能推送更改到某些子目錄或特定檔。若要強制執行此操作, 您將將這些規則寫入位於伺服器上的裸裸 Git 存儲庫中的名為
acl的檔。您將看到這些規則的update鉤子, 查看正在為推送的所有提交引入哪些檔, 並確定執行推送的使用者是否有權更新所有這些檔。
你要做的第一件事就是寫你的 ACL。在這裡, 您將使用一種非常類似于 CVS ACL 機制的格式: 它使用一系列行, 其中第一個欄位是
avail的或unavail的, 下一個欄位是規則應用到的使用者的逗號分隔清單, 最後一個欄位是該格式的路徑h 該規則適用 (空白表示開放訪問)。所有這些欄位都由管道 (|字元分隔。
在這種情況下, 您有幾個管理員、一些文檔編寫器可以訪問
doc目錄, 還有一個開發人員只能訪問lib和tests目錄, 您的 acl 檔如下所示:avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests
首先, 您可以將此資料讀入一個可以使用的結構中。在這種情況下, 為了使示例簡單, 您將只強制執行
avail指令。下面是一個為您提供關聯陣列的方法, 其中鍵是使用者名, 值是使用者具有寫存取權限的路徑陣列: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"]}
現在, 您已經對許可權進行了排序, 您需要確定正在推送的提交已修改的路徑, 以便確保推送的使用者有權訪問所有這些路徑。
您可以很容易地看到哪些檔已在單個提交中被修改,
git log命令的- --name-only名稱選項 (在git中簡要提及基礎):$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rb
如果使用
get_acl_access_data方法返回的 acl 結構, 並根據每個提交中列出的檔對其進行檢查, 則可以確定使用者是否有權推送所有提交:# 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
您將獲得一個新提交的清單, 這些提交被推送到您的伺服器
git rev-list然後, 對於這些提交中的每個提交, 您都會找到哪些檔被修改, 並確保推送的使用者有權訪問所有被修改的路徑。
現在, 您的使用者不能使用格式錯誤的消息或在其指定路徑之外使用修改後的檔來推送任何提交。
測試出來
如果您運行
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'
這裡有幾件有趣的事情。首先, 你會看到鉤子開始運行的地方。
Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)
請記住, 您在更新腳本的開頭就列印出來了。您的腳本回
stdout的任何內容都將傳輸到用戶端。
接下來你會注意到的是錯誤訊息。
[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
第一行是由你列印出來的, 另外兩行是 Git 告訴你更新腳本退出非零, 這就是你的推送下降的原因。最後, 您有以下:
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
對於您的鉤子拒絕的每個引用, 您都會看到一條遠端被拒絕的消息, 它告訴您, 它是由於鉤子失敗而被拒絕的。
此外, 如果有人試圖編輯他們無法訪問的檔並推送包含該檔的提交, 他們將看到類似的內容。例如, 如果文檔作者試圖推
lib目錄中的內容, 他們會看到[POLICY] You do not have access to push to lib/test.rb
從現在開始, 只要
update腳本存在且可執行, 您的存儲庫將永遠不會有沒有您的模式的提交消息, 並且您的使用者將被沙箱。用戶端掛鉤
這種方法的缺點是, 當使用者的提交推送被拒絕時, 必然會產生抱怨。讓他們精心打造的作品在最後一刻被拒絕可能是極其令人沮喪和困惑的;而且, 他們還必須編輯自己的歷史來糾正, 這並不總是為膽小的人準備的。
解決此難題的方法是提供一些用戶端掛鉤, 使用者可以在執行伺服器可能拒絕的操作時運行以通知他們。這樣, 他們就可以在承諾之前和這些問題變得更加難以解決之前糾正任何問題。由於掛鉤不是通過專案的克隆進行傳輸的, 因此必須以其他方式分發這些腳本, 然後讓使用者將它們複製到其
.git/hooks並使其可執行。您可以在專案中或在單獨的專案中分發這些掛鉤, 但 Git 不會自動設置它們。
首先, 您應該在每次提交都被記錄之前檢查您的提交消息, 這樣您就知道伺服器不會因為提交消息的格式不正確而拒絕您的更改。為此, 您可以添加
commit-msg。如果您讓它讀取作為第一個參數傳遞的檔中的消息, 並將其與模式進行比較, 則可以在沒有匹配項的情況下強制 Git 中止提交:#!/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中) 和可執行檔, 並且您提交的消息格式不正確, 則會看到:$ git commit -am 'test'
[POLICY] Your message is not formatted correctly
在該實例中沒有完成提交。但是, 如果您的郵件包含正確的模式, Git 允許您提交:
$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
1 file changed, 1 insertions(+), 0 deletions(-)
接下來, 您要確保不修改 ACL 範圍之外的檔。如果專案的
.git目錄包含您以前使用的 acl 檔的副本, 則下面的pre-commit腳本將為您強制實施這些約束:#!/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
這與伺服器端部分的腳本大同小異, 但有兩個重要區別。首先, ACL 檔位於不同的位置, 因為此腳本從您的工作目錄運行, 而不是從
.git目錄運行。您必須從此更改 ACL 檔的路徑。access = get_acl_access_data('acl')
到此:
access = get_acl_access_data('.git/acl')
另一個重要區別是您獲取已更改的檔案清單的方式。由於伺服器端方法查看提交的日誌, 並且此時尚未記錄提交, 因此必須從臨時區域獲取檔案清單。而不是
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
你必須使用
files_modified = `git diff-index --cached --name-only HEAD`
但這是唯一的兩個區別-否則, 腳本的工作方式相同。需要注意的是, 它希望您在本地以推送到遠端電腦的同一使用者的身份運行。如果這不同, 則必須手動設置
$user變數。
我們在這裡可以做的另一件事是確保使用者不會推送非快速轉發的引用。若要獲取不是快進的引用, 您要麼必須重新建立基礎, 通過已向上推送的提交, 要麼嘗試將不同的本地分支推送到同一個遠端分支。
據推測, 伺服器已經配置了
receive.denyDeletes deny效賽和receive.denyNonFastForwards此策略, 因此您可以嘗試捕獲的唯一意外的事情是重新建立已推送的提交的基礎。
下面是一個檢查它的預重新基腳本的示例。它將獲取您要重寫的所有提交的清單, 並檢查它們是否存在於您的任何遠端參照中。如果它看到一個可以從您的遠端參照之一訪問, 它將中止重新基。
#!/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
此腳本使用的語法未在修訂選擇中介紹。您將獲得已通過運行以下操作推送的提交清單:
`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
SHA^@解析為該提交的所有父項。您正在尋找從遙控器上的最後一次提交中可以訪問的任何提交, 並且無法從您試圖推送的任何 Sha-1 的任何父提交中訪問這些提交--這意味著它是一個快進。
這種方法的主要缺點是它可能非常緩慢, 而且通常是不必要的--如果您不嘗試用
-f強制推送, 伺服器將警告您, 而不接受推送。然而, 這是一個有趣的練習, 理論上可以説明你避免重新開始, 你以後可能不得不回去修復。
沒有留言:
張貼留言