Git Server for Bare

自建 Git Server 时,单人场景最佳方案是使用裸仓。

结论:如果没有网络环境或数据隐私要求,使用免费的私有或公共仓库吧。比如:github.com,另外,也是有免费的组织。

Git 协议本身只负责数据同步,没有远程创建仓库的 API。所以,客户端无法直接远程新建仓库,只有安装 gitea 等服务器后,才可以在 WEB 上创建仓库。

自托管的运行环境要求

这里的内容,也是客户端所需要的。

$ git --version
# 使用PPA源安装吧,Ubuntu 是23年的2.43版本,而ppa里是2.53。
# 如果提示无 add-apt-repository 命令,需要先 `sudo apt install software-properties-common` 。另外,PPA 的源,在国内下载速度,都很慢。
$ sudo add-apt-repository ppa:git-core/ppa
$ sudo apt update
$ sudo apt install git

# 可选:Git LFS 用于二进制大文件支持
$ git lfs version
# 0. apt install 前,先添加 PackageCloud 仓库安装 https://github.com/git-lfs/git-lfs/blob/main/INSTALLING.md
$ curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash
# 1. 安装, https://packagecloud.io/github/git-lfs/ubuntu 下载速度慢。
$ sudo apt install git-lfs
# 2. 全局注册过滤器(只需执行一次)
$ git lfs install

单人场景:祼仓

使用 git init --bare 命令,再通过 SSH 协议直接访问服务器上的裸仓库文件,无任何常驻服务进程。

优点缺点
零额外内存占用:没有后台服务,只有 SSH 连接时才短暂调用 git 进程
– 零安装成本,系统自带 git 即可
– 绝对稳定,不存在服务崩溃问题
– 无 Web 界面,无法浏览器浏览代码、看提交记录
无细粒度权限:只能靠 Linux 系统用户 / 文件权限控制,所有人要么全读写要么全只读
– 仓库创建、密钥管理全部手动操作

可选:创建专用账号

# 创建 git 用户,且切换 git
$ sudo useradd -m -s /bin/bash git
$ sudo passwd git
$ su - git

# git 用户下执行
$ mkdir .ssh/
$ touch .ssh/authorized_keys
$ chmod u=rw,g=,o= .ssh/authorized_keys
$ chmod u=rwx,g=,o= .ssh/
$ vim .ssh/authorized_keys      # 把公钥放进去
$ exit

快速部署过程

# ssh客户端连接
ssh.exe git@t725.cn -i abc.pem

# 创建祼仓
$ git config --global init.defaultBranch master
$ mkdir -p ~/repo/demo.git/
$ cd ~/repo/demo.git
$ git init --bare    # 创建祼仓
$ exit

客户端连接与同步

  1. 配置 ~/.ssh/config 文件
  2. git clone git@t725.cn:repo/demo.git d:\demo 注意路径,要与服务器上一样
  3. 没有在服务器上,提前创建目录与执行git init --bare,git 客户端是不可用的。

安全配置

无论哪一种安全方式,都要先让  git-shell 注册为合法 shell(否则 SSH 会拒绝登录),因为默认是不注册的。

$ cat /etc/shells                    # 没有注册前的内容
# /etc/shells: valid login shells
/bin/sh
/usr/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/usr/bin/dash
/usr/bin/screen
/usr/bin/tmux

注册为shell

$ grep -qxF '/usr/bin/git-shell' /etc/shells || echo '/usr/bin/git-shell' | sudo tee -a /etc/shells

sudo -u git cat ~/.ssh/authorized_keys 的写法是错误的,因为 shell 展开 ~ 发生在 sudo 之前,所以 ~ 仍然被展开为当前用户 $home 的路径,而非 git 用户的家目录。正确写法:

# 任选其一,分别是:使用绝对路径、让目标用户的 shell 来展开、直接以 root 读取(绕过权限检查)
$ sudo -u git cat /var/lib/git/.ssh/authorized_keys
$ sudo -u git bash -c 'cat ~/.ssh/authorized_keys'
$ sudo cat /var/lib/git/.ssh/authorized_keys

方式一:git 用户的 shell 为 /usr/bin/git-shell

$ sudo adduser --system --group --home /var/lib/git --shell /usr/bin/git-shell git

注意:

  1. home 是 /var/lib/git,目录默认权限是 drwxr-xr-x 2 root root 4.0K Apr 23 2024 ./
  2. 后续,这个账号在终端里登录后,也只有git-shell
  3. Linux 目录权限规则:要进入一个子目录,必须对路径上每一级父目录都拥有 x(execute/search)权限,所以也要注意上级目录的权限。可以没有r权限,但要有x权限。

方式二:authorized_keys 里限制命令,也是 gitea 等 git 服务器的标准做法

  1. 正常添加用户,比如:
# -p '!' 是代表不能使用密码登录
$ useradd -r -s /bin/bash -c 'Git Version Control' -m -d /home/git -U -p '!' git
  1. .ssh/authorized_keys 添加 git 客户端证书时,要先处理,示例:
    • 原PUB证书:ssh-ed25519 xxxCret-Pubxxx Git@T725.CN
