SMB备份导致的 Gitea 分支推送成功但界面不可见故障复盘

Gitea 分支"推送成功但界面不可见"故障复盘

环境:Gitea 1.24.3(Docker 部署,数据目录 ./gitea 挂载到容器 /data)

结论先行:迁移链路中 rclone(SMB 后端)挂载不保存 Linux 执行权限位,导致旧仓库的 Git hook 脚本全部变成 644/664 失去执行权限;Git 对不可执行的 hook 设计为静默跳过,于是 push 写入 git 磁盘成功、但负责把分支同步进数据库的 hook 从未执行,Gitea 界面/API 读的是数据库 branch 表,因此"显示成功但看不到"。


1. 故障现象

现象 说明
业务侧推送"成功" 业务系统执行 git push 后拿到成功响应和 commit hash,读回校验也通过
Gitea 界面看不到分支 /<owner>/<repo>/branches、REST API /api/v1/repos/.../branches、/graph 页面均只显示迁移前的 main
界面手工创建分支同样失效 在 Gitea 网页上点"新建分支",提示创建成功,但分支列表中不出现
影响范围 所有迁移前已存在的仓库;迁移后新建的仓库完全正常
git 命令行正常 git ls-remote 能看到全部分支,clone 也正常

2. 技术背景:Gitea 的双存储模型

理解本故障的关键前提:Gitea 对分支信息维护着两份数据,靠 hook 保持一致。

复制代码
┌─────────────────────────────────────────────┐
│ ① Git 磁盘存储(唯一真相)                     │
│   repositories/<owner>/<repo>.git/           │
│     refs/heads/<branch> → commit             │
│     objects/                                 │
│   读者:git ls-remote / git clone / git push  │
│   push 写入这里成功 = git 层面已成功           │
└─────────────────────────────────────────────┘
                  ⇡ 靠 hook 同步 ⇣
┌─────────────────────────────────────────────┐
│ ② 数据库 branch 表(展示缓存)                 │
│   读者:/branches 页面、REST API、/graph 页面、│
│        分支保护、ahead/behind 统计            │
│   "界面上看到的分支" = 这张表里的行           │
└─────────────────────────────────────────────┘

界面分支列表的数据源可以从源码确认(Gitea 1.24):

services/repository/branch.go 的 LoadBranches() 执行 db.FindAndCount[git_model.Branch] ------ 只查数据库,不执行 git for-each-ref 8。

2.1 界面"创建分支"的底层流程

一个反直觉的事实:在 Gitea 网页上创建分支,底层是一次真实的 git push------Gitea 把自己仓库的磁盘路径当作 remote,向自己推送。

源码链路(Gitea 1.24,已逐文件核对):

复制代码
浏览器 POST /{owner}/{repo}/branches/_new/branch/<源分支>
  └─ routers/web/repo/branch.go:178   CreateBranch()          解析表单
  └─ services/repository/branch.go:42 CreateNewBranch()       取源分支 commit
  └─ services/repository/branch.go:378 CreateNewBranchFromCommit()
        git.Push(ctx, repo.RepoPath(), git.PushOptions{
            Remote: repo.RepoPath(),                    // remote 就是仓库自己
            Branch: "<commit>:refs/heads/<新分支>",
        })

上述链路对应源码:网页入口 CreateBranch() 7,服务层 CreateNewBranchFromCommit() 的 git push 实现 8。

2.2 push 之后的完整链路(界面创建与外部 push 殊途同归)

复制代码
git-receive-pack 接收 push
  ├─ ① 写入磁盘:objects/ + refs/heads/<branch>        ✅ 到这里 push 就返回"成功"
  │
  └─ ② 执行钩子 hooks/post-receive(dispatcher)
        └─ hooks/post-receive.d/gitea
              内容:/usr/local/bin/gitea hook --config=/data/gitea/conf/app.ini post-receive
              └─ 回调 Gitea 内部 API /api/internal/hook/post-receive/...
                    └─ services/repository/push.go:57  PushUpdates() 进入队列
                          └─ pushNewBranch() (push.go:183)
                                └─ models/git/branch.go:206  AddBranches()
                                      → 数据库 branch 表插入新行   ✅ 界面从此可见

② 的回调与写库链路对应源码:PushUpdates()/pushNewBranch() 9,AddBranches() 10。

也就是说:git 磁盘写入(①)与数据库同步(②)是两个独立步骤,① 的成功不以 ② 为前提。② 完全依赖 hook 脚本能够被 git 执行。

