鍍金池/ 教程/ Java/ 自定義 Git
起步
Git 分支
自定義 Git
Git 基礎
Git 工具
Git 與其他系統(tǒng)
服務器上的 Git
Git 內部原理
分布式 Git

自定義 Git

到目前為止,我闡述了 Git 基本的運作機制和使用方式,介紹了 Git 提供的許多工具來幫助你簡單且有效地使用它。 在本章,我將會介紹 Git 的一些重要的配置方法和鉤子機制以滿足自定義的要求。通過這些工具,它會和你和公司或團隊配合得天衣無縫。

配置 Git

如第一章所言,用git config配置 Git,要做的第一件事就是設置名字和郵箱地址:

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

從現(xiàn)在開始,你會了解到一些類似以上但更為有趣的設置選項來自定義 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 能夠識別的配置項被分為了兩大類:客戶端和服務器端,其中大部分基于你個人工作偏好,屬于客戶端配置。盡管有數(shù)不盡的選項,但我只闡述 其中經常使用或者會對你的工作流產生巨大影響的選項,如果你想觀察你當前的 Git 能識別的選項列表,請運行

$ git config --help

git config的手冊頁(譯注:以man命令的顯示方式)非常細致地羅列了所有可用的配置項。

core.editor

Git默認會調用你的環(huán)境變量editor定義的值作為文本編輯器,如果沒有定義的話,會調用Vi來創(chuàng)建和編輯提交以及標簽信息, 你可以使用core.editor改變默認編輯器:

$ git config --global core.editor emacs

現(xiàn)在無論你的環(huán)境變量editor被定義成什么,Git 都會調用Emacs編輯信息。

commit.template

如果把此項指定為你系統(tǒng)上的一個文件,當你提交的時候, Git 會默認使用該文件定義的內容。 例如:你創(chuàng)建了一個模板文件$HOME/.gitmessage.txt,它看起來像這樣:

subject line

what happened

[ticket: X]

設置commit.template,當運行git commit時, Git 會在你的編輯器中顯示以上的內容, 設置commit.template如下:

$ git config --global commit.template $HOME/.gitmessage.txt
$ git commit

然后當你提交時,在編輯器中顯示的提交信息如下:

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)建一個模板文件,設置 Git 默認使用它,這樣當提交時,你的策略每次都會被運用。

core.pager

core.pager指定 Git 運行諸如log、diff等所使用的分頁器,你能設置成用more或者任何你喜歡的分頁器(默認用的是less), 當然你也可以什么都不用,設置空字符串:

$ git config --global core.pager ''

這樣不管命令的輸出量多少,都會在一頁顯示所有內容。

user.signingkey

如果你要創(chuàng)建經簽署的含附注的標簽(正如第二章所述),那么把你的GPG簽署密鑰設置為配置項會更好,設置密鑰ID如下:

$ git config --global user.signingkey <gpg-key-id>

現(xiàn)在你能夠簽署標簽,從而不必每次運行git tag命令時定義密鑰:

$ git tag -s <tag-name>

core.excludesfile

正如第二章所述,你能在項目庫的.gitignore文件里頭用模式來定義那些無需納入 Git 管理的文件,這樣它們不會出現(xiàn)在未跟蹤列表, 也不會在你運行git add后被暫存。然而,如果你想用項目庫之外的文件來定義那些需被忽略的文件的話,用core.excludesfile 通知 Git 該文件所處的位置,文件內容和.gitignore類似。

help.autocorrect

該配置項只在 Git 1.6.1及以上版本有效,假如你在Git 1.6中錯打了一條命令,會顯示:

$ 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會按照你需要自動為大部分的輸出加上顏色,你能明確地規(guī)定哪些需要著色以及怎樣著色,設置color.ui為true來打開所有的默認終端著色。

$ git config --global color.ui true

設置好以后,當輸出到終端時,Git 會為之加上顏色。其他的參數(shù)還有false和always,false意味著不為輸出著色,而always則表明在任何情況下都要著色,即使 Git 命令被重定向到文件或管道。Git 1.5.5版本引進了此項配置,如果你擁有的版本更老,你必須對顏色有關選項各自進行詳細地設置。

你會很少用到color.ui = always,在大多數(shù)情況下,如果你想在被重定向的輸出中插入顏色碼,你能傳遞--color標志給 Git 命令來迫使它這么做,color.ui = true應該是你的首選。

color.*

想要具體到哪些命令輸出需要被著色以及怎樣著色或者 Git 的版本很老,你就要用到和具體命令有關的顏色配置選項,它們都能被置為true、falsealways

color.branch
color.diff
color.interactive
color.status

除此之外,以上每個選項都有子選項,可以被用來覆蓋其父設置,以達到為輸出的各個部分著色的目的。例如,讓diff輸出的改變信息以粗體、藍色前景和黑色背景的形式顯示:

$ 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 自己實現(xiàn)了diff,而且到目前為止你一直在使用它,但你能夠用一個外部的工具替代它,除此以外,你還能用一個圖形化的工具來合并和解決沖突從而不必自己手動解決。有一個不錯且免費的工具可以被用來做比較和合并工作,它就是P4Merge(譯注:Perforce圖形化合并工具),我會展示它的安裝過程。

P4Merge可以在所有主流平臺上運行,現(xiàn)在開始大膽嘗試吧。對于向你展示的例子,在Mac和Linux系統(tǒng)上,我會使用路徑名,在Windows上,/usr/local/bin應該被改為你環(huán)境中的可執(zhí)行路徑。