command="git-shell -c \"$SSH_ORIGINAL_COMMAND\"",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 xxxCret-Pubxxx Git@T725.CN

echo的写法

$ echo 'command="git-shell -c \"$SSH_ORIGINAL_COMMAND\"",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA... user@host' | sudo tee /var/lib/git/.ssh/authorized_keys
选项作用
command="..."强制无论客户端请求什么命令,都只执行引号内的命令;$SSH_ORIGINAL_COMMAND 保存了客户端实际请求的原始命令(如 git-upload-pack 'repo.git'
no-port-forwarding禁止 SSH 端口转发
no-X11-forwarding禁止 X11 图形转发
no-agent-forwarding禁止 SSH Agent 转发
no-pty禁止分配伪终端(阻止交互式 shell)

便捷部置脚本

这个脚本不一定要登录到服务器上执行,也可以通过 ssh user@server "~/init wms_axure_html_to_md" 直接执行

$ vim ~/init
$ chmod +x ~/init

init内容

#!/bin/bash

set -e
# 1. 检查并设置 init.defaultBranch
DEFAULT_BRANCH=$(git config init.defaultBranch)
if [[ -z "$DEFAULT_BRANCH" ]]; then
    echo "[INFO] 配置默认分支名 master"
    git config --global init.defaultBranch master
fi

# 2. 检查入参
REPO_NAME="$1"
if [[ -z "$REPO_NAME" ]]; then
    echo "[ERROR] 请提供仓库名称!用法:$0 <仓库名>"
    exit 1
fi
REPO_DIR="$HOME/repo/$REPO_NAME"
if [[ -d "$REPO_DIR" ]]; then
    echo "[ERROR] 目录已存在:$REPO_DIR"
    exit 1
fi

# 3. 初始化 bare 仓库
mkdir -p "$REPO_DIR"
cd "$REPO_DIR"
git init --bare

# 4. 切回原始目录并列出仓库内容
cd -
echo "[INFO] 仓库创建完成:"
ls -CF "$REPO_DIR"
echo ""
echo "首次同步,二选一:"
echo "git clone git@t725.cn:repo/$REPO_NAME D:\$REPO_NAME"
echo "git remote add origin git@t725.cn:repo/$REPO_NAME && git push -u origin master"

客户端执行

PS D:\Downloads> gitShell "~/init axure_html_to_md"
Initialized empty Git repository in /data/www/gitBare/axure_html_to_md/
/home/git
[INFO] 仓库创建完成,目录内容如下:
HEAD  config  description  hooks/  info/  objects/  refs/

首次同步: git remote add origin git@t725.cn:repo/axure_html_to_md && git push -u origin master
PS D:\Downloads>

首次同步

和Github等不一样(init时有LICENSE与README.md文件),祼仓 git init --bare 在创建后是空的,没有工作区,也没有分支名,所以是需要从客户端做一次push后,才有分支名的。

  1. 先在服务器上,执行祼仓 git init --bare 创建
  2. 再到客户端上,执行push与分支跟踪
# 已初化完成
PS D:> git remote add origin git@t725.cn:repo/BookmarkSync
PS D:> git push -u origin master   # -u 首次必须要有
PS D:> git branch -r               # 在没有上传前,是无值的。
  1. 后面,就可以正常使用了。再回到服务器上执行 git branch 就有值了

在 git-shell 环境下实现远程初始化裸仓库

原理:

  1. 登录 Shell ≠ 脚本解释器,用户的 git-shell 限制是会话级别的,不是进程级别的。
  2.  git-shell 以硬编码方式,允许了 ~/git-shell-commands/ 目录下的自定义命令。而这些自定义命令,是以用户身份,创建子进程执行。

管理员在服务器上配置

$ sudo adduser --system --group --home /var/lib/git --shell /usr/bin/git-shell git
$ sudo mkdir -p /var/lib/git/git-shell-commands
$ sudo vim /var/lib/git/git-shell-commands/init
$ sudo chmod +x /var/lib/git/git-shell-commands/init

init文件内容:

#!/bin/bash

set -euo pipefail
REPO_BASE="/repo"

if [[ $# -ne 1 ]]; then
    echo "Usage: init <repo-name>" >&2
    exit 1
fi
REPO_NAME="$1"
# 安全校验:禁止路径遍历、绝对路径、特殊字符
if [[ "$REPO_NAME" =~ ^(/|\.\./|\.\/) ]] || [[ "$REPO_NAME" != "${REPO_NAME//[^a-zA-Z0-9._-]/}" ]]; then
    echo "Error: Invalid repository name '${REPO_NAME}'" >&2
    echo "Only alphanumeric characters, dots, hyphens and underscores are allowed." >&2
    exit 1
fi
REPO_PATH="${REPO_BASE}/${REPO_NAME}"
# 检查是否已存在
if [[ -e "$REPO_PATH" ]]; then
    echo "Error: Path already exists: ${REPO_PATH}" >&2
    exit 1
fi
# 检查基础目录是否可写
if [[ ! -w "$REPO_BASE" ]]; then
    echo "Error: No write permission to ${REPO_BASE}" >&2
    exit 1
fi

# ========== 创建裸仓库 ==========
mkdir -p "$REPO_PATH"
git init --bare "$REPO_PATH"

echo "✔ Initialized bare repository: ${REPO_PATH}"
echo "First sync, choose one of the two: "
echo "git clone git@t725.cn:${REPO_PATH} D:\$REPO_NAME"
echo "git remote add origin git@t725.cn:${REPO_PATH} && git push -u origin master"

用户在客户端执行,完成服务器上的祼仓库创建与初始化

ssh git@server "init myproject.git"

附录

Git LFS 是什么?

Git LFS (Large File Storage) 是 Git 的一个开源扩展,专门解决 Git 不擅长管理大文件 的问题。因为 Git 原生设计面向文本代码,对二进制大文件存在严重缺陷:

  • 每次修改大文件都会完整存储一份副本 → 仓库体积爆炸
  • clone / fetch 必须下载全部历史版本 → 极慢
  • 无法有效 diff / merge 二进制文件

LFS 的解决方案

原始工作流:  commit → 存储完整 100MB 视频文件 × N个版本 = 仓库膨胀

LFS 工作流:  commit → 存储 ~130B 指针文件(存入 Git)
                    → 实际文件上传到 LFS 服务器(独立存储)
             clone  → 按需拉取当前版本的大文件

🔍 检查 Git LFS 是否已使用

  1. 检查命令是否存在
git lfs version
# ✅ 已安装: git-lfs/3.5.1 (GitHub; linux amd64; go 1.21.0)
# ❌ 未安装: git: 'lfs' is not a git command. See 'git --help'.
  1. 检查全局过滤器是否注册
git config --global filter.lfs.smudge
# ✅ 应输出: git-lfs smudge -- %f
git config --global filter.lfs.clean
# ✅ 应输出: git-lfs clean -- %f

⚠️ 即使 git lfs 命令存在,如果过滤器未注册,LFS 也不会生效。运行 git lfs install 可自动注册。

  1. 检查当前仓库是否启用了 LFS
cd /your/repo

# 方法一:查看是否有 LFS 跟踪规则
cat .gitattributes
# ✅ 包含类似行: *.png filter=lfs diff=lfs merge=lfs -text

# 方法二:列出所有被 LFS 跟踪的文件模式
git lfs track
# ✅ 输出: Listing tracked patterns
#           *.psd (.gitattributes)
#           *.mp4 (.gitattributes)

# 方法三:检查预提交钩子
ls .git/hooks/pre-push
# ✅ LFS 启用时该钩子由 git lfs install 自动创建

🛠️ LFS 初始化与注意事项

# 在仓库中启用并指定跟踪模式
cd /your/repo
git lfs track "*.bin"
git lfs track "*.model"
git add .gitattributes            # ⚠️ 别忘了提交这个文件!
git commit -m "chore: enable git-lfs for binary assets"
  • .gitattributes 必须提交到仓库:否则其他协作者 clone 后 LFS 不会生效
  • 已有大文件需迁移:直接 git lfs track 只对新提交生效,历史中的大文件需用 git lfs migrate import 重写历史
  • 服务端支持:GitHub/GitLab/Gitea 均内置 LFS 支持;自建 Gitea 需在 app.ini 中确认 [lfs] 段已启用
  • 克隆加速:首次 clone 可用 GIT_LFS_SKIP_SMUDGE=1 git clone 跳过大文件下载,后续按需 git lfs pull

git lfs env 一键诊断

git lfs env 输出示例及关键检查点:

git-lfs/3.5.1 (GitHub; linux amd64; go 1.21.0)
Endpoint=https://github.com/user/repo.git/info/lfs (auth=none)     ← LFS 服务端地址
LocalWorkingDir=/home/user/repo                                    ← 本地工作目录
LocalGitDir=/home/user/repo/.git                                   ← Git 目录
LocalMediaDir=/home/user/repo/.git/lfs/objects                     ← 本地缓存目录
TempDir=/tmp                                                       ← 临时目录
ConcurrentTransfers=8                                              ← 并发传输数
BasicTransfersOnly=false                                           ← 是否仅用基础传输
SkipDownloadErrors=false                                           ← 是否跳过下载错误
FetchRecentAlways=false                                            ← 是否总是获取最近引用
FetchRecentRefsDays=7                                              ← 获取最近引用的天数
FetchRecentCommitsDays=0                                           ← 获取最近提交的天数
FetchRecentRefsIncludeRemotes=true                                 ← 是否包含远程引用
PruneOffsetDays=3                                                  ← 修剪偏移天数
Access=download                                                    ← 访问权限

💡 如果 Endpoint 为空或报错,说明 LFS 服务端未正确配置,即使客户端安装了也无法正常工作。

发表回复