Files
MailUI4Agents/.githooks/pre-push
JianFeeeee 4d78fc1580 跨端: 钩子删 ref 的**误红**(全零 sha ≠ 空串)+ 闹钟改读**远端真值**(本地 ref 可被改坏)
pi 2026-09-15 复核时找到两条**一行级**的错,两条都对,都已修。

## 一、`.githooks/pre-push`:删除 ref 被误判成"无法解析范围"

根因一行(原 `:33`):

```bash
[ -z "${lsha:-}" ] && continue        # 判的是**空**,而 git 表示"删除"用的是**全零 sha**
```

于是删除时走 `else`,拼出 `旧sha..0000…`,`git log` 解析不了 ⇒ `exit 1` 中止。实测复现(真 bare 仓):

```
$ git push /tmp/bare-del --delete tmp
pre-push: 无法解析范围 '6702cc2f…..0000…' —— 中止推送(宁可推不出去,也不要盲推)
error: failed to push some refs
```

**为什么这不是小事**:**删除不发布任何东西** ⇒ 这是**纯误红**;
而它**恰恰因为"宁可推不出去"这条正确的方向而不可见** —— 看起来就像设计好的安全行为。
同族形状:**一道闸消费了不属于它管辖的东西**("到期闸把汇总一起吃掉"也是这个)。
附带效果:`install.sh --check` 会一直说"钩子已接",而删分支/删 tag 全被挡。

★ **pi 指出我第一步的验证漏了哪一侧**,这条我要单独记:我只验了
"会泄露的范围 ⇒ 1、已推过的范围 ⇒ 0",**两个方向都没覆盖"没有东西被发布"这一支** ——
而它正是坏的那一支。**和"只判下界"是同一条:把边界少判了一侧。**

修后三支全验(真 bare 仓):

| 情形 | 修前 | 修后 |
|---|---|---|
| 干净推入 | 0 | **0** |
| **删除分支** | **1(误红)** | **0** |
| 含 AGC 的范围 | 1 | **1**(仍拦) |

并且验了远端**真的没收到** main(`git --git-dir=… rev-parse --verify refs/heads/main` 不存在)。

## 二、闹钟读的 `origin/main` 是**本地可改**的那条 —— 我自己演示过

pi 的推论很硬:我上一轮判"从未发布 ⇒ 不轮换",用的就是 `git log origin/main -- <路径>`;
而**这条 ref 我一度指到自己伪造的提交上**。
⇒ 只读它的话,"**ref 被改坏了**"与"**它其实被推过**"是**同一个盲区**。
(他并且**对着远端真值重核了那条裁定**,结论仍成立 —— 但那是他补的,不是那条判据自己保证的。)

改法:先问**远端真值**,并且**必须能证明本地 ref == 远端 tip**,本地历史才可信。三值语义:

| 状态 | 处置 |
|---|---|
| 一致 | 判本地历史 |
| **不一致** | **红** —— 本地 ref 陈旧或被改过,此时"绿"毫无意义 |
| **问不到** | **红并明说"这是不知道,不是安全"** |

★ 第三支是**特意**做成红的:一条专门用来抓"绕过"的闹钟,如果因为"不 fetch / 网络不通"
就报绿,那它**自己就能被绕过**。仓库里已有这个三值形状(`PROBES` 的 `unknown`),照它做。
(保留 `localTip.status` 检查:`rev-parse` 失败时报"没有 ≠ 干净"。)

## 三、验证:四态 + 三支,都在**独立副本/临时 bare 仓**里做

```
闹钟  基线(本地有该路径、远端没有)  → ok 6(绿)
      状态2 本地 ref ≠ 远端真值        → not ok(红)
      状态3 问不到远端                 → not ok(红,且理由是"不知道")
      真泄露 远端 main 含该路径        → not ok(红,点名"按已泄露处理/轮换")
钩子  干净推 0 / 删分支 0 / 含 AGC 1
```

**共享仓库一个字节都没动**:`origin/main = 6702cc2` 与远端一致,
refs 只有 `refs/heads/main` 与 `refs/remotes/origin/main`(无漂移)。

★ 这个验证脚本我改了**三版**,每一版的毛病都同族(**读数器没先被证明是好的**),
已全部写进脚本头注释:① `cp -a src/.git dst/.git` 在 dst 不存在时失败 ⇒ 在**没有 .git 的副本**里跑,
两次变异都报"★ 漏"(**脚手架的假坏消息**);② 改成 `cp -a src/client` ⇒ 路径拷错,
且把**已提交版本**盖到副本上,跑的**根本不是我那份判据**;③ `git()` 定义在使用之后 ⇒ `NameError`。
定稿只用 `git clone`、只同步**改过的单个文件**,并**断言副本 md5 == 源树 md5** 才往下走。

## 四、pi 那条去重我确认:`RESULT` 口径已查完

他在 `42631181` 里已经把它查成真 bug(累加块两份),我已在 `d23ef69` 修掉并加了自检 5。
本轮不重复处理。
2026-09-15 12:48:51 +08:00