下載P4Merge:

http://www.perforce.com/product/components/perforce-visual-merge-and-diff-tools

首先把你要運行的命令放入外部包裝腳本中,我會使用Mac系統(tǒng)上的路徑來指定該腳本的位置,在其他系統(tǒng)上,它應該被放置在二進制文件p4merge所在的目錄中。創(chuàng)建一個merge包裝腳本,名字叫作extMerge,讓它帶參數(shù)調用p4merge二進制文件:

$ cat /usr/local/bin/extMerge
#!/bin/sh
/Applications/p4merge.app/Contents/MacOS/p4merge $*

diff包裝腳本首先確定傳遞過來7個參數(shù),隨后把其中2個傳遞給merge包裝腳本,默認情況下, Git 傳遞以下參數(shù)給diff:

path old-file old-hex old-mode new-file new-hex new-mode

由于你僅僅需要old-filenew-file參數(shù),用diff包裝腳本來傳遞它們吧。

$ cat /usr/local/bin/extDiff
#!/bin/sh
[ $# -eq 7 ] && /usr/local/bin/extMerge "$2" "$5"

確認這兩個腳本是可執(zhí)行的:

$ sudo chmod +x /usr/local/bin/extMerge
$ sudo chmod +x /usr/local/bin/extDiff

現(xiàn)在來配置使用你自定義的比較和合并工具吧。這需要許多自定義設置:merge.tool通知 Git 使用哪個合并工具;mergetool.*.cmd規(guī)定命令運行的方式;mergetool.trustExitCode會通知 Git 程序的退出是否指示合并操作成功;diff.external通知 Git 用什么命令做比較。因此,你能運行以下4條配置命令:

$ 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文件如下:

[merge]
  tool = extMerge
[mergetool "extMerge"]
  cmd = extMerge \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"
  trustExitCode = false
[diff]
  external = extDiff

設置完畢后,運行diff命令:

$ git diff 32d1776b1^ 32d1776b1

命令行居然沒有發(fā)現(xiàn)diff命令的輸出,其實,Git 調用了剛剛設置的P4Merge,它看起來像圖7-1這樣:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0701-tn.png" alt="" />

Figure 7-1. P4Merge.

當你設法合并兩個分支,結果卻有沖突時,運行git mergetool,Git 會調用P4Merge讓你通過圖形界面來解決沖突。

設置包裝腳本的好處是你能簡單地改變diff和merge工具,例如把extDiffextMerge改成KDiff3,要做的僅僅是編輯extMerge腳本文件:

$ cat /usr/local/bin/extMerge
#!/bin/sh
/Applications/kdiff3.app/Contents/MacOS/kdiff3 $*

現(xiàn)在 Git 會使用KDiff3來做比較、合并和解決沖突。

Git預先設置了許多其他的合并和解決沖突的工具,而你不必設置cmd??梢园押喜⒐ぞ咴O置為:kdiff3、opendiff、tkdiff、meld、xxdiff、emerge、vimdiff、gvimdiff。如果你不想用到KDiff3的所有功能,只是想用它來合并,那么kdiff3正符合你的要求,運行:

$ git config --global merge.tool kdiff3

如果運行了以上命令,沒有設置extMergeextDiff文件,Git 會用KDiff3做合并,讓通常內設的比較工具來做比較。

格式化與空白

格式化與空白是許多開發(fā)人員在協(xié)作時,特別是在跨平臺情況下,遇到的令人頭疼的細小問題。由于編輯器的不同或者Windows程序員在跨平臺項目中的文件行尾加入了回車換行符,一些細微的空格變化會不經意地進入大家合作的工作或提交的補丁中。不用怕,Git 的一些配置選項會幫助你解決這些問題。

core.autocrlf

假如你正在Windows上寫程序,又或者你正在和其他人合作,他們在Windows上編程,而你卻在其他系統(tǒng)上,在這些情況下,你可能會遇到行尾結束符問題。這是因為Windows使用回車和換行兩個字符來結束一行,而Mac和Linux只使用換行一個字符。雖然這是小問題,但它會極大地擾亂跨平臺協(xié)作。

Git可以在你提交時自動地把行結束符CRLF轉換成LF,而在簽出代碼時把LF轉換成CRLF。用core.autocrlf來打開此項功能,如果是在Windows系統(tǒng)上,把它設置成true,這樣當簽出代碼時,LF會被轉換成CRLF:

$ git config --global core.autocrlf true

Linux或Mac系統(tǒng)使用LF作為行結束符,因此你不想 Git 在簽出文件時進行自動的轉換;當一個以CRLF為行結束符的文件不小心被引入時你肯定想進行修正,把core.autocrlf設置成input來告訴 Git 在提交時把CRLF轉換成LF,簽出時不轉換:

$ git config --global core.autocrlf input

這樣會在Windows系統(tǒng)上的簽出文件中保留CRLF,會在Mac和Linux系統(tǒng)上,包括倉庫中保留LF。

如果你是Windows程序員,且正在開發(fā)僅運行在Windows上的項目,可以設置false取消此功能,把回車符記錄在庫中:

$ git config --global core.autocrlf false

core.whitespace

Git預先設置了一些選項來探測和修正空白問題,其4種主要選項中的2個默認被打開,另2個被關閉,你可以自由地打開或關閉它們。

默認被打開的2個選項是trailing-spacespace-before-tab,trailing-space會查找每行結尾的空格,space-before-tab會查找每行開頭的制表符前的空格。

默認被關閉的2個選項是indent-with-non-tabcr-at-eol,indent-with-non-tab會查找8個以上空格(非制表符)開頭的行,cr-at-eol讓 Git 知道行尾回車符是合法的。

設置core.whitespace,按照你的意圖來打開或關閉選項,選項以逗號分割。通過逗號分割的鏈中去掉選項或在選項前加-來關閉,例如,如果你想要打開除了cr-at-eol之外的所有選項:

$ git config --global core.whitespace \
    trailing-space,space-before-tab,indent-with-non-tab

當你運行git diff命令且為輸出著色時,Git 探測到這些問題,因此你也許在提交前能修復它們,當你用git apply打補丁時同樣也會從中受益。如果正準備運用的補丁有特別的空白問題,你可以讓 Git 發(fā)警告:

$ git apply --whitespace=warn <patch>

或者讓 Git 在打上補丁前自動修正此問題:

$ git apply --whitespace=fix <patch>

這些選項也能運用于衍合。如果提交了有空白問題的文件但還沒推送到上游,你可以運行帶有--whitespace=fix選項的rebase來讓Git在重寫補丁時自動修正它們。

服務器端配置

Git服務器端的配置選項并不多,但仍有一些饒有生趣的選項值得你一看。

receive.fsckObjects

Git默認情況下不會在推送期間檢查所有對象的一致性。雖然會確認每個對象的有效性以及是否仍然匹配SHA-1檢驗和,但 Git 不會在每次推送時都檢查一致性。對于 Git 來說,庫或推送的文件越大,這個操作代價就相對越高,每次推送會消耗更多時間,如果想在每次推送時 Git 都檢查一致性,設置 receive.fsckObjects 為true來強迫它這么做:

$ git config --system receive.fsckObjects true

現(xiàn)在 Git 會在每次推送生效前檢查庫的完整性,確保有問題的客戶端沒有引入破壞性的數(shù)據(jù)。

receive.denyNonFastForwards

如果對已經被推送的提交歷史做衍合,繼而再推送,又或者以其它方式推送一個提交歷史至遠程分支,且該提交歷史沒在這個遠程分支中,這樣的推送會被拒絕。這通常是個很好的禁止策略,但有時你在做衍合并確定要更新遠程分支,可以在push命令后加-f標志來強制更新。

要禁用這樣的強制更新功能,可以設置receive.denyNonFastForwards

$ git config --system receive.denyNonFastForwards true

稍后你會看到,用服務器端的接收鉤子也能達到同樣的目的。這個方法可以做更細致的控制,例如:禁用特定的用戶做強制更新。

receive.denyDeletes

規(guī)避denyNonFastForwards策略的方法之一就是用戶刪除分支,然后推回新的引用。在更新的 Git 版本中(從1.6.1版本開始),把receive.denyDeletes設置為true:

$ git config --system receive.denyDeletes true

這樣會在推送過程中阻止刪除分支和標簽 — 沒有用戶能夠這么做。要刪除遠程分支,必須從服務器手動刪除引用文件。通過用戶訪問控制列表也能這么做,在本章結尾將會介紹這些有趣的方式。

Git屬性

一些設置項也能被運用于特定的路徑中,這樣,Git 以對一個特定的子目錄或子文件集運用那些設置項。這些設置項被稱為 Git 屬性,可以在你目錄中的.gitattributes文件內進行設置(通常是你項目的根目錄),也可以當你不想讓這些屬性文件和項目文件一同提交時,在.git/info/attributes進行設置。

使用屬性,你可以對個別文件或目錄定義不同的合并策略,讓 Git 知道怎樣比較非文本文件,在你提交或簽出前讓 Git 過濾內容。你將在這部分了解到能在自己的項目中使用的屬性,以及一些實例。

二進制文件

你可以用 Git 屬性讓其知道哪些是二進制文件(以防 Git 沒有識別出來),以及指示怎樣處理這些文件,這點很酷。例如,一些文本文件是由機器產生的,而且無法比較,而一些二進制文件可以比較 — 你將會了解到怎樣讓 Git 識別這些文件。

識別二進制文件

一些文件看起來像是文本文件,但其實是作為二進制數(shù)據(jù)被對待。例如,在Mac上的Xcode項目含有一個以.pbxproj結尾的文件,它是由記錄設置項的IDE寫到磁盤的JSON數(shù)據(jù)集(純文本javascript數(shù)據(jù)類型)。雖然技術上看它是由ASCII字符組成的文本文件,但你并不認為如此,因為它確實是一個輕量級數(shù)據(jù)庫 — 如果有2人改變了它,你通常無法合并和比較內容,只有機器才能進行識別和操作,于是,你想把它當成二進制文件。

讓 Git 把所有pbxproj文件當成二進制文件,在.gitattributes文件中設置如下:

*.pbxproj -crlf -diff

現(xiàn)在,Git 會嘗試轉換和修正CRLF(回車換行)問題,也不會當你在項目中運行git showgit diff時,比較不同的內容。在Git 1.6及之后的版本中,可以用一個宏代替-crlf -diff

*.pbxproj binary

比較二進制文件

你可以使用 Git 屬性來有效地比較兩個二進制文件(binary files,譯注:指非文本文件)。那么第一步要做的是,告訴 Git 怎么把你的二進制文件轉化為純文本格式,從而讓普通的 diff 命令可以進行文本對比。但是,我們怎么把二進制文件轉化為文本呢?最好的解決方法是找到一個轉換工具幫助我們進行轉化。但是,大部分的二進制文件不能表示為可讀的文本,例如語音文件就很難轉化為文本文件。如果你遇到這些情況,比較簡單的解決方法是從這些二進制文件中獲取元數(shù)據(jù)。雖然這些元數(shù)據(jù)并不能完全描述一個二進制文件,但大多數(shù)情況下,都是能夠概括文件情況的。

下面,我們將會展示,如何使用轉化工具進行二進制文件的比較。

邊注:有一些二進制文件雖然包含文字,但是卻難以轉換。(譯注:例如 Word 文檔。)在這些情況,你可以嘗試使用 strings 工具來獲取其中的文字。但如果當這些文檔包含 UTF-16 編碼,或者其他代碼頁(codepages),strings 也可能無補于事。strings 在大部分的 Mac 和 Linux 下都有安裝。當遇到有二進制文件需要轉換的時候,你可以試試這個工具。

Word文檔

這個特性很酷,而且鮮為人知,因此我會結合實例來講解。首先,要解決的是最令人頭疼的問題:對Word文檔進行版本控制。很多人對Word文檔又恨又愛,如果想對其進行版本控制,你可以把文件加入到 Git 庫中,每次修改后提交即可。但這樣做沒有一點實際意義,因為運行git diff命令后,你只能得到如下的結果:

$ 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文件:

*.doc diff=word

當你要看比較結果時,如果文件擴展名是"doc",Git 調用"word"過濾器。什么是"word"過濾器呢?其實就是 Git 使用strings 程序,把Word文檔轉換成可讀的文本文件,之后再進行比較:

$ git config diff.word.textconv catdoc

這個命令會在你的 .git/config 文件中增加一節(jié):

[diff "word"]
    textconv = catdoc

現(xiàn)在如果在兩個快照之間比較以.doc結尾的文件,Git 對這些文件運用"word"過濾器,在比較前把Word文件轉換成文本文件。

下面展示了一個實例,我把此書的第一章納入 Git 管理,在一個段落中加入了一些文本后保存,之后運行git diff命令,得到結果如下:

$ git diff
diff --git a/chapter1.doc b/chapter1.doc
index c1c8a0a..b93c9e4 100644
--- a/chapter1.doc
+++ b/chapter1.doc
@@ -128,7 +128,7 @@ and data size)
 Since its birth in 2005, Git has evolved and matured to be easy to use
 and yet retain these initial qualities. It’s incredibly fast, it’s
 very efficient with large projects, and it has an incredible branching