3. 排查过程

  1. 对比两个数据源 :git ls-remote 返回 全部分支,而 REST API /branches 只返回 main(且其记录停留在迁移前)。结论:git 磁盘数据完整,数据库 branch 表没有同步 → 问题在"git → DB"的同步链路,即 hook。
  2. 检查受影响仓库的 hooks 目录 (容器卷 gitea/git/repositories/<owner>/<repo>.git/hooks/):hook 脚本存在、内容正确(无硬编码旧路径),但权限为 -rw-rw-r--(664),没有执行位。
  3. 检查迁移备份源:备份服务器及更早的 rclone 挂载点上,同一批 hook 文件同样是 664 → 权限在迁移链路中就已经丢失,不是新服务器上被破坏的。
  4. 对照组:迁移后新建的仓库 hook 为 755(Gitea 在容器内生成),工作正常。

关键证据

受影响仓库的 hook 文件(备份服务器上):

复制代码
$ ls -ln post-receive.d/gitea
-rw-rw-r-- 1 1000 1000 134 Dec 11  2025 post-receive.d/gitea   ← 无执行位

hook 内容本身没有任何问题(排除硬编码旧路径的嫌疑):

bash 复制代码
# hooks/post-receive.d/gitea ------ 内容正确,唯一的问题是权限
#!/usr/bin/env bash
/usr/local/bin/gitea hook --config=/data/gitea/conf/app.ini post-receive

Gitea 自动生成的 dispatcher(hooks/post-receive)中对不可执行 hook 的处理方式------静默跳过:

bash 复制代码
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
  test -x "${hook}" && test -f "${hook}" || continue   # ← 664 时 test -x 失败,直接 continue
  echo "${data}" | "${hook}"
done

4. 根因:执行权限丢失的因果链

复制代码
原服务器(Docker 卷 ./gitea:/data)
  hooks 755 ✅(Gitea 容器内生成)
      │
      ▼  rclone mount(远端为 SMB 协议后端)
  SMB 协议不承载 Linux 权限位(mode bits)
  挂载点上读出的文件权限由 rclone 按 --file-perms/umask 合成(最终权限 = 权限模板 & ~umask [11]) [1][2]
  → hooks 在挂载点上已经表现为 664 ❌(权限位在这一跳丢失,且不可逆)
      │
      ▼  rsync -avh 备份到备份服务器
  rsync -a(归档模式,保留权限/属主/时间等属性 [4])忠实保留"源端呈现出的权限" → 备份副本 664 ❌
      │
      ▼  rsync -avhP 恢复到新服务器
  目标 hooks 仍为 664 ❌
      │
      ▼  运行期
  git receive-pack 执行 hook 前 test -x 失败 → 静默跳过
  → ① 磁盘写入成功(push 返回成功)
  → ② DB 同步从未发生(界面/API 不可见)

📖 rclone 官方文档对此有明确说明:挂载点默认权限为 --file-perms 0666 --dir-perms 0777(--umask 默认 002),并直接警告 "This means you will not be able to start any programs from the mount. To be able to do that you must add execute permissions, e.g. --file-perms 0777" 1。本故障中的 hook 脚本正是被这句话命中的"programs"。

三个机制叠加,造成了"无报错的全静默失败":

  1. SMB 不保存 Linux 权限位:权限在 rclone 挂载这一跳就已经丢失,之后任何忠实复制(rsync -a)都只能复制到已经损坏的权限。这不是 rsync 的错。
  2. git 对不可执行 hook 静默忽略:Git 官方文档要求 hook 必须可执行才会被调用 3,对不可执行的 hook 静默跳过是既定设计(避免一个坏 hook 阻塞所有推送),所以 push 全程没有任何报错,客户端拿到的是货真价实的成功。
  3. Gitea 界面读 DB 而非 git:磁盘与 DB 脱节时,界面忠实展示了 DB 的落后状态。

为什么只有旧仓库受影响、新仓库正常

新仓库的 hooks 是迁移完成后由 Gitea 在容器内直接生成的(755),未经过 rclone/SMB/rsync 链路。

隐藏副作用:迁移期间分支保护形同虚设

负责推送前校验的 pre-receive / proc-receive hook 同样因无执行位被静默跳过。这意味着在故障存续期间,分支保护规则、推送权限校验均未生效,所有推送直接放行。复盘时应评估这期间是否有本应被拦截的推送。

5. 修复过程

在 Gitea 站点管理后台执行两个操作,分别对应链路的不同环节:

操作 作用
「重新同步所有仓库的 pre-receive、update 和 post-receive 钩子」 修断链点:重新生成所有仓库的 hook 文件并恢复正确权限(755),此后的 push 恢复完整链路
「将缺少的分支从 git 数据同步到数据库」 补历史欠账:扫描 git 磁盘 refs,把故障期间漏同步的分支批量回填 DB branch 表

等价的命令行方式(容器内执行)5:

bash 复制代码
gitea admin regenerate hooks             # 重建所有仓库 hook
gitea doctor --run sync-branches --fix   # 将 git 数据中的分支同步到数据库

修复后验证:ls-remote 与 REST API 分支列表一致;界面手工创建分支、业务侧推送新建分支均即时可见。

6. 预防与改进建议

6.1 备份/迁移方式(根因层)

  • Gitea 数据目录不要经 rclone(SMB/网盘类后端)挂载点做文件级备份。这类后端不保存 Linux 权限位,任何"先挂载再 rsync"的链路都会丢权限。
  • 推荐替代方案(任选其一):
    • tar 打包后再传输:tar 归档内完整保存权限与属主,如 tar -czpf gitea-backup.tar.gz gitea/;
    • 使用 gitea dump 做应用级备份(Gitea 官方备份与恢复指南还明确要求:恢复后要 chown -R git:git /data 并 admin regenerate hooks 6);
    • 若必须经 rsync 直接传,源端应是原生 Linux 文件系统而非 rclone 挂载点。
  • 若只能以 rclone 挂载点为源,可在挂载时加 --file-perms 0777 --dir-perms 0777 --umask 000 让挂载点合成可执行位,rsync 即可保留执行权限。注意副作用:所有文件都会带上执行位,粒度不可控,仅作规避手段而非推荐方案。

7. 参考文献

  1. rclone 官方文档 --- rclone mount 命令与选项:--file-perms(默认 0666)、--dir-perms(默认 0777)、--umask(默认 002)的定义与默认值;官方明确说明默认权限下 "you will not be able to start any programs from the mount"。
  2. rclone GitHub Wiki --- Systemd rclone mount:官方推荐的 systemd 挂载模板,其中默认环境变量 RCLONE_MOUNT_FILE_PERMS="0666"、RCLONE_MOUNT_UMASK="022",印证默认合成权限为 0664。
  3. Git 官方文档 --- githooks:Git 钩子机制官方说明,hook 必须可执行(executable)才会被 Git 调用。
  4. rsync(1) man page:-a, --archive 等价于 -rlptgoD,递归并保留权限、属主、时间戳等属性------它保留的是源端呈现出的权限。
  5. Gitea 官方文档 --- 命令行(admin regenerate hooks):regenerate hooks:重新生成所有仓库的 Git Hooks。
  6. Gitea 官方文档 --- 备份与恢复:恢复流程要求调整文件权限(chown -R git:git /data)并重新生成仓库 Git 钩子,否则 push 相关功能异常。
  7. Gitea 源码 release/v1.24 --- routers/web/repo/branch.go:网页创建分支入口 CreateBranch()。
  8. Gitea 源码 release/v1.24 --- services/repository/branch.go:CreateNewBranchFromCommit() 内部以 git push 创建分支;LoadBranches() 仅从数据库读取分支列表。
  9. Gitea 源码 release/v1.24 --- services/repository/push.go:hook 回调后的 PushUpdates() 队列与 pushNewBranch()。
  10. Gitea 源码 release/v1.24 --- models/git/branch.go:AddBranches() 写入数据库 branch 表。
  11. Ask Ubuntu --- What is "umask" and how does it work?:权限位与 umask 的按位计算规则(最终权限 = 权限模板 & ~umask)。
相关推荐
zhangzeyuaaa1 小时前
StarRocks 的技术版图:在分析、检索与流处理之间找到位置
大数据·数据仓库
漫步人生走在路上1 小时前
外贸SEO中的品牌词与产品词策略:大卖家和小卖家的不同打法
大数据·javascript·网络·数据库·人工智能·搜索引擎
2503_931712481 小时前
数据服务公司怎么帮助企业更高效找到行业数据与商业情报
大数据·人工智能·microsoft
AI Workforce1 小时前
数据质量:4 层去重 + 黑名单机制——宁可漏杀,不可错杀
大数据·人工智能·ai·架构·prompt
星辰云-2 小时前
基于机器学习的敦煌壁画修复数据分析系统
大数据·人工智能·机器学习·数据分析·毕设·文物修复·壁画修复
Raas1002 小时前
MemT-1.0 接入设计指南:硬件核算、三方案选型与写入触发点
大数据·网络·人工智能·机器学习·数据挖掘·ai agent
小熊美家熊猫系统2 小时前
从“科技兴商“意见看家政行业:可追溯体系的技术实现路径
大数据·科技
pjj198542 小时前
Spark RDD 核心知识详解:算子、依赖与执行模型
大数据·分布式·spark
Aloudata2 小时前
Data Agent vs AI Native BI:企业数据分析的下一代入口到底是什么?
大数据·人工智能·数据分析·data·语义层