原文:《Pro Git》
自定義 Git
到目前為止,我闡述了 Git 基本的運作機制和使用方式,介紹了 Git 提供的許多工具來幫助你簡單且有效地使用它。 在本章,我將會介紹 Git 的一些重要的配置方法和鉤子機制以滿足自定義的要求。通過這些工具,它會和你和公司或團隊配合得天衣無縫。(伯樂線上注:如果你對Git還不瞭解,建議從本Git系列第一篇文章開始閱讀)
7.1 配置 Git
如第一章所言,用git config
配置 Git,要做的第一件事就是設定名字和郵箱地址:
1 2 |
$ git config --global user.name "John Doe" $ git config --global user.email johndoe@ example.com |
從現在開始,你會了解到一些類似以上但更為有趣的設定選項來自定義 Git。 先過一遍第一章中提到的 Git 配置細節。Git 使用一系列的配置檔案來儲存你定義的偏好,它首先會查詢/etc/gitconfig
檔案,該檔案含有 對系統上所有使用者及他們所擁有的倉庫都生效的配置值(譯註:gitconfig是全域性配置檔案), 如果傳遞--system
選項給git config
命令, Git 會讀寫這個檔案。 接下來 Git 會查詢每個使用者的~/.gitconfig
檔案,你能傳遞--global
選項讓 Git讀寫該檔案。 最後 Git 會查詢由使用者定義的各個庫中 Git 目錄下的配置檔案(.git/config
),該檔案中的值只對屬主庫有效。 以上闡述的三層配置從一般到特殊層層推進,如果定義的值有衝突,以後面層中定義的為準,例如:在.git/config
和/etc/gitconfig
的較量中,.git/config
取得了勝利。雖然你也可以直接手動編輯這些配置檔案,但是執行git config
命令將會來得簡單些。
客戶端基本配置
Git 能夠識別的配置項被分為了兩大類:客戶端和伺服器端,其中大部分基於你個人工作偏好,屬於客戶端配置。儘管有數不盡的選項,但我只闡述 其中經常使用或者會對你的工作流產生巨大影響的選項,如果你想觀察你當前的 Git 能識別的選項列表,請執行
1 |
$ git config --help |
git config
的手冊頁(譯註:以man命令的顯示方式)非常細緻地羅列了所有可用的配置項。
core.editor
Git預設會呼叫你的環境變數editor定義的值作為文字編輯器,如果沒有定義的話,會呼叫Vi來建立和編輯提交以及標籤資訊, 你可以使用core.editor
改變預設編輯器:
1 |
$ git config --global core.editor emacs |
現在無論你的環境變數editor被定義成什麼,Git 都會呼叫Emacs編輯資訊。
commit.template
如果把此項指定為你係統上的一個檔案,當你提交的時候, Git 會預設使用該檔案定義的內容。 例如:你建立了一個模板檔案$HOME/.gitmessage.txt
,它看起來像這樣:
1 2 3 4 5 |
subject line what happened [ticket: X] |
設定commit.template
,當執行git commit
時, Git 會在你的編輯器中顯示以上的內容, 設定commit.template
如下:
1 2 |
$ git config --global commit.template $HOME/.gitmessage.txt $ git commit |
然後當你提交時,在編輯器中顯示的提交資訊如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
subject line what happened [ticket: X] # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: lib/test.rb # ~ ~ ".git/COMMIT_EDITMSG" 14L, 297C |
如果你有特定的策略要運用在提交資訊上,在系統上建立一個模板檔案,設定 Git 預設使用它,這樣當提交時,你的策略每次都會被運用。
core.pager
core.pager指定 Git 執行諸如log
、diff
等所使用的分頁器,你能設定成用more
或者任何你喜歡的分頁器(預設用的是less
), 當然你也可以什麼都不用,設定空字串:
1 |
$ git config --global core.pager '' |
這樣不管命令的輸出量多少,都會在一頁顯示所有內容。
user.signingkey
如果你要建立經簽署的含附註的標籤(正如第二章所述),那麼把你的GPG簽署金鑰設定為配置項會更好,設定金鑰ID如下:
1 |
$ git config --global user.signingkey <gpg-key-id> |
現在你能夠簽署標籤,從而不必每次執行git tag
命令時定義金鑰:
1 |
$ git tag -s <tag-name> |
core.excludesfile
正如第二章所述,你能在專案庫的.gitignore
檔案裡頭用模式來定義那些無需納入 Git 管理的檔案,這樣它們不會出現在未跟蹤列表, 也不會在你執行git add
後被暫存。然而,如果你想用專案庫之外的檔案來定義那些需被忽略的檔案的話,用core.excludesfile
通知 Git 該檔案所處的位置,檔案內容和.gitignore
類似。
help.autocorrect
該配置項只在 Git 1.6.1及以上版本有效,假如你在Git 1.6中錯打了一條命令,會顯示:
1 2 3 4 5 |
$ git com git: 'com' is not a git-command. See 'git --help'. Did you mean this? commit |
如果你把help.autocorrect
設定成1(譯註:啟動自動修正),那麼在只有一個命令被模糊匹配到的情況下,Git 會自動執行該命令。
Git中的著色
Git能夠為輸出到你終端的內容著色,以便你可以憑直觀進行快速、簡單地分析,有許多選項能供你使用以符合你的偏好。
color.ui
Git會按照你需要自動為大部分的輸出加上顏色,你能明確地規定哪些需要著色以及怎樣著色,設定color.ui
為true來開啟所有的預設終端著色。
1 |
$ git config --global color.ui true |
設定好以後,當輸出到終端時,Git 會為之加上顏色。其他的引數還有false和always,false意味著不為輸出著色,而always則表明在任何情況下都要著色,即使 Git 命令被重定向到檔案或管道。Git 1.5.5版本引進了此項配置,如果你擁有的版本更老,你必須對顏色有關選項各自進行詳細地設定。 你會很少用到color.ui = always
,在大多數情況下,如果你想在被重定向的輸出中插入顏色碼,你能傳遞--color
標誌給 Git 命令來迫使它這麼做,color.ui = true
應該是你的首選。
color.*
想要具體到哪些命令輸出需要被著色以及怎樣著色或者 Git 的版本很老,你就要用到和具體命令有關的顏色配置選項,它們都能被置為true
、false
或always
:
1 2 3 4 |
color.branch color.diff color.interactive color.status |
除此之外,以上每個選項都有子選項,可以被用來覆蓋其父設定,以達到為輸出的各個部分著色的目的。例如,讓diff輸出的改變資訊以粗體、藍色前景和黑色背景的形式顯示:
1 |
$ git config --global color.diff.meta “blue black bold” |
你能設定的顏色值如:normal、black、red、green、yellow、blue、magenta、cyan、white,正如以上例子設定的粗體屬性,想要設定字型屬性的話,可以選擇如:bold、dim、ul、blink、reverse。 如果你想配置子選項的話,可以參考git config
幫助頁。
外部的合併與比較工具
雖然 Git 自己實現了diff,而且到目前為止你一直在使用它,但你能夠用一個外部的工具替代它,除此以外,你還能用一個圖形化的工具來合併和解決衝突從而不必自己手動解決。有一個不錯且免費的工具可以被用來做比較和合並工作,它就是P4Merge(譯註:Perforce圖形化合並工具),我會展示它的安裝過程。 P4Merge可以在所有主流平臺上執行,現在開始大膽嘗試吧。對於向你展示的例子,在Mac和Linux系統上,我會使用路徑名,在Windows上,/usr/local/bin
應該被改為你環境中的可執行路徑。 下載P4Merge:
1 |
http://www.perforce.com/perforce/downloads/component.html |
首先把你要執行的命令放入外部包裝指令碼中,我會使用Mac系統上的路徑來指定該指令碼的位置,在其他系統上,它應該被放置在二進位制檔案p4merge
所在的目錄中。建立一個merge包裝指令碼,名字叫作extMerge
,讓它帶引數呼叫p4merge
二進位制檔案:
1 2 3 |
$ cat /usr/local/bin/extMerge #!/bin/sh /Applications/p4merge.app/Contents/MacOS/p4merge $* |
diff包裝指令碼首先確定傳遞過來7個引數,隨後把其中2個傳遞給merge包裝指令碼,預設情況下, Git 傳遞以下引數給diff:
1 |
path old-file old-hex old-mode new-file new-hex new-mode |
由於你僅僅需要old-file
和new-file
引數,用diff包裝指令碼來傳遞它們吧。
1 2 3 |
$ cat /usr/local/bin/extDiff #!/bin/sh [ $# -eq 7 ] && /usr/local/bin/extMerge "$2" "$5" |
確認這兩個指令碼是可執行的:
1 2 |
$ sudo chmod +x /usr/local/bin/extMerge $ sudo chmod +x /usr/local/bin/extDiff |
現在來配置使用你自定義的比較和合並工具吧。這需要許多自定義設定:merge.tool
通知 Git 使用哪個合併工具;mergetool.*.cmd
規定命令執行的方式;mergetool.trustExitCode
會通知 Git 程式的退出是否指示合併操作成功;diff.external
通知 Git 用什麼命令做比較。因此,你能執行以下4條配置命令:
1 2 3 4 5 |
$ git config --global merge.tool extMerge $ git config --global mergetool.extMerge.cmd \ 'extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"' $ git config --global mergetool.trustExitCode false $ git config --global diff.external extDiff |
或者直接編輯~/.gitconfig
檔案如下:
1 2 3 4 5 6 7 |
[merge] tool = extMerge [mergetool "extMerge"] cmd = extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED" trustExitCode = false [diff] external = extDiff |
設定完畢後,執行diff命令:
1 |
; html-script: false ]$ git diff 32d1776b1^ 32d1776b1 |
命令列居然沒有發現diff命令的輸出,其實,Git 呼叫了剛剛設定的P4Merge,它看起來像圖7-1這樣:
Figure 7-1. P4Merge.
當你設法合併兩個分支,結果卻有衝突時,執行git mergetool
,Git 會呼叫P4Merge讓你通過圖形介面來解決衝突。 設定包裝指令碼的好處是你能簡單地改變diff和merge工具,例如把extDiff
和extMerge
改成KDiff3,要做的僅僅是編輯extMerge
指令碼檔案:
1 2 3 |
$ cat /usr/local/bin/extMerge #!/bin/sh /Applications/kdiff3.app/Contents/MacOS/kdiff3 $* |
現在 Git 會使用KDiff3來做比較、合併和解決衝突。 Git預先設定了許多其他的合併和解決衝突的工具,而你不必設定cmd。可以把合併工具設定為:kdiff3、opendiff、tkdiff、 meld、xxdiff、emerge、vimdiff、gvimdiff。如果你不想用到KDiff3的所有功能,只是想用它來合併,那麼kdiff3 正符合你的要求,執行:
1 |
$ git config --global merge.tool kdiff3 |
如果執行了以上命令,沒有設定extMerge
和extDiff
檔案,Git 會用KDiff3做合併,讓通常內設的比較工具來做比較。
格式化與空白
格式化與空白是許多開發人員在協作時,特別是在跨平臺情況下,遇到的令人頭疼的細小問題。由於編輯器的不同或者Windows程式設計師在跨平臺專案中的檔案行尾加入了回車換行符,一些細微的空格變化會不經意地進入大家合作的工作或提交的補丁中。不用怕,Git 的一些配置選項會幫助你解決這些問題。
core.autocrlf
假如你正在Windows上寫程式,又或者你正在和其他人合作,他們在Windows上程式設計,而你卻在其他系統上,在這些情況下,你可能會遇到行尾結束符問題。這是因為Windows使用回車和換行兩個字元來結束一行,而Mac和Linux只使用換行一個字元。雖然這是小問題,但它會極大地擾亂跨平臺協作。 Git可以在你提交時自動地把行結束符CRLF轉換成LF,而在簽出程式碼時把LF轉換成CRLF。用core.autocrlf
來開啟此項功能,如果是在Windows系統上,把它設定成true
,這樣當簽出程式碼時,LF會被轉換成CRLF:
1 |
$ git config --global core.autocrlf true |
Linux或Mac系統使用LF作為行結束符,因此你不想 Git 在簽出檔案時進行自動的轉換;當一個以CRLF為行結束符的檔案不小心被引入時你肯定想進行修正,把core.autocrlf
設定成input來告訴 Git 在提交時把CRLF轉換成LF,簽出時不轉換:
1 |
$ git config --global core.autocrlf input |
這樣會在Windows系統上的簽出檔案中保留CRLF,會在Mac和Linux系統上,包括倉庫中保留LF。 如果你是Windows程式設計師,且正在開發僅執行在Windows上的專案,可以設定false
取消此功能,把回車符記錄在庫中:
1 |
$ git config --global core.autocrlf false |
core.whitespace
Git預先設定了一些選項來探測和修正空白問題,其4種主要選項中的2個預設被開啟,另2個被關閉,你可以自由地開啟或關閉它們。 預設被開啟的2個選項是trailing-space
和space-before-tab
,trailing-space
會查詢每行結尾的空格,space-before-tab
會查詢每行開頭的製表符前的空格。 預設被關閉的2個選項是indent-with-non-tab
和cr-at-eol
,indent-with-non-tab
會查詢8個以上空格(非製表符)開頭的行,cr-at-eol
讓 Git 知道行尾回車符是合法的。 設定core.whitespace
,按照你的意圖來開啟或關閉選項,選項以逗號分割。通過逗號分割的鏈中去掉選項或在選項前加-
來關閉,例如,如果你想要開啟除了cr-at-eol
之外的所有選項:
1 2 |
$ git config --global core.whitespace \ trailing-space,space-before-tab,indent-with-non-tab |
當你執行git diff
命令且為輸出著色時,Git 探測到這些問題,因此你也許在提交前能修復它們,當你用git apply
打補丁時同樣也會從中受益。如果正準備運用的補丁有特別的空白問題,你可以讓 Git 發警告:
1 |
$ git apply --whitespace=warn <patch> |
或者讓 Git 在打上補丁前自動修正此問題:
1 |
$ git apply --whitespace=warn <patch> |
這些選項也能運用於衍合。如果提交了有空白問題的檔案但還沒推送到上流,你可以執行帶有--whitespace=fix
選項的rebase
來讓Git在重寫補丁時自動修正它們。
伺服器端配置
Git伺服器端的配置選項並不多,但仍有一些饒有生趣的選項值得你一看。
receive.fsckObjects
Git預設情況下不會在推送期間檢查所有物件的一致性。雖然會確認每個物件的有效性以及是否仍然匹配SHA-1檢驗和,但 Git 不會在每次推送時都檢查一致性。對於 Git 來說,庫或推送的檔案越大,這個操作代價就相對越高,每次推送會消耗更多時間,如果想在每次推送時 Git 都檢查一致性,設定receive.fsckObjects
為true來強迫它這麼做:
1 |
$ git config --system receive.fsckObjects true |
現在 Git 會在每次推送生效前檢查庫的完整性,確保有問題的客戶端沒有引入破壞性的資料。
receive.denyNonFastForwards
如果對已經被推送的提交歷史做衍合,繼而再推送,又或者以其它方式推送一個提交歷史至遠端分支,且該提交歷史沒在這個遠端分支中,這樣的推送會被拒絕。這通常是個很好的禁止策略,但有時你在做衍合併確定要更新遠端分支,可以在push命令後加-f
標誌來強制更新。 要禁用這樣的強制更新功能,可以設定receive.denyNonFastForwards
:
1 |
$ git config --system receive.denyNonFastForwards true |
稍後你會看到,用伺服器端的接收鉤子也能達到同樣的目的。這個方法可以做更細緻的控制,例如:禁用特定的使用者做強制更新。
receive.denyDeletes
規避denyNonFastForwards
策略的方法之一就是使用者刪除分支,然後推回新的引用。在更新的 Git 版本中(從1.6.1版本開始),把receive.denyDeletes
設定為true:
1 |
$ git config --system receive.denyDeletes true |
這樣會在推送過程中阻止刪除分支和標籤 — 沒有使用者能夠這麼做。要刪除遠端分支,必須從伺服器手動刪除引用檔案。通過使用者訪問控制列表也能這麼做,在本章結尾將會介紹這些有趣的方式。
7.2 Git屬性
一些設定項也能被運用於特定的路徑中,這樣,Git 以對一個特定的子目錄或子檔案集運用那些設定項。這些設定項被稱為 Git 屬性,可以在你目錄中的.gitattributes
檔案內進行設定(通常是你專案的根目錄),也可以當你不想讓這些屬性檔案和專案檔案一同提交時,在.git/info/attributes
進行設定。 使用屬性,你可以對個別檔案或目錄定義不同的合併策略,讓 Git 知道怎樣比較非文字檔案,在你提交或簽出前讓 Git 過濾內容。你將在這部分了解到能在自己的專案中使用的屬性,以及一些例項。
二進位制檔案
你可以用 Git 屬性讓其知道哪些是二進位制檔案(以防 Git 沒有識別出來),以及指示怎樣處理這些檔案,這點很酷。例如,一些文字檔案是由機器產生的,而且無法比較,而一些二進位制檔案可以比較 — 你將會了解到怎樣讓 Git 識別這些檔案。
識別二進位制檔案
一些檔案看起來像是文字檔案,但其實是作為二進位制資料被對待。例如,在Mac上的Xcode專案含有一個以.pbxproj
結尾的檔案,它是由記錄設定項的IDE寫到磁碟的JSON資料集(純文字javascript資料型別)。雖然技術上看它是由ASCII字元組成的文字檔案,但你並不認為如此,因為它確實是一個輕量級資料庫 — 如果有2人改變了它,你通常無法合併和比較內容,只有機器才能進行識別和操作,於是,你想把它當成二進位制檔案。 讓 Git 把所有pbxproj
檔案當成二進位制檔案,在.gitattributes
檔案中設定如下:
1 |
*.pbxproj -crlf -diff |
現在,Git 會嘗試轉換和修正CRLF(回車換行)問題,也不會當你在專案中執行git show或git diff時,比較不同的內容。在Git 1.6及之後的版本中,可以用一個巨集代替-crlf -diff
:
1 |
*.pbxproj binary |
比較二進位制檔案
在Git 1.6及以上版本中,你能利用 Git 屬性來有效地比較二進位制檔案。可以設定 Git 把二進位制資料轉換成文字格式,用通常的diff來比較。 這個特性很酷,而且鮮為人知,因此我會結合例項來講解。首先,要解決的是最令人頭疼的問題:對Word文件進行版本控制。很多人對Word文件又恨又愛,如果想對其進行版本控制,你可以把檔案加入到 Git 庫中,每次修改後提交即可。但這樣做沒有一點實際意義,因為執行git diff
命令後,你只能得到如下的結果:
1 2 3 4 |
$ git diff diff --git a/chapter1.doc b/chapter1.doc index 88839c4..4afcb7c 100644 Binary files a/chapter1.doc and b/chapter1.doc differ |
你不能直接比較兩個不同版本的Word檔案,除非進行手動掃描,不是嗎? Git 屬效能很好地解決此問題,把下面的行加到.gitattributes
檔案:
1 |
*.doc diff=word |
當你要看比較結果時,如果副檔名是”doc”,Git 呼叫”word”過濾器。什麼是”word”過濾器呢?其實就是 Git 使用strings
程式,把Word文件轉換成可讀的文字檔案,之後再進行比較:
1 |
$ git config diff.word.textconv strings |
現在如果在兩個快照之間比較以.doc
結尾的檔案,Git 對這些檔案運用”word”過濾器,在比較前把Word檔案轉換成文字檔案。 下面展示了一個例項,我把此書的第一章納入 Git 管理,在一個段落中加入了一些文字後儲存,之後執行git diff
命令,得到結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
$ git diff diff --git a/chapter1.doc b/chapter1.doc index c1c8a0a..b93c9e4 100644 --- a/chapter1.doc +++ b/chapter1.doc @@ -8,7 +8,8 @@ re going to cover Version Control Systems (VCS) and Git basics re going to cover how to get it and set it up for the first time if you don t already have it on your system. In Chapter Two we will go over basic Git usage - how to use Git for the 80% -s going on, modify stuff and contribute changes. If the book spontaneously +s going on, modify stuff and contribute changes. If the book spontaneously +Let's see if this works. |
Git 成功且簡潔地顯示出我增加的文字”Let’s see if this works”。雖然有些瑕疵,在末尾顯示了一些隨機的內容,但確實可以比較了。如果你能找到或自己寫個Word到純文字的轉換器的話,效果可能會更好。strings
可以在大部分Mac和Linux系統上執行,所以它是處理二進位制格式的第一選擇。 你還能用這個方法比較影象檔案。當比較時,對JPEG檔案運用一個過濾器,它能提煉出EXIF資訊 — 大部分影象格式使用的後設資料。如果你下載並安裝了exiftool
程式,可以用它參照後設資料把影象轉換成文字。比較的不同結果將會用文字向你展示:
1 2 |
$ echo '*.png diff=exif' >> .gitattributes $ git config diff.exif.textconv exiftool |
如果在專案中替換了一個影象檔案,執行git diff
命令的結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
diff --git a/image.png b/image.png index 88839c4..4afcb7c 100644 --- a/image.png +++ b/image.png @@ -1,12 +1,12 @@ ExifTool Version Number : 7.74 -File Size : 70 kB -File Modification Date/Time : 2009:04:21 07:02:45-07:00 +File Size : 94 kB +File Modification Date/Time : 2009:04:21 07:02:43-07:00 File Type : PNG MIME Type : image/png -Image Width : 1058 -Image Height : 889 +Image Width : 1056 +Image Height : 827 Bit Depth : 8 Color Type : RGB with Alpha |
你會發現檔案的尺寸大小發生了改變。
關鍵字擴充套件
使用SVN或CVS的開發人員經常要求關鍵字擴充套件。在 Git 中,你無法在一個檔案被提交後修改它,因為 Git 會先對該檔案計算校驗和。然而,你可以在簽出時注入文字,在提交前刪除它。 Git 屬性提供了2種方式這麼做。 首先,你能夠把blob的SHA-1校驗和自動注入檔案的$Id$
欄位。如果在一個或多個檔案上設定了此欄位,當下次你簽出分支的時候,Git 用blob的SHA-1值替換那個欄位。注意,這不是提交物件的SHA校驗和,而是blob本身的校驗和:
1 2 |
$ echo '*.txt ident' >> .gitattributes $ echo '$Id$' > test.txt |
下次簽出檔案時,Git 入了blob的SHA值:
1 2 3 4 |
$ rm text.txt $ git checkout -- text.txt $ cat test.txt $Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $ |
然而,這樣的顯示結果沒有多大的實際意義。這個SHA的值相當地隨機,無法區分日期的前後,所以,如果你在CVS或Subversion中用過關鍵字替換,一定會包含一個日期值。 因此,你能寫自己的過濾器,在提交檔案到暫存區或簽出檔案時替換關鍵字。有2種過濾器,”clean”和”smudge”。在.gitattributes
檔案中,你能對特定的路徑設定一個過濾器,然後設定處理檔案的指令碼,這些指令碼會在檔案簽出前(”smudge”,見圖 7-2)和提交到暫存區前(”clean”,見圖7-3)被呼叫。這些過濾器能夠做各種有趣的事。
圖7-2. 簽出時,“smudge”過濾器被觸發。
圖7-3. 提交到暫存區時,“clean”過濾器被觸發。
這裡舉一個簡單的例子:在暫存前,用indent
(縮排)程式過濾所有C原始碼。在.gitattributes
檔案中設定”indent”過濾器過濾*.c
檔案:
1 |
*.c filter=indent |
然後,通過以下配置,讓 Git 知道”indent”過濾器在遇到”smudge”和”clean”時分別該做什麼:
1 2 |
$ git config --global filter.indent.clean indent $ git config --global filter.indent.smudge cat |
於是,當你暫存*.c
檔案時,indent
程式會被觸發,在把它們簽出之前,cat
程式會被觸發。但cat
程式在這裡沒什麼實際作用。這樣的組合,使C原始碼在暫存前被indent
程式過濾,非常有效。 另一個例子是類似RCS的$Date$
關鍵字擴充套件。為了演示,需要一個小指令碼,接受檔名引數,得到專案的最新提交日期,最後把日期寫入該檔案。下面用Ruby指令碼來實現:
1 2 3 4 |
#! /usr/bin/env ruby data = STDIN.read last_date = `git log --pretty=format:"%ad" -1` puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$') |
該指令碼從git log
命令中得到最新提交日期,找到檔案中的所有$Date$
字串,最後把該日期填充到$Date$
字串中 — 此指令碼很簡單,你可以選擇你喜歡的程式語言來實現。把該指令碼命名為expand_date
,放到正確的路徑中,之後需要在 Git 中設定一個過濾器(dater
),讓它在簽出檔案時呼叫expand_date
,在暫存檔案時用Perl清除之:
1 2 |
$ git config filter.dater.smudge expand_date $ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"' |
這個Perl小程式會刪除$Date$
字串裡多餘的字元,恢復$Date$
原貌。到目前為止,你的過濾器已經設定完畢,可以開始測試了。開啟一個檔案,在檔案中輸入$Date$
關鍵字,然後設定 Git 屬性:
1 2 |
$ echo '# $Date$' > date_test.txt $ echo 'date*.txt filter=dater' >> .gitattributes |
如果暫存該檔案,之後再簽出,你會發現關鍵字被替換了:
1 2 3 4 5 6 |
$ git add date_test.txt .gitattributes $ git commit -m "Testing date expansion in Git" $ rm date_test.txt $ git checkout date_test.txt $ cat date_test.txt # $Date: Tue Apr 21 07:26:52 2009 -0700$ |
雖說這項技術對自定義應用來說很有用,但還是要小心,因為.gitattributes
檔案會隨著專案一起提交,而過濾器(例如:dater
)不會,所以,過濾器不會在所有地方都生效。當你在設計這些過濾器時要注意,即使它們無法正常工作,也要讓整個專案運作下去。
匯出倉庫
Git屬性在匯出專案歸檔時也能發揮作用。
export-ignore
當產生一個歸檔時,可以設定 Git 不匯出某些檔案和目錄。如果你不想在歸檔中包含一個子目錄或檔案,但想他們納入專案的版本管理中,你能對應地設定export-ignore
屬性。 例如,在test/
子目錄中有一些測試檔案,在專案的壓縮包中包含他們是沒有意義的。因此,可以增加下面這行到 Git 屬性檔案中:
1 |
test/ export-ignore |
現在,當執行git archive來建立專案的壓縮包時,那個目錄不會在歸檔中出現。
export-subst
還能對歸檔做一些簡單的關鍵字替換。在第2章中已經可以看到,可以以--pretty=format
形式的簡碼在任何檔案中放入$Format:$
字串。例如,如果想在專案中包含一個叫作LAST_COMMIT
的檔案,當執行git archive
時,最後提交日期自動地注入進該檔案,可以這樣設定:
1 2 3 4 |
$ echo 'Last commit date: $Format:%cd$' > LAST_COMMIT $ echo "LAST_COMMIT export-subst" >> .gitattributes $ git add LAST_COMMIT .gitattributes $ git commit -am 'adding LAST_COMMIT file for archives' |
執行git archive
後,開啟該檔案,會發現其內容如下:
1 2 |
$ cat LAST_COMMIT Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$ |
合併策略
通過 Git 屬性,還能對專案中的特定檔案使用不同的合併策略。一個非常有用的選項就是,當一些特定檔案發生衝突,Git 會嘗試合併他們,而使用你這邊的合併。 如果專案的一個分支有歧義或比較特別,但你想從該分支合併,而且需要忽略其中某些檔案,這樣的合併策略是有用的。例如,你有一個資料庫設定檔案database.xml,在2個分支中他們是不同的,你想合併一個分支到另一個,而不弄亂該資料庫檔案,可以設定屬性如下:
1 |
database.xml merge=ours |
如果合併到另一個分支,database.xml檔案不會有合併衝突,顯示如下:
1 2 3 |
$ git merge topic Auto-merging database.xml Merge made by recursive. |
這樣,database.xml會保持原樣。
7.3 Git掛鉤
和其他版本控制系統一樣,當某些重要事件發生時,Git 以呼叫自定義指令碼。有兩組掛鉤:客戶端和伺服器端。客戶端掛鉤用於客戶端的操作,如提交和合並。伺服器端掛鉤用於 Git 伺服器端的操作,如接收被推送的提交。你可以隨意地使用這些掛鉤,下面會講解其中一些。
安裝一個掛鉤
掛鉤都被儲存在 Git 目錄下的hooks
子目錄中,即大部分專案中的.git/hooks
。 Git 預設會放置一些指令碼樣本在這個目錄中,除了可以作為掛鉤使用,這些樣本本身是可以獨立使用的。所有的樣本都是shell指令碼,其中一些還包含了Perl的指令碼,不過,任何正確命名的可執行指令碼都可以正常使用 — 可以用Ruby或Python,或其他。在Git 1.6版本之後,這些樣本名都是以.sample結尾,因此,你必須重新命名。在Git 1.6版本之前,這些樣本名都是正確的,但這些樣本不是可執行檔案。 把一個正確命名且可執行的檔案放入 Git 目錄下的hooks
子目錄中,可以啟用該掛鉤指令碼,因此,之後他一直會被 Git 呼叫。隨後會講解主要的掛鉤指令碼。
客戶端掛鉤
有許多客戶端掛鉤,以下把他們分為:提交工作流掛鉤、電子郵件工作流掛鉤及其他客戶端掛鉤。
提交工作流掛鉤
有 4個掛鉤被用來處理提交的過程。pre-commit
掛鉤在鍵入提交資訊前執行,被用來檢查即將提交的快照,例如,檢查是否有東西被遺漏,確認測試是否執行,以及檢查程式碼。當從該掛鉤返回非零值時,Git 放棄此次提交,但可以用git commit --no-verify
來忽略。該掛鉤可以被用來檢查程式碼錯誤(執行類似lint的程式),檢查尾部空白(預設掛鉤是這麼做的),檢查新方法(譯註:程式的函式)的說明。 prepare-commit-msg
掛鉤在提交資訊編輯器顯示之前,預設資訊被建立之後執行。因此,可以有機會在提交作者看到預設資訊前進行編輯。該掛鉤接收一些選項:擁有提交資訊的檔案路徑,提交型別,如果是一次修訂的話,提交的SHA-1校驗和。該掛鉤對通常的提交來說不是很有用,只在自動產生的預設提交資訊的情況下有作用,如提交資訊模板、合併、壓縮和修訂提交等。可以和提交模板配合使用,以程式設計的方式插入資訊。 commit-msg
掛鉤接收一個引數,此引數是包含最近提交資訊的臨時檔案的路徑。如果該掛鉤指令碼以非零退出,Git 放棄提交,因此,可以用來在提交通過前驗證專案狀態或提交資訊。本章上一小節已經展示了使用該掛鉤核對提交資訊是否符合特定的模式。 post-commit
掛鉤在整個提交過程完成後執行,他不會接收任何引數,但可以執行git log -1 HEAD
來獲得最後的提交資訊。總之,該掛鉤是作為通知之類使用的。 提交工作流的客戶端掛鉤指令碼可以在任何工作流中使用,他們經常被用來實施某些策略,但值得注意的是,這些指令碼在clone期間不會被傳送。可以在伺服器端實施策略來拒絕不符合某些策略的推送,但這完全取決於開發者在客戶端使用這些指令碼的情況。所以,這些指令碼對開發者是有用的,由他們自己設定和維護,而且在任何時候都可以覆蓋或修改這些指令碼。
E-mail工作流掛鉤
有3個可用的客戶端掛鉤用於e-mail工作流。當執行git am
命令時,會呼叫他們,因此,如果你沒有在工作流中用到此命令,可以跳過本節。如果你通過e-mail接收由git format-patch
產生的補丁,這些掛鉤也許對你有用。 首先執行的是applypatch-msg
掛鉤,他接收一個引數:包含被建議提交資訊的臨時檔名。如果該指令碼非零退出,Git 放棄此補丁。可以使用這個指令碼確認提交資訊是否被正確格式化,或讓指令碼編輯資訊以達到標準化。 下一個在git am
執行期間呼叫是pre-applypatch
掛鉤。該掛鉤不接收引數,在補丁被運用之後執行,因此,可以被用來在提交前檢查快照。你能用此指令碼執行測試,檢查工作樹。如果有些什麼遺漏,或測試沒通過,指令碼會以非零退出,放棄此次git am
的執行,補丁不會被提交。 最後在git am
執行期間呼叫的是post-applypatch
掛鉤。你可以用他來通知一個小組或獲取的補丁的作者,但無法阻止打補丁的過程。
其他客戶端掛鉤
pre- rebase
掛鉤在衍合前執行,指令碼以非零退出可以中止衍合的過程。你可以使用這個掛鉤來禁止衍合已經推送的提交物件,Git pre- rebase
掛鉤樣本就是這麼做的。該樣本假定next是你定義的分支名,因此,你可能要修改樣本,把next改成你定義過且穩定的分支名。 在git checkout
成功執行後,post-checkout
掛鉤會被呼叫。他可以用來為你的專案環境設定合適的工作目錄。例如:放入大的二進位制檔案、自動產生的文件或其他一切你不想納入版本控制的檔案。 最後,在merge
命令成功執行後,post-merge
掛鉤會被呼叫。他可以用來在 Git 無法跟蹤的工作樹中恢復資料,諸如許可權資料。該掛鉤同樣能夠驗證在 Git 控制之外的檔案是否存在,因此,當工作樹改變時,你想這些檔案可以被複制。
伺服器端掛鉤
除了客戶端掛鉤,作為系統管理員,你還可以使用兩個伺服器端的掛鉤對專案實施各種型別的策略。這些掛鉤指令碼可以在提交物件推送到伺服器前被呼叫,也可以在推送到伺服器後被呼叫。推送到伺服器前呼叫的掛鉤可以在任何時候以非零退出,拒絕推送,返回錯誤訊息給客戶端,還可以如你所願設定足夠複雜的推送策略。
pre-receive 和 post-receive
處理來自客戶端的推送(push)操作時最先執行的指令碼就是 pre-receive
。它從標準輸入(stdin)獲取被推送引用的列表;如果它退出時的返回值不是0,所有推送內容都不會被接受。利用此掛鉤指令碼可以實現類似保證最新的索引中不包含非fast-forward型別的這類效果;抑或檢查執行推送操作的使用者擁有建立,刪除或者推送的許可權或者他是否對將要修改的每一個檔案都有訪問許可權。 post-receive
掛鉤在整個過程完結以後執行,可以用來更新其他系統服務或者通知使用者。它接受與 pre-receive
相同的標準輸入資料。應用例項包括給某郵件列表發信,通知實時整合資料的伺服器,或者更新軟體專案的問題追蹤系統 —— 甚至可以通過分析提交資訊來決定某個問題是否應該被開啟,修改或者關閉。該指令碼無法組織推送程式,不過客戶端在它完成執行之前將保持連線狀態;所以在用它作一些消耗時間的操作之前請三思。
update
update 指令碼和 pre-receive
指令碼十分類似。不同之處在於它會為推送者更新的每一個分支執行一次。假如推送者同時向多個分支推送內容,pre-receive
只執行一次,相比之下 update 則會為每一個更新的分支執行一次。它不會從標準輸入讀取內容,而是接受三個引數:索引的名字(分支),推送前索引指向的內容的 SHA-1 值,以及使用者試圖推送內容的 SHA-1 值。如果 update 指令碼以退出時返回非零值,只有相應的那一個索引會被拒絕;其餘的依然會得到更新。
7.4 Git 強制策略例項
在本節中,我們應用前面學到的知識建立這樣一個Git 工作流程:檢查提交資訊的格式,只接受純fast-forward內容的推送,並且指定使用者只能修改專案中的特定子目錄。我們將寫一個客戶端角本來提示開發人員他們推送的內容是否會被拒絕,以及一個服務端指令碼來實際執行這些策略。 這些指令碼使用 Ruby 寫成,一半由於它是作者傾向的指令碼語言,另外作者覺得它是最接近虛擬碼的指令碼語言;因而即便你不使用 Ruby 也能大致看懂。不過任何其他語言也一樣適用。所有 Git 自帶的樣例指令碼都是用 Perl 或 Bash 寫的。所以從這些指令碼中能找到相當多的這兩種語言的掛鉤樣例。
服務端掛鉤
所有服務端的工作都在hooks(掛鉤)目錄的 update(更新)指令碼中制定。update 指令碼為每一個得到推送的分支執行一次;它接受推送目標的索引,該分支原來指向的位置,以及被推送的新內容。如果推送是通過 SSH 進行的,還可以獲取發出此次操作的使用者。如果設定所有操作都通過公匙授權的單一帳號(比如"git")進行,就有必要通過一個 shell 包裝依據公匙來判斷使用者的身份,並且設定環境變數來表示該使用者的身份。下面假設嘗試連線的使用者儲存在$USER
環境變數裡,我們的 update 指令碼首先蒐集一切需要的資訊:
1 2 3 4 5 6 7 8 |
#!/usr/bin/env ruby $refname = ARGV[0] $oldrev = ARGV[1] $newrev = ARGV[2] $user = ENV['USER'] puts "Enforcing Policies... \n(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})" |
沒錯,我在用全域性變數。別鄙視我——這樣比較利於演示過程。
指定特殊的提交資訊格式
我們的第一項任務是指定每一條提交資訊都必須遵循某種特殊的格式。作為演示,假定每一條資訊必須包含一條形似 “ref: 1234” 這樣的字串,因為我們需要把每一次提交和專案的問題追蹤系統。我們要逐一檢查每一條推送上來的提交內容,看看提交資訊是否包含這麼一個字串,然後,如果該提交裡不包含這個字串,以非零返回值退出從而拒絕此次推送。 把 $newrev
和 $oldrev
變數的值傳給一個叫做 git rev-list
的 Git plumbing 命令可以獲取所有提交內容的 SHA-1 值列表。git rev-list
基本類似git log
命令,但它預設只輸出 SHA-1 值而已,沒有其他資訊。所以要獲取由 SHA 值表示的從一次提交到另一次提交之間的所有 SHA 值,可以執行:
1 2 3 4 5 6 |
$ git rev-list 538c33..d14fc7 d14fc7c847ab946ec39590d87783c69b031bdfb7 9f585da4401b0a3999e84113824d15245c13f0be 234071a1be950e2a8d078e6141f5cd20c1e61ad3 dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a 17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475 |
擷取這些輸出內容,迴圈遍歷其中每一個 SHA 值,找出與之對應的提交資訊,然後用正規表示式來測試該資訊包含的格式話的內容。 下面要搞定如何從所有的提交內容中提取出提交資訊。使用另一個叫做 git cat-file
的 Git plumbing 工具可以獲得原始的提交資料。我們將在第九章瞭解到這些 plumbing 工具的細節;現在暫時先看一下這條命令的輸出:
1 2 3 4 5 6 7 |
$ 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
命令來實現該效果:
1 2 |
$ git cat-file commit ca82a6 | sed '1,/^$/d' changed the version number |
這條咒語從每一個待提交內容裡提取提交資訊,並且會在提取資訊不符合要求的情況下退出。為了退出指令碼和拒絕此次推送,返回一個非零值。整個指令碼大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$regex = /\[ref: (\d+)\]/ # 指定提交資訊格式 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)系統
假設你需要新增一個使用訪問許可權控制列表的機制來指定哪些使用者對專案的哪些部分有推送許可權。某些使用者具有全部的訪問權,其他人只對某些子目錄或者特定的檔案具有推送許可權。要搞定這一點,所有的規則將被寫入一個位於伺服器的原始 Git 倉庫的acl
檔案。我們讓 update
掛鉤檢閱這些規則,審視推送的提交內容中需要修改的所有檔案,然後決定執行推送的使用者是否對所有這些檔案都有許可權。 我們首先要建立這個列表。這裡使用的格式和 CVS 的 ACL 機制十分類似:它由若干行構成,第一項內容是 avail
或者unavail
,接著是逗號分隔的規則生效使用者列表,最後一項是規則生效的目錄(空白表示開放訪問)。這些專案由 |
字元隔開。 下例中,我們指定幾個管理員,幾個對 doc
目錄具有許可權的文件作者,以及一個對 lib
和 tests
目錄具有許可權的開發人員,相應的 ACL 檔案如下:
1 2 3 4 |
avail|nickh,pjhyett,defunkt,tpw avail|usinclair,cdickens,ebronte|doc avail|schacon|lib avail|schacon|tests |
首先把這些資料讀入你編寫的資料結構。本例中,為保持簡潔,我們暫時只實現 avail
的規則(譯註:也就是省略了unavail
部分)。下面這個方法生成一個關聯陣列,它的主鍵是使用者名稱,值是一個該使用者有寫許可權的所有目錄組成的陣列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
方法返回的資料結構如下:
1 2 3 4 5 6 7 8 |
{"defunkt"=>[nil], "tpw"=>[nil], "nickh"=>[nil], "pjhyett"=>[nil], "schacon"=>["lib", "tests"], "cdickens"=>["doc"], "usinclair"=>["doc"], "ebronte"=>["doc"]} |
搞定了使用者許可權的資料,下面需要找出哪些位置將要被提交的內容修改,從而確保試圖推送的使用者對這些位置有全部的許可權。 使用 git log
的 --name-only
選項(在第二章裡簡單的提過)我們可以輕而易舉的找出一次提交裡修改的檔案:
1 2 3 4 |
$ git log -1 --name-only --pretty=format:'' 9f585d README lib/test.rb |
使用 get_acl_access_data
返回的 ACL 結構來一一核對每一次提交修改的檔案列表,就能找出該使用者是否有許可權推送所有的提交內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# 僅允許特定使用者修改專案中的特定子目錄 def check_directory_perms access = get_acl_access_data('acl') # 檢查是否有人在向他沒有許可權的地方推送內容 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 # 使用者擁有完全訪問許可權 || (path.index(access_path) == 0) # 或者對此位置有訪問許可權 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
獲取推送到伺服器內容的提交列表。然後,針對其中每一項,找出它試圖修改的檔案然後確保執行推送的使用者對這些檔案具有許可權。一個不太容易理解的 Ruby 技巧石path.index(access_path) ==0
這句,它的返回真值如果路徑以 access_path
開頭——這是為了確保access_path
並不是只在允許的路徑之一,而是所有準許全選的目錄都在該目錄之下。 現在你的使用者沒法推送帶有不正確的提交資訊的內容,也不能在准許他們訪問範圍之外的位置做出修改。
只允許 Fast-Forward 型別的推送
剩下的最後一項任務是指定只接受 fast-forward 的推送。在 Git 1.6 或者更新版本里,只需要設定 receive.denyDeletes
和receive.denyNonFastForwards
選項就可以了。但是通過掛鉤的實現可以在舊版本的 Git 上工作,並且通過一定的修改它它可以做到只針對某些使用者執行,或者更多以後可能用的到的規則。 檢查這一項的邏輯是看看提交裡是否包含從舊版本里能找到但在新版本里卻找不到的內容。如果沒有,那這是一次純 fast-forward 的推送;如果有,那我們拒絕此次推送:
1 2 3 4 5 6 7 8 9 10 11 |
# 只允許純 fast-forward 推送 def check_fast_forward missed_refs = `git rev-list #{$newrev}..#{$oldrev}` missed_ref_count = missed_refs.split("\n").size if missed_ref_count > 0 puts "[POLICY] Cannot push a non fast-forward reference" exit 1 end end check_fast_forward |
一切都設定好了。如果現在執行 chmod u+x .git/hooks/update
—— 修改包含以上內容檔案的許可權,然後嘗試推送一個包含非 fast-forward 型別的索引,會得到一下提示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ 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] Cannot push a non-fast-forward reference 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' |
這裡有幾個有趣的資訊。首先,我們可以看到掛鉤執行的起點:
1 2 |
Enforcing Policies... (refs/heads/master) (fb8c72) (c56860) |
注意這是從 update 指令碼開頭輸出到標準你輸出的。所有從指令碼輸出的提示都會傳送到客戶端,這點很重要。 下一個值得注意的部分是錯誤資訊。
1 2 3 |
[POLICY] Cannot push a non fast-forward reference error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master |
第一行是我們的指令碼輸出的,在往下是 Git 在告訴我們 update 指令碼退出時返回了非零值因而推送遭到了拒絕。最後一點:
1 2 3 |
To git@gitserver:project.git ! [remote rejected] master -> master (hook declined) error: failed to push some refs to 'git@gitserver:project.git' |
我們將為每一個被掛鉤拒之門外的索引受到一條遠端資訊,解釋它被拒絕是因為一個掛鉤的原因。 而且,如果那個 ref 字串沒有包含在任何的提交裡,我們將看到前面指令碼里輸出的錯誤資訊:
1 |
[POLICY] Your message is not formatted correctly |
又或者某人想修改一個自己不具備許可權的檔案然後推送了一個包含它的提交,他將看到類似的提示。比如,一個文件作者嘗試推送一個修改到 lib
目錄的提交,他會看到
1 |
[POLICY] You do not have access to push to lib/test.rb |
全在這了。從這裡開始,只要 update
指令碼存在並且可執行,我們的倉庫永遠都不會遭到迴轉或者包含不符合要求資訊的提交內容,並且使用者都被鎖在了沙箱裡面。
客戶端掛鉤
這種手段的缺點在於使用者推送內容遭到拒絕後幾乎無法避免的抱怨。辛辛苦苦寫成的程式碼在最後時刻慘遭拒絕是十分悲劇切具迷惑性的;更可憐的是他們不得不修改提交歷史來解決問題,這怎麼也算不上王道。 逃離這種兩難境地的法寶是給使用者一些客戶端的掛鉤,在他們作出可能悲劇的事情的時候給以警告。然後呢,使用者們就能在提交–問題變得更難修正之前解除隱患。由於掛鉤本身不跟隨克隆的專案副本分發,所以必須通過其他途徑把這些掛鉤分發到使用者的 .git/hooks 目錄並設為可執行檔案。雖然可以在相同或單獨的專案內 容里加入並分發它們,全自動的解決方案是不存在的。 首先,你應該在每次提交前核查你的提交註釋資訊,這樣你才能確保伺服器不會因為不合條件的提交註釋資訊而拒絕你的更改。為了達到這個目的,你可以增加’commit-msg’掛鉤。如果你使用該掛鉤來閱讀作為第一個引數傳遞給git的提交註釋資訊,並且與規定的模式作對比,你就可以使git在提交註釋資訊不符合條件的情況下,拒絕執行提交。
1 2 3 4 5 6 7 8 9 10 |
#!/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
) 並且是可執行的, 並且你的提交註釋資訊不是符合要求的,你會看到:
1 2 |
$ git commit -am 'test' [POLICY] Your message is not formatted correctly |
在這個例項中,提交沒有成功。然而如果你的提交註釋資訊是符合要求的,git會允許你提交:
1 2 3 |
$ git commit -am 'test [ref: 132]' [master e05c914] test [ref: 132] 1 files changed, 1 insertions(+), 0 deletions(-) |
接下來我們要保證沒有修改到 ACL 允許範圍之外的檔案。加入你的 .git 目錄裡有前面使用過的 ACL 檔案,那麼以下的 pre-commit 指令碼將把裡面的規定執行起來:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#!/usr/bin/env ruby $user = ENV['USER'] # [ insert acl_access_data method from above ] # 只允許特定使用者修改專案重特定子目錄的內容 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 檔案的目錄必須從
1 |
access = get_acl_access_data('acl') |
修改成:
1 |
access = get_acl_access_data('.git/acl') |
另一個重要區別是獲取被修改檔案列表的方式。在服務端的時候使用了檢視提交紀錄的方式,可是目前的提交都還沒被記錄下來呢,所以這個列表只能從暫存區域獲取。和原來的
1 |
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}` |
不同,現在要用
1 |
files_modified = `git diff-index --cached --name-only HEAD` |
不同的就只有這兩點——除此之外,該指令碼完全相同。一個小陷阱在於它假設在本地執行的賬戶和推送到遠端服務端的相同。如果這二者不一樣,則需要手動設定一下 $user
變數。 最後一項任務是檢查確認推送內容中不包含非 fast-forward 型別的索引,不過這個需求比較少見。要找出一個非 fast-forward 型別的索引,要麼衍合超過某個已經推送過的提交,要麼從本地不同分支推送到遠端相同的分支上。 既然伺服器將給出無法推送非 fast-forward 內容的提示,而且上面的掛鉤也能阻止強制的推送,唯一剩下的潛在問題就是衍合一次已經推送過的提交內容。 下面是一個檢查這個問題的 pre-rabase 指令碼的例子。它獲取一個所有即將重寫的提交內容的列表,然後檢查它們是否在遠端的索引裡已經存在。一旦發現某個提交可以從遠端索引裡衍變過來,它就放棄衍合操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#!/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 |
這個指令碼利用了一個第六章“修訂版本選擇”一節中不曾提到的語法。通過這一句可以獲得一個所有已經完成推送的提交的列表:
1 |
git rev-list ^#{sha}^@ refs/remotes/#{remote_ref} |
SHA^@
語法解析該次提交的所有祖先。這裡我們從檢查遠端最後一次提交能夠衍變獲得但從所有我們嘗試推送的提交的 SHA 值祖先無法衍變獲得的提交內容——也就是 fast-forward 的內容。 這個解決方案的硬傷在於它有可能很慢而且常常沒有必要——只要不用 -f
來強制推送,伺服器會自動給出警告並且拒絕推送內容。然而,這是個不錯的練習而且理論上能幫助使用者避免一次將來不得不折回來修改的衍合操作。
7.5 總結
你已經見識過絕大多數通過自定義 Git 客戶端和服務端來來適應自己工作流程和專案內容的方式了。無論你創造出了什麼樣的工作流程,Git 都能用的順手。