天天看點

Git 系列(六):如何搭建你自己的 Git 伺服器

現在我們将要學習如何搭建 git 伺服器,如何編寫自定義的 git 鈎子來在特定的事件觸發相應的動作(例如通知),或者是釋出你的代碼到一個站點。

直到現在,我們主要讨論的還是以一個使用者的身份與 git 進行互動。這篇文章中我将讨論 git 的管理,并且設計一個靈活的 git 架構。你可能會覺得這聽起來是 “高階 git 技術” 或者 “隻有狂熱粉才能閱讀”的一句委婉的說法,但是事實是這裡面的每個任務都不需要很深的知識或者其他特殊的訓練,就能基本了解 git 的工作原理,有可能需要一丁點關于 linux 的知識。

<a target="_blank"></a>

建立你自己的共享 git 伺服器意外地簡單,而且在很多情況下,遇到的這點麻煩是完全值得的。不僅僅是因為它保證你有權限檢視自己的代碼,它還可以通過擴充為 git 的使用敞開了一扇大門,例如個人 git 鈎子、無限制的資料存儲、和持續內建與分發(ci &amp; cd)。

如果你知道如何使用 git 和 ssh,那麼你已經知道怎麼建立一個 git 伺服器了。git 的設計方式,就是讓你在建立或者 clone 一個倉庫的時候,就完成了一半伺服器的搭建。然後允許用 ssh 通路倉庫,而且任何有權限通路的人都可以使用你的倉庫作為 clone 的新倉庫的基礎。

但是,這是一個小的點對點環境ad-hoc。按照一些方案你可以建立一些帶有同樣的功能的設計優良的 git 伺服器,同時有更好的拓展性。

首要之事:确認你的使用者們,現在的使用者以及之後的使用者都要考慮。如果你是唯一的使用者那麼沒有任何改動的必要。但是如果你試圖邀請其他的代碼貢獻者使用,那麼你應該允許一個專門的分享系統使用者給你的開發者們。

一旦你啟用了 ssh 密鑰認證,建立 <code>gituser</code> 使用者。這是給你的所有授權的使用者們的公共使用者:

<code>$ su -c 'adduser gituser'</code>

然後切換到剛建立的 <code>gituser</code> 使用者,建立一個 <code>~/.ssh</code> 的架構,并設定好合适的權限。這很重要,如果權限設定得太開放會使自己所保護的 ssh 沒有意義。

<code>$ su - gituser</code>

<code>$ mkdir .ssh &amp;&amp; chmod 700 .ssh</code>

<code>$ touch .ssh/authorized_keys</code>

<code>$ chmod 600 .ssh/authorized_keys</code>

<code>authorized_keys</code> 檔案裡包含所有你的開發者們的 ssh 公鑰,你開放權限允許他們可以在你的 git 項目上工作。他們必須建立他們自己的 ssh 密鑰對然後把他們的公鑰給你。複制公鑰到 gituser 使用者下的<code>authorized_keys</code> 檔案中。例如,為一個叫 bob 的開發者,執行以下指令:

<code>$ cat ~/path/to/id_rsa.bob.pub &gt;&gt; /home/gituser/.ssh/authorized_keys</code>

隻要開發者 bob 有私鑰并且把相對應的公鑰給你,bob 就可以用 <code>gituser</code> 使用者通路伺服器。

但是,你并不是想讓你的開發者們能使用伺服器,即使隻是以 <code>gituser</code> 的身份通路。你隻是想給他們通路 git 倉庫的權限。因為這個特殊的原因,git 提供了一個限制的 shell,準确的說是 <code>git-shell</code>。以 root 身份執行以下指令,把 <code>git-shell</code> 添加到你的系統中,然後設定成 <code>gituser</code> 使用者的預設 shell。

<code># grep git-shell /etc/shells || su -c "echo `which git-shell` &gt;&gt; /etc/shells"</code>

<code># su -c 'usermod -s git-shell gituser'</code>

現在 <code>gituser</code> 使用者隻能使用 ssh 來 push 或者 pull git 倉庫,并且無法使用任何一個可以登入的 shell。你應該把你自己添加到和 <code>gituser</code> 一樣的組中,在我們的樣例伺服器中這個組的名字也是 <code>gituser</code>。

舉個例子:

<code># usermod -a -g gituser seth</code>

僅剩下的一步就是建立一個 git 倉庫。因為沒有人能在伺服器上直接與 git 互動(也就是說,你之後不能 ssh 到伺服器然後直接操作這個倉庫),是以建立一個空的倉庫 。如果你想使用這個放在伺服器上的倉庫來完成工作,你可以從它的所在處 <code>clone</code> 下來,然後在你的 home 目錄下進行工作。

