☰
git push --mirror 报 pre-receive hook declined:TaoToken 场景下的排查与配置骨架
2026/9/26 10:58:29 网站建设 项目流程

1. 从一次真实的git push --mirror翻车说起

git push --mirror是仓库迁移、双活同步、灾备镜像场景里最省事的命令,它会把本地镜像克隆里的所有 ref(分支、标签、甚至refs/merge-requests/*这类隐藏引用)原样推到目标仓库。但很多人第一次遇到pre-receive hook declined时,会下意识以为是网络问题或者权限没配好,反复重试、换 SSH key、甚至怀疑目标仓库挂了。实际上这个报错是目标仓库的服务端钩子主动拒绝,跟你的网络、跟你的 key 权限往往没关系。

这篇聚焦一个具体场景:你用git clone --mirror把源仓库拉到本地,再git push --mirror推到目标仓库,结果所有分支被拒,日志里出现类似The keywords cherry-pick and revert are branch names reserved by CodeHub, You are not allowed to create.的提示。我会把根因拆成三类——权限、分支保护、钩子脚本——并给出一套可复制的排查路径。同时结合 TaoToken 统一 Key/API 通道,把 CI 或本地钩子脚本里调用模型做日志归因的配置骨架也一并交付,方便你在批量迁移脚本里自动判断失败原因。

适合谁看:正在做仓库批量迁移的运维/后端、写 CI 同步流水线的工程师、以及被pre-receive hook declined卡住想快速定位根因的人。核心检索词就是git push --mirror、pre-receive hook declined、分支保护、钩子脚本。

2. 先搞清楚pre-receive hook declined到底是谁在拒绝

2.1 报错发生在服务端,不在你的本地

pre-receive是 Git 服务端在接收推送前执行的钩子。你的git push把对象和 ref 更新请求发过去,服务端先跑这个钩子,钩子返回非零退出码,整个推送就被拒,客户端看到的就是pre-receive hook declined。所以排查方向永远在目标仓库侧,本地git config、ssh -T能通不代表推送能过。

2.2 三类根因的区分方法

根因类型典型现象快速判断
权限不足提示You are not allowed to push、permission denied用同账号在 Web 端能否手动建分支
分支保护提示受保护分支、protected branch看目标仓库分支保护规则列表
钩子脚本提示保留分支名、自定义校验文案报错文案里带业务关键词,如cherry-pick、revert

--mirror的特殊之处在于它会推送所有 ref,包括源仓库里那些临时分支、合并中间态分支。源仓库允许存在的分支名,目标仓库的钩子可能明确禁止,这就是本次报错的直接来源。

2.3 为什么--mirror特别容易踩这个坑

普通git push origin main只推一个分支,钩子校验范围小。--mirror等于把源仓库的 ref 命名空间整体搬过去,源仓库里合并请求产生的tmp-branch-xxxx、refs/merge-requests/*都会被尝试创建。目标仓库如果对分支命名有白名单/黑名单,或者保留了cherry-pick、revert这类系统关键字,就会在钩子层被拦下,而且一个 ref 被拒会导致整批推送失败,表现就是"所有分支都被拒绝"。

3. TaoToken 前置:把统一 Key 通道接进迁移脚本

3.1 为什么迁移脚本里要接模型通道

批量迁移时,失败原因五花八门,人工一条条看日志效率很低。我的做法是在迁移脚本的失败分支里,把git push的 stderr 丢给模型做一次归因,输出"权限/分支保护/钩子脚本"的分类和下一步建议。TaoToken 提供统一的 Key 和 API 通道,本地脚本和 CI 用同一套配置即可,不用为每个环境单独维护多套凭证。

官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

API 基地址(不带 UTM):https://taotoken.net/api

3.2 拿 Key 与选通道

登录后进入控制台创建 API Key,模型对话、Coding Plan、API Keys 各有独立入口,按用途选:

  • 只是脚本里做日志归因、单次问答:用模型对话通道即可。
  • 长期跑 CI、Agent 类任务:走 Coding Plan 更划算。
  • 需要程序化调用:在 API Keys 页面生成 Key,配合接入文档。

控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= API Keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

注意:Key 只放在环境变量或 CI Secret 里,不要写进迁移脚本提交到仓库。下面骨架统一用TAOTOKEN_API_KEY读取。

4. 可复制配置:config.toml 与 settings.json 骨架

4.1 config.toml 骨架(本地/CI 通用)

# ~/.config/taotoken/config.toml # 统一 Key 通道配置,迁移脚本与 CI 共用 [api] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读取,不落盘 timeout_seconds = 60 [model] # 日志归因用,按需替换为可用模型名 name = "default" max_tokens = 1024 temperature = 0.2 [git_mirror] # 迁移脚本相关参数 tmp_dir = "/tmp/git_mirror" log_dir = "./logs" reserved_branch_keywords = ["cherry-pick", "revert", "tmp-branch"]

4.2 settings.json 骨架(CI 侧)

{ "taotoken": { "baseUrl": "https://taotoken.net/api", "apiKeyEnv": "TAOTOKEN_API_KEY", "channel": "coding-plan" }, "gitMirror": { "cloneMode": "mirror", "pushMode": "mirror", "failFast": false, "classifyOnError": true, "reservedKeywords": ["cherry-pick", "revert", "tmp-branch"] } }

4.3 迁移脚本里接入归因的关键片段

#!/bin/bash # 在原有 push 失败分支里追加归因调用 push_mirror() { local target="$1" local err if err=$(git push --mirror "$target" 2>&1); then echo "[OK] push success: $target" return 0 fi echo "[FAIL] push rejected: $target" echo "$err" | tee -a "$LOG_FILE" # 调用 TaoToken 做失败归因 curl -sS "${TAOTOKEN_BASE_URL:-https://taotoken.net/api}/chat/completions" \ -H "Authorization: Bearer ${TAOTOKEN_API_KEY}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg log "$err" '{ model: "default", messages: [ {role: "system", content: "你是 Git 排障助手,只输出:根因分类(权限/分支保护/钩子脚本) + 下一步命令。"}, {role: "user", content: $log} ], temperature: 0.2 }')" | jq -r '.choices[0].message.content' return 1 }

这段逻辑不改变原有迁移流程,只在失败时多打一份归因,方便你批量跑的时候快速分流。

5. 验证请求与一次完整的成功推送

5.1 先验证 Key 通道可用

export TAOTOKEN_API_KEY="你的Key" curl -sS https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"default","messages":[{"role":"user","content":"ping"}]}' \ | jq -r '.choices[0].message.content'

返回正常文本说明通道通了,再往下做迁移验证。

5.2 定位被拒的 ref

# 进入镜像克隆目录 cd /tmp/git_mirror/repo1 # 列出所有 ref,重点看 tmp 开头和保留关键字 git for-each-ref --format='%(refname)' | sort # 过滤可疑分支 git for-each-ref --format='%(refname)' | grep -E 'tmp-branch|cherry-pick|revert'

5.3 删除问题分支后重新推送

# 删除本地镜像里的临时分支 git branch -D tmp-branch-xxxx # 方式一:整体镜像推送 git push --mirror git@target.example.com:group/repo1.git # 方式二:只推分支命名空间,避开 merge-requests 等隐藏 ref git push git@target.example.com:group/repo1.git '+refs/heads/*:refs/heads/*'

方式二是我实测下来更稳的做法:--mirror会把refs/merge-requests/*这类目标仓库不接受的引用也带上,改成只推refs/heads/*就能绕开大部分钩子对隐藏 ref 的校验。

5.4 成功结果长什么样

Enumerating objects: 128, done. Counting objects: 100% (128/128), done. Delta compression using up to 8 threads Compressing objects: 100% (76/76), done. Writing objects: 100% (128/128), 45.21 KiB | 3.20 MiB/s, done. Total 128 (delta 52), reused 0 (delta 0) remote: Resolving deltas: 100% (52/52), done. To git@target.example.com:group/repo1.git * [new branch] main -> main * [new branch] release -> release

看到[new branch]且没有pre-receive hook declined,说明钩子校验通过。

6. 本篇常见错排查

6.1 报错文案带保留关键字

The keywords cherry-pick and revert are branch names reserved by CodeHub这类提示,根因是钩子脚本,不是权限。处理方式就是删掉源仓库里对应名字的临时分支,或者改用+refs/heads/*:refs/heads/*只推分支命名空间。

6.2 只有部分分支被拒

如果日志里只有个别分支失败,先看是不是受保护分支。用git ls-remote对比源和目标:

git ls-remote --heads git@source.example.com:group/repo1.git > src.txt git ls-remote --heads git@target.example.com:group/repo1.git > dst.txt diff src.txt dst.txt

6.3 权限类报错的判断

# 测试 SSH 认证是否通过 ssh -T git@target.example.com # 查看当前 remote git remote -v

如果ssh -T返回欢迎语但 push 仍被拒,基本可以排除认证问题,转向分支保护和钩子脚本。

6.4 钩子日志怎么定位

服务端钩子日志通常在目标仓库的 hooks 目录或服务端日志里,本地拿不到。你能做的是把git push的完整 stderr 保留下来,用第 4.3 节的归因片段分类。常见钩子拒绝文案对照:

文案关键词根因
reserved by、not allowed to create钩子脚本命名校验
protected branch、not allowed to push分支保护
permission denied、not authorized权限

6.5 迁移脚本的清理逻辑别写错

原脚本里cd "$TMP_DIR" && rm -rf "$REPO_NAME"在 push 失败时也会执行,导致问题现场被删。建议改成失败时保留目录:

if push_mirror "$TARGET_REPO"; then cd "$TMP_DIR" && rm -rf "$REPO_NAME" else echo "[KEEP] 保留现场: $TMP_DIR/$REPO_NAME" fi

7. 把归因通道固定进你的迁移流水线

排查pre-receive hook declined的核心就一句话:报错在服务端,先分类再动手。权限、分支保护、钩子脚本三类根因的文案特征不同,用第 6.4 节的对照表基本能秒判。--mirror的坑在于它会带上隐藏 ref 和临时分支,改用+refs/heads/*:refs/heads/*往往比删分支更快。

如果你要把这套归因逻辑固化到 CI 或本地脚本里,建议把 Key 通道统一走 TaoToken,本地和流水线共用一份config.toml,失败时自动分类,省去人工翻日志。接入文档里有完整的请求格式和参数说明,照着改curl那段就能跑通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询