1. 镜像推送被 pre-receive hook declined 拦截的真实场景
git push --mirror报pre-receive hook declined,是代码仓库迁移和周期性同步里最让人头疼的一类问题。它不像认证失败那样直接告诉你密码错了,也不像网络超时那样重试就行,而是远端仓库在收到你的推送数据之后,由服务端的钩子脚本判定「这批引用不允许写入」,然后整批拒绝。你看到的往往是一长串! [remote rejected],每个分支后面都跟着pre-receive hook declined,最后error: failed to push some refs to ...。
这个报错的核心含义是:推送动作本身没问题,是远端仓库的策略不允许你推这些引用。常见触发原因有几类。第一类是分支命名被保留,比如某些代码托管平台会把cherry-pick、revert这类名字保留为内部用途,你镜像过来的仓库里恰好存在同名分支,钩子直接拒绝。第二类是目标分支受保护,master、main、release/*这些分支不允许强制推送或不允许非管理员推送。第三类是权限不足,你的账号对目标仓库只有读权限,或者 SSH Key 没有写权限。第四类是镜像推送会带上refs/pull/*、refs/merge-requests/*这类特殊引用,而目标平台不允许创建。
我遇到过一次很典型的情况:用git clone --mirror把源仓库完整镜像下来,再git push --mirror推到新仓库,结果所有分支全被拒。报错信息里明确提到某个tmp-开头的分支名是平台保留的,而这个分支是源仓库里一次未完成合并请求留下的临时分支。源仓库自己不管这些临时分支,但目标仓库的钩子会拦截。删掉这个分支之后重新推送,问题立刻消失。
所以排查思路应该是:先看报错里有没有点名具体分支或引用,再检查目标仓库的分支保护规则和账号权限,最后确认镜像里是否带了不该带的引用。这篇内容会从远端钩子策略、分支保护、权限配置三个方向切入,给出可复制的git config和push命令组合,并演示如何通过 TaoToken 统一 Key/API 通道验证推送结果,目标是一次性定位拒绝原因并完成镜像推送。
适合谁看:正在做仓库迁移、双仓库周期同步、CI 镜像备份的开发和运维同学。如果你只是偶尔 push 一个分支,基本不会碰到这个问题;但只要你用--mirror,就迟早会遇到。
2. TaoToken 统一 Key 通道的前置准备与接入配置
在讲推送修复之前,先说明为什么这里要引入 TaoToken。镜像推送本身是纯 Git 操作,和模型通道没关系。但在实际工程里,仓库迁移往往伴随着 CI/CD 流水线重建、自动化脚本改造、以及用 AI 辅助排查报错。TaoToken 提供的是统一 Key 和 API 通道,让你在多个模型和工具之间用同一套凭证,省去到处配置的麻烦。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。
前置准备分两步。第一步是拿到统一 Key。进入控制台的 API Keys 页面创建密钥,地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。创建后复制保存,后面所有工具都用这一个 Key。第二步是确认你要接入的工具类型。如果你用的是 Claude Code 这类编码 Agent,需要配置 Base URL、Key、Model ID 三件套;如果你只是想在脚本里调用模型做报错分析,直接用 API 通道即可。
对于 Claude Code 的接入,配置文件通常放在用户目录下的 settings 文件里。下面是一个可复制的 JSON 片段,路径和字段名按实际工具要求填写:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的统一Key", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }如果你用的是 Codex 类的工具,配置写在auth.json里,同样是 Base URL、Key、Model ID 三件套:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的统一Key", "model": "gpt-4.1" }Cline 或 MCP 类的工具,配置方式类似,核心是把请求地址指向统一通道,把 Key 填成你创建的那一个。这里要强调:Base URL 必须写完整,Key 必须和创建时一致,Model ID 必须是通道支持的模型名,三者缺一不可。很多人接入失败就是因为 Model ID 写了个不存在的名字,或者 Base URL 少写了路径。
配置完成后,你可以用模型对话页面快速验证通道是否通,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。如果对话能正常返回,说明 Key 和通道没问题,接下来就可以把精力放回 Git 推送本身。如果你需要长期做编码和 Agent 任务,可以考虑 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到字段不确定的时候对照文档改。Claude Code 的专项接入说明在 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。
3. 可复制的 git config 与 push 命令组合修复镜像推送
现在进入正题。假设你已经用git clone --mirror把源仓库镜像到本地,目录名叫repo.git,目标仓库地址是git@target.com:group/repo.git。第一步不是急着 push,而是先看清楚本地镜像里到底有哪些引用。
cd repo.git git for-each-ref --format='%(refname)' | sort这条命令会列出所有引用,包括refs/heads/*、refs/tags/*,以及可能存在的refs/pull/*、refs/merge-requests/*。如果看到refs/pull/或refs/merge-requests/开头的引用,这些在目标平台通常不允许创建,需要排除。
第二步,检查目标仓库的分支保护规则。这个在网页端设置里看,不同平台位置不同,但关键词都是「分支保护」「Protected Branches」「Branch Permissions」。确认你的账号对目标分支有推送权限,尤其是master、main、release/*。如果目标仓库要求走合并请求,那--mirror的强制推送一定会被拒。
第三步,配置 Git 的推送行为。下面这组git config可以帮你减少不必要的引用推送:
git config --local push.default matching git config --local remote.origin.mirror true git config --local http.postBuffer 524288000push.default matching让推送行为和镜像语义一致,http.postBuffer调大缓冲区,避免大仓库推送时因为缓冲区不足中断。如果你走 SSH,http.postBuffer不影响,但留着无害。
第四步,用精确的 refspec 推送,而不是无脑--mirror。这是解决pre-receive hook declined的关键技巧。--mirror会推送所有引用,包括那些目标平台不接受的。改成只推分支和标签:
git push git@target.com:group/repo.git \ +refs/heads/*:refs/heads/* \ +refs/tags/*:refs/tags/*前面的+表示允许强制更新,镜像场景下必须加。如果你确认某些分支不能推,可以进一步收窄:
git push git@target.com:group/repo.git \ +refs/heads/main:refs/heads/main \ +refs/heads/develop:refs/heads/develop \ +refs/tags/*:refs/tags/*第五步,如果报错点名了某个分支,比如tmp-branch-xxxx或cherry-pick,先在本地删掉这个引用再推:
git branch -D tmp-branch-xxxx git push git@target.com:group/repo.git +refs/heads/*:refs/heads/*注意,git branch -D删的是本地镜像仓库里的分支引用,不影响源仓库。删完之后git for-each-ref再确认一次,确保没有残留。
第六步,如果目标仓库确实需要保留所有分支,但平台又保留了某些名字,那就只能重命名。比如把cherry-pick改成cherry-pick-backup:
git branch -m cherry-pick cherry-pick-backup git push git@target.com:group/repo.git +refs/heads/*:refs/heads/*重命名之后,源仓库和目标仓库的分支名会不一致,这点要提前和团队确认。如果只是做备份镜像,名字不一致通常可以接受;如果要做双向同步,就得另想办法。
第七步,把上面的命令固化成脚本。下面是一个可复制的 bash 片段,处理单个仓库的镜像推送:
#!/bin/bash set -euo pipefail SOURCE_REPO="$1" TARGET_REPO="$2" TMP_DIR="/tmp/git_mirror_$(date +%s)" REPO_NAME=$(basename "$SOURCE_REPO" .git) mkdir -p "$TMP_DIR" cd "$TMP_DIR" git clone --mirror "$SOURCE_REPO" "$REPO_NAME" cd "$REPO_NAME" # 删除平台保留的临时分支 for br in $(git for-each-ref --format='%(refname:short)' refs/heads/ | grep -E '^(tmp-|cherry-pick$|revert$)'); do echo "删除保留分支: $br" git branch -D "$br" done # 精确推送分支和标签 git push "$TARGET_REPO" \ +refs/heads/*:refs/heads/* \ +refs/tags/*:refs/tags/* cd "$TMP_DIR" rm -rf "$REPO_NAME" echo "镜像推送完成: $REPO_NAME"这个脚本比原始版本多了两个关键点:一是主动清理保留分支,二是用精确 refspec 代替--mirror。实测下来,这两步能解决大部分pre-receive hook declined。
4. 验证推送请求与成功结果
推送命令执行后,怎么确认真的成功了?不能只看终端没报错,因为有些平台会返回部分成功。下面几个验证步骤建议都做一遍。
第一步,看推送输出。成功的输出长这样:
Enumerating objects: 1234, done. Counting objects: 100% (1234/1234), done. Delta compression using up to 8 threads Compressing objects: 100% (800/800), done. Writing objects: 100% (1234/1234), 2.5 MiB | 3.2 MiB/s, done. Total 1234 (delta 400), reused 1000 (delta 300) remote: Resolving deltas: 100% (400/400), done. To git@target.com:group/repo.git * [new branch] main -> main * [new branch] develop -> develop * [new tag] v1.0.0 -> v1.0.0如果看到[new branch]或[updated],说明引用写入成功。如果看到! [remote rejected],说明还有引用被拒,需要回到上一步继续排查。
第二步,在目标仓库端验证引用。最直接的方式是重新克隆一个裸仓库,对比引用列表:
git clone --mirror git@target.com:group/repo.git /tmp/verify.git cd /tmp/verify.git git for-each-ref --format='%(refname)' | sort > /tmp/target_refs.txt然后在本地镜像仓库里生成源引用列表:
cd /path/to/repo.git git for-each-ref --format='%(refname)' | sort > /tmp/source_refs.txt对比两个文件:
diff /tmp/source_refs.txt /tmp/target_refs.txt如果没有输出,说明引用完全一致,镜像成功。如果有差异,差异行就是没推上去的引用,针对性处理。
第三步,验证提交内容。引用一致不代表提交对象完整,可以抽查一个分支的 HEAD:
git ls-remote git@target.com:group/repo.git refs/heads/main把返回的 commit hash 和源仓库对比:
git rev-parse refs/heads/main两个 hash 一致,说明该分支的提交对象完整推送。
第四步,用 TaoToken 通道做辅助验证。如果你在 CI 脚本里集成了模型调用,可以让模型帮你分析推送日志。比如把推送输出贴给模型,问「这批引用里哪些被拒了,可能原因是什么」。模型对话入口是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。这一步不是必须的,但在批量迁移几十个仓库时,用模型快速归类报错能省不少时间。
第五步,检查目标仓库的钩子日志。有些平台会在服务端记录pre-receive钩子的拒绝原因,位置在仓库设置或管理后台的日志里。如果终端输出不够详细,去这里看原始拒绝信息,通常能直接看到是哪个规则触发的。
第六步,确认标签也推上去了。很多人只检查分支,忘了标签。git ls-remote --tags可以列出远端所有标签:
git ls-remote --tags git@target.com:group/repo.git对比源仓库的标签列表,数量一致才算完整。
5. 本篇常见报错排查对照
这一节把镜像推送过程中最常见的报错列出来,对照着排查。
报错一:pre-receive hook declined且点名cherry-pick或revert
remote: The keywords cherry-pick and revert are branch names reserved by CodeHub remote: You are not allowed to create. ! [remote rejected] cherry-pick -> cherry-pick (pre-receive hook declined)原因:目标平台保留了这些分支名。解决:本地删除或重命名该分支,再推。
git branch -D cherry-pick git push git@target.com:group/repo.git +refs/heads/*:refs/heads/*报错二:pre-receive hook declined且点名master或main
! [remote rejected] main -> main (pre-receive hook declined)原因:目标分支受保护,不允许强制推送。解决:在目标仓库设置里临时关闭分支保护,或者用有权限的账号推送。如果平台要求走合并请求,那--mirror本身就不适用,需要改用普通 push 加 PR 流程。
报错三:remote: You are not allowed to push code to this project
remote: You are not allowed to push code to this project. fatal: unable to access '...': The requested URL returned error: 403原因:账号权限不足。解决:确认 SSH Key 已添加到目标平台账号,且该账号对目标仓库有写权限。如果是 HTTPS,检查 token 是否过期。
报错四:local proxy failed或连接超时
fatal: unable to access '...': Failed to connect to ... port 443: Connection timed out原因:网络不通或地址写错。解决:检查目标仓库地址是否正确,确认当前网络能访问目标平台。注意,这里不涉及任何网络工具,纯粹是地址和网络连通性问题。
报错五:error: cannot spawn git: No such file or directory
原因:Git 环境变量或 PATH 配置有问题。解决:确认git --version能正常输出,检查脚本里的 Git 路径。
报错六:reading choices相关错误
如果你在 CI 里用模型辅助分析日志,偶尔会看到reading choices之类的返回解析错误。这通常是模型响应格式和脚本预期不一致导致的。解决:检查 API 返回的 JSON 结构,确认解析字段名正确。TaoToken 的接入文档里有返回格式说明,对照 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 调整。
报错七:OAuth相关认证失败
remote: OAuth authentication failed原因:凭证过期或权限范围不足。解决:重新生成 token 或重新授权。如果是 SSH,检查 Key 是否被平台吊销。
报错八:推送大仓库时中断
error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500原因:仓库太大,单次推送超时或缓冲区不足。解决:调大http.postBuffer,或者分批推送分支。
git config --local http.postBuffer 1048576000 git push git@target.com:group/repo.git +refs/heads/main:refs/heads/main报错九:refs/pull/*被拒
! [remote rejected] refs/pull/1/head -> refs/pull/1/head (pre-receive hook declined)原因:镜像里带了平台特有的 pull 引用。解决:不要用--mirror,改用精确 refspec 只推分支和标签。
报错十:deny updating a hidden ref
原因:推送了隐藏引用。解决:同样改用精确 refspec,排除refs/pull/*和refs/merge-requests/*。
排查顺序建议:先看报错点名了哪个引用,再查目标仓库的保护规则和权限,最后检查本地镜像的引用列表。大部分问题在前两步就能定位。
6. 长期镜像同步的工程化建议与通道选择
单次修复解决的是眼前问题,但如果你要做的是周期性同步,比如每天或每小时把源仓库镜像到目标仓库,那就需要工程化处理。下面几条是实际踩过坑之后总结的建议。
第一,不要用--mirror做周期性推送。--mirror的语义是「让目标仓库和源仓库完全一致」,包括删除目标仓库里源仓库没有的引用。这在首次迁移时有用,但周期性同步时很危险,一旦源仓库临时删了个分支,目标仓库也会跟着删。改用精确 refspec,只推分支和标签,不删远端。
第二,把保留分支的清理逻辑固化到脚本里。不同平台的保留分支名不一样,但常见的就那么几个:cherry-pick、revert、tmp-*、temp-*。在 clone 之后、push 之前,统一过滤一遍。
第三,给推送加超时和重试。大仓库推送偶尔会因为网络抖动中断,脚本里加个重试逻辑:
for i in 1 2 3; do if git push "$TARGET_REPO" +refs/heads/*:refs/heads/* +refs/tags/*:refs/tags/*; then break fi echo "推送失败,第 $i 次重试..." sleep 5 done第四,记录每次同步的引用差异。同步完成后,对比源和目标引用列表,把差异写进日志。这样出问题时能快速定位是哪个分支没同步。
第五,如果同步任务里需要模型辅助分析日志或生成报告,用统一 Key 通道能省去多套凭证管理。Coding Plan 适合长期跑 Agent 任务的场景,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。API Keys 管理入口在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
第六,目标仓库的分支保护规则要提前和团队对齐。如果目标仓库要求所有变更走合并请求,那镜像推送这条路本身就走不通,需要改用「推送特性分支 + 自动创建 PR」的方案。这个决策要在迁移开始前定好,不要等推送被拒了才发现。
最后说一个实际经验:镜像推送失败时,终端输出往往只给一行pre-receive hook declined,真正的拒绝原因在服务端钩子日志里。如果平台不暴露钩子日志,就靠报错里点名的引用反推。大多数情况下,报错里提到的那个分支名就是问题所在。删掉它,或者重命名它,再推一次,问题就解决了。