← 仓库列表
cigit
README.md
# cigit
最小本地 Git 仓库管理 daemon。沿用 email 的单文件哲学:运行时代码全部放在
`main.go`,只用 Go 标准库和系统 Git。
daemon 只监听 Unix socket,`cigit cmd` 通过 socket 发送管理指令,所有仓库修改由
daemon 执行。网页可通过独立 Unix socket 提供 HTTP 给 Nginx;没有 TCP 监听、
SSH 服务、RPC 框架、数据库或后台维护任务。
支持 macOS / Linux。
默认仓库根目录为 `/var/git/repos`,用 `-root` 覆盖。没有数据库或独立应用配置文件;
可见性保存在仓库原生 Git config 的 `cigit.visibility` 中。
```sh
./build.sh
cp "dist/$(go env GOOS)-$(go env GOARCH)/cigit" ./cigit
./cigit daemon -socket ./cigit.sock -root ./repos
```
`build.sh` 将 UTC 构建时间写入二进制,`cigit --version` 输出 `cigit YYYY-MM-DDTHH:MM:SSZ`。
构建时间就是版本。直接执行未带链接参数的 `go build` 会得到 `dev` 开发版本。
可通过 `GOOS=linux GOARCH=amd64 ./build.sh` 交叉构建;产物、时间文件和 SHA-256
校验和放在 `dist/<os>-<arch>/`,支持 Linux/macOS 的 amd64/arm64。
安装或更新已发布版本:
```sh
curl -fsSL https://gitstatus.tinfra.cc/install.sh -o /tmp/cigit-install.sh
sh /tmp/cigit-install.sh
# 只检查版本,不安装:
sh /tmp/cigit-install.sh --check
```
root 安装到 `/usr/bin/cigit`,普通用户安装到 `~/.local/bin/cigit`(需将目录加入 PATH)。
macOS 的系统保护通常不允许写入 `/usr/bin`,请使用普通用户安装。
每次运行安装器都会检测系统架构并查询远端构建时间;相同或更新的已安装版本跳过,
旧版或开发版自动更新。下载后验证 SHA-256 和二进制内的构建时间,再原子替换文件。
`CIGIT_RELEASE_URL` 可覆盖发布源,默认 `https://gitstatus.tinfra.cc/releases`。
安装器不常驻轮询;已运行的 daemon 在更新后需 `systemctl restart cigit`。
另一个终端(同一工作目录):
```sh
./cigit cmd create demo.git
./cigit cmd list
./cigit visible demo.git public
./cigit cmd delete demo.git
```
自定义 socket 时,每条命令写作 `cigit cmd -socket /path/cigit.sock create demo.git`。
`visible` 写作 `cigit visible -socket /path/cigit.sock demo.git private`。
管理命令优先使用当前目录的 `cigit.sock`;不存在时自动使用已部署的 `/run/cigit/control.sock`。
成功的修改命令不输出内容;`list` 每行输出一个仓库名;失败输出到 stderr 并返回非零。
`visible` 统一控制网页和 HTTPS clone:
| 值 | 页面列表、README 和提交历史 | HTTPS clone/fetch | HTTPS push | SSH |
|---|---|---|---|---|
| `public` | 展示 | 允许匿名访问 | 禁止 | 系统公钥 |
| `private`(默认) | 隐藏,直接请求返回 404 | 禁止 | 禁止 | 系统公钥 |
只接受 `private/public`,不接受 `true/false`,没有 `showinpage` 或 token。
管理命令 `list` 始终列出所有仓库。设置立即生效并持久保存;已经开始的 Git 传输可继续完成。
生成仓库概览页面,在浏览器中打开输出文件:
```sh
./cigit cmd html > repos.html
```
`repos.html.tmpl` 和 `repo.html.tmpl` 使用 Go `html/template`,编译时嵌入二进制;修改后重新构建。
daemon 渲染完整 HTML,通过现有 Unix socket 的 JSON 响应 `html` 字段返回;命令原样
输出到 stdout。请求为 `{"command":"html"}` 加换行。不监听任何 TCP 端口。列表只显示仓库名称、
最近更新时间和大小;名称链接进入仓库详情,不显示 public、hash、摘要或 email。
最近更新时间取最新提交的 committer 时间,空仓库显示 `—`。
大小是仓库目录内普通文件的逻辑字节数合计(包括 Git 对象、配置和 hooks),不跟随
符号链接,不等同于文件系统分配的磁盘块大小。最近提交取 `git log --all -1`,覆盖所有
refs,即使 HEAD 尚未指向已有分支也可显示;时间和 email 使用 committer 字段。
页面是生成时的快照,重新运行命令刷新。Git 仍可同时读写,统计不是事务快照;单个仓库
读取失败会在对应行显示错误。
点击仓库名称进入 `/<仓库名>`:顶部显示 HTTPS clone/fetch 和 SSH clone/push 地址,
可点击复制按钮复制。默认显示 Git `HEAD` 指向分支的根目录 README,
`?commit` 显示该分支提交历史(每页 50 条);`?branch=分支名` 切换分支,
`?commit&branch=分支名` 查看对应历史。新仓库默认分支为 `main`;原有仓库以自身 HEAD
为准,不自动切换到其他分支。README 按纯文本安全显示(最多 1 MiB),不执行 HTML。
给 Nginx 提供页面时启用单独的 HTTP Unix socket:
```sh
./cigit daemon -root ./repos -socket ./cigit.sock -html-socket ./cigit-html.sock
curl --unix-socket ./cigit-html.sock http://localhost/
```
网页及 Git Smart HTTP 共用 HTTP Unix socket:列表和详情支持 GET/HEAD,
clone/fetch 由系统 `git http-backend` 处理,receive-pack 在入口被拒绝。
HTTP socket 权限 `0660`,管理 socket 权限 `0600`。
Nginx 只连接网页 socket,不接触管理指令。
服务器 `rifty4c8g` 的手动部署使用 `cigit.service` 和 `gitstatus.nginx.conf`:
- 二进制 `/usr/bin/cigit`;systemd 服务 `cigit.service`,运行用户 `cigit`、组 `www-data`。
- 仓库 `/var/git/repos`;管理 socket `/run/cigit/control.sock`;网页 socket `/run/cigit/html.sock`。
- 页面 `https://gitstatus.tinfra.cc/`;Nginx 配置 `/etc/nginx/conf.d/gitstatus.tinfra.cc.conf`。
- 安装脚本 `/var/www/cigit/install.sh`;发布产物 `/var/www/cigit/releases/<os>-<arch>/`。
- `systemctl status cigit` 查看状态,`journalctl -u cigit` 查看日志。
管理示例:`sudo -u cigit /usr/bin/cigit cmd -socket /run/cigit/control.sock create demo.git`。
发布时将 `dist/` 各平台产物复制到上述 releases 目录,最后更新对应 `build-time`;
运行安装器后重启服务。更改模板需要重新构建和发布二进制。
### cigit 自身的 post-receive 自动部署
每个仓库独立设置自己的 hook 脚本,脚本自行决定执行什么、重启哪个服务。
cigit 不内置通用部署调度器,也不将不同仓库的任务交给同一个固定 systemd 服务。
本仓库的 `.github/cigit/post-receive` 只在 main 的非删除 push 后调用专用启动器
`/usr/local/libexec/cigit-start-deploy`。启动器用 `systemd-run` 为每次调用建立独立临时任务,
然后立即返回。任务名为 `cigit-deploy-<commit>-<时间戳>`,结束后自动回收。
任务与 SSH 会话、`cigit.service` 分属不同 cgroup,重启 cigit 不会杀死替换脚本。
`.github/cigit/start-deploy.sh` 和 `deploy.sh` 是管理员安装到
`/usr/local/libexec/` 的辅助脚本;它们不是 Git hook,不会自动执行。
部署脚本导出固定提交,以 cigit 用户直接构建服务器的 linux-amd64 二进制,
限制两路编译并复用缓存,不运行 CI、测试或其他平台构建;构建时间写入版本。
仅本仓库的部署任务通过文件锁串行执行,过期 main 提交跳过,不影响其他仓库的 hook。
构建成功后原子替换 `/usr/bin/cigit`、重启并检查 Unix socket 页面;健康检查失败恢复旧版。
构建失败不替换运行版本。成功后更新下载产物并记录已部署 commit。
```sh
systemctl list-units 'cigit-deploy-*'
journalctl -u 'cigit-deploy-*' -f
cat /var/lib/cigit-deploy/deployed-commit
```
这些部署脚本由 root 安装;sudoers 只允许 cigit 调用专用启动器,启动器校验提交 hash,
只执行本仓库的固定部署脚本。其他仓库按自己的需要配置 hook,不会自动使用它。
修改运维脚本需手动同步;自动部署只替换应用二进制和下载产物。
服务器需要 Git、Go(支持自动 toolchain)、tar、curl、sudo、flock 和 systemd-run。
构建缓存位于 `/var/lib/cigit-deploy/cache/`。
仓库是 `<root>/<name>` 下的裸仓库,只有一层,不支持 owner/namespace。
名称必须以英文字母或数字开头,后续只能使用英文字母、数字、`.`、`_`、`-`,最多
200 个字符;磁盘名称原样使用,不自动添加 `.git`,HTTPS 地址接受 `.git` 后缀。
`log`(及 `log.git`)保留给执行日志,不能创建同名仓库。
不允许同时创建 `demo` 和 `demo.git`,避免地址冲突。只列出、删除和配置 cigit 创建的仓库。
`delete` 直接永久删除仓库;执行前应停止该仓库的 Git 读写。
将 Bash 脚本随源码提交到 `.github/cigit/<Git hook 名>`,例如:
```text
.github/cigit/pre-receive
.github/cigit/post-receive
.github/cigit/update
```
cigit 自动读取裸仓库默认分支(HEAD)中对应的文件,用 `/bin/bash` 执行。
不需要设置路径或 executable 权限;没有对应文件时跳过。
参数与原生 Git hook 一致,不额外传入 hook 类型;stdin、环境、工作目录和退出码保留。
默认分支的首次 push 可以触发新提交中的 post-receive;
pre-receive、update 在引用更新前使用原有默认分支的脚本,首次 push 时还不存在。
非默认分支的 push 也使用默认分支的脚本,脚本可根据输入的 ref 自行筛选。
仅识别原生 hook 名,不会把目录中的辅助 `.sh` 文件当作 hook。
例如 `.github/cigit/post-receive`:
```bash
#!/bin/bash
while read -r old new ref; do
[[ "$ref" = refs/heads/main ]] || continue
[[ "$new" =~ ^0+$ ]] && continue
# 在这里启动本仓库自己的后台任务。
printf 'main updated to %s\n' "$new"
done
```
脚本从 Git 对象读入临时文件,运行后删除;当前目录仍是裸仓库,
不会自动 checkout 源码。模板仓库可以携带同样的目录,push 后直接使用。
daemon 启动时更新已有仓库的入口。`cigit cmd hook` 和旧的 `cigit.hook` 配置不再使用。
Git 决定触发哪些 hook;裸仓库不会触发工作区中的 commit/checkout hook。
### Hook 执行日志
每次实际执行 hook 都在 `<root>/log/<UTC时间戳>.<唯一后缀>/` 创建独立目录,
默认路径示例:
```text
/var/git/repos/log/20260914T120000Z.a1b2c3d4/post-receive.stdout
/var/git/repos/log/20260914T120000Z.a1b2c3d4/post-receive.stderr
```
stdout、stderr 分别实时记录,同时保留 Git 原有输出和退出码,方便客户端看到错误,
也不破坏使用 stdout 通信的 hook。未找到脚本时不创建日志。
目录权限为 0700、日志为 0600;日志不作为仓库展示,也不通过 HTTP 提供。
`-root` 同时改变仓库和日志根目录。时间戳后缀避免并发执行互相覆盖;
日志保留到管理员手动清理,不自动轮转。
Bash 执行环境提供 `CIGIT_HOOK_LOGDIR`,值为本次日志目录。
如果 hook detach 了任务,需要把这个路径显式传给后台执行器,继续追加同名文件。
本项目及 fma、dnsuck、metadata 的 systemd 启动器已经传递该路径;
后台构建、替换、重启的输出以 cigit 用户追加到原始 `post-receive.stdout/.stderr`,
同时可用 `journalctl` 查看对应临时任务。root 执行器不会以 root 身份打开仓库可写的日志文件。
```sh
tail -f /var/git/repos/log/<timestamp>/post-receive.stdout \
/var/git/repos/log/<timestamp>/post-receive.stderr
```
Git 使用示例:
```sh
git clone https://gitstatus.tinfra.cc/demo.git
git clone cigit@gitstatus.tinfra.cc:demo.git
# HTTPS clone 后要 push,单独设置 SSH push 地址:
git remote set-url --push origin cigit@gitstatus.tinfra.cc:demo.git
```
daemon 与 SSH 使用同一个系统用户 `cigit`,SSH 由系统 sshd 提供,登录 shell 为
`/usr/bin/git-shell`,登录目录为 `/var/git/repos`,公钥保存在该目录的 `.ssh/authorized_keys`。
SSH 使用相对仓库名,无需在 URL 中写服务器路径。若修改 `-root`,同时用 `usermod -d`
将 SSH 账号的登录目录改到新根目录。该账号授权的公钥
可通过 SSH 读写全部仓库,不做逐仓库 ACL。仓库目录权限为 `0700`。
daemon 前台运行,Ctrl-C / SIGTERM 关闭并移除 socket;异常退出残留的 socket
需确认原进程已退出后手动删除。仓库根目录通过文件锁限制为一个 daemon 使用。