搭建git服务器

对于几个人的小团队,可以自行在公司内网搭建git服务器,实现版本控制。配合gitolite实现权限控制。

我搭建的服务器框架大致如下图:

那么以下的搭建操作就是基于这个图进行配置。每个节点都是ubuntu 20.04发行版,图示有4个节点

节点 功能
Server 中心化的git仓库,本文假设IP为192.168.100.100
Alice 网管,负责创建仓库或者各种访问权限
Bob 项目组长,负责Code-Review和Merge分支到master,拥有修改仓库权限
Carl 项目开发者,只能在自己分支上面修改

搭建服务器

搭建服务器操作在Server和Alice节点进行

Alice端

生成一个rsa密钥对

cd /tmp
ssh-keygen -t rsa -b 4096 -C "alice"

假设生成的公私钥为

# 私钥
~/.ssh/alice
# 公钥
~/.ssh/alice.pub

将私钥写入当前用户ssh配置文件中

vi ~/.ssh/config
# 添加
Host server
  User git
  Hostname 192.168.100.100
  Port 22
  ServerAliveInterval 30
  IdentityFile ~/.ssh/alice

将公钥上传到Server备用

scp ~/.ssh/alice.pub root@server:/tmp/

Server端

留意gitolite的README,提到依赖的软件,有最低版本的要求

  • git 1.6.6 or later
  • perl 5.8.8 or later
  • openssh 5.0 or later

创建git账户并切换到新帐户

adduser git
su git
cd ~

创建空的ssh配置目录

mkdir -p ~/.ssh

克隆gitolite仓库

git clone https://github.com/sitaramc/gitolite
cd gitolite

创建一个目录存放gitolite二进制文件,然后安装

mkdir -p ~/bin
./install -to ~/bin

设置Alice的公钥,这样Alice就成为了gitolite管理员

~/bin/gitolite setup -pk /tmp/alice.pub

执行上面一条命令后,/tmp/alice.pub 被拷贝到~/.gitolite/keydir目录下,并且仓库gitolite-admin克隆到本地后,keydir目录也有一份alice.pub。

所有仓库存放在~/repositories下,gitolite会自动修改~/.ssh/authorized_keys实现不同用户的访问。

因此单独使用一个git用户的目的是,不希望用户手动修改authorized_keys里面的内容,而是通过gitolite来间接修改它。

仓库创建与权限

修改访问权限在Alice节点进行

克隆admin仓库,因为服务器只有Alice的公钥,其它用户无权访问。

cd ~
git clone git@server:gitolite-admin
cd gitolite-admin

直接编辑这个conf文件实现权限管理

conf/gitolite.conf

详细的权限和仓库创建可以参考官方README:

http://gitolite.com/gitolite/conf
https://github.com/sitaramc/gitolite#adding-users-and-repos

比如我修改为

repo foo
    RW+                     =   bob
    -   master              =   carl
    -   refs/tags/v[0-9]    =   carl
    RW+ carl                =   carl
    R                       =   carl

那么达到的效果是:

  1. 创建了一个名字为foo的仓库
  2. RW+表示可读可写可overwrite,Bob拥有仓库最大权限
  3. 减号说明Carl没有master分支和tags的读写权限
  4. Carl只能在自己分支(carl分支)上面进行修改,拥有carl分支的最大权限
  5. Carl可以读取其它分支,这时候就可以读master分支了

注意等号后面的名字是跟ssh公钥文件名字对应的,如果gitolite-admin/keydir目录下的公钥文件名字是

carl_ssh_key.pub

那么等号后面的内容就不是carl,而是carl_ssh_key

为了与conf/gitolite.conf中的帐户对应,创建ssh公私鈅要保存为正确的文件名。

# 输出id_rsa的时候,保存为~/my_gitolite_keys/bob
ssh-keygen -t rsa -b 4096 -C "bob"

# 输出id_rsa的时候,保存为~/my_gitolite_keys/carl
ssh-keygen -t rsa -b 4096 -C "carl"

把~/my_gitolite_keys/中对应的公私鈅交给Bob和Carl,下面测试步骤,要用到公私鈅

将公钥文件添加到gitolite-admin仓库中

