欢迎光临

Git Bare 仓库深度实战:从零构建轻量级自托管 Git 服务与自动化镜像同步方案

为什么需要 Bare 仓库

在日常的 Git 工作流中,我们习惯于使用

1
git clone

1
git init

创建的普通工作仓库。这些仓库包含一个

1
.git

目录用于存储版本控制数据,以及项目的实际工作文件。然而,当你需要一个纯粹作为协作中心的远程仓库时——比如公司内部不想依赖 GitHub/GitLab 的场景——Bare 仓库就是最佳选择。

Bare 仓库(裸仓库)与普通仓库的核心区别在于:它没有工作目录,只包含 Git 对象数据库和引用。这意味着你无法直接在 Bare 仓库中编辑文件,但它可以完美地充当远程推送和拉取的中心节点。在服务器上,Bare 仓库就是 Git 协议的源站。

本文将从 Bare 仓库的基础概念出发,逐步带你构建一个具备权限控制、自动化钩子和镜像同步能力的轻量级自托管 Git 服务方案。

Git Server

Bare 仓库的本质与内部结构

创建 Bare 仓库

创建一个 Bare 仓库非常简单:


1
2
3
4
5
6
7
8
9
# 在服务器上创建 bare 仓库
mkdir -p /opt/git/repos
git init --bare /opt/git/repos/my-project.git

# 查看目录结构
ls -la /opt/git/repos/my-project.git/
# 输出:
# HEAD  branches/  config  description  hooks/  info/
# objects/  packed-refs  refs/

注意目录名通常以

1
.git

结尾,这是 Git 裸仓库的命名惯例。整个目录内容等价于普通仓库中

1
.git/

目录的内容。没有

1
.git

子目录,没有工作文件,只有版本控制的元数据和对象。

Bare 与 Non-Bare 的关键差异

特性 普通仓库 Bare 仓库
工作目录
1
git checkout
可以切换分支 不可用
1
git add / commit
正常使用 不可用
1
git push

接收

默认拒绝(需配置 receive.denyCurrentBranch) 正常接收
1
core.bare

配置

false true
典型用途 开发者本地开发 服务器端远程中心

一个重要的细节:

1
core.bare

的值决定了 Git 如何解释仓库类型。你可以手动修改这个值来在两种模式间切换,但强烈不建议这么做——因为工作目录的存在与否是客观事实,改配置不会凭空生成或删除工作文件。

搭建 SSH 协议的 Git 服务器

Git 原生支持四种传输协议:Local、SSH、HTTP 和 Git。其中 SSH 协议是最常用的自托管方案,它兼顾了安全性和配置简便性。

创建 Git 专用用户


1
2
3
4
5
6
7
8
9
# 创建 git 用户,禁用 shell 登录
sudo useradd -m -s /usr/bin/git-shell git

# 可选:将你的 SSH 公钥添加到 git 用户的 authorized_keys
sudo mkdir -p /home/git/.ssh
sudo cp ~/.ssh/id_rsa.pub /home/git/.ssh/authorized_keys
sudo chown -R git:git /home/git/.ssh
sudo chmod 700 /home/git/.ssh
sudo chmod 600 /home/git/.ssh/authorized_keys

这里使用

1
git-shell

作为登录 shell 是一个安全最佳实践。

1
git-shell

是 Git 自带的受限 shell,只允许执行

1
git push

1
git pull

等 Git 操作,用户无法获得完整的 bash 访问权限。

配置仓库权限与共享


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 将仓库所有权交给 git 用户
sudo chown -R git:git /opt/git/repos/my-project.git

# 启用共享仓库模式
cd /opt/git/repos/my-project.git
git config core.sharedRepository group

# 设置组写权限
sudo chmod -R g+ws .

# 创建 git 用户组并添加成员
sudo groupadd gitusers
sudo usermod -aG gitusers git
sudo usermod -aG gitusers alice
sudo usermod -aG gitusers bob
1
core.sharedRepository

设置为

1
group

(或

1
true

1
0x660

等)后,Git 会自动确保新建的对象文件和引用具有正确的组权限,避免多用户协作时的权限冲突问题。

克隆与推送测试


1
2
3
4
5
6
7
8
9
# 从客户端克隆
git clone git@your-server:/opt/git/repos/my-project.git

# 添加文件并推送
cd my-project
echo '# Hello' > README.md
git add .
git commit -m 'Initial commit'
git push origin main