-system for non-linear development.
+system for non-linear development (See Chapter 3).

Git 成功且簡潔地顯示出我增加的文本"(See Chapter 3)"。工作的很完美!

OpenDocument文本文檔

我們用于處理Word文檔(*.doc)的方法同樣適用于處理OpenOffice.org創(chuàng)建的OpenDocument文本文檔(*.odt)。

把下面這行添加到.gitattributes文件:

*.odt diff=odt

然后在.git/config 文件中設置odt過濾器:

[diff "odt"]
    binary = true
    textconv = /usr/local/bin/odt-to-txt

OpenDocument文檔實際上是多個文件(包括一個XML文件和表格、圖片等文件)的壓縮包。我們需要寫一個腳本來提取其中純文本格式的內容。創(chuàng)建一個文件/usr/local/bin/odt-to-txt(你也可以放到其他目錄下),寫入下面內容:

#! /usr/bin/env perl
# Simplistic OpenDocument Text (.odt) to plain text converter.
# Author: Philipp Kempgen

if (! defined($ARGV[0])) {
    print STDERR "No filename given!\n";
    print STDERR "Usage: $0 filename\n";
    exit 1;
}

my $content = '';
open my $fh, '-|', 'unzip', '-qq', '-p', $ARGV[0], 'content.xml' or die $!;
{
    local $/ = undef;  # slurp mode
    $content = <$fh>;
}
close $fh;
$_ = $content;
s/<text:span\b[^>]*>//g;           # remove spans
s/<text:h\b[^>]*>/\n\n*****  /g;   # headers
s/<text:list-item\b[^>]*>\s*<text:p\b[^>]*>/\n    --  /g;  # list items
s/<text:list\b[^>]*>/\n\n/g;       # lists
s/<text:p\b[^>]*>/\n  /g;          # paragraphs
s/<[^>]+>//g;                      # remove all XML tags
s/\n{2,}/\n\n/g;                   # remove multiple blank lines
s/\A\n+//;                         # remove leading blank lines
print "\n", $_, "\n\n";