94 lines
4.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#
# pre-push:**推送前**拦住"不该进版本库的文件"。
#
# ★ 为什么要有这个钩子,而不是只靠 `criteria-hygiene` 里那条判据:
# 那条判据读的是 **index**(`git ls-files`),它守的是"**不会被再加回来**",
# 不是"**不会被推出去**"。而 blob 恰恰躺在**未推送的提交**里 ——
# 也就是说 `git rm --cached` 之后,东西已经从 index 里消失了,
# 可是**历史里还有**,下一次 `git push` 会连它一起发出去,而**没有任何东西会红**。
# ⇒ "先 ignore + `rm --cached`、再 push"这条顺序**只活在提交信息里**,
# 而"只活在文字里的规则等于没有规则"正是我们加那条判据的理由。
# 判据是**泄露之后**响的闹钟;这个钩子是**不让它泄露**。两件事都要有。
#
# 这个钩子由 `deploy/install.sh --git-hooks` 装(`core.hooksPath` 指向 `.githooks`),
# 所以它**跟着仓库走**:换一台机器 clone 下来,装一次就都装上了。
#
# 退出码:0 放行,1 拦下(git 会中止 push)。**绝不返回 2** ——
# 钩子里非 0 一律中止推送,所以"钩子自己坏了"与"真的有违规"都会拦下来,
# 这是**故意的方向**:宁可推不出去,也不要静默推出去。
set -uo pipefail
# 不许进版本库的路径(**加新路径就加在这里**,别写进判据里到处复制)
FORBIDDEN=(
"client/harmony/entry/src/main/resources/rawfile/agconnect-services.json"
)
# 生效范围:推送的所有 ref 区间。钩子从 stdin 收到:
# <local ref> <local sha> <remote ref> <remote sha>
# 远端 sha 全 0 = 新分支 ⇒ 要查**整个可达历史**。
zero="0000000000000000000000000000000000000000"
declare -a RANGES=()
while read -r _lref lsha _rref rsha; do
# ★ **删除**用**全零** sha 表示,不是空串(原来是 `[ -z "${lsha:-}" ] && continue`)。
# 少了这一支的后果:删除时 lsha=全零、rsha=旧值 ⇒ 走 else ⇒ 拼出
# `旧sha..0000…` ⇒ `git log` 解析不了 ⇒ **exit 1 中止**。
# 实测(真 bare 仓):
# $ git push /tmp/bare-del.git --delete tmp
# pre-push: 无法解析范围 '6702cc2f…..0000000000000000000000000000000000000000' —— 中止推送
#
# 为什么这不是小事:**删除不发布任何东西**,所以这是**纯误红**;
# 而它恰恰因为"宁可推不出去"这条**正确的**方向而不可见 —— 看起来就像设计好的安全行为。
# 同族形状:**一道闸消费了不属于它管辖的东西**("到期闸把汇总一起吃掉"也是这个)。
# 附带效果:`install.sh --check` 会一直说"钩子已接",而实际上删分支/删 tag 全被挡。
#
# ★ 我第一步只验了"会泄露的范围 ⇒ 1、已推过的范围 ⇒ 0"——
# **两个方向都没覆盖"没有东西被发布"这一支**,而它正是坏的那一支。
# 和"只判下界"是同一条:**把边界少判了一侧**。
if [ -z "${lsha:-}" ] || [ "$lsha" = "$zero" ]; then
continue # 没有东西被发布(删除 / 空 ref)—— 放行
fi
if [ "$rsha" = "$zero" ]; then
# 新分支/新 tag:从根开始查(`--not --remotes` 会漏掉已被别的远端包含的情况)
RANGES+=("$lsha")
else
RANGES+=("${rsha}..${lsha}")
fi
done
if [ "${#RANGES[@]}" -eq 0 ]; then
exit 0 # 没有任何 ref 要推(例如只删远端分支)—— 放行
fi
problems=0
for path in "${FORBIDDEN[@]}"; do
for range in "${RANGES[@]}"; do
# `--all` 覆盖"这次推送带出去的所有提交里,这个路径是否被改动过"
if ! hits="$(git log --format=' %h %s' "$range" -- "$path" 2>/dev/null)"; then
echo "pre-push: 无法解析范围 '$range' —— 中止推送(宁可推不出去,也不要盲推)" >&2
exit 1
fi
if [ -n "$hits" ]; then
problems=1
{
echo ""
echo "✗ 这次推送会把 **$path** 的历史一起发出去。"
echo " 它是 AGC 客户端凭证,而本仓的镜像是**匿名可 clone 的公开项目**。"
echo " 涉及这些提交:"
echo "$hits"
echo ""
echo " 怎么处置(按顺序):"
echo " 1) 如果这些提交**已经**推上去过:按\"已泄露\"处理 —— 去 AGC 轮换,"
echo " 并且**先**在本地把这些提交改掉(rebase/filter-repo),再推。"
echo " 2) 如果还没推过:把涉及它的提交从待推范围里摘掉,或者先把历史里的 blob 清掉。"
echo " 3) 确认它**不在**要推的范围里之后,再 push。"
echo ""
echo " (这条钩子的存在理由:判据读的是 index,只证明\"不会再被加回来\";"
echo " 而 blob 还躺在未推送的提交里,只有推送前拦得住。)"
} >&2
fi
done
done
exit "$problems"