cp ~/my_gitolite_keys/*.pub ~/gitolite-admin/keydir/

修改conf和生成密钥完毕,就可以commit,然后将新配置push给server端

cd ~/gitolite-admin
git add *
git commit -m "add user Bob, Carl; generate keys"
git push

那么server端在push结束后自动执行perl脚本,实现权限管理。

测试

测试git在Bob和Carl节点进行

将由Alice交给Bob和Carl的公私鈅,分别存放到各自节点的.ssh目录下

节点 公私鈅存放路径
Bob 公钥 ~/.ssh/bob.pub
私钥 ~/.ssh/bob
Carl 公钥 ~/.ssh/carl.pub
私钥 ~/.ssh/carl

Bob节点

将私钥写入当前用户ssh配置文件中

vi ~/.ssh/config
# 添加
Host server
  User git
  Hostname 192.168.100.100
  Port 22
  ServerAliveInterval 30
  IdentityFile ~/.ssh/bob

本地克隆服务器上的foo仓库,测试修改代码

cd ~
git clone git@server:foo.git
cd foo
echo "hello world" > README.md
git add README.md
git commit -m "print hello world"
git push

这样即验证了Bob有读写服务器master分支的权限

Carl节点

将私钥写入当前用户ssh配置文件中

vi ~/.ssh/config
# 添加
Host server
  User git
  Hostname 192.168.100.100
  Port 22
  ServerAliveInterval 30
  IdentityFile ~/.ssh/carl

本地克隆服务器上的foo仓库,测试修改代码

cd ~
git clone git@server:foo.git
cd foo
echo "try to modify branch master" > README.md
git add README.md
git commit -m "invalid commit"
git push

这么做push的话会被拒绝,即验证了Carl没有写服务器master分支的权限。

下面测试在carl分支的工作流程

情况1: 如果上游没有carl分支,可以添加一个carl分支并Push到服务器上

git checkout -b carl
echo "create branch carl" > README.md
git add README.md
git commit -m "branch: carl created"
git push --set-upstream origin carl

情况2: 如果上游已经存在carl分支,直接切换到carl分支

git checkout carl

经常性使用git pull以拉取服务器上最新的代码版本。

其它设置

安全项

修改默认的shell为gitolite专用的,而不是默认的bash,这样让用户无法通过sshkey登陆到终端,且在root用户调用su git也无法切换(提示GL_USER not set)

sudo vi /etc/passwd
# 修改git用户的shell程序为/home/git/bin/gitolite-shell

不允许git用户使用密码登陆

sudo vi /etc/ssh/sshd_config
# 增加以下
Match User git
    PasswordAuthentication no

# 重启ssh服务
sudo systemctl restart sshd.service

语言项

避免每次git push都产生如下警告

perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
        LANGUAGE = "en_US:en",
        LC_ALL = (unset),
        LC_CTYPE = "zh_CN.UTF-8",
        LANG = "en_US.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to a fallback locale ("en_US.UTF-8")

首先重新生成locale

sudo dpkg-reconfigure locales

然后在sshd_config中禁止客户端传递环境变量给服务器,因为服务器一般才使用一种语言,而不是采用客户端的语言环境。

vi /etc/ssh/sshd_config
# 注释掉这一行
# AcceptEnv LANG LC_*

保存并退出,重启ssh服务

sudo systemctl restart sshd.service

git-hooks

设置pre-receive, post-receive等钩子,实现语法检测或者邮件推送。参考这篇文章进行设置。

在RC文件中取消注释数行:

vi ~/.gitolite.rc

# 取消注释这一行,表示可以从admin仓库中增加local文件,注意不要取消错了,有两行很相似
LOCAL_CODE => "$rc{GL_ADMIN_BASE}/local",

# 取消注释这一行,表示启用单独仓库配置hook
'repo-specific-hooks'

保存,然后在克隆好的gitolite-admin仓库根目录下建立文件夹

cd /path/to/your/gitolite-admin-cloned
mkdir -p local/hooks/repo-specific
cd local/hooks/repo-specific

在此目录创建对应的hook可执行文件即可,比如我使用这个cppcheck检测Qt工程。文件名不要使用git标准的pre-receive, post-receive, post-update等

#!/bin/bash
# works only in gitolite 3.6+
# hook as pre-receive

REQUIRE_BINS=("cppcheck" "git")
for b in "${REQUIRE_BINS[@]}"; do
    type $b >/dev/null 2>&1 || { echo >&2 "$b is required, but was not found. exited"; exit 1; }
done

# https://github.com/danmar/cppcheck/blob/main/cfg/qt.cfg
QT_CFG=qt.cfg
if [ -z $GL_ADMIN_BASE ];then
    QT_CFG=$PWD/$QT_CFG
else
    QT_CFG=$GL_ADMIN_BASE/local/cppcheck-libs/$QT_CFG
fi

[[ -f $QT_CFG || -L $QT_CFG  ]] || { echo >&2 "Cannot find $QT_CFG. exited"; exit 1; }

NULL_SHA1="0000000000000000000000000000000000000000" # 40 0's
TMP_DIR=$( mktemp -d /tmp/cppcheck-XXXXX )
EXIT_CODE=0

while read oldrev newrev ref; do
    if [ "$oldrev" = "$NULL_SHA1" ]; then
        oldrev="$newrev^"
    fi

    echo -n "new commit "
    git log --pretty=oneline --abbrev-commit -n 1 $newrev
    echo -n "old commit "
    git log --pretty=oneline --abbrev-commit -n 1 $oldrev

    for filename in $( git diff --name-only $oldrev $newrev | grep -E '*\.(c|cpp|cc|cxx|h|hpp)$' ); do
        short_file_name=$( basename $filename )
        git show $newrev:$filename > $TMP_DIR/$short_file_name
        echo "checking $filename ..."
        cppcheck --error-exitcode=1 --quiet --library=$QT_CFG $TMP_DIR/$short_file_name
        [ "x$?" = "x0" ] || EXIT_CODE=1
    done
done

[ -d "$TMP_DIR" ] && rm -rf $TMP_DIR
exit $EXIT_CODE

保存以上内容为my-hook,并设置为可执行权限

echo 'echo good; exit 0' > my-hook
chmod +x my-hook

下载qt.cfg作为cppcheck的语法文件

mkdir -p /path/to/your/gitolite-admin-cloned/local/cppcheck-libs
wget https://github.com/danmar/cppcheck/blob/main/cfg/qt.cfg
mv qt.cfg /path/to/your/gitolite-admin-cloned/local/cppcheck-libs/

修改conf文件,指定hook

vi /path/to/your/gitolite-admin-cloned/conf/gitolite.conf

# 设置hook
repo foo
      option hook.pre-receive = my-hook

提交并push到远端gitolite-admin,下次推送foo仓库即可生效。

参考链接

ssh key配置
gitolite-README
gitolite搭建git仓库管理服务