然后把它設為可執(zhí)行文件

chmod +x /usr/local/bin/odt-to-txt

現(xiàn)在git diff命令就可以顯示.odt文件的變更了。

圖像文件

你還能用這個方法比較圖像文件。當比較時,對JPEG文件運用一個過濾器,它能提煉出EXIF信息 — 大部分圖像格式使用的元數(shù)據(jù)。如果你下載并安裝了exiftool程序,可以用它參照元數(shù)據(jù)把圖像轉換成文本。比較的不同結果將會用文本向你展示:

$ echo '*.png diff=exif' >> .gitattributes
$ git config diff.exif.textconv exiftool

如果在項目中替換了一個圖像文件,運行git diff命令的結果如下:

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:17 10:12:35-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ā)生了改變。

關鍵字擴展

使用SVN或CVS的開發(fā)人員經常要求關鍵字擴展。在 Git 中,你無法在一個文件被提交后修改它,因為 Git 會先對該文件計算校驗和。然而,你可以在簽出時注入文本,在提交前刪除它。 Git 屬性提供了2種方式這么做。

首先,你能夠把blob的SHA-1校驗和自動注入文件的$Id$字段。如果在一個或多個文件上設置了此字段,當下次你簽出分支的時候,Git 用blob的SHA-1值替換那個字段。注意,這不是提交對象的SHA校驗和,而是blob本身的校驗和:

$ echo '*.txt ident' >> .gitattributes
$ echo '$Id$' > test.txt

下次簽出文件時,Git 入了blob的SHA值:

$ rm test.txt
$ git checkout -- test.txt
$ cat test.txt
$Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

然而,這樣的顯示結果沒有多大的實際意義。這個SHA的值相當?shù)仉S機,無法區(qū)分日期的前后,所以,如果你在CVS或Subversion中用過關鍵字替換,一定會包含一個日期值。

因此,你能寫自己的過濾器,在提交文件到暫存區(qū)或簽出文件時替換關鍵字。有2種過濾器,"clean"和"smudge"。在 .gitattributes文件中,你能對特定的路徑設置一個過濾器,然后設置處理文件的腳本,這些腳本會在文件簽出前("smudge",見圖 7-2)和提交到暫存區(qū)前("clean",見圖7-3)被調用。這些過濾器能夠做各種有趣的事。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0702-tn.png" alt="" />

圖7-2. 簽出時,"smudge"過濾器被觸發(fā)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0703-tn.png" alt="" />

圖7-3. 提交到暫存區(qū)時,"clean"過濾器被觸發(fā)。

這里舉一個簡單的例子:在暫存前,用indent(縮進)程序過濾所有C源代碼。在.gitattributes文件中設置"indent"過濾器過濾*.c文件:

*.c     filter=indent

然后,通過以下配置,讓 Git 知道"indent"過濾器在遇到"smudge"和"clean"時分別該做什么:

$ git config --global filter.indent.clean indent
$ git config --global filter.indent.smudge cat

于是,當你暫存*.c文件時,indent程序會被觸發(fā),在把它們簽出之前,cat程序會被觸發(fā)。但cat程序在這里沒什么實際作用。這樣的組合,使C源代碼在暫存前被indent程序過濾,非常有效。

另一個例子是類似RCS的$Date$關鍵字擴展。為了演示,需要一個小腳本,接受文件名參數(shù),得到項目的最新提交日期,最后把日期寫入該文件。下面用Ruby腳本來實現(xiàn):

#! /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 中設置一個過濾器(dater),讓它在簽出文件時調用expand_date,在暫存文件時用Perl清除之:

$ git config filter.dater.smudge expand_date
$ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'

這個Perl小程序會刪除$Date$字符串里多余的字符,恢復$Date$原貌。到目前為止,你的過濾器已經設置完畢,可以開始測試了。打開一個文件,在文件中輸入$Date$關鍵字,然后設置 Git 屬性:

$ echo '# $Date$' > date_test.txt
$ echo 'date*.txt filter=dater' >> .gitattributes

如果暫存該文件,之后再簽出,你會發(fā)現(xiàn)關鍵字被替換了:

$ 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屬性在導出項目歸檔時也能發(fā)揮作用。

export-ignore

當產生一個歸檔時,可以設置 Git 不導出某些文件和目錄。如果你不想在歸檔中包含一個子目錄或文件,但想他們納入項目的版本管理中,你能對應地設置export-ignore屬性。

例如,在test/子目錄中有一些測試文件,在項目的壓縮包中包含他們是沒有意義的。因此,可以增加下面這行到 Git 屬性文件中:

test/ export-ignore

現(xiàn)在,當運行 git archive 來創(chuàng)建項目的壓縮包時,那個目錄不會在歸檔中出現(xiàn)。

export-subst

還能對歸檔做一些簡單的關鍵字替換。在第2章中已經可以看到,可以以--pretty=format形式的簡碼在任何文件中放入$Format:$ 字符串。例如,如果想在項目中包含一個叫作LAST_COMMIT的文件,當運行git archive時,最后提交日期自動地注入進該文件,可以這樣設置:

$ 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)其內容如下:

$ cat LAST_COMMIT
Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$

合并策略

通過 Git 屬性,還能對項目中的特定文件使用不同的合并策略。一個非常有用的選項就是,當一些特定文件發(fā)生沖突,Git 會嘗試合并他們,而使用你這邊的合并。

如果項目的一個分支有歧義或比較特別,但你想從該分支合并,而且需要忽略其中某些文件,這樣的合并策略是有用的。例如,你有一個數(shù)據(jù)庫設置文件database.xml,在2個分支中他們是不同的,你想合并一個分支到另一個,而不弄亂該數(shù)據(jù)庫文件,可以設置屬性如下:

database.xml merge=ours

如果合并到另一個分支,database.xml文件不會有合并沖突,顯示如下:

$ git merge topic
Auto-merging database.xml
Merge made by recursive.

這樣,database.xml會保持原樣。

