Files
MailUI4Agents/client/electron
JianFeeeee 2887ea8acd 跨端: AGC 闹钟:纠正"未 fetch 的对象也在其列"这一假前提 + 自抓缺失对象(良性 tag 不再误红)+ 报错补修法
pi 2026-09-15 驳倒了我这条前提,**他是对的,我照他的办法重测复现了**。

## 一★ 我写过"未 fetch 的对象也在其列(这一点我实测过)" —— **不成立**

`git log <sha>` **必须先有这个对象**才能走可达历史。本地没有 ⇒ `fatal: bad object`(退出码 128)。

实测(`/tmp` 一次性仓库,clone **之后**才把新提交推到新 ref):

```
$ git cat-file -e 953c6138   → 没有
$ git log --oneline 953c6138 -- agc.json
fatal: bad object 953c6138a7320d73773d9b1253042d6680a97364
```

★ 我那次"实测过"大概是测到了**对象恰好在本地**的情形 —— 那一轮我推的 tag 指向的提交
**同时也在 main 上**,clone 时就跟着下来了。**又是"读数器没先被证明是好的",
而这次我把一次假读数写成了"实测过"。** 这比单纯写错更坏:它让那句错话带着证据的外衣。

## 二★ 于是代码实现的规则与那段理由**相反**:它**确实**给日常 tag 设了卡

未 fetch 的 ref 落进 `unresolved` ⇒ 红。实测(旧行为,把 self-fetch 去掉后):
远端加一条**完全良性**的 tag-only ref ⇒

```
not ok 6 … 这几条远端 ref 的 tip 拿不到、也抓不回来
```

方向我仍认("查不了 ≠ 干净"),但**理由改了**:
真正的不变量是"**这个 clone 必须拿到远端每一条 ref 的对象,否则本条红**"。
不改这句,读的人会以为良性 tag 是"无事发生",第一次撞红时当误报消掉 ——
**那正是这条判据最可能被消掉的路径。**

## 三★ `git fetch` 修不好它,只有 `--tags` 行

pi 实测,我也复现:

```
$ git fetch origin         → 分支的对象有了;**tag-only 的还是没有**
$ git fetch --tags origin  → 这才有
```

(tag 跟随只跟随"指向本地已有对象的 tag"。)

**所以我让这条判据自己把缺的对象拿回来**,而且是**精确抓**:

```
git fetch --no-tags origin +refs/tags/<name>:refs/agentmail-probe/<name>
```

**不碰用户的 ref、不拉全仓、不动工作树**;判完 **`update-ref -d` 删掉探针 ref**(不留垃圾)。
拿不到才报红,并且**报错自带修法**(本仓规矩),且修法里写明**只 `git fetch` 不够**。

抓过对象时必须**说出来**(`(本条本次临时抓了 N 条…)`):
一条判据在对仓库做写操作,读者有权知道"这个绿是在什么前提下拿到的"——
不说就等于让判据偷偷改仓库。

顺带改掉两处错话:
1. 旧报错写"annotated tag 指向 tag 对象,**或该对象本地没有且远端也不可达**"——
   后半句是错的:sha 是从 `ls-remote` 拿的,**远端当然可达它**;真因是**本地没有这个对象**。
2. 旧报错**没给修法** —— 而"报错自带修法"是本仓的规矩(AGC 那条正文自己就这么写)。
   **不给修法的红会被当噪音**,而这条判据最不需要的就是被当噪音。

## 四、验证(`/tmp` 一次性仓,共享仓只读)

| 情形 | 旧 | 新 |
|---|---|---|
| 良性 tag-only ref(不含该路径) | **红(误红)** | **ok 6**,并打印"临时抓了 1 条" |
| **真泄露**在 tag-only ref 上(本地无该对象) | 红,但理由错("抓不回来") | **红,点名 `[refs/tags/leak-orphan]`** |
| annotated tag / 旁支 / 干净 main | 红 / 红 / 绿 | 同(未退化) |

探针 ref 每次判完 **0 条残留**;用户的 ref 未被改(当时只有 `refs/heads/localfull`
与 `refs/remotes/origin/main`)。

## 五、★★ 我又把共享仓改坏了 —— 这次是 `.git/config` 里的 `origin` URL

做上面那些对照时我在 `/tmp/w*` 里跑 `git remote set-url origin /tmp/q-origin`,
**以为那是 clone** —— 但 `/tmp/w*` 是我用 `git clone /home/program/agentmail` 建的,
而那棵树里 **`.git` 是指向共享仓的 worktree 指针**(`/tmp/pi-verify-head`、`/tmp/wt-p2` 同)。
`git remote set-url` 写的是 `$GIT_COMMON_DIR/config` ⇒ **改了共享仓的配置**。

后果:`git ls-remote origin` 变 `'/tmp/q-origin' does not appear to be a git repository`,
AGC 判据红。

**恢复方式(对着远端真值,不是对着记忆)**:

```
$ git ls-remote https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git refs/heads/main
6702cc2f5e    ← 与本地 origin/main 一致
$ git remote set-url origin https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git
$ 复核:ls-remote == refs/remotes/origin/main  ✓
```

改动面:只动了 `.git/config` 的一个 key,其余节(`core.hooksPath`、`user`、`branch.main`)
未被触碰;两个 `/tmp` worktree 现在都解析到正确的 origin。

★ 教训(**与上次弄坏 `refs/remotes/origin/main` 是同一族、这是第二次**):
**在共享仓里"以为自己在副本里"是最危险的一类操作。**
上次我损坏的是 ref,这次损坏的是 config —— 两次都是**写操作打在共享 git 目录上**。
可执行的自保:**在 /tmp 造隔离副本前,先断言它有自己的 `.git` 目录**:

```
[ -d /tmp/xxx/.git ]        # 不是文件(worktree 的 .git 是**文件**,指向共享 gitdir)
git -C /tmp/xxx rev-parse --git-common-dir   # 必须等于 /tmp/xxx/.git
```

`git clone <本地路径>` 会因为 hardlink/worktree 语义把 `.git` 指回共享仓 ——
**所以"克隆一份来试"这个动作本身就可能已经在碰共享状态**。
下次做这类对照,先建一个**真 bare 源**再 clone,或直接断言上面两条。
2026-09-15 13:09:12 +08:00
..