嚴格地講,你不是必須建立這個空的倉庫;它和一個正常的倉庫一樣工作。但是,一個空的倉庫沒有工作分支(working tree) (也就是說,使用 <code>checkout</code> 并沒有任何分支顯示)。這很重要,因為不允許遠端使用者們 <code>push</code> 到一個有效的分支上(如果你正在 <code>dev</code> 分支工作然後突然有人把一些變更 <code>push</code> 到你的工作分支,你會有怎麼樣的感受?)。因為一個空的倉庫可以沒有有效的分支,是以這不會成為一個問題。

你可以把這個倉庫放到任何你想放的地方,隻要你想要放開權限給使用者和使用者組,讓他們可以在倉庫下工作。千萬不要儲存目錄到比如說一個使用者的 home 目錄下,因為那裡有嚴格的權限限制。儲存到一個正常的共享位址,例如 <code>/opt</code> 或者 <code>/usr/local/share</code>。

以 root 身份建立一個空的倉庫:

<code># git init --bare /opt/jupiter.git</code>

<code># chown -r gituser:gituser /opt/jupiter.git</code>

<code># chmod -r 770 /opt/jupiter.git</code>

現在任何一個使用者,隻要他被認證為 <code>gituser</code> 或者在 <code>gituser</code> 組中,就可以從 jupiter.git 庫中讀取或者寫入。在本地機器嘗試以下操作:

<code>$ git clone [email protected]:/opt/jupiter.git jupiter.clone</code>

<code>cloning into 'jupiter.clone'...</code>

<code>warning: you appear to have cloned an empty repository.</code>

謹記:開發者們一定要把他們的 ssh 公鑰加入到 <code>gituser</code> 使用者下的 <code>authorized_keys</code> 檔案裡,或者說,如果他們有伺服器上的使用者(如果你給了他們使用者),那麼他們的使用者必須屬于 <code>gituser</code> 使用者組。

運作你自己的 git 伺服器最贊的一件事之一就是可以使用 git 鈎子。git 托管服務有時提供一個鈎子類的接口,但是他們并不會給你真正的 git 鈎子來讓你通路檔案系統。git 鈎子是一個腳本,它将在一個 git 過程的某些點運作;鈎子可以運作在當一個倉庫即将接收一個 <code>commit</code> 時、或者接受一個 <code>commit</code> 之後,或者即将接收一次 <code>push</code> 時,或者一次 <code>push</code> 之後等等。

這是一個簡單的系統:任何放在 <code>.git/hooks</code> 目錄下的腳本、使用标準的命名體系,就可按設計好的時間運作。一個腳本是否應該被運作取決于它的名字; <code>pre-push</code> 腳本在 <code>push</code> 之前運作,<code>post-receive</code> 腳本在接受 <code>commit</code> 之後運作等等。這或多或少的可以從名字上看出來。

腳本可以用任何語言寫;如果在你的系統上有可以執行的腳本語言,例如輸出 ‘hello world’ ,那麼你就可以這個語言來寫 git 鈎子腳本。git 預設帶了一些例子,但是并不有啟用。

想要動手試一個?這很簡單。如果你沒有現成的 git 倉庫,首先建立一個 git 倉庫:

<code>$ mkdir jupiter</code>

<code>$ cd jupiter</code>

<code>$ git init .</code>

然後寫一個 “hello world” 的 git 鈎子。因為我為了支援老舊系統而使用 tsch,是以我仍然用它作為我的腳本語言,你可以自由的使用自己喜歡的語言(bash,python,ruby,perl,rust,swift,go):

<code>$ echo "#\!/bin/tcsh" &gt; .git/hooks/post-commit</code>

<code>$ echo "echo 'post-commit script triggered'" &gt;&gt; ~/jupiter/.git/hooks/post-commit</code>

<code>$ chmod +x ~/jupiter/.git/hooks/post-commit</code>

現在測試它的輸出:

<code>$ echo "hello world" &gt; foo.txt</code>

<code>$ git add foo.txt</code>

<code>$ git commit -m 'first commit'</code>

<code>! post-commit script triggered</code>

<code>[master (root-commit) c8678e0] first commit</code>

<code>1 file changed, 1 insertion(+)</code>

<code>create mode 100644 foo.txt</code>

現在你已經實作了:你的第一個有功能的 git 鈎子。

git 鈎子最流行的用法就是自動 <code>push</code> 更改的代碼到一個正在使用中的産品級 web 伺服器目錄下。這是擺脫 ftp 的很好的方式,對于正在使用的産品保留完整的版本控制,整合并自動化内容的釋出。