Git掛鉤

和其他版本控制系統(tǒng)一樣,當某些重要事件發(fā)生時,Git 以調用自定義腳本。有兩組掛鉤:客戶端和服務器端??蛻舳藪煦^用于客戶端的操作,如提交和合并。服務器端掛鉤用于 Git 服務器端的操作,如接收被推送的提交。你可以隨意地使用這些掛鉤,下面會講解其中一些。

安裝一個掛鉤

掛鉤都被存儲在 Git 目錄下的hooks子目錄中,即大部分項目中的.git/hooks。 Git 默認會放置一些腳本樣本在這個目錄中,除了可以作為掛鉤使用,這些樣本本身是可以獨立使用的。所有的樣本都是shell腳本,其中一些還包含了Perl的腳本,不過,任何正確命名的可執(zhí)行腳本都可以正常使用 — 可以用Ruby或Python,或其他。在Git 1.6版本之后,這些樣本名都是以.sample結尾,因此,你必須重新命名。在Git 1.6版本之前,這些樣本名都是正確的,但這些樣本不是可執(zhí)行文件。

把一個正確命名且可執(zhí)行的文件放入 Git 目錄下的hooks子目錄中,可以激活該掛鉤腳本,因此,之后他一直會被 Git 調用。隨后會講解主要的掛鉤腳本。

客戶端掛鉤

有許多客戶端掛鉤,以下把他們分為:提交工作流掛鉤、電子郵件工作流掛鉤及其他客戶端掛鉤。

提交工作流掛鉤

有 4個掛鉤被用來處理提交的過程。pre-commit掛鉤在鍵入提交信息前運行,被用來檢查即將提交的快照,例如,檢查是否有東西被遺漏,確認測試是否運行,以及檢查代碼。當從該掛鉤返回非零值時,Git 放棄此次提交,但可以用git commit --no-verify來忽略。該掛鉤可以被用來檢查代碼錯誤(運行類似lint的程序),檢查尾部空白(默認掛鉤是這么做的),檢查新方法(譯注:程序的函數(shù))的說明。

prepare-commit-msg掛鉤在提交信息編輯器顯示之前,默認信息被創(chuàng)建之后運行。因此,可以有機會在提交作者看到默認信息前進行編輯。該掛鉤接收一些選項:擁有提交信息的文件路徑,提交類型,如果是一次修訂的話,提交的SHA-1校驗和。該掛鉤對通常的提交來說不是很有用,只在自動產生的默認提交信息的情況下有作用,如提交信息模板、合并、壓縮和修訂提交等。可以和提交模板配合使用,以編程的方式插入信息。

commit-msg掛鉤接收一個參數(shù),此參數(shù)是包含最近提交信息的臨時文件的路徑。如果該掛鉤腳本以非零退出,Git 放棄提交,因此,可以用來在提交通過前驗證項目狀態(tài)或提交信息。本章上一小節(jié)已經展示了使用該掛鉤核對提交信息是否符合特定的模式。

post-commit掛鉤在整個提交過程完成后運行,他不會接收任何參數(shù),但可以運行git log -1 HEAD來獲得最后的提交信息??傊?,該掛鉤是作為通知之類使用的。

提交工作流的客戶端掛鉤腳本可以在任何工作流中使用,他們經常被用來實施某些策略,但值得注意的是,這些腳本在clone期間不會被傳送??梢栽诜掌鞫藢嵤┎呗詠砭芙^不符合某些策略的推送,但這完全取決于開發(fā)者在客戶端使用這些腳本的情況。所以,這些腳本對開發(fā)者是有用的,由他們自己設置和維護,而且在任何時候都可以覆蓋或修改這些腳本。

E-mail工作流掛鉤

有3個可用的客戶端掛鉤用于e-mail工作流。當運行git am命令時,會調用他們,因此,如果你沒有在工作流中用到此命令,可以跳過本節(jié)。如果你通過e-mail接收由git format-patch產生的補丁,這些掛鉤也許對你有用。

首先運行的是applypatch-msg掛鉤,他接收一個參數(shù):包含被建議提交信息的臨時文件名。如果該腳本非零退出,Git 放棄此補丁??梢允褂眠@個腳本確認提交信息是否被正確格式化,或讓腳本編輯信息以達到標準化。

下一個在git am運行期間調用是pre-applypatch掛鉤。該掛鉤不接收參數(shù),在補丁被運用之后運行,因此,可以被用來在提交前檢查快照。你能用此腳本運行測試,檢查工作樹。如果有些什么遺漏,或測試沒通過,腳本會以非零退出,放棄此次git am的運行,補丁不會被提交。

最后在git am運行期間調用的是post-applypatch掛鉤。你可以用他來通知一個小組或獲取的補丁的作者,但無法阻止打補丁的過程。

其他客戶端掛鉤

pre-rebase掛鉤在衍合前運行,腳本以非零退出可以中止衍合的過程。你可以使用這個掛鉤來禁止衍合已經推送的提交對象,Git pre-rebase掛鉤樣本就是這么做的。該樣本假定next是你定義的分支名,因此,你可能要修改樣本,把next改成你定義過且穩(wěn)定的分支名。

git checkout成功運行后,post-checkout掛鉤會被調用。他可以用來為你的項目環(huán)境設置合適的工作目錄。例如:放入大的二進制文件、自動產生的文檔或其他一切你不想納入版本控制的文件。

