-
1. 開始
-
2. Git 基礎
-
3. Git 分支
-
4. 伺服器上的 Git
- 4.1 協定
- 4.2 在伺服器上取得 Git
- 4.3 產生您的 SSH 公開金鑰
- 4.4 設定伺服器
- 4.5 Git Daemon
- 4.6 智慧型 HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方託管選項
- 4.10 總結
-
5. 分散式 Git
-
A1. 附錄 A:其他環境中的 Git
- A1.1 圖形介面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 總結
-
A2. 附錄 B:將 Git 嵌入到您的應用程式中
-
A3. 附錄 C:Git 指令
8.4 自訂 Git - 一個範例 Git 強制政策
一個範例 Git 強制政策
在本節中,您將使用您所學到的知識來建立 Git 工作流程,該工作流程會檢查自訂的提交訊息格式,並且只允許特定使用者修改專案中的特定子目錄。您將建置用戶端腳本,以幫助開發人員了解他們的推送是否會被拒絕,以及實際強制執行政策的伺服器腳本。
我們展示的腳本是用 Ruby 編寫的;部分原因是我們的慣性,但也因為 Ruby 易於閱讀,即使您不一定能編寫它。然而,任何語言都可以使用 – Git 隨附的所有範例鉤子腳本都是用 Perl 或 Bash 編寫的,因此您也可以透過查看範例來查看這些語言中的許多鉤子範例。
伺服器端鉤子
所有伺服器端的工作都將進入您 hooks
目錄中的 update
檔案。每個要推送的分支都會執行一次 update
鉤子,並接受三個引數
-
要推送到的參考名稱
-
該分支所在的舊修訂版本
-
要推送的新修訂版本
如果推送是透過 SSH 執行的,您也可以存取執行推送的使用者。如果您允許每個人透過公開金鑰驗證以單一使用者(例如「git」)連線,則可能必須為該使用者提供一個 shell 包裝器,該包裝器會根據公開金鑰判斷哪個使用者正在連線,並據此設定環境變數。在這裡,我們假設連線使用者在 $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
值並將它們傳遞給名為 git rev-list
的 Git 底層指令,來取得所有正在推送的提交的 SHA-1 值清單。這基本上是 git log
指令,但預設情況下它只會列印 SHA-1 值,而不列印其他資訊。因此,要取得一個提交 SHA-1 和另一個提交 SHA-1 之間引入的所有提交 SHA-1 清單,您可以執行如下命令
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
你可以取得那個輸出,迴圈遍歷每個 commit 的 SHA-1 值,抓取其訊息,並針對該訊息測試是否符合特定模式的正規表達式。
你必須找出如何從每個 commit 取得 commit 訊息以進行測試。若要取得原始 commit 資料,你可以使用另一個名為 git cat-file
的底層命令。我們會在Git 內部機制中詳細介紹所有這些底層命令;但現在,這裡說明該命令會提供你什麼
$ 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 值時,從 commit 取得 commit 訊息的一個簡單方法是找到第一個空白行,並取其後的所有內容。你可以在 Unix 系統上使用 sed
命令來做到這一點
$ git cat-file commit ca82a6 | sed '1,/^$/d'
Change the version number
你可以使用這個指令來抓取每個嘗試推送的 commit 的 commit 訊息,如果發現任何不符合的內容就退出。若要退出腳本並拒絕推送,請以非零值退出。完整的方法如下所示
$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
腳本中,將會拒絕包含不符合規則的訊息的 commit 的更新。
強制執行基於用戶的 ACL 系統
假設你想要加入一個機制,該機制使用存取控制列表 (ACL),指定哪些用戶可以將變更推送至你的專案的哪些部分。有些人擁有完全存取權,而其他人則只能將變更推送至特定的子目錄或特定檔案。為了強制執行此操作,你會將這些規則寫入名為 acl
的檔案中,該檔案位於伺服器上的裸 Git 儲存庫中。你會讓 update
hook 檢查這些規則,查看所有被推送的 commit 中引入了哪些檔案,並判斷執行推送的使用者是否有權更新所有這些檔案。
你要做的第一件事是編寫你的 ACL。在這裡,你將使用與 CVS ACL 機制非常相似的格式:它使用一系列行,其中第一個欄位是 avail
或 unavail
,下一個欄位是以逗號分隔的規則所適用的使用者列表,最後一個欄位是規則適用的路徑(空白表示開放存取)。所有這些欄位都以管道符號 (|
) 字元分隔。
在這個例子中,你有幾個管理員、一些可以存取 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"]}
既然你已經整理好權限,你需要判斷正在推送的 commit 修改了哪些路徑,以便你可以確保正在推送的使用者有權存取所有這些路徑。
你可以使用 git log
命令的 --name-only
選項(在Git 基礎中簡要提及)輕鬆查看單個 commit 中修改了哪些檔案
$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rb
如果你使用從 get_acl_access_data
方法傳回的 ACL 結構,並針對每個 commit 中列出的檔案進行檢查,你可以判斷使用者是否有權推送他們的所有 commit
# 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
取得推送到伺服器的新 commit 列表。然後,對於每個 commit,你找出修改了哪些檔案,並確保正在推送的使用者有權存取所有正在修改的路徑。
現在,你的使用者無法推送任何具有格式錯誤的訊息或修改其指定路徑之外的檔案的 commit。
測試它
如果你執行 chmod u+x .git/hooks/update
,這是你應該將所有這些程式碼放入其中的檔案,然後嘗試推送具有不符合規範訊息的 commit,你會得到類似這樣的內容
$ 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'
這裡有幾個有趣的地方。首先,你會看到 hook 從這裡開始執行。
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'
你會看到每個 hook 拒絕的參考的遠端拒絕訊息,並且會告訴你這是因為 hook 失敗而被拒絕的。
此外,如果有人嘗試編輯他們沒有存取權限的檔案並推送包含該檔案的 commit,他們會看到類似的內容。例如,如果文件作者嘗試推送一個修改 lib
目錄中內容的 commit,他們會看到
[POLICY] You do not have access to push to lib/test.rb
從現在開始,只要該 update
腳本存在且可執行,你的儲存庫將永遠不會有任何 commit 訊息沒有你的模式,並且你的使用者將會被放入沙箱中。
用戶端 Hook
這種方法的缺點是,當你的用戶的 commit 推送被拒絕時,不可避免地會導致抱怨。他們精心製作的工作在最後一刻被拒絕可能會令人感到非常沮喪和困惑;此外,他們還必須編輯他們的歷史記錄才能更正它,這對於膽小的人來說並不總是那麼容易。
解決這個困境的方法是提供一些用戶端 hook,用戶可以執行這些 hook 來通知他們何時正在執行伺服器很可能會拒絕的操作。這樣,他們可以在 commit 之前以及在這些問題變得更難修復之前更正任何問題。由於 hook 不會隨著專案的複製而傳輸,因此你必須以其他方式分發這些腳本,然後讓你的使用者將它們複製到他們的 .git/hooks
目錄並使其可執行。你可以在專案中或在單獨的專案中分發這些 hook,但 Git 不會自動設定它們。
首先,你應該在每次記錄 commit 之前檢查你的 commit 訊息,以便你了解伺服器不會因為 commit 訊息格式錯誤而拒絕你的變更。若要執行此操作,你可以新增 commit-msg
hook。如果讓它從作為第一個引數傳遞的檔案中讀取訊息,並將其與模式進行比較,則可以在沒有匹配的情況下強制 Git 中止 commit
#!/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
在該實例中沒有完成任何 commit。但是,如果你的訊息包含正確的模式,Git 會允許你 commit
$ 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')
另一個重要的差異是你取得已變更檔案列表的方式。由於伺服器端方法會查看 commit 的日誌,並且在這一點上,commit 尚未被記錄,因此你必須從暫存區域取得你的檔案列表。而不是
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
你必須使用
files_modified = `git diff-index --cached --name-only HEAD`
但這只是兩個差異 – 否則,該腳本的工作方式相同。一個需要注意的是,它期望你以與你推送至遠端電腦時相同的用戶身分在本地執行。如果不同,你必須手動設定 $user
變數。
我們可以在此處執行的另一件事是確保使用者不會推送非快轉的參考。若要取得不是快轉的參考,你必須重新設定基準過去你已推送的 commit,或嘗試將不同的本地分支推送至相同的遠端分支。
據推測,伺服器已設定 receive.denyDeletes
和 receive.denyNonFastForwards
來強制執行此原則,因此你唯一可以嘗試捕獲的意外事情是重新設定已推送 commit 的基準。
以下是一個檢查該情況的 pre-rebase 腳本範例。它會取得你即將重寫的所有 commit 的列表,並檢查它們是否存在於你的任何遠端參考中。如果它看到一個可以從你的遠端參考中訪問到的 commit,它會中止重新設定基準。
#!/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
此腳本使用版本選擇中未涵蓋的語法。你可以執行以下命令來取得已推送的 commit 列表
`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
SHA^@
語法會解析為該 commit 的所有父系。你正在尋找任何可以從遠端的最後一個 commit 訪問的 commit,並且該 commit 無法從你嘗試推送的任何 SHA-1 的任何父系訪問 – 這表示它是快轉。
這種方法的主要缺點是它可能非常慢且通常是不必要的 – 如果你沒有嘗試使用 -f
強制推送,伺服器將會警告你並且不會接受推送。但是,這是一個有趣的練習,並且在理論上可以幫助你避免重新設定基準,而你可能稍後必須回去修復它。