[轉]使用Git Hooks實現開發部署任務自動化
提供:ZStack社區
前言
版本控制,這是現代軟件開發的核心需求之一。有了它,軟件項目可以安全的跟蹤代碼變更並執行回溯、完整性檢查、協同開發等多種操作。在各種版本控制軟件中,Git
是近年來最流行的軟件之一,它的去中心化架構以及源碼變更交換的速度被很多開發者青睞。
在git
的衆多優點中,最有用的一點莫過於它的靈活性。通過“hooks”(鉤子)系統,開發者和管理員們可以指定git在不同事件、不同動作下執行特定的腳本。
本文將介紹git hooks的基本思路以及用法,示範如何在你的環境中實現自動化的任務。本文所用的操作系統是Ubuntu 14.04服務器版,理論上任何可以跑git的系統都可以用同樣的方法來做。
前提條件
首先你的服務器上先要安裝過git
。Ubuntu 14.04的用戶可以查看這篇教程瞭解如何在Ubuntu 14.04上安裝git。
其次你應該能夠進行基本的git操作。如果你覺得對git不太熟,可以先看看這個Git入門教程。
上述條件達成後,請繼續往下閱讀。
Git Hooks的基本思路
Git hooks的概念相當簡單,它是爲了一個單一需求而被設計實現的。在一個共享項目(或者說多人協同開發的項目)的開發過程中,團隊成員需要確保其編碼風格的統一,確保部署方式的統一,等等(git的用戶經常會涉及到此類場景),而這些工作會造成大量的重複勞動。
Git hooks是基於事件的(event-based)。當你執行特定的git指令時,該軟件會從git倉庫下的hooks
目錄下檢查是否有相對應的腳本,如果有則執行之。
有些腳本是在動作執行之前被執行的,這種“先行腳本”可用於實現代碼規範的統一、完整性檢查、環境搭建等功能。有些腳本則在事件之後被執行,這種“後行腳本”可用於實現代碼的部署、權限錯誤糾正(git在這方面的功能有點欠缺)等功能。
總體來說,git hooks可以實現策略強制執行、確保一致性、環境控制、部署任務處理等多種功能。
Scott Chacon在他的Pro Git一書中將hooks劃分爲如下類型:
-
客戶端的hook:此類hook在提交者(committer)的計算機上被調用執行。此類hook又分爲如下幾類:
- 代碼提交相關的工作流hook:提交類hook作用在代碼提交的動作前後,通常用於運行完整性檢查、提交信息生成、信息內容驗證等功能,也可以用來發送通知。
- Email相關工作流hook:Email類hook主要用於使用Email提交的代碼補丁。像是Linux內核這樣的項目是採用Email進行補丁提交的,就可以使用此類hook。工作方式和提交類hook類似,而且項目維護者可以用此類hook直接完成打補丁的動作。
- 其他類:包括代碼合併、簽出(check out)、rebase、重寫(rewrite)、以及軟件倉庫的清理等工作。
-
服務器端hook:此類hook作用在服務器端,一般用於接收推送,部署在項目的git倉庫主幹(main)所在的服務器上。Chacon將服務器端hook分爲兩類:
- 接受觸發類:在服務器接收到一個推送之前或之後執行動作,前觸發常用於檢查,後觸發常用於部署。
- 更新:類似於前觸發,不過更新類hook是以分支(branch)作爲作用對象,在每一個分支更新通過之前執行代碼。
上述分類有助於我們對hook建立一個整體的概念,瞭解它可以用於哪類事件。當然了,要能夠實際的運用它,還需要親自動手操作、調試。
有些hook可以接受參數。也就是說,當git調用了hook的腳本時,我們可以傳遞一些數據給這個腳本。可用的hook列表如下:
Hook名稱 | 觸發指令 | 描述 | 參數的個數與描述 |
---|---|---|---|
applypatch-msg | `git am` | 可以編輯commit時提交的message。通常用於驗證或糾正補丁提交的信息以符合項目標準。 | (1) 包含預備commit信息的文件名 |
pre-applypatch | `git am` | 雖然這個hook的名稱是“打補丁前”,不過實際上的調用時機是打補丁之後、變更commit之前。如果以非0的狀態退出,會導致變更成爲uncommitted狀態。可用於在實際進行commit之前檢查代碼樹的狀態。 | 無 |
post-applypatch | `git am` | 本hook的調用時機是打補丁後、commit完成提交後。因此,本hook無法用於取消進程,而主要用於通知。 | 無 |
pre-commit | `git commit` | 本hook的調用時機是在獲取commit message之前。如果以非0的狀態退出則會取消本次commit。主要用於檢查commit本身(而不是message) | 無 |
prepare-commit-msg | `git commit` | 本hook的調用時機是在接收默認commit message之後、啓動commit message編輯器之前。非0的返回結果會取消本次commit。本hook可用於強制應用指定的commit message。 | 1. 包含commit message的文件名。2. commit message的源(message、template、merge、squash或commit)。3. commit的SHA-1(在現有commit上操作的情況)。 |
commit-msg | `git commit` | 可用於在message提交之後修改message的內容或打回message不合格的commit。非0的返回結果會取消本次commit。 | (1) 包含message內容的文件名。 |
post-commit | `git commit` | 本hook在commit完成之後調用,因此無法用於打回commit。主要用於通知。 | 無 |
pre-rebase | `git rebase` | 在執行rebase的時候調用,可用於中斷不想要的rebase。 | 1. 本次fork的上游。2. 被rebase的分支(如果rebase的是當前分支則沒有此參數) |
post-checkout | `git checkout` 和 `git clone` | 更新工作樹後調用checkout時調用,或者執行 git clone後調用。主要用於驗證環境、顯示變更、配置環境。 | 1. 之前的HEAD的ref。 2. 新HEAD的ref。 3. 一個標籤,表示其是一次branch checkout還是file checkout。 |
post-merge | `git merge` 或 `git pull` | 合併後調用,無法用於取消合併。可用於進行權限操作等git無法執行的動作。 | (1) 一個標籤,表示是否是一次標註爲squash的merge。 |
pre-push | `git push` | 在往遠程push之前調用。本hook除了攜帶參數之外,還同時給stdin輸入瞭如下信息:” ”(每項之間有空格)。這些信息可以用來做一些檢查,比如說,如果本地(local)sha1爲40個零,則本次push是一個刪除操作;如果遠程(remote)sha1是40個零,則是一個新的分支。非0的返回結果會取消本次push。 | 1. 遠程目標的名稱。 2. 遠程目標的位置。 |
pre-receive | 遠程repo進行`git-receive-pack` | 本hook在遠程repo更新剛被push的ref之前調用。非0的返回結果會中斷本次進程。本hook雖然不攜帶參數,但是會給stdin輸入如下信息:” ”。 | 無 |
update | 遠程repo進行`git-receive-pack` | 本hook在遠程repo每一次ref被push的時候調用(而不是每一次push)。可以用於滿足“所有的commit只能快進”這樣的需求。 | 1. 被更新的ref名稱。2. 老的對象名稱。3. 新的對象名稱。 |
post-receive | 遠程repo進行`git-receive-pack` | 本hook在遠程repo上所有ref被更新後,push操作的時候調用。本hook不攜帶參數,但可以從stdin接收信息,接收格式爲” ”。因爲hook的調用在更新之後進行,因此無法用於終止進程。 | 無 |
post-update | 遠程repo進行`git-receive-pack` | 本hook僅在所有的ref被push之後執行一次。它與post-receive很像,但是不接收舊值與新值。主要用於通知。 | 每個被push的repo都會生成一個參數,參數內容是ref的名稱 |
pre-auto-gc | `git gc –auto` | 用於在自動清理repo之前做一些檢查。 | 無 |
post-rewrite | `git commit –amend`,`git-rebase` | 本hook在git命令重寫(rewrite)已經被commit的數據時調用。除了其攜帶的參數之外,本hook還從stdin接收信息,信息格式爲” ”。 | 觸發本hook的命令名稱(amend或者rebase) |
下面我們通過幾個場景來說明git hook的使用方法。
設置軟件倉庫
首先,在用戶目錄下創建一個新的空倉庫,命名爲 proj
。
mkdir ~/proj
cd ~/proj
git init
Initialized empty Git repository in /home/demo/proj/.git/
我們現在已經處於這個git控制的目錄下,目錄下還沒有任何內容。在添加任何內容之前,我們先進入 .git
這個隱藏目錄下:
cd .git
ls -F
branches/ config description HEAD hooks/ info/ objects/ refs/
這裏可以看到一些文件和目錄。我們感興趣的是 hooks
這個目錄:
cd hooks
ls -l
total 40
-rwxrwxr-x 1 demo demo 452 Aug 8 16:50 applypatch-msg.sample
-rwxrwxr-x 1 demo demo 896 Aug 8 16:50 commit-msg.sample
-rwxrwxr-x 1 demo demo 189 Aug 8 16:50 post-update.sample
-rwxrwxr-x 1 demo demo 398 Aug 8 16:50 pre-applypatch.sample
-rwxrwxr-x 1 demo demo 1642 Aug 8 16:50 pre-commit.sample
-rwxrwxr-x 1 demo demo 1239 Aug 8 16:50 prepare-commit-msg.sample
-rwxrwxr-x 1 demo demo 1352 Aug 8 16:50 pre-push.sample
-rwxrwxr-x 1 demo demo 4898 Aug 8 16:50 pre-rebase.sample
-rwxrwxr-x 1 demo demo 3611 Aug 8 16:50 update.sample
這裏面已經有了一些東西。首先可以看到的是,目錄下的每一個文件都被標記爲“可執行”。腳本通過文件名被調用,因此它們必須是可執行的,而且其內容的第一行必須有一個Shebang魔術數字(#!)引用至正確的腳本解析器。常用的腳本語言有bash、perl、Python等。
其次,我們可以看到現在所有的文件都有一個 .sample
後綴名。Git決定是否執行一個hook文件完全是通過其文件名來判定的,
.sample
代表不執行,所以如果要激活某個hook,則需要將這個後綴名刪除。
現在,回到項目的根目錄:
cd ../..
示範1:用“提交後觸發”類hook在本地Web服務器上部署代碼
第一個示範將用到 post-commit
hook 來自動給本地Web服務器提交代碼。我們會讓git在每次commit提交後都做一次部署——這當然不適用於生產環境,但你明白這個意思就行。
首先安裝一個Apache:
sudo apt-get update
sudo apt-get install apache2
我們的腳本需要能夠修改 /var/www/html
路徑(Web服務器根目錄)下的內容,因此需要添加寫權限。我們可以直接將當前系統用戶設置爲該目錄的owner:
sudo chown -R `whoami`:`id -gn` /var/www/html
接下來,回到我們的項目目錄,創建一個 index.html
文件:
cd ~/proj
nano index.html
裏面隨便寫點什麼內容:
<h1>Here is a title!</h1>
<p>Please deploy me!</p>
保存退出,然後告訴git跟蹤這個文件:
git add .
現在,我們就要開始給這個倉庫設置 post-commit
hook了。在 .git/hooks
目錄下創建這個文件:
vim .git/hooks/post-commit
在編寫這個文件之前,我們先來了解一下git在運行hook的時候是如何設置環境的。
有關Git hooks的環境變量
調用hook的時候會涉及一些環境變量。要讓我們的腳本完成工作,我們需要把git在調用 post-commit
hook 時變更的環境變量再改回去。
這是編寫git hook時需要特別注意的一點。Git在調用不同hook的時候會設置不同的環境變量。也就是說,不同的hook會導致git從不同的環境拉取信息。
這樣一來,你的腳本環境會變得不可控,你可能根本沒意識到哪些變量被自動更改了。糟糕的是,這些變更的變量完全沒有在git的文檔中說明。
幸運的是,Mark Longair找到了一種測試方法來檢查每個hook被調用時所變更的環境變量。這個測試方法只需要你把下面這幾行代碼粘貼到你的git hook腳本中即可:
#!/bin/bash
echo Running $BASH_SOURCE
set | egrep GIT
echo PWD is $PWD
他這篇文章是在2011年寫的,當時的git版本在1.7.1。我寫這篇文章的時間是2014年8月,用的git版本是1.9.1,操作系統是Ubuntu 14.04,應該說還是有一些變化。總之,下面是我的測試結果:
在以下測試中,本地項目目錄爲 /home/demo/test_hooks
,遠程路徑爲 /home/demo/origin/test_hooks.git
。
-
Hooks:
applypatch-msg
、pre-applypatch
、post-applypatch
- 環境變量:
-
GIT_AUTHOR_DATE=’Mon, 11 Aug 2014 11:25:16 -0400’
-
GIT_AUTHOR_NAME=’Demo User’
-
GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu
-
GIT_REFLOG_ACTION=am
-
工作目錄: /home/demo/test_hooks
-
Hooks:
pre-commit
、prepare-commit-msg
、commit-msg
、post-commit
- 環境變量:
-
GIT_AUTHOR_DATE=’@1407774159 -0400’
-
GIT_AUTHOR_NAME=’Demo User’
-
GIT_DIR=.git
-
GIT_EDITOR=:
-
GIT_INDEX_FILE=.git/index
-
GIT_PREFIX=
-
工作目錄: /home/demo/test_hooks
-
Hooks:
pre-rebase
- 環境變量:
-
GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu
-
GIT_REFLOG_ACTION=rebase
-
工作目錄: /home/demo/test_hooks
-
Hooks:
post-checkout
- 環境變量:
-
GIT_DIR=.git
-
GIT_PREFIX=
-
工作目錄: /home/demo/test_hooks
-
Hooks:
post-merge
- 環境變量:
-
GITHEAD_4b407c…
-
GIT_DIR=.git
-
GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu
-
GIT_PREFIX=
-
GIT_REFLOG_ACTION=’pull other master’
-
工作目錄: /home/demo/test_hooks
-
Hooks:
pre-push
- 環境變量:
-
GIT_PREFIX=
-
工作目錄: /home/demo/test_hooks
-
Hooks:
pre-receive
,update
,post-receive
,post-update
- 環境變量:
-
GIT_DIR=.
-
工作目錄: /home/demo/origin/test_hooks.git
-
Hooks:
pre-auto-gc
- 這個很難測試所以信息缺失
-
Hooks:
post-rewrite
- 環境變量:
-
GIT_AUTHOR_DATE=’@1407773551 -0400’
-
GIT_AUTHOR_NAME=’Demo User’
-
GIT_DIR=.git
-
GIT_PREFIX=
-
工作目錄: /home/demo/test_hooks
以上就是git在調用不同hook時所看到的環境。有了這些信息,我們可以回去繼續編寫我們的腳本了。
繼續回來寫腳本
我們現在知道了 post-commit
hook 會改變的環境變量。把這個信息記錄下來。
Git hooks是標準的腳本,所以要在第一行告訴git用什麼解釋器:
#!/bin/bash
然後,我們要讓git把最新版本的代碼倉庫(最新一次提交後)解包到Web服務器的根目錄下。這需要把工作目錄設置爲Apache的文件根目錄,把git目錄設置爲軟件倉庫的目錄。
同時,我們還需要確保這個過程每次都能成功,即使出現了衝突也要強制執行。接下來的腳本是這樣寫的:
#!/bin/bash
git --work-tree=/var/www/html --git-dir=/home/demo/proj/.git checkout -f
At this point, we are almost done. However, we need to look extra close at the environmental variables that are set each time the
post-commit
hook is called. In particular, the GIT_INDEX_FILE
is set to.git/index
.
這樣就基本完成了。接下來的工作就是有關環境變量的工作了。post-commit
hook被調用時所變更的環境變量中,有一個
GIT_INDEX_FILE
被變更爲 .git/index
,這個是我們關注的重點。
這個路徑是相對於工作路徑的,而我們現在的工作路徑是 /var/www/html
,而這下面是沒有 .git/index
目錄的,導致腳本出錯。所以,我們需要手動的把這個變量改回正確的路徑。這個unset指令需要放在checkout指令之前,像這樣:
#!/bin/bash
unset GIT_INDEX_FILE
git --work-tree=/var/www/html --git-dir=/home/demo/proj/.git checkout -f
很多時候,這種問題是很難跟蹤到的。如果你在使用git hook之前沒意識到環境變量的問題,往往會到處踩坑。
總之,我們的腳本完成了,現在保存退出。
然後,我們需要給這個腳本文件添加執行權限:
chmod +x .git/hooks/post-commit
現在回到項目所在的目錄,來一發commit試試~
cd ~/proj
git commit -m "here we go..."
現在到瀏覽器裏看看效果,是不是我們剛纔寫的 index.html
的內容:
http://你的服務器IP
正如我們所看到的,剛纔提交的代碼已經自動部署到Web服務器的文件根目錄下啦。再來更新點內容試試:
echo "<p>Here is a change.</p>" >> index.html
git add .
git commit -m "First change"
刷新瀏覽器頁面,看看變更生效沒:
你看,這讓本地測試變得方便了很多。當然正如我們前面說的,生產環境上是不能這麼用的。要上生產環境的代碼一定要仔細的測試驗證過才行。
使用Git hook往另一臺生產服務器上部署
下面我將示範往生產環境服務器上部署代碼的正確姿勢。我將使用push-to-deploy模型,在我們往一個裸git倉庫(bare git repo)推送代碼的時候觸發線上web服務器的代碼更新。
我們剛纔的那臺機器現在就當作開發機,我們每次commit之後這裏都會自動部署,可隨時查看變更效果。
接下來,我會設置另一臺服務器做我們的生產服務器。這臺服務器上有一個裸倉庫用於接收推送,還有一個能夠被推送行爲觸發的git hook。然後,以普通用戶在sudo權限下執行如下步驟。
設置生產服務器的post-receive hook
首先,在生產服務器上安裝Web服務器:
sudo apt-get update
sudo apt-get install apache2
別忘了給git設置權限:
sudo chown -R `whoami`:`id -gn` /var/www/html
也別忘了安裝git:
sudo apt-get install git
然後,還是在用戶主目錄下創建同樣名稱的項目目錄。然後,在這個目錄下初始化一個裸倉庫。裸倉庫是沒有工作路徑的,它比較適合不經常直接操作的服務器。
mkdir ~/proj
cd ~/proj
git init --bare
因爲這是裸倉庫,所以它沒有工作路徑,而一個正常git倉庫的 .git
路徑下的所有文件都會直接出現在這個裸倉庫的根目錄下。
現在,創建我們的 post-receive
hook,這個hook在服務器收到 git push
時被觸發。用編輯器打開這個文件:
nano hooks/post-receive
第一行還是要定義我們的腳本類型。然後,告訴git我們想做什麼,還是跟之前的 post-commit
做的事情一樣,把文件解包到這臺Web服務器的文件根目錄下:
#!/bin/bash
git --work-tree=/var/www/html --git-dir=/home/demo/proj checkout -f
因爲是裸倉庫,所以 --git-dir
需要指定一個絕對路徑。其他的都差不多。
然後,我們需要添加一些額外的邏輯,因爲我們不希望把標記爲 test-feature
的分支代碼部署到生產服務器。我們的生產服務器僅僅部署
master
分支的內容。
在之前的那張表格中可以看到, post-receive
hook能夠從git接受三個通過標準輸入(standard input)寫到腳本中的內容,包括上一版的commit hash(),最新版的commit hash(),以及引用名稱。我們可以用這些信息檢查ref是否是master分支。
首先我們需要從標準輸入讀取內容。每一個ref被推送時,上述三條信息都會以標準輸入的格式被提供給腳本,三條信息之間由空格分隔。我們可以在一個 while
循環中讀取這些信息,把上面的git命令放進這個循環中:
#!/bin/bash
while read oldrev newrev ref
do
git --work-tree=/var/www/html --git-dir=/home/demo/proj checkout -f
done
然後我們需要添加一個判定條件。一個來自master分支的push,其ref通常會包含一個 refs/heads/master
字段。這可以作爲我們判定的依據:
#!/bin/bash
while read oldrev newrev ref
do
if [[ $ref =~ .*/master$ ]];
then
git --work-tree=/var/www/html --git-dir=/home/demo/proj checkout -f
fi
done
另一方面,服務器端的hook可以讓git傳遞一些消息返回給客戶端。發送到標準輸出的內容都會被轉發給客戶端,我們可以用這個功能給用戶發送通知。
這個通知應該包含一些場景描述以及系統最終執行了什麼動作。對於來自非master的推送,我們也應該給用戶返回信息,告訴他們爲什麼這次推送是成功的但代碼並沒有部署到線上:
#!/bin/bash
while read oldrev newrev ref
do
if [[ $ref =~ .*/master$ ]];
then
echo "Master ref received. Deploying master branch to production..."
git --work-tree=/var/www/html --git-dir=/home/demo/proj checkout -f
else
echo "Ref $ref successfully received. Doing nothing: only the master branch may be deployed on this server."
fi
done
編輯完畢後,保存退出。
最後,別忘了把腳本文件設置爲可執行:
chmod +x hooks/post-receive
現在,我們就可以在我們的客戶端訪問這個遠程服務器了。
在客戶端上配置遠程服務器
現在回到我們的客戶端,也就是開發機上,進入項目目錄:
cd ~/proj
我們要在這個目錄下將我們的遠程服務器添加進來,就叫做 production
。你需要知道遠程服務器上的用戶名、服務器的IP或者域名、以及裸倉庫相對於用戶home目錄的路徑。整個操作指令看起來差不多是這樣的:
git remote add production demo@server_domain_or_IP:proj
來push一個看看:
git push production master
如果你的SSH密鑰還沒設置,則需要敲入你的密碼。服務器返回的內容看起來應該是這樣的:
Counting objects: 8, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 473 bytes | 0 bytes/s, done.
Total 4 (delta 0), reused 0 (delta 0)
remote: Master ref received. Deploying master branch...
To [email protected]:proj
009183f..f1b9027 master -> master
我們在這裏能夠看到剛纔在post-receive
hook裏面寫的信息了。如果我們從瀏覽器裏訪問遠程服務器的IP或者域名,則應該能看到最新版的頁面:
看起來,這個hook已經成功的把我們的代碼部署到生產環境啦。
現在繼續來測試。我們在開發機上創建一個新的分支test_feature
,簽入到這個分支下面:
git checkout -b test_feature
現在,我們所做的變更都會在 test_feature
這個測試分支中進行。來改點東西先:
echo "<h2>New Feature Here</h2>" >> index.html
git add .
git commit -m "Trying out new feature"
這樣commit之後,在瀏覽器裏輸入開發機的IP,你應該能看到這個變更:
正如我們所需要的那樣,開發機上的Web服務器內容更新了。這樣進行本地測試再方便不過。
然後,試試把這個 test_feature
推送到遠程服務器上:
git push production test_feature
從post-receive
hook返回的結果應該是這樣的:
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 301 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Ref refs/heads/test_feature successfully received. Doing nothing: only the master branch may be deployed on this server
To [email protected]:proj
83e9dc4..5617b50 test_feature -> test_feature
在瀏覽器裏輸入生產服務器的IP地址,應該是啥變化都沒有。這正是我們需要的,因爲我們的變更沒有提交到master。
現在,如果我們完成了測試,想把這個變更推送到生產服務器上,我們可以這樣做。首先,簽入到master
分支,把剛纔的test_feature
分支合並進來:
git checkout master
git merge test_feature
合併完成後,再推送到生產服務器:
git push production master
現在再到瀏覽器裏輸入生產服務器的IP看看,變更被成功部署了:
這樣的工作流,在開發機上實現了實時部署,在生產環境上實現了推送master就部署,皆大歡喜。
總結
至此,你對於git hooks的用法應該有了一個大致的瞭解,對如何使用它來實現你的任務自動化有了概念。它可以用於部署代碼,可以用於維護代碼質量,拒絕任何不符合要求的變更。
雖然git hooks很好用,但實際運用往往不容易掌握,遇到問題後的排障過程也很煩人。要編寫出高效的hook,需要長期的練習,把各種配置、參數、標準輸入、環境變量都玩清楚。這會花費相當長的時間,但這些投入最終會幫助你和你的團隊免除大量的手動操作,帶來更高的回報。
本文來源自DigitalOcean Community。英文原文:How To Use Git Hooks To Automate Development and Deployment Tasks by Justin Ellingwood