每一個 git 鈎子都有一系列不同的變量對應觸發鈎子的不同 git 行為。你需不需要這些變量,主要取決于你寫的程式。如果你隻是需要一個當某人 push 代碼時候的通用郵件通知,那麼你就不需要什麼特殊的東西,甚至也不需要編寫額外的腳本,因為已經有現成的适合你的樣例腳本。如果你想在郵件裡檢視 <code>commit</code> 資訊和<code>commit</code> 的作者,那麼你的腳本就會變得相對麻煩些。

git 鈎子并不是被使用者直接執行,是以要弄清楚如何收集可能會混淆的重要資訊。事實上,git 鈎子腳本類似于其他的腳本,像 bash、python、c++ 等等一樣從标準輸入讀取參數。不同的是,我們不會給它提供這個輸入,是以,你在使用的時候,需要知道可能的輸入參數。

在寫 git 鈎子之前,看一下 git 在你的項目目錄下 <code>.git/hooks</code> 目錄中提供的一些例子。舉個例子,在這個<code>pre-push.sample</code> 檔案裡,注釋部分說明了如下内容:

<code># $1 -- 即将 push 的遠端倉庫的名字</code>

<code># $2 -- 即将 push 的遠端倉庫的 url</code>

<code># 如果 push 的時候,并沒有一個命名的遠端倉庫,那麼這兩個參數将會一樣。</code>

<code>#</code>

<code># 送出的資訊将以下列形式按行發送給标準輸入</code>

<code># &lt;local ref&gt; &lt;local sha1&gt; &lt;remote ref&gt; &lt;remote sha1&gt;</code>

我發現,對于生産環境來說有一個共同的需求,就是需要一個隻有在特定分支被修改之後,才會觸發事件的鈎子。以下就是如何跟蹤分支的示例。

首先,git 鈎子本身是不受版本控制的。 git 并不會跟蹤它自己的鈎子,因為對于鈎子來說,它是 git 的一部分,而不是你倉庫的一部分。是以,git 鈎子可以監控你的 git 伺服器上的一個空倉庫的 <code>commit</code> 記錄和<code>push</code> 記錄,而不是你本地倉庫的一部分。

我們來寫一個 <code>post-receive</code>(也就是說,在 <code>commit</code> 被接受之後觸發)鈎子。第一步就是需要确定分支名:

<code>#!/bin/tcsh</code>

<code></code>

<code>foreach arg ( $&lt; )</code>

<code>set argv = ( $arg )</code>

<code>set refname = $1</code>

<code>end</code>

這個 for 循環用來讀入第一個參數 <code>$1</code> ,然後循環用第二個參數 <code>$2</code> 去覆寫它,然後用第三個參數 <code>$3</code> 再這樣。在 bash 中有一個更好的方法,使用 <code>read</code> 指令,并且把值放入數組裡。但是,這裡是 tcsh,并且變量的順序可以預測的,是以,這個方法也是可行的。

當我們有了 commit 記錄的 <code>refname</code>,我們就能使用 git 去找到這個分支的供人看的名字:

<code>set branch = `git rev-parse --symbolic --abbrev-ref $refname`</code>

<code>echo $branch #debug</code>

然後把這個分支名和我們想要觸發的事件的分支名關鍵字進行比較:

<code>if ( "$branch" == "master" ) then</code>

<code>echo "branch detected: master"</code>

<code>git \</code>

<code>--work-tree=/path/to/where/you/want/to/copy/stuff/to \</code>

<code>checkout -f $branch || echo "master fail"</code>

<code>else if ( "$branch" == "dev" ) then</code>

<code>echo "branch detected: dev"</code>

<code>checkout -f $branch || echo "dev fail"</code>

<code>else</code>

<code>echo "your push was successful."</code>

<code>echo "private branch detected. no action triggered."</code>

<code>endif</code>

給這個腳本配置設定可執行權限:

<code>$ chmod +x ~/jupiter/.git/hooks/post-receive</code>

現在,當一個使用者送出到伺服器的 master 分支,那些代碼就會被複制到一個生産環境的目錄,送出到 dev 分支則會被複制到另外的地方,其他分支将不會觸發這些操作。

同時,創造一個 <code>pre-commit</code> 腳本也很簡單。比如,判斷一個使用者是否在他們不該 <code>push</code> 的分支上 <code>push</code>代碼,或者對 commit 資訊進行解析等等。

git 鈎子也可以變得複雜,而且它們因為 git 的工作流的抽象層次不同而變得難以了解,但是它們确實是一個強大的系統,讓你能夠在你的 git 基礎設施上針對所有的行為進行對應的操作。如果你是一個 git 重度使用者,或者一個全職 git 管理者,那麼 git 鈎子是值得學習的,隻有當你熟悉這個過程,你才能真正掌握它。

原文釋出時間為:2016-09-23

本文來自雲栖社群合作夥伴“linux中國”