原文:《Pro Git》
自定義 git
到目前為止,我闡述了 Git 基本的運作機制和使用方式,介紹了 Git 提供的許多工具來幫助你簡單且有效地使用它。 在本章,我將會介紹 Git 的一些重要的配置方法和鉤子機制以滿足自定義的要求。通過這些工具,它會和你和公司或團隊配合得天衣無縫。(伯樂在線注:如果你對Git還不了解,建議從本Git系列第一篇文章開始閱讀)
7.1 配置 Git
如第一章所言,用git config 配置 Git,要做的第一件事就是設(shè)置名字和郵箱地址:
1 2 | $ git config --global user.name "John Doe"
$ git config --global user.email johndoe@ example.com
|
從現(xiàn)在開始,你會了解到一些類似以上但更為有趣的設(shè)置選項來自定義 Git。 先過一遍第一章中提到的 Git 配置細節(jié)。Git 使用一系列的配置文件來存儲你定義的偏好,它首先會查找/etc/gitconfig 文件,該文件含有 對系統(tǒng)上所有用戶及他們所擁有的倉庫都生效的配置值(譯注:gitconfig是全局配置文件), 如果傳遞--system 選項給git config 命令, Git 會讀寫這個文件。 接下來 Git 會查找每個用戶的~/.gitconfig 文件,你能傳遞--global 選項讓 Git讀寫該文件。 最后 Git 會查找由用戶定義的各個庫中 Git 目錄下的配置文件(.git/config ),該文件中的值只對屬主庫有效。 以上闡述的三層配置從一般到特殊層層推進,如果定義的值有沖突,以后面層中定義的為準,例如:在.git/config 和/etc/gitconfig 的較量中,.git/config 取得了勝利。雖然你也可以直接手動編輯這些配置文件,但是運行git config 命令將會來得簡單些。
客戶端基本配置
Git 能夠識別的配置項被分為了兩大類:客戶端和服務(wù)器端,其中大部分基于你個人工作偏好,屬于客戶端配置。盡管有數(shù)不盡的選項,但我只闡述 其中經(jīng)常使用或者會對你的工作流產(chǎn)生巨大影響的選項,如果你想觀察你當前的 Git 能識別的選項列表,請運行
git config 的手冊頁(譯注:以man命令的顯示方式)非常細致地羅列了所有可用的配置項。
core.editor
Git默認會調(diào)用你的環(huán)境變量editor定義的值作為文本編輯器,如果沒有定義的話,會調(diào)用Vi來創(chuàng)建和編輯提交以及標簽信息, 你可以使用core.editor 改變默認編輯器:
1 | $ git config --global core.editor emacs
|
現(xiàn)在無論你的環(huán)境變量editor被定義成什么,Git 都會調(diào)用Emacs編輯信息。
commit.template
如果把此項指定為你系統(tǒng)上的一個文件,當你提交的時候, Git 會默認使用該文件定義的內(nèi)容。 例如:你創(chuàng)建了一個模板文件$HOME/.gitmessage.txt ,它看起來像這樣:
1 2 3 4 5 | subject line
what happened
[ticket: X]
|
設(shè)置commit.template ,當運行git commit 時, Git 會在你的編輯器中顯示以上的內(nèi)容, 設(shè)置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
|
如果你有特定的策略要運用在提交信息上,在系統(tǒng)上創(chuàng)建一個模板文件,設(shè)置 Git 默認使用它,這樣當提交時,你的策略每次都會被運用。
core.pager
core.pager指定 Git 運行諸如log 、diff 等所使用的分頁器,你能設(shè)置成用more 或者任何你喜歡的分頁器(默認用的是less ), 當然你也可以什么都不用,設(shè)置空字符串:
1 | $ git config --global core.pager ''
|
這樣不管命令的輸出量多少,都會在一頁顯示所有內(nèi)容。
user.signingkey
如果你要創(chuàng)建經(jīng)簽署的含附注的標簽(正如第二章所述),那么把你的GPG簽署密鑰設(shè)置為配置項會更好,設(shè)置密鑰ID如下:
1 | $ git config --global user.signingkey <gpg-key- id >
|
現(xiàn)在你能夠簽署標簽,從而不必每次運行git tag 命令時定義密鑰:
core.excludesfile
正如第二章所述,你能在項目庫的.gitignore 文件里頭用模式來定義那些無需納入 Git 管理的文件,這樣它們不會出現(xiàn)在未跟蹤列表, 也不會在你運行git add 后被暫存。然而,如果你想用項目庫之外的文件來定義那些需被忽略的文件的話,用core.excludesfile 通知 Git 該文件所處的位置,文件內(nèi)容和.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 設(shè)置成1(譯注:啟動自動修正),那么在只有一個命令被模糊匹配到的情況下,Git 會自動運行該命令。
Git中的著色
Git能夠為輸出到你終端的內(nèi)容著色,以便你可以憑直觀進行快速、簡單地分析,有許多選項能供你使用以符合你的偏好。
color.ui
Git會按照你需要自動為大部分的輸出加上顏色,你能明確地規(guī)定哪些需要著色以及怎樣著色,設(shè)置color.ui 為true來打開所有的默認終端著色。
1 | $ git config --global color.ui true
|
設(shè)置好以后,當輸出到終端時,Git 會為之加上顏色。其他的參數(shù)還有false和always,false意味著不為輸出著色,而always則表明在任何情況下都要著色,即使 Git 命令被重定向到文件或管道。Git 1.5.5版本引進了此項配置,如果你擁有的版本更老,你必須對顏色有關(guān)選項各自進行詳細地設(shè)置。 你會很少用到color.ui = always ,在大多數(shù)情況下,如果你想在被重定向的輸出中插入顏色碼,你能傳遞--color 標志給 Git 命令來迫使它這么做,color.ui = true 應(yīng)該是你的首選。
color.*
想要具體到哪些命令輸出需要被著色以及怎樣著色或者 Git 的版本很老,你就要用到和具體命令有關(guān)的顏色配置選項,它們都能被置為true 、false 或always :
1 2 3 4 | color.branch
color. diff
color.interactive
color.status
|
除此之外,以上每個選項都有子選項,可以被用來覆蓋其父設(shè)置,以達到為輸出的各個部分著色的目的。例如,讓diff輸出的改變信息以粗體、藍色前景和黑色背景的形式顯示:
1 | $ git config --global color. diff .meta “blue black bold”
|
你能設(shè)置的顏色值如:normal、black、red、green、yellow、blue、magenta、cyan、white,正如以上例子設(shè)置的粗體屬性,想要設(shè)置字體屬性的話,可以選擇如:bold、dim、ul、blink、reverse。 如果你想配置子選項的話,可以參考git config 幫助頁。
外部的合并與比較工具
雖然 Git 自己實現(xiàn)了diff,而且到目前為止你一直在使用它,但你能夠用一個外部的工具替代它,除此以外,你還能用一個圖形化的工具來合并和解決沖突從而不必自己手動解決。有一個不錯且免費的工具可以被用來做比較和合并工作,它就是P4Merge(譯注:Perforce圖形化合并工具),我會展示它的安裝過程。 P4Merge可以在所有主流平臺上運行,現(xiàn)在開始大膽嘗試吧。對于向你展示的例子,在Mac和Linux系統(tǒng)上,我會使用路徑名,在Windows上,/usr/local/bin 應(yīng)該被改為你環(huán)境中的可執(zhí)行路徑。 下載P4Merge:
1 | http: //www .perforce.com /perforce/downloads/component .html
|
首先把你要運行的命令放入外部包裝腳本中,我會使用Mac系統(tǒng)上的路徑來指定該腳本的位置,在其他系統(tǒng)上,它應(yīng)該被放置在二進制文件p4merge 所在的目錄中。創(chuàng)建一個merge包裝腳本,名字叫作extMerge ,讓它帶參數(shù)調(diào)用p4merge 二進制文件:
1 2 3 | $ cat /usr/local/bin/extMerge
#!/bin/sh
/Applications/p4merge .app /Contents/MacOS/p4merge $*
|
diff包裝腳本首先確定傳遞過來7個參數(shù),隨后把其中2個傳遞給merge包裝腳本,默認情況下, Git 傳遞以下參數(shù)給diff:
1 | path old- file old-hex old-mode new- file new-hex new-mode
|
由于你僅僅需要old-file 和new-file 參數(shù),用diff包裝腳本來傳遞它們吧。
1 2 3 | $ cat /usr/local/bin/extDiff
#!/bin/sh
[ $ # -eq 7 ] && /usr/local/bin/extMerge "$2" "$5"
|
確認這兩個腳本是可執(zhí)行的:
1 2 | $ sudo chmod +x /usr/local/bin/extMerge
$ sudo chmod +x /usr/local/bin/extDiff
|
現(xiàn)在來配置使用你自定義的比較和合并工具吧。這需要許多自定義設(shè)置:merge.tool 通知 Git 使用哪個合并工具;mergetool.*.cmd 規(guī)定命令運行的方式;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
|
設(shè)置完畢后,運行diff命令:
1 | $ git diff 32d1776b1^ 32d1776b1
|
命令行居然沒有發(fā)現(xiàn)diff命令的輸出,其實,Git 調(diào)用了剛剛設(shè)置的P4Merge,它看起來像圖7-1這樣:
Figure 7-1. P4Merge.
當你設(shè)法合并兩個分支,結(jié)果卻有沖突時,運行git mergetool ,Git 會調(diào)用P4Merge讓你通過圖形界面來解決沖突。 設(shè)置包裝腳本的好處是你能簡單地改變diff和merge工具,例如把extDiff 和extMerge 改成KDiff3,要做的僅僅是編輯extMerge 腳本文件:
1 2 3 | $ cat /usr/local/bin/extMerge
#!/bin/sh
/Applications/kdiff3 .app /Contents/MacOS/kdiff3 $*
|
現(xiàn)在 Git 會使用KDiff3來做比較、合并和解決沖突。 Git預(yù)先設(shè)置了許多其他的合并和解決沖突的工具,而你不必設(shè)置cmd??梢园押喜⒐ぞ咴O(shè)置為:kdiff3、opendiff、tkdiff、 meld、xxdiff、emerge、vimdiff、gvimdiff。如果你不想用到KDiff3的所有功能,只是想用它來合并,那么kdiff3 正符合你的要求,運行:
1 | $ git config --global merge.tool kdiff3
|
如果運行了以上命令,沒有設(shè)置extMerge 和extDiff 文件,Git 會用KDiff3做合并,讓通常內(nèi)設(shè)的比較工具來做比較。
格式化與空白
格式化與空白是許多開發(fā)人員在協(xié)作時,特別是在跨平臺情況下,遇到的令人頭疼的細小問題。由于編輯器的不同或者Windows程序員在跨平臺項目中的文件行尾加入了回車換行符,一些細微的空格變化會不經(jīng)意地進入大家合作的工作或提交的補丁中。不用怕,Git 的一些配置選項會幫助你解決這些問題。
core.autocrlf
假如你正在Windows上寫程序,又或者你正在和其他人合作,他們在Windows上編程,而你卻在其他系統(tǒng)上,在這些情況下,你可能會遇到行尾結(jié)束符問題。這是因為Windows使用回車和換行兩個字符來結(jié)束一行,而Mac和Linux只使用換行一個字符。雖然這是小問題,但它會極大地擾亂跨平臺協(xié)作。 Git可以在你提交時自動地把行結(jié)束符CRLF轉(zhuǎn)換成LF,而在簽出代碼時把LF轉(zhuǎn)換成CRLF。用core.autocrlf 來打開此項功能,如果是在Windows系統(tǒng)上,把它設(shè)置成true ,這樣當簽出代碼時,LF會被轉(zhuǎn)換成CRLF:
1 | $ git config --global core.autocrlf true
|
Linux或Mac系統(tǒng)使用LF作為行結(jié)束符,因此你不想 Git 在簽出文件時進行自動的轉(zhuǎn)換;當一個以CRLF為行結(jié)束符的文件不小心被引入時你肯定想進行修正,把core.autocrlf 設(shè)置成input來告訴 Git 在提交時把CRLF轉(zhuǎn)換成LF,簽出時不轉(zhuǎn)換:
1 | $ git config --global core.autocrlf input
|
這樣會在Windows系統(tǒng)上的簽出文件中保留CRLF,會在Mac和Linux系統(tǒng)上,包括倉庫中保留LF。 如果你是Windows程序員,且正在開發(fā)僅運行在Windows上的項目,可以設(shè)置false 取消此功能,把回車符記錄在庫中:
1 | $ git config --global core.autocrlf false
|
core.whitespace
Git預(yù)先設(shè)置了一些選項來探測和修正空白問題,其4種主要選項中的2個默認被打開,另2個被關(guān)閉,你可以自由地打開或關(guān)閉它們。 默認被打開的2個選項是trailing-space 和space-before-tab ,trailing-space 會查找每行結(jié)尾的空格,space-before-tab 會查找每行開頭的制表符前的空格。 默認被關(guān)閉的2個選項是indent-with-non-tab 和cr-at-eol ,indent-with-non-tab 會查找8個以上空格(非制表符)開頭的行,cr-at-eol 讓 Git 知道行尾回車符是合法的。 設(shè)置core.whitespace ,按照你的意圖來打開或關(guān)閉選項,選項以逗號分割。通過逗號分割的鏈中去掉選項或在選項前加- 來關(guān)閉,例如,如果你想要打開除了cr-at-eol 之外的所有選項:
1 2 | $ git config --global core.whitespace \
trailing-space,space-before-tab,indent-with-non-tab
|
當你運行git diff 命令且為輸出著色時,Git 探測到這些問題,因此你也許在提交前能修復(fù)它們,當你用git apply 打補丁時同樣也會從中受益。如果正準備運用的補丁有特別的空白問題,你可以讓 Git 發(fā)警告:
1 | $ git apply --whitespace=warn <patch>
|
或者讓 Git 在打上補丁前自動修正此問題:
1 | $ git apply --whitespace=warn <patch>
|
這些選項也能運用于衍合。如果提交了有空白問題的文件但還沒推送到上流,你可以運行帶有--whitespace=fix 選項的rebase 來讓Git在重寫補丁時自動修正它們。
服務(wù)器端配置
Git服務(wù)器端的配置選項并不多,但仍有一些饒有生趣的選項值得你一看。
receive.fsckObjects
Git默認情況下不會在推送期間檢查所有對象的一致性。雖然會確認每個對象的有效性以及是否仍然匹配SHA-1檢驗和,但 Git 不會在每次推送時都檢查一致性。對于 Git 來說,庫或推送的文件越大,這個操作代價就相對越高,每次推送會消耗更多時間,如果想在每次推送時 Git 都檢查一致性,設(shè)置receive.fsckObjects 為true來強迫它這么做:
1 | $ git config --system receive.fsckObjects true
|
現(xiàn)在 Git 會在每次推送生效前檢查庫的完整性,確保有問題的客戶端沒有引入破壞性的數(shù)據(jù)。
receive.denyNonFastForwards
如果對已經(jīng)被推送的提交歷史做衍合,繼而再推送,又或者以其它方式推送一個提交歷史至遠程分支,且該提交歷史沒在這個遠程分支中,這樣的推送會被拒絕。這通常是個很好的禁止策略,但有時你在做衍合并確定要更新遠程分支,可以在push命令后加-f 標志來強制更新。 要禁用這樣的強制更新功能,可以設(shè)置receive.denyNonFastForwards :
1 | $ git config --system receive.denyNonFastForwards true
|
稍后你會看到,用服務(wù)器端的接收鉤子也能達到同樣的目的。這個方法可以做更細致的控制,例如:禁用特定的用戶做強制更新。
receive.denyDeletes
規(guī)避denyNonFastForwards 策略的方法之一就是用戶刪除分支,然后推回新的引用。在更新的 Git 版本中(從1.6.1版本開始),把receive.denyDeletes 設(shè)置為true:
1 | $ git config --system receive.denyDeletes true
|
這樣會在推送過程中阻止刪除分支和標簽 — 沒有用戶能夠這么做。要刪除遠程分支,必須從服務(wù)器手動刪除引用文件。通過用戶訪問控制列表也能這么做,在本章結(jié)尾將會介紹這些有趣的方式。
7.2 Git屬性
一些設(shè)置項也能被運用于特定的路徑中,這樣,Git 以對一個特定的子目錄或子文件集運用那些設(shè)置項。這些設(shè)置項被稱為 Git 屬性,可以在你目錄中的.gitattributes 文件內(nèi)進行設(shè)置(通常是你項目的根目錄),也可以當你不想讓這些屬性文件和項目文件一同提交時,在.git/info/attributes 進行設(shè)置。 使用屬性,你可以對個別文件或目錄定義不同的合并策略,讓 Git 知道怎樣比較非文本文件,在你提交或簽出前讓 Git 過濾內(nèi)容。你將在這部分了解到能在自己的項目中使用的屬性,以及一些實例。
二進制文件
你可以用 Git 屬性讓其知道哪些是二進制文件(以防 Git 沒有識別出來),以及指示怎樣處理這些文件,這點很酷。例如,一些文本文件是由機器產(chǎn)生的,而且無法比較,而一些二進制文件可以比較 — 你將會了解到怎樣讓 Git 識別這些文件。
識別二進制文件
一些文件看起來像是文本文件,但其實是作為二進制數(shù)據(jù)被對待。例如,在Mac上的Xcode項目含有一個以.pbxproj 結(jié)尾的文件,它是由記錄設(shè)置項的IDE寫到磁盤的JSON數(shù)據(jù)集(純文本javascript數(shù)據(jù)類型)。雖然技術(shù)上看它是由ASCII字符組成的文本文件,但你并不認為如此,因為它確實是一個輕量級數(shù)據(jù)庫 — 如果有2人改變了它,你通常無法合并和比較內(nèi)容,只有機器才能進行識別和操作,于是,你想把它當成二進制文件。 讓 Git 把所有pbxproj 文件當成二進制文件,在.gitattributes 文件中設(shè)置如下:
現(xiàn)在,Git 會嘗試轉(zhuǎn)換和修正CRLF(回車換行)問題,也不會當你在項目中運行g(shù)it show或git diff時,比較不同的內(nèi)容。在Git 1.6及之后的版本中,可以用一個宏代替-crlf -diff :
比較二進制文件
在Git 1.6及以上版本中,你能利用 Git 屬性來有效地比較二進制文件??梢栽O(shè)置 Git 把二進制數(shù)據(jù)轉(zhuǎn)換成文本格式,用通常的diff來比較。 這個特性很酷,而且鮮為人知,因此我會結(jié)合實例來講解。首先,要解決的是最令人頭疼的問題:對Word文檔進行版本控制。很多人對Word文檔又恨又愛,如果想對其進行版本控制,你可以把文件加入到 Git 庫中,每次修改后提交即可。但這樣做沒有一點實際意義,因為運行git diff 命令后,你只能得到如下的結(jié)果:
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 文件:
當你要看比較結(jié)果時,如果文件擴展名是”doc”,Git 調(diào)用”word”過濾器。什么是”word”過濾器呢?其實就是 Git 使用strings 程序,把Word文檔轉(zhuǎn)換成可讀的文本文件,之后再進行比較:
1 | $ git config diff .word.textconv strings
|
現(xiàn)在如果在兩個快照之間比較以.doc 結(jié)尾的文件,Git 對這些文件運用”word”過濾器,在比較前把Word文件轉(zhuǎn)換成文本文件。 下面展示了一個實例,我把此書的第一章納入 Git 管理,在一個段落中加入了一些文本后保存,之后運行git diff 命令,得到結(jié)果如下:
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”。雖然有些瑕疵,在末尾顯示了一些隨機的內(nèi)容,但確實可以比較了。如果你能找到或自己寫個Word到純文本的轉(zhuǎn)換器的話,效果可能會更好。strings 可以在大部分Mac和Linux系統(tǒng)上運行,所以它是處理二進制格式的第一選擇。 你還能用這個方法比較圖像文件。當比較時,對JPEG文件運用一個過濾器,它能提煉出EXIF信息 — 大部分圖像格式使用的元數(shù)據(jù)。如果你下載并安裝了exiftool 程序,可以用它參照元數(shù)據(jù)把圖像轉(zhuǎn)換成文本。比較的不同結(jié)果將會用文本向你展示:
1 2 | $ echo '*.png diff=exif' >> .gitattributes
$ git config diff .exif.textconv exiftool
|
如果在項目中替換了一個圖像文件,運行git diff 命令的結(jié)果如下:
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
|
你會發(fā)現(xiàn)文件的尺寸大小發(fā)生了改變。
關(guān)鍵字擴展
使用SVN或CVS的開發(fā)人員經(jīng)常要求關(guān)鍵字擴展。在 Git 中,你無法在一個文件被提交后修改它,因為 Git 會先對該文件計算校驗和。然而,你可以在簽出時注入文本,在提交前刪除它。 Git 屬性提供了2種方式這么做。 首先,你能夠把blob的SHA-1校驗和自動注入文件的$Id$ 字段。如果在一個或多個文件上設(shè)置了此字段,當下次你簽出分支的時候,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 $
|
然而,這樣的顯示結(jié)果沒有多大的實際意義。這個SHA的值相當?shù)仉S機,無法區(qū)分日期的前后,所以,如果你在CVS或Subversion中用過關(guān)鍵字替換,一定會包含一個日期值。 因此,你能寫自己的過濾器,在提交文件到暫存區(qū)或簽出文件時替換關(guān)鍵字。有2種過濾器,”clean”和”smudge”。在.gitattributes 文件中,你能對特定的路徑設(shè)置一個過濾器,然后設(shè)置處理文件的腳本,這些腳本會在文件簽出前(”smudge”,見圖 7-2)和提交到暫存區(qū)前(”clean”,見圖7-3)被調(diào)用。這些過濾器能夠做各種有趣的事。
圖7-2. 簽出時,“smudge”過濾器被觸發(fā)。
圖7-3. 提交到暫存區(qū)時,“clean”過濾器被觸發(fā)。
這里舉一個簡單的例子:在暫存前,用indent (縮進)程序過濾所有C源代碼。在.gitattributes 文件中設(shè)置”indent”過濾器過濾*.c 文件:
然后,通過以下配置,讓 Git 知道”indent”過濾器在遇到”smudge”和”clean”時分別該做什么:
1 2 | $ git config --global filter.indent.clean indent
$ git config --global filter.indent.smudge cat
|
于是,當你暫存*.c 文件時,indent 程序會被觸發(fā),在把它們簽出之前,cat 程序會被觸發(fā)。但cat 程序在這里沒什么實際作用。這樣的組合,使C源代碼在暫存前被indent 程序過濾,非常有效。 另一個例子是類似RCS的$Date$ 關(guān)鍵字擴展。為了演示,需要一個小腳本,接受文件名參數(shù),得到項目的最新提交日期,最后把日期寫入該文件。下面用Ruby腳本來實現(xiàn):
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$ 字符串中 — 此腳本很簡單,你可以選擇你喜歡的編程語言來實現(xiàn)。把該腳本命名為expand_date ,放到正確的路徑中,之后需要在 Git 中設(shè)置一個過濾器(dater ),讓它在簽出文件時調(diào)用expand_date ,在暫存文件時用Perl清除之:
1 2 | $ git config filter.dater.smudge expand_date
$ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'
|
這個Perl小程序會刪除$Date$ 字符串里多余的字符,恢復(fù)$Date$ 原貌。到目前為止,你的過濾器已經(jīng)設(shè)置完畢,可以開始測試了。打開一個文件,在文件中輸入$Date$ 關(guān)鍵字,然后設(shè)置 Git 屬性:
1 2 | $ echo '# $Date$' > date_test.txt
$ echo 'date*.txt filter=dater' >> .gitattributes
|
如果暫存該文件,之后再簽出,你會發(fā)現(xiàn)關(guān)鍵字被替換了:
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$
|
雖說這項技術(shù)對自定義應(yīng)用來說很有用,但還是要小心,因為.gitattributes 文件會隨著項目一起提交,而過濾器(例如:dater )不會,所以,過濾器不會在所有地方都生效。當你在設(shè)計這些過濾器時要注意,即使它們無法正常工作,也要讓整個項目運作下去。
導(dǎo)出倉庫
Git屬性在導(dǎo)出項目歸檔時也能發(fā)揮作用。
export-ignore
當產(chǎn)生一個歸檔時,可以設(shè)置 Git 不導(dǎo)出某些文件和目錄。如果你不想在歸檔中包含一個子目錄或文件,但想他們納入項目的版本管理中,你能對應(yīng)地設(shè)置export-ignore 屬性。 例如,在test/ 子目錄中有一些測試文件,在項目的壓縮包中包含他們是沒有意義的。因此,可以增加下面這行到 Git 屬性文件中:
現(xiàn)在,當運行g(shù)it archive來創(chuàng)建項目的壓縮包時,那個目錄不會在歸檔中出現(xiàn)。
export-subst
還能對歸檔做一些簡單的關(guān)鍵字替換。在第2章中已經(jīng)可以看到,可以以--pretty=format 形式的簡碼在任何文件中放入$Format:$ 字符串。例如,如果想在項目中包含一個叫作LAST_COMMIT 的文件,當運行git archive 時,最后提交日期自動地注入進該文件,可以這樣設(shè)置:
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 后,打開該文件,會發(fā)現(xiàn)其內(nèi)容如下:
1 2 | $ cat LAST_COMMIT
Last commit date : $Format:Tue Apr 21 08:38:48 2009 -0700$
|
合并策略
通過 Git 屬性,還能對項目中的特定文件使用不同的合并策略。一個非常有用的選項就是,當一些特定文件發(fā)生沖突,Git 會嘗試合并他們,而使用你這邊的合并。 如果項目的一個分支有歧義或比較特別,但你想從該分支合并,而且需要忽略其中某些文件,這樣的合并策略是有用的。例如,你有一個數(shù)據(jù)庫設(shè)置文件database.xml,在2個分支中他們是不同的,你想合并一個分支到另一個,而不弄亂該數(shù)據(jù)庫文件,可以設(shè)置屬性如下:
如果合并到另一個分支,database.xml文件不會有合并沖突,顯示如下:
1 2 3 | $ git merge topic
Auto-merging database.xml
Merge made by recursive.
|
這樣,database.xml會保持原樣。
7.3 Git掛鉤
和其他版本控制系統(tǒng)一樣,當某些重要事件發(fā)生時,Git 以調(diào)用自定義腳本。有兩組掛鉤:客戶端和服務(wù)器端??蛻舳藪煦^用于客戶端的操作,如提交和合并。服務(wù)器端掛鉤用于 Git 服務(wù)器端的操作,如接收被推送的提交。你可以隨意地使用這些掛鉤,下面會講解其中一些。
安裝一個掛鉤
掛鉤都被存儲在 Git 目錄下的hooks 子目錄中,即大部分項目中的.git/hooks 。 Git 默認會放置一些腳本樣本在這個目錄中,除了可以作為掛鉤使用,這些樣本本身是可以獨立使用的。所有的樣本都是shell腳本,其中一些還包含了Perl的腳本,不過,任何正確命名的可執(zhí)行腳本都可以正常使用 — 可以用Ruby或Python,或其他。在Git 1.6版本之后,這些樣本名都是以.sample結(jié)尾,因此,你必須重新命名。在Git 1.6版本之前,這些樣本名都是正確的,但這些樣本不是可執(zhí)行文件。 把一個正確命名且可執(zhí)行的文件放入 Git 目錄下的hooks 子目錄中,可以激活該掛鉤腳本,因此,之后他一直會被 Git 調(diào)用。隨后會講解主要的掛鉤腳本。
客戶端掛鉤
有許多客戶端掛鉤,以下把他們分為:提交工作流掛鉤、電子郵件工作流掛鉤及其他客戶端掛鉤。
提交工作流掛鉤
有 4個掛鉤被用來處理提交的過程。pre-commit 掛鉤在鍵入提交信息前運行,被用來檢查即將提交的快照,例如,檢查是否有東西被遺漏,確認測試是否運行,以及檢查代碼。當從該掛鉤返回非零值時,Git 放棄此次提交,但可以用git commit --no-verify 來忽略。該掛鉤可以被用來檢查代碼錯誤(運行類似lint的程序),檢查尾部空白(默認掛鉤是這么做的),檢查新方法(譯注:程序的函數(shù))的說明。 prepare-commit-msg 掛鉤在提交信息編輯器顯示之前,默認信息被創(chuàng)建之后運行。因此,可以有機會在提交作者看到默認信息前進行編輯。該掛鉤接收一些選項:擁有提交信息的文件路徑,提交類型,如果是一次修訂的話,提交的SHA-1校驗和。該掛鉤對通常的提交來說不是很有用,只在自動產(chǎn)生的默認提交信息的情況下有作用,如提交信息模板、合并、壓縮和修訂提交等??梢院吞峤荒0迮浜鲜褂?,以編程的方式插入信息。 commit-msg 掛鉤接收一個參數(shù),此參數(shù)是包含最近提交信息的臨時文件的路徑。如果該掛鉤腳本以非零退出,Git 放棄提交,因此,可以用來在提交通過前驗證項目狀態(tài)或提交信息。本章上一小節(jié)已經(jīng)展示了使用該掛鉤核對提交信息是否符合特定的模式。 post-commit 掛鉤在整個提交過程完成后運行,他不會接收任何參數(shù),但可以運行git log -1 HEAD 來獲得最后的提交信息。總之,該掛鉤是作為通知之類使用的。 提交工作流的客戶端掛鉤腳本可以在任何工作流中使用,他們經(jīng)常被用來實施某些策略,但值得注意的是,這些腳本在clone期間不會被傳送??梢栽诜?wù)器端實施策略來拒絕不符合某些策略的推送,但這完全取決于開發(fā)者在客戶端使用這些腳本的情況。所以,這些腳本對開發(fā)者是有用的,由他們自己設(shè)置和維護,而且在任何時候都可以覆蓋或修改這些腳本。
E-mail工作流掛鉤
有3個可用的客戶端掛鉤用于e-mail工作流。當運行git am 命令時,會調(diào)用他們,因此,如果你沒有在工作流中用到此命令,可以跳過本節(jié)。如果你通過e-mail接收由git format-patch 產(chǎn)生的補丁,這些掛鉤也許對你有用。 首先運行的是applypatch-msg 掛鉤,他接收一個參數(shù):包含被建議提交信息的臨時文件名。如果該腳本非零退出,Git 放棄此補丁。可以使用這個腳本確認提交信息是否被正確格式化,或讓腳本編輯信息以達到標準化。 下一個在git am 運行期間調(diào)用是pre-applypatch 掛鉤。該掛鉤不接收參數(shù),在補丁被運用之后運行,因此,可以被用來在提交前檢查快照。你能用此腳本運行測試,檢查工作樹。如果有些什么遺漏,或測試沒通過,腳本會以非零退出,放棄此次git am 的運行,補丁不會被提交。 最后在git am 運行期間調(diào)用的是post-applypatch 掛鉤。你可以用他來通知一個小組或獲取的補丁的作者,但無法阻止打補丁的過程。
其他客戶端掛鉤
pre- rebase 掛鉤在衍合前運行,腳本以非零退出可以中止衍合的過程。你可以使用這個掛鉤來禁止衍合已經(jīng)推送的提交對象,Git pre- rebase 掛鉤樣本就是這么做的。該樣本假定next是你定義的分支名,因此,你可能要修改樣本,把next改成你定義過且穩(wěn)定的分支名。 在git checkout 成功運行后,post-checkout 掛鉤會被調(diào)用。他可以用來為你的項目環(huán)境設(shè)置合適的工作目錄。例如:放入大的二進制文件、自動產(chǎn)生的文檔或其他一切你不想納入版本控制的文件。 最后,在merge 命令成功執(zhí)行后,post-merge 掛鉤會被調(diào)用。他可以用來在 Git 無法跟蹤的工作樹中恢復(fù)數(shù)據(jù),諸如權(quán)限數(shù)據(jù)。該掛鉤同樣能夠驗證在 Git 控制之外的文件是否存在,因此,當工作樹改變時,你想這些文件可以被復(fù)制。
服務(wù)器端掛鉤
除了客戶端掛鉤,作為系統(tǒng)管理員,你還可以使用兩個服務(wù)器端的掛鉤對項目實施各種類型的策略。這些掛鉤腳本可以在提交對象推送到服務(wù)器前被調(diào)用,也可以在推送到服務(wù)器后被調(diào)用。推送到服務(wù)器前調(diào)用的掛鉤可以在任何時候以非零退出,拒絕推送,返回錯誤消息給客戶端,還可以如你所愿設(shè)置足夠復(fù)雜的推送策略。
pre-receive 和 post-receive
處理來自客戶端的推送(push)操作時最先執(zhí)行的腳本就是 pre-receive 。它從標準輸入(stdin)獲取被推送引用的列表;如果它退出時的返回值不是0,所有推送內(nèi)容都不會被接受。利用此掛鉤腳本可以實現(xiàn)類似保證最新的索引中不包含非fast-forward類型的這類效果;抑或檢查執(zhí)行推送操作的用戶擁有創(chuàng)建,刪除或者推送的權(quán)限或者他是否對將要修改的每一個文件都有訪問權(quán)限。 post-receive 掛鉤在整個過程完結(jié)以后運行,可以用來更新其他系統(tǒng)服務(wù)或者通知用戶。它接受與 pre-receive 相同的標準輸入數(shù)據(jù)。應(yīng)用實例包括給某郵件列表發(fā)信,通知實時整合數(shù)據(jù)的服務(wù)器,或者更新軟件項目的問題追蹤系統(tǒng) —— 甚至可以通過分析提交信息來決定某個問題是否應(yīng)該被開啟,修改或者關(guān)閉。該腳本無法組織推送進程,不過客戶端在它完成運行之前將保持連接狀態(tài);所以在用它作一些消耗時間的操作之前請三思。
update
update 腳本和 pre-receive 腳本十分類似。不同之處在于它會為推送者更新的每一個分支運行一次。假如推送者同時向多個分支推送內(nèi)容,pre-receive 只運行一次,相比之下 update 則會為每一個更新的分支運行一次。它不會從標準輸入讀取內(nèi)容,而是接受三個參數(shù):索引的名字(分支),推送前索引指向的內(nèi)容的 SHA-1 值,以及用戶試圖推送內(nèi)容的 SHA-1 值。如果 update 腳本以退出時返回非零值,只有相應(yīng)的那一個索引會被拒絕;其余的依然會得到更新。
7.4 Git 強制策略實例
在本節(jié)中,我們應(yīng)用前面學(xué)到的知識建立這樣一個Git 工作流程:檢查提交信息的格式,只接受純fast-forward內(nèi)容的推送,并且指定用戶只能修改項目中的特定子目錄。我們將寫一個客戶端角本來提示開發(fā)人員他們推送的內(nèi)容是否會被拒絕,以及一個服務(wù)端腳本來實際執(zhí)行這些策略。 這些腳本使用 Ruby 寫成,一半由于它是作者傾向的腳本語言,另外作者覺得它是最接近偽代碼的腳本語言;因而即便你不使用 Ruby 也能大致看懂。不過任何其他語言也一樣適用。所有 Git 自帶的樣例腳本都是用 Perl 或 Bash 寫的。所以從這些腳本中能找到相當多的這兩種語言的掛鉤樣例。
服務(wù)端掛鉤
所有服務(wù)端的工作都在hooks(掛鉤)目錄的 update(更新)腳本中制定。update 腳本為每一個得到推送的分支運行一次;它接受推送目標的索引,該分支原來指向的位置,以及被推送的新內(nèi)容。如果推送是通過 SSH 進行的,還可以獲取發(fā)出此次操作的用戶。如果設(shè)定所有操作都通過公匙授權(quán)的單一賬號(比如"git")進行,就有必要通過一個 shell 包裝依據(jù)公匙來判斷用戶的身份,并且設(shè)定環(huán)境變量來表示該用戶的身份。下面假設(shè)嘗試連接的用戶儲存在$USER 環(huán)境變量里,我們的 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]})"
|
沒錯,我在用全局變量。別鄙視我——這樣比較利于演示過程。
指定特殊的提交信息格式
我們的第一項任務(wù)是指定每一條提交信息都必須遵循某種特殊的格式。作為演示,假定每一條信息必須包含一條形似 “ref: 1234” 這樣的字符串,因為我們需要把每一次提交和項目的問題追蹤系統(tǒng)。我們要逐一檢查每一條推送上來的提交內(nèi)容,看看提交信息是否包含這么一個字符串,然后,如果該提交里不包含這個字符串,以非零返回值退出從而拒絕此次推送。 把 $newrev 和 $oldrev 變量的值傳給一個叫做 git rev-list 的 Git plumbing 命令可以獲取所有提交內(nèi)容的 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
|
截取這些輸出內(nèi)容,循環(huán)遍歷其中每一個 SHA 值,找出與之對應(yīng)的提交信息,然后用正則表達式來測試該信息包含的格式話的內(nèi)容。 下面要搞定如何從所有的提交內(nèi)容中提取出提交信息。使用另一個叫做 git cat-file 的 Git plumbing 工具可以獲得原始的提交數(shù)據(jù)。我們將在第九章了解到這些 plumbing 工具的細節(jié);現(xiàn)在暫時先看一下這條命令的輸出:
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 值獲得提交內(nèi)容中的提交信息的一個簡單辦法是找到提交的第一行,然后取從它往后的所有內(nèi)容??梢允褂?Unix 系統(tǒng)的 sed 命令來實現(xiàn)該效果:
1 2 | $ git cat - file commit ca82a6 | sed '1,/^$/d'
changed the version number
|
這條咒語從每一個待提交內(nèi)容里提取提交信息,并且會在提取信息不符合要求的情況下退出。為了退出腳本和拒絕此次推送,返回一個非零值。整個腳本大致如下:
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 腳本里,所有包含不符合指定規(guī)則的提交都會遭到拒絕。
實現(xiàn)基于用戶的訪問權(quán)限控制列表(ACL)系統(tǒng)
假設(shè)你需要添加一個使用訪問權(quán)限控制列表的機制來指定哪些用戶對項目的哪些部分有推送權(quán)限。某些用戶具有全部的訪問權(quán),其他人只對某些子目錄或者特定的文件具有推送權(quán)限。要搞定這一點,所有的規(guī)則將被寫入一個位于服務(wù)器的原始 Git 倉庫的acl 文件。我們讓 update 掛鉤檢閱這些規(guī)則,審視推送的提交內(nèi)容中需要修改的所有文件,然后決定執(zhí)行推送的用戶是否對所有這些文件都有權(quán)限。 我們首先要創(chuàng)建這個列表。這里使用的格式和 CVS 的 ACL 機制十分類似:它由若干行構(gòu)成,第一項內(nèi)容是 avail 或者unavail ,接著是逗號分隔的規(guī)則生效用戶列表,最后一項是規(guī)則生效的目錄(空白表示開放訪問)。這些項目由 | 字符隔開。 下例中,我們指定幾個管理員,幾個對 doc 目錄具有權(quán)限的文檔作者,以及一個對 lib 和 tests 目錄具有權(quán)限的開發(fā)人員,相應(yīng)的 ACL 文件如下:
1 2 3 4 | avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests
|
首先把這些數(shù)據(jù)讀入你編寫的數(shù)據(jù)結(jié)構(gòu)。本例中,為保持簡潔,我們暫時只實現(xiàn) avail 的規(guī)則(譯注:也就是省略了unavail 部分)。下面這個方法生成一個關(guān)聯(lián)數(shù)組,它的主鍵是用戶名,值是一個該用戶有寫權(quán)限的所有目錄組成的數(shù)組:
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 規(guī)則文件,這個 get_acl_access_data 方法返回的數(shù)據(jù)結(jié)構(gòu)如下:
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" ]}
|
搞定了用戶權(quán)限的數(shù)據(jù),下面需要找出哪些位置將要被提交的內(nèi)容修改,從而確保試圖推送的用戶對這些位置有全部的權(quán)限。 使用 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 結(jié)構(gòu)來一一核對每一次提交修改的文件列表,就能找出該用戶是否有權(quán)限推送所有的提交內(nèi)容:
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' )
# 檢查是否有人在向他沒有權(quán)限的地方推送內(nèi)容
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 # 用戶擁有完全訪問權(quán)限
|| (path.index(access_path) == 0) # 或者對此位置有訪問權(quán)限
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
|
以上的大部分內(nèi)容應(yīng)該都比較容易理解。通過 git rev-list 獲取推送到服務(wù)器內(nèi)容的提交列表。然后,針對其中每一項,找出它試圖修改的文件然后確保執(zhí)行推送的用戶對這些文件具有權(quán)限。一個不太容易理解的 Ruby 技巧石path.index(access_path) ==0 這句,它的返回真值如果路徑以 access_path 開頭——這是為了確保access_path 并不是只在允許的路徑之一,而是所有準許全選的目錄都在該目錄之下。 現(xiàn)在你的用戶沒法推送帶有不正確的提交信息的內(nèi)容,也不能在準許他們訪問范圍之外的位置做出修改。
只允許 Fast-Forward 類型的推送
剩下的最后一項任務(wù)是指定只接受 fast-forward 的推送。在 Git 1.6 或者更新版本里,只需要設(shè)定 receive.denyDeletes 和receive.denyNonFastForwards 選項就可以了。但是通過掛鉤的實現(xiàn)可以在舊版本的 Git 上工作,并且通過一定的修改它它可以做到只針對某些用戶執(zhí)行,或者更多以后可能用的到的規(guī)則。 檢查這一項的邏輯是看看提交里是否包含從舊版本里能找到但在新版本里卻找不到的內(nèi)容。如果沒有,那這是一次純 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
|
一切都設(shè)定好了。如果現(xiàn)在運行 chmod u+x .git/hooks/update —— 修改包含以上內(nèi)容文件的權(quán)限,然后嘗試推送一個包含非 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 腳本開頭輸出到標準你輸出的。所有從腳本輸出的提示都會發(fā)送到客戶端,這點很重要。 下一個值得注意的部分是錯誤信息。
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
|
又或者某人想修改一個自己不具備權(quán)限的文件然后推送了一個包含它的提交,他將看到類似的提示。比如,一個文檔作者嘗試推送一個修改到 lib 目錄的提交,他會看到
1 | [POLICY] You do not have access to push to lib /test .rb
|
全在這了。從這里開始,只要 update 腳本存在并且可執(zhí)行,我們的倉庫永遠都不會遭到回轉(zhuǎn)或者包含不符合要求信息的提交內(nèi)容,并且用戶都被鎖在了沙箱里面。
客戶端掛鉤
這種手段的缺點在于用戶推送內(nèi)容遭到拒絕后幾乎無法避免的抱怨。辛辛苦苦寫成的代碼在最后時刻慘遭拒絕是十分悲劇切具迷惑性的;更可憐的是他們不得不修改提交歷史來解決問題,這怎么也算不上王道。 逃離這種兩難境地的法寶是給用戶一些客戶端的掛鉤,在他們作出可能悲劇的事情的時候給以警告。然后呢,用戶們就能在提交–問題變得更難修正之前解除隱患。由于掛鉤本身不跟隨克隆的項目副本分發(fā),所以必須通過其他途徑把這些掛鉤分發(fā)到用戶的 .git/hooks 目錄并設(shè)為可執(zhí)行文件。雖然可以在相同或單獨的項目內(nèi) 容里加入并分發(fā)它們,全自動的解決方案是不存在的。 首先,你應(yīng)該在每次提交前核查你的提交注釋信息,這樣你才能確保服務(wù)器不會因為不合條件的提交注釋信息而拒絕你的更改。為了達到這個目的,你可以增加’commit-msg’掛鉤。如果你使用該掛鉤來閱讀作為第一個參數(shù)傳遞給git的提交注釋信息,并且與規(guī)定的模式作對比,你就可以使git在提交注釋信息不符合條件的情況下,拒絕執(zhí)行提交。
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 ) 并且是可執(zhí)行的, 并且你的提交注釋信息不是符合要求的,你會看到:
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 腳本將把里面的規(guī)定執(zhí)行起來:
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 ]
# 只允許特定用戶修改項目重特定子目錄的內(nèi)容
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
|
這和服務(wù)端的腳本幾乎一樣,除了兩個重要區(qū)別。第一,ACL 文件的位置不同,因為這個腳本在當前工作目錄運行,而非 Git 目錄。ACL 文件的目錄必須從
1 | access = get_acl_access_data( 'acl' )
|
修改成:
1 | access = get_acl_access_data( '.git/acl' )
|
另一個重要區(qū)別是獲取被修改文件列表的方式。在服務(wù)端的時候使用了查看提交紀錄的方式,可是目前的提交都還沒被記錄下來呢,所以這個列表只能從暫存區(qū)域獲取。和原來的
1 | files_modified = `git log -1 --name-only --pretty= format : '' #{ref}`
|
不同,現(xiàn)在要用
1 | files_modified = `git diff -index --cached --name-only HEAD`
|
不同的就只有這兩點——除此之外,該腳本完全相同。一個小陷阱在于它假設(shè)在本地運行的賬戶和推送到遠程服務(wù)端的相同。如果這二者不一樣,則需要手動設(shè)置一下 $user 變量。 最后一項任務(wù)是檢查確認推送內(nèi)容中不包含非 fast-forward 類型的索引,不過這個需求比較少見。要找出一個非 fast-forward 類型的索引,要么衍合超過某個已經(jīng)推送過的提交,要么從本地不同分支推送到遠程相同的分支上。 既然服務(wù)器將給出無法推送非 fast-forward 內(nèi)容的提示,而且上面的掛鉤也能阻止強制的推送,唯一剩下的潛在問題就是衍合一次已經(jīng)推送過的提交內(nèi)容。 下面是一個檢查這個問題的 pre-rabase 腳本的例子。它獲取一個所有即將重寫的提交內(nèi)容的列表,然后檢查它們是否在遠程的索引里已經(jīng)存在。一旦發(fā)現(xiàn)某個提交可以從遠程索引里衍變過來,它就放棄衍合操作:
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
|
這個腳本利用了一個第六章“修訂版本選擇”一節(jié)中不曾提到的語法。通過這一句可以獲得一個所有已經(jīng)完成推送的提交的列表:
1 | git rev-list ^ #{sha}^@ refs/remotes/#{remote_ref}
|
SHA^@ 語法解析該次提交的所有祖先。這里我們從檢查遠程最后一次提交能夠衍變獲得但從所有我們嘗試推送的提交的 SHA 值祖先無法衍變獲得的提交內(nèi)容——也就是 fast-forward 的內(nèi)容。 這個解決方案的硬傷在于它有可能很慢而且常常沒有必要——只要不用 -f 來強制推送,服務(wù)器會自動給出警告并且拒絕推送內(nèi)容。然而,這是個不錯的練習(xí)而且理論上能幫助用戶避免一次將來不得不折回來修改的衍合操作。
7.5 總結(jié)
你已經(jīng)見識過絕大多數(shù)通過自定義 Git 客戶端和服務(wù)端來來適應(yīng)自己工作流程和項目內(nèi)容的方式了。無論你創(chuàng)造出了什么樣的工作流程,Git 都能用的順手。
|