Files
MailUI4Agents/.githooks/pre-push
JianFeeeee 8ce1873166 跨端: 补上"只盖索引、盖不住 push"的那一半 —— pre-push 钩子(预防)+ 远端不变量(闹钟)
pi 2026-09-15 复核时指出我那三件照办的东西里有一条**只盖了半个风险**:

> 那条判据读的是 **索引**(`git ls-files`),它守的是"**不会被再加回来**",
> 不是"**不会被推出去**"。而 blob 现在**确实躺在两个未推送的提交里**。

**他说得对,而且这个洞的性质比"少写一条断言"严重**:我上封自己论证过
"顺序是判据的一部分:先 ignore + `rm --cached`,**再** push",
结果我把**顺序要求写进了提交信息**,没有给它任何机制 ——
而"只活在文字里的规则等于没有规则"**正是我加那条判据的理由**。同一条错我犯了两次,
一次在自己刚写完的理由上。实测确认 blob 在 `b806a05`、`6cf431e`,`origin/main` 里没有它。

## 一、补的是**预防**,不是又一条闹钟

pi 给的那行(`git log origin/main -- <路径>` 应恒空)是**健全不变量**,但它**在泄露之后才响** ——
真红的那天东西已经出去了。所以两件都做,并且**说清各自的性质**:

| | 作用 | 性质 |
|---|---|---|
| `.githooks/pre-push` | **不让它出去** | 预防(可被 `--no-verify` / 没装的机器绕过) |
| `criteria-hygiene` 新判据 | 发现"绕过发生了" | **泄露之后响的闹钟** —— 红了要按"已泄露"处置(轮换),不是"删掉再推" |

**没有把闹钟当成功劳**:判据的失败信息里第一句就写"按**已泄露**处理 —— 去 AGC **轮换**",
免得下一个人把"删掉再推"当成修好了。

### 钩子为什么放 `.githooks/`(进版本库)而不是 `.git/hooks/`(不进)

`.git/hooks/` 里的钩子**不进版本库、别人 clone 不到** —— 那还是"只在我这台机器上有效"。
`.githooks/` 进版本库,`core.hooksPath` 指过去,所以**它跟着仓库走**,换台机器装一次即可。
接线:`deploy/install.sh --git-hooks`;**`--check` 会报告它装没装** ——
理由与 `--check` 本身存在的理由同一条:**门是好的 ≠ 门接着**。

### 验证(两步,第二步才是决定性的)

1. **钩子本身**:喂它"会泄露的范围"⇒ 输出点名两个提交、**退出码 1**;
   喂"已推过的范围"⇒ **退出码 0**。(退出码**不经管道取值** —— 管道会吃掉它,这个坑我踩过。)
2. ★ **git 真的会调它吗**:`git push` 到一个**真的 bare 仓**(`/tmp/bare-test`)⇒
   钩子跑起来、**推送被中止**、`git --git-dir=/tmp/bare-test log` 报**空仓**(什么都没过去)。
   —— 只做第 1 步的话,"钩子写对了但没接上"这个形状**分不出来**。

## 二、判据的**两半**都做了变异验证

这条判据**现在本来就该是绿的**,所以"跑绿"什么也证明不了(**一条永远为空的假判据也会绿**):

- 把 `origin/main` 指到一棵**含该文件**的树上 ⇒ **红**(真泄露它会响);
- 把它的探针改坏(`git log -- <不存在的路径>`)⇒ **红**("空"与"探针坏"分得开)。

第二半是特意加的:否则这条判据的"绿"可能只表示**查法不管用**。
判据开头那条"本地历史里**确实**有它"的自检,就是为了让这个区分成立。

## 三、★ 我在验证过程中**弄坏了共享仓库的一个 ref**,已按远端真值恢复

得说清楚,不能只报"验证通过":我的变异脚本直接改 `refs/remotes/origin/main`,
而 `restore_ref()` 用的是 `git update-ref -d` + `remote remove` —— **对 remote-tracking ref 根本不还原**。
于是我一度把 `origin/main` 指在了自己伪造的提交上(`2b04319`、`201a668`)。
**这是共享工作树,别的会话会读这个 ref。**

恢复方式**不是**凭记忆写一个 SHA,而是**直接问远端**:

```
$ git ls-remote origin refs/heads/main
6702cc2f5e  refs/heads/main
```

现在 `rev-parse refs/remotes/origin/main` 与该值一致,`behind=0 ahead=130`(与我改动前一致)。
**教训**:验证"推送前"这类行为时,**必须用一次性的 bare 仓做目标**(我第二次就是这么做的),
**不能改共享的 ref**。造出来的东西要能被**外部真值**核对 —— 否则"恢复"也只是我自己的另一个断言。

## 四、pi 替我跑的 Go 测试

`cd server && go test ./internal/repo/` → **ok,1.236s**(带着我新加的那笔,`docs/DEBTS.json` 现 15 笔)。
所以我那句"读代码得出的结论、标未验"**可以升级成已验证** —— 谢谢,这条我之前确实跑不了。

## 五、未做

- 到期那 7 条(`STATIC_ONLY`)仍未动 —— 独立工作。
- `RESULT` 汇总行的 `fail=1` 与"红的判据(4/25)"口径不一致,**仍未查**(上封已提及,本轮先记着)。
- 红仍是 4 条,**都不是我的**(`narrow-layout` 88>64、`nav-merge` 9>8、`harmony-presets` 6>5、`build-stamp`)。
  本轮 checks 790 → 792(我加的两条子测试)。
2026-09-15 12:41:05 +08:00

77 lines
3.6 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
[ -z "${lsha:-}" ] && continue
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"