Server Infrastructure

利用 Hooks 实现自动化工作流

Bare 仓库的

1
hooks/

目录是自托管 Git 服务中最强大的自动化入口。与普通仓库的客户端钩子不同,Bare 仓库的服务端钩子可以对所有推送者的行为进行统一管控。

post-receive:推送后的自动部署

1
post-receive

是最常用的服务端钩子,它在所有引用更新完成后触发。以下是实现自动部署到 Web 目录的脚本:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env bash
# /opt/git/repos/my-project.git/hooks/post-receive

DEPLOY_DIR="/var/www/my-project"
GIT_DIR="/opt/git/repos/my-project.git"

while read oldrev newrev refname; do
    branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
   
    if [ "$branch" = "main" ]; then
        echo "Deploying branch $branch to $DEPLOY_DIR..."
       
        # 使用 git archive 导出文件(无需 clone)
        mkdir -p "$DEPLOY_DIR"
        git --work-tree="$DEPLOY_DIR" --git-dir="$GIT_DIR" checkout -f main
       
        # 执行部署后脚本
        cd "$DEPLOY_DIR"
        if [ -f deploy.sh ]; then
            bash deploy.sh
        fi
       
        echo "Deployment complete."
    fi
done

关键技巧:

1
git --work-tree=... --git-dir=...

允许你从 Bare 仓库中检出文件到指定目录,而无需先 clone 整个仓库。这是 Bare 仓库特有的用法,因为普通仓库的工作目录就是当前目录,无需指定。

pre-receive:推送前的权限校验

1
pre-receive

在引用更新之前触发,如果脚本返回非零退出码,整个推送将被拒绝:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env bash
# /opt/git/repos/my-project.git/hooks/pre-receive

# 规则1:禁止强制推送到 main 分支
while read oldrev newrev refname; do
    branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
   
    # 检测是否为强制推送
    if [ "$branch" = "main" ]; then
        merge_base=$(git merge-base "$oldrev" "$newrev" 2>/dev/null)
        if [ "$merge_base" != "$oldrev" ]; then
            echo "ERROR: Force push to main branch is not allowed!"
            exit 1
        fi
    fi
done

# 规则2:检查提交者邮箱是否在允许列表中
ALLOWED_DOMAINS="@company.com @company.cn"
while read oldrev newrev refname; do
    for commit in $(git rev-list "$oldrev".."$newrev"); do
        email=$(git show -s --format='%ae' "$commit")
        matched=false
        for domain in $ALLOWED_DOMAINS; do
            if [[ "$email" == *"$domain"* ]]; then
                matched=true
                break
            fi
        done
        if [ "$matched" = false ]; then
            echo "ERROR: Commit $commit by $email is not from allowed domain!"
            exit 1
        fi
    done
done

exit 0

update:细粒度的分支级控制

1
update

钩子与

1
pre-receive

类似,但它是为每个被更新的引用单独调用一次,而非批量处理。签名格式为

1
update <ref> <oldrev> <newrev>

,更易于编写针对特定分支的规则:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bash
# /opt/git/repos/my-project.git/hooks/update

refname="$1"
oldrev="$2"
newrev="$3"
branch=$(git rev-parse --symbolic --abbrev-ref "$refname")

