Git Hooks實現開發部署任務自動化

[轉]使用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

  • Hooksapplypatch-msgpre-applypatchpost-applypatch

    • 環境變量
    • GIT_AUTHOR_DATE=’Mon, 11 Aug 2014 11:25:16 -0400’

    • [email protected]

    • GIT_AUTHOR_NAME=’Demo User’

    • GIT_INTERNAL_GETTEXT_SH_SCHEME=gnu

    • GIT_REFLOG_ACTION=am

    • 工作目錄: /home/demo/test_hooks

  • Hookspre-commitprepare-commit-msgcommit-msgpost-commit

    • 環境變量
    • GIT_AUTHOR_DATE=’@1407774159 -0400’

    • [email protected]

    • 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’

    • [email protected]

    • 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

Test index.html

正如我們所看到的,剛纔提交的代碼已經自動部署到Web服務器的文件根目錄下啦。再來更新點內容試試:

echo "<p>Here is a change.</p>" >> index.html
git add .
git commit -m "First change"

刷新瀏覽器頁面,看看變更生效沒:

deploy changes

你看,這讓本地測試變得方便了很多。當然正如我們前面說的,生產環境上是不能這麼用的。要上生產環境的代碼一定要仔細的測試驗證過才行。

使用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或者域名,則應該能看到最新版的頁面:

pushed production

看起來,這個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,你應該能看到這個變更:

commit changes

正如我們所需要的那樣,開發機上的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看看,變更被成功部署了:

Pushed to production

這樣的工作流,在開發機上實現了實時部署,在生產環境上實現了推送master就部署,皆大歡喜。

總結

至此,你對於git hooks的用法應該有了一個大致的瞭解,對如何使用它來實現你的任務自動化有了概念。它可以用於部署代碼,可以用於維護代碼質量,拒絕任何不符合要求的變更。

雖然git hooks很好用,但實際運用往往不容易掌握,遇到問題後的排障過程也很煩人。要編寫出高效的hook,需要長期的練習,把各種配置、參數、標準輸入、環境變量都玩清楚。這會花費相當長的時間,但這些投入最終會幫助你和你的團隊免除大量的手動操作,帶來更高的回報。

本文來源自DigitalOcean Community。英文原文:How To Use Git Hooks To Automate Development and Deployment Tasks by Justin Ellingwood

發佈了58 篇原創文章 · 獲贊 92 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章