最后,在merge命令成功執(zhí)行后,post-merge掛鉤會被調用。他可以用來在 Git 無法跟蹤的工作樹中恢復數(shù)據(jù),諸如權限數(shù)據(jù)。該掛鉤同樣能夠驗證在 Git 控制之外的文件是否存在,因此,當工作樹改變時,你想這些文件可以被復制。

服務器端掛鉤

除了客戶端掛鉤,作為系統(tǒng)管理員,你還可以使用兩個服務器端的掛鉤對項目實施各種類型的策略。這些掛鉤腳本可以在提交對象推送到服務器前被調用,也可以在推送到服務器后被調用。推送到服務器前調用的掛鉤可以在任何時候以非零退出,拒絕推送,返回錯誤消息給客戶端,還可以如你所愿設置足夠復雜的推送策略。

pre-receive 和 post-receive

處理來自客戶端的推送(push)操作時最先執(zhí)行的腳本就是 pre-receive 。它從標準輸入(stdin)獲取被推送引用的列表;如果它退出時的返回值不是0,所有推送內容都不會被接受。利用此掛鉤腳本可以實現(xiàn)類似保證最新的索引中不包含非fast-forward類型的這類效果;抑或檢查執(zhí)行推送操作的用戶擁有創(chuàng)建,刪除或者推送的權限或者他是否對將要修改的每一個文件都有訪問權限。

post-receive 掛鉤在整個過程完結以后運行,可以用來更新其他系統(tǒng)服務或者通知用戶。它接受與 pre-receive 相同的標準輸入數(shù)據(jù)。應用實例包括給某郵件列表發(fā)信,通知實時整合數(shù)據(jù)的服務器,或者更新軟件項目的問題追蹤系統(tǒng) —— 甚至可以通過分析提交信息來決定某個問題是否應該被開啟,修改或者關閉。該腳本無法組織推送進程,不過客戶端在它完成運行之前將保持連接狀態(tài);所以在用它作一些消耗時間的操作之前請三思。

update

update 腳本和 pre-receive 腳本十分類似。不同之處在于它會為推送者更新的每一個分支運行一次。假如推送者同時向多個分支推送內容,pre-receive 只運行一次,相比之下 update 則會為每一個更新的分支運行一次。它不會從標準輸入讀取內容,而是接受三個參數(shù):索引的名字(分支),推送前索引指向的內容的 SHA-1 值,以及用戶試圖推送內容的 SHA-1 值。如果 update 腳本以退出時返回非零值,只有相應的那一個索引會被拒絕;其余的依然會得到更新。

Git 強制策略實例

在本節(jié)中,我們應用前面學到的知識建立這樣一個Git 工作流程:檢查提交信息的格式,只接受純fast-forward內容的推送,并且指定用戶只能修改項目中的特定子目錄。我們將寫一個客戶端腳本來提示開發(fā)人員他們推送的內容是否會被拒絕,以及一個服務端腳本來實際執(zhí)行這些策略。

這些腳本使用 Ruby 寫成,一半由于它是作者傾向的腳本語言,另外作者覺得它是最接近偽代碼的腳本語言;因而即便你不使用 Ruby 也能大致看懂。不過任何其他語言也一樣適用。所有 Git 自帶的樣例腳本都是用 Perl 或 Bash 寫的。所以從這些腳本中能找到相當多的這兩種語言的掛鉤樣例。

服務端掛鉤

所有服務端的工作都在hooks(掛鉤)目錄的 update(更新)腳本中制定。update 腳本為每一個得到推送的分支運行一次;它接受推送目標的索引,該分支原來指向的位置,以及被推送的新內容。如果推送是通過 SSH 進行的,還可以獲取發(fā)出此次操作的用戶。如果設定所有操作都通過公匙授權的單一帳號(比如"git")進行,就有必要通過一個 shell 包裝依據(jù)公匙來判斷用戶的身份,并且設定環(huán)境變量來表示該用戶的身份。下面假設嘗試連接的用戶儲存在 $USER 環(huán)境變量里,我們的 update 腳本首先搜集一切需要的信息:

#!/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" 這樣的字符串,因為我們需要把每一次提交和項目的問題追蹤系統(tǒng)。我們要逐一檢查每一條推送上來的提交內容,看看提交信息是否包含這么一個字符串,然后,如果該提交里不包含這個字符串,以非零返回值退出從而拒絕此次推送。

$newrev$oldrev 變量的值傳給一個叫做 git rev-list 的 Git plumbing 命令可以獲取所有提交內容的 SHA-1 值列表。git rev-list 基本類似 git log 命令,但它默認只輸出 SHA-1 值而已,沒有其他信息。所以要獲取由 SHA 值表示的從一次提交到另一次提交之間的所有 SHA 值,可以運行:

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

截取這些輸出內容,循環(huán)遍歷其中每一個 SHA 值,找出與之對應的提交信息,然后用正則表達式來測試該信息包含的格式話的內容。

下面要搞定如何從所有的提交內容中提取出提交信息。使用另一個叫做 git cat-file 的 Git plumbing 工具可以獲得原始的提交數(shù)據(jù)。我們將在第九章了解到這些 plumbing 工具的細節(jié);現(xiàn)在暫時先看一下這條命令的輸出:

$ 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 系統(tǒng)的 sed 命令來實現(xiàn)該效果:

$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number