# 保护 release/* 分支:只允许合并提交
case "$branch" in
    release/*)
        for commit in $(git rev-list "$oldrev".."$newrev"); do
            parents=$(git show -s --format='%P' "$commit" | wc -w)
            if [ "$parents" -lt 2 ]; then
                echo "ERROR: Only merge commits allowed on $branch."
                echo "Commit $commit is a non-merge commit."
                exit 1
            fi
        done
        ;;
esac

exit 0

Bare 仓库与 Git Worktree 的协同实战

前面提到 Bare 仓库没有工作目录,但

1
git worktree

命令为 Bare 仓库打开了一扇新的大门——你可以在一个 Bare 仓库上附加多个工作树,每个工作树检出不同的分支。这在 CI/CD 和多环境部署场景下极为实用。

为 Bare 仓库添加工作树


1
2
3
4
5
6
7
8
9
10
# 从 bare 仓库创建工作树
cd /opt/git/repos/my-project.git
git worktree add /var/www/staging staging
git worktree add /var/www/production main

# 查看所有工作树
git worktree list
# /opt/git/repos/my-project.git        (bare)
# /var/www/staging                      abc1234 [staging]
# /var/www/production                   def5678 [main]

现在,每当开发者推送代码到

1
staging

分支,你可以通过

1
post-receive

钩子自动在 staging 工作树中拉取最新代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash
# post-receive hook with worktree auto-update

while read oldrev newrev refname; do
    branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
   
    case "$branch" in
        staging)
            echo "Updating staging worktree..."
            git --work-tree=/var/www/staging checkout staging
            cd /var/www/staging && npm install && npm run build
            echo "Staging updated and built."
            ;;
        main)
            echo "Updating production worktree..."
            git --work-tree=/var/www/production checkout main
            cd /var/www/production && npm install && npm run build
            echo "Production updated and built."
            ;;
    esac
done

这种方案的优势在于:多个工作树共享同一个 Git 对象数据库,无需为每个环境完整 clone 一份仓库,既节省磁盘空间,又保证了引用的一致性。

Code Deployment

构建自动化镜像同步方案

许多团队需要在自托管仓库和 GitHub/GitLab 之间保持双向同步——可能是出于灾备、开源发布或合规审计的需求。利用 Bare 仓库的

1
post-receive

钩子和

1
git mirror

功能,可以构建一套零延迟的镜像同步方案。

单向镜像:自托管到 GitHub


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env bash
# /opt/git/repos/my-project.git/hooks/post-receive

# 镜像推送到 GitHub
GITHUB_REMOTE="github-mirror"

while read oldrev newrev refname; do
    # 跳过删除操作
    if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
        continue
    fi
   
    branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
   
    # 只同步 main 和 release/* 分支
    case "$branch" in
        main|release/*)
            echo "Mirroring $branch to GitHub..."
            git push "$GITHUB_REMOTE" "$refname" 2>&1 || {
                echo "WARNING: Mirror push to GitHub failed for $branch"
            }
            ;;
    esac
done

首先需要在 Bare 仓库中添加 GitHub 远程:


1
2
cd /opt/git/repos/my-project.git
git remote add github-mirror git@github.com:company/my-project.git

完整双向镜像同步

双向同步需要额外处理:从上游拉取变更并注入到本地仓库。推荐使用一个独立的同步脚本配合 cron:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env bash
# /opt/git/scripts/sync-mirror.sh

REPO_DIR="/opt/git/repos/my-project.git"
UPSTREAM="github-mirror"
LOG_FILE="/var/log/git-mirror.log"

echo "[$(date)] Starting mirror sync..." >> "$LOG_FILE"

cd "$REPO_DIR" || exit 1

# 从 GitHub 拉取所有引用
git fetch "$UPSTREAM" '+refs/*:refs/*' 2>&1 >> "$LOG_FILE"

# 检查是否有新提交需要同步到本地分支
for branch in $(git branch -r | grep "$UPSTREAM" | sed "s|$UPSTREAM/||"); do
    local_sha=$(git rev-parse "refs/heads/$branch" 2>/dev/null)
    remote_sha=$(git rev-parse "refs/remotes/$UPSTREAM/$branch" 2>/dev/null)
   
    if [ "$local_sha" != "$remote_sha" ] && [ -n "$remote_sha" ]; then
        echo "[$(date)] Syncing $branch: $local_sha -> $remote_sha" >> "$LOG_FILE"
        git update-ref "refs/heads/$branch" "$remote_sha"
    fi
done

echo "[$(date)] Mirror sync complete." >> "$LOG_FILE"

配合 crontab 实现定时同步:


1
2
# 每5分钟同步一次
*/5 * * * * /opt/git/scripts/sync-mirror.sh

使用 git clone –mirror 实现一键完整镜像

如果你需要创建一个与上游完全一致的只读镜像仓库,

1
git clone --mirror

是最简单的方式:


1
2
3
4
5
6
7
8
9
# 创建镜像仓库
git clone --mirror git@github.com:company/large-project.git

# 定期更新
cd large-project.git
git remote update

# --mirror 等价于 --bare + 配置 remote.origin.fetch 为 +refs/*:refs/*
# 这意味着它不仅同步分支,还同步标签、notes 等所有引用

安全加固与最佳实践

SSH 密钥管理

对于多用户环境,不要把所有公钥都写入

1
authorized_keys

。推荐使用

1
ssh-command

方案或轻量级授权管理工具:


1
2
3
4
# /home/git/.ssh/authorized_keys 格式示例
# 通过 command= 限制每个密钥可执行的命令
command="/usr/local/bin/git-auth alice",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-rsa AAAA... alice@laptop
command="/usr/local/bin/git-auth bob",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-rsa BBBB... bob@desktop

其中

1
git-auth

脚本可以根据用户身份实施仓库级别的访问控制:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env bash
# /usr/local/bin/git-auth

USER="$1"
ORIGINAL_CMD="$SSH_ORIGINAL_COMMAND"

# 解析仓库名
REPO=$(echo "$ORIGINAL_CMD" | grep -oP "'/?.+.git" | tr -d "'")

# 访问控制列表
case "$USER" in
    alice)
        ALLOWED="my-project shared-lib"
        ;;
    bob)
        ALLOWED="my-project api-service infra-config"
        ;;
esac

# 验证权限
for repo in $ALLOWED; do
    if [[ "$REPO" == *"$repo"* ]]; then
        exec git-shell -c "$ORIGINAL_CMD"
    fi
done

echo "Access denied: $USER cannot access $REPO"
exit 1

日志与审计

1
post-receive

钩子中记录所有推送事件,为安全审计提供数据:


1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash
# 在 post-receive 开头添加审计日志
AUDIT_LOG="/var/log/git-audit.log"
PUSHER="$GL_USER"  # 如果使用 Gitolite; 否则用 $USER

while read oldrev newrev refname; do
    branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
   
    echo "$timestamp | $PUSHER | $branch | $oldrev -> $newrev" >> "$AUDIT_LOG"
done

性能优化建议

  • 定期执行
    1
    git gc

    :对 Bare 仓库执行

    1
    git gc --aggressive

    可以压缩对象存储,减少磁盘占用和克隆时间。建议在低峰期通过 cron 每周执行一次。

  • 启用
    1
    core.preloadIndex

    :在大仓库中,设置为

    1
    true

    可以并行加载索引,加速

    1
    git status

    1
    git diff

    操作。

  • 配置
    1
    pack.windowMemory

    :限制打包时的内存使用,防止

    1
    git gc

    1
    git push

    在大仓库上消耗过多内存:

    1
    git config pack.windowMemory 256m
  • 使用
    1
    git repack -a -d

    :将所有松散对象和旧 pack 文件合并为一个新 pack,消除冗余。

完整方案:从零搭建的自动化流程

将上述所有组件组合起来,我们得到一套完整的自托管 Git 服务方案:

Workflow Diagram


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 创建目录结构
sudo mkdir -p /opt/git/repos
sudo mkdir -p /opt/git/scripts
sudo mkdir -p /var/log/git

# 2. 创建 bare 仓库
sudo git init --bare /opt/git/repos/my-project.git

# 3. 添加 GitHub 镜像远程
cd /opt/git/repos/my-project.git
git remote add github-mirror git@github.com:company/my-project.git

# 4. 创建 worktree 用于部署
git worktree add /var/www/staging staging
git worktree add /var/www/production main

# 5. 设置权限
sudo chown -R git:git /opt/git
sudo chown -R git:git /var/www/staging /var/www/production

总结与对比

通过 Bare 仓库 + Hooks + Worktree 的组合,我们构建了一套轻量但功能完备的自托管 Git 服务。下面与主流方案做个对比:

方案 资源占用 学习成本 权限管理 CI/CD 集成 适用场景
本文方案(Bare+Hooks) 极低 中等 自定义脚本 原生支持 小团队、内网环境
Gitolite 较高 配置文件驱动 需额外配置 中团队、精细权限
Gitea 中等 Web 界面 内置 Actions 中团队、Web 管理
GitLab CE 高(4GB+ RAM) Web 界面 内置 CI/CD 大团队、全功能

本文方案的核心价值在于:极致轻量——一台 512MB 内存的最小 VPS 就能稳定运行,无需数据库,无需 Web 服务器,只需 SSH 和 Git。同时,通过 Hooks 的可编程性,你可以实现与 GitLab CI 等重量级方案相同的自动化效果,只不过以脚本的方式呈现,更加透明和可控。

当团队规模增长到需要 Web 界面管理、代码审查和 Issue 追踪时,再平滑迁移到 Gitea 或 GitLab 即可——你的 Bare 仓库可以直接作为它们的存储后端,无需重新导入历史。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Git Bare 仓库深度实战:从零构建轻量级自托管 Git 服务与自动化镜像同步方案
分享到: 更多 (0)