這條咒語從每一個待提交內容里提取提交信息,并且會在提取信息不符合要求的情況下退出。為了退出腳本和拒絕此次推送,返回一個非零值。整個腳本大致如下:

$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)基于用戶的訪問權限控制列表(ACL)系統(tǒng)

假設你需要添加一個使用訪問權限控制列表的機制來指定哪些用戶對項目的哪些部分有推送權限。某些用戶具有全部的訪問權,其他人只對某些子目錄或者特定的文件具有推送權限。要搞定這一點,所有的規(guī)則將被寫入一個位于服務器的原始 Git 倉庫的 acl 文件。我們讓 update 掛鉤檢閱這些規(guī)則,審視推送的提交內容中需要修改的所有文件,然后決定執(zhí)行推送的用戶是否對所有這些文件都有權限。

我們首先要創(chuàng)建這個列表。這里使用的格式和 CVS 的 ACL 機制十分類似:它由若干行構成,第一項內容是 avail 或者 unavail,接著是逗號分隔的規(guī)則生效用戶列表,最后一項是規(guī)則生效的目錄(空白表示開放訪問)。這些項目由 | 字符隔開。

下例中,我們指定幾個管理員,幾個對 doc 目錄具有權限的文檔作者,以及一個對 libtests 目錄具有權限的開發(fā)人員,相應的 ACL 文件如下:

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

首先把這些數(shù)據(jù)讀入你編寫的數(shù)據(jù)結構。本例中,為保持簡潔,我們暫時只實現(xiàn) avail 的規(guī)則(譯注:也就是省略了 unavail 部分)。下面這個方法生成一個關聯(lián)數(shù)組,它的主鍵是用戶名,值是一個該用戶有寫權限的所有目錄組成的數(shù)組:

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ù)結構如下:

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

搞定了用戶權限的數(shù)據(jù),下面需要找出哪些位置將要被提交的內容修改,從而確保試圖推送的用戶對這些位置有全部的權限。

使用 git log--name-only 選項(在第二章里簡單的提過)我們可以輕而易舉的找出一次提交里修改的文件:

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

README
lib/test.rb

使用 get_acl_access_data 返回的 ACL 結構來一一核對每一次提交修改的文件列表,就能找出該用戶是否有權限推送所有的提交內容:

# 僅允許特定用戶修改項目中的特定子目錄
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 獲取推送到服務器內容的提交列表。然后,針對其中每一項,找出它試圖修改的文件然后確保執(zhí)行推送的用戶對這些文件具有權限。一個不太容易理解的 Ruby 技巧是 path.index(access_path) ==0 這句,它的返回真值如果路徑以 access_path 開頭——這是為了確保 access_path 并不是只在允許的路徑之一,而是所有準許全選的目錄都在該目錄之下。

現(xiàn)在你的用戶沒法推送帶有不正確的提交信息的內容,也不能在準許他們訪問范圍之外的位置做出修改。

只允許 Fast-Forward 類型的推送

剩下的最后一項任務是指定只接受 fast-forward 的推送。在 Git 1.6 或者更新版本里,只需要設定 receive.denyDeletesreceive.denyNonFastForwards 選項就可以了。但是通過掛鉤的實現(xiàn)可以在舊版本的 Git 上工作,并且通過一定的修改它它可以做到只針對某些用戶執(zhí)行,或者更多以后可能用的到的規(guī)則。

檢查這一項的邏輯是看看提交里是否包含從舊版本里能找到但在新版本里卻找不到的內容。如果沒有,那這是一次純 fast-forward 的推送;如果有,那我們拒絕此次推送:

# 只允許純 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

一切都設定好了。如果現(xiàn)在運行 chmod u+x .git/hooks/update —— 修改包含以上內容文件的權限,然后嘗試推送一個包含非 fast-forward 類型的索引,會得到一下提示:

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

這里有幾個有趣的信息。首先,我們可以看到掛鉤運行的起點:

Enforcing Policies...
(refs/heads/master) (8338c5) (c5b616)

注意這是從 update 腳本開頭輸出到標準你輸出的。所有從腳本輸出的提示都會發(fā)送到客戶端,這點很重要。

下一個值得注意的部分是錯誤信息。

[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 腳本退出時返回了非零值因而推送遭到了拒絕。最后一點:

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

我們將為每一個被掛鉤拒之門外的索引受到一條遠程信息,解釋它被拒絕是因為一個掛鉤的原因。

而且,如果那個 ref 字符串沒有包含在任何的提交里,我們將看到前面腳本里輸出的錯誤信息:

[POLICY] Your message is not formatted correctly

又或者某人想修改一個自己不具備權限的文件然后推送了一個包含它的提交,他將看到類似的提示。比如,一個文檔作者嘗試推送一個修改到 lib 目錄的提交,他會看到

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

全在這了。從這里開始,只要 update 腳本存在并且可執(zhí)行,我們的倉庫永遠都不會遭到回轉或者包含不符合要求信息的提交內容,并且用戶都被鎖在了沙箱里面。

客戶端掛鉤

這種手段的缺點在于用戶推送內容遭到拒絕后幾乎無法避免的抱怨。辛辛苦苦寫成的代碼在最后時刻慘遭拒絕是十分悲劇且具有迷惑性的;更可憐的是他們不得不修改提交歷史來解決問題,這怎么也算不上王道。

逃離這種兩

上一篇:Git 工具下一篇:Git 與其他系統(tǒng)