跨端: 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,或直接断言上面两条。
This commit is contained in:
@ -410,15 +410,43 @@ test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是
|
||||
* 可达性那一侧我立了"问不到 ⇒ 红",**覆盖面**这一侧却把
|
||||
* "我没枚举到的 ref"**静默当成干净** —— **"看不到 ⇒ 绿"**。
|
||||
*
|
||||
* 修法(pi 给的,**不需要 fetch 任何对象**):`ls-remote` 问**全部 ref**,
|
||||
* 对**每一条**的 tip 都查该路径是否在其可达历史里。查明"远端有哪些 ref"是
|
||||
* `ls-remote` 的免费信息,没有理由只问一条。
|
||||
* 修法(pi 给的):`ls-remote` 问**全部 ref**,对**每一条**的 tip 都查该路径是否在其
|
||||
* 可达历史里。查明"远端有哪些 ref"是 `ls-remote` 的免费信息,没有理由只问一条。
|
||||
*
|
||||
* ⚠️ 我**没有**把"远端 ref 集合恰好等于 HEAD + refs/heads/main"写成不变量 ——
|
||||
* 那会在加第一个 tag / 第一条正常旁支时误红(**为了抓泄露而给日常操作设卡**,
|
||||
* 与 `pre-push` 删 ref 那次同族:**一道闸消费了不属于它管辖的东西**)。
|
||||
* 真正要判的性质是"**有没有哪条 ref 的可达历史里有那个 blob**",
|
||||
* 它对**任意** ref 集合都成立。
|
||||
*
|
||||
* ★★ 但我当时写的**理由**是错的,pi 2026-09-15 驳倒了它,我照他的办法重测也复现了:
|
||||
* 我写过"`git log <sha> -- <路径>` 查的是从该 sha 可达的全部历史,**所以未 fetch 的
|
||||
* 对象也在其列**(这一点我实测过)"。**不成立**:
|
||||
* `git log <sha>` **必须先有这个对象**才能走可达历史;本地没有 ⇒
|
||||
* `fatal: bad object <sha>`(退出码 128)。
|
||||
* 实测(`/tmp` 一次性仓库,clone 之后才把新提交推到新 ref):
|
||||
* $ git cat-file -e <sha> → 没有
|
||||
* $ git log --oneline <sha> -- agc.json
|
||||
* fatal: bad object 953c6138…
|
||||
* 我那次"实测过"大概是测到了**对象恰好在本地**的情形(那一轮我推的 tag 指向的提交
|
||||
* 同时也在 main 上,clone 时就跟着下来了)——
|
||||
* **又是"读数器没先被证明是好的",而且这次我把一次假读数写成了"实测过"。**
|
||||
*
|
||||
* ⇒ 于是**代码实现的规则和那段理由相反**:未 fetch 的 ref 会落进 `unresolved` ⇒ 红。
|
||||
* 也就是说**同事在远端新建一条完全良性的 tag,只要这个 clone 没 fetch 到,这条判据就红**。
|
||||
* 我不反对这个方向("查不了 ≠ 干净",方向安全),但**理由必须改成这个说法**:
|
||||
* 真正的不变量是"**这个 clone 必须拿到远端每一条 ref 的对象,否则本条红**"。
|
||||
* 否则读那段理由的人会以为良性 tag 是"无事发生",第一次撞红时会当成误报去消掉它 ——
|
||||
* 那正是这条判据最可能被消掉的路径。
|
||||
*
|
||||
* ★★ 而且**照最自然的做法 fetch 也修不好它**(pi 实测,我也复现):
|
||||
* $ git fetch origin → 分支的对象有了;**tag-only 的还是没有**
|
||||
* $ git fetch --tags origin → 这才有
|
||||
* (tag 跟随只跟随"指向本地已有对象的 tag",所以不在任何分支上的 tag 普通 fetch 拉不下来。)
|
||||
*
|
||||
* ⇒ 所以这条判据**自己把缺的对象拿回来**(做法见下):精确抓**那一条** ref 到
|
||||
* `refs/agentmail-probe/*` 命名空间 —— **不碰用户的 ref、不拉全仓、不动工作树**,
|
||||
* 拉完再判。拿不到才报红,并且**报错自带修法**(本仓规矩)。
|
||||
*/
|
||||
const lsr = git(['ls-remote', 'origin']);
|
||||
assert.equal(lsr.status, 0,
|
||||
@ -442,24 +470,45 @@ test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是
|
||||
+ `\n 原始输出:${JSON.stringify(lsr.stdout.slice(0, 200))}`);
|
||||
|
||||
/*
|
||||
* 对**每一条**远端 ref 的 tip 查该路径。`git log <sha> -- <路径>` 查的是
|
||||
* **从该 sha 可达的全部历史**,所以未 fetch 的对象也在其列(这一点我实测过才这么写)。
|
||||
* 解析不了的(annotated tag 指向 tag 对象等)**报 unknown 而不是绿**。
|
||||
* 对**每一条**远端 ref 的 tip 查该路径(`git log <sha> -- <路径>` = 从该 sha 可达的历史)。
|
||||
* **本地没有那个对象就抓那一条**(精确,见上),抓完再查。抓不到 ⇒ unknown(红),不当绿。
|
||||
*/
|
||||
const probeRef = ref => `refs/agentmail-probe/${ref.replace(/^refs\//, '').replace(/\//g, '-')}`;
|
||||
const published = [];
|
||||
const unresolved = [];
|
||||
const fetched = [];
|
||||
for (const { ref, sha } of remoteRefs) {
|
||||
const lg = git(['log', '--oneline', sha, '--', AGC_PATH]);
|
||||
if (lg.status !== 0) { unresolved.push(`${ref}(${sha.slice(0, 8)})`); continue; }
|
||||
let lg = git(['log', '--oneline', sha, '--', AGC_PATH]);
|
||||
if (lg.status !== 0) {
|
||||
// 本地缺这个对象 ⇒ 精确抓这一条(--no-tags 免得顺带拉别的 tag)
|
||||
const dst = probeRef(ref);
|
||||
const f = git(['fetch', '--no-tags', 'origin', `+${ref}:${dst}`]);
|
||||
lg = git(['log', '--oneline', sha, '--', AGC_PATH]);
|
||||
if (f.status !== 0 || lg.status !== 0) {
|
||||
unresolved.push(`${ref}(${sha.slice(0, 8)})`
|
||||
+ (f.status !== 0 ? `\n fetch 也失败:${(f.stderr || '').trim().split('\n')[0].slice(0, 90)}` : ''));
|
||||
continue;
|
||||
}
|
||||
fetched.push(`${ref}(抓到 ${dst},本次判定后删除)`);
|
||||
}
|
||||
if (lg.stdout.trim() !== '') {
|
||||
published.push(`[${ref}] ${sha.slice(0, 8)}\n`
|
||||
+ lg.stdout.trim().split('\n').map(l => ' ' + l).join('\n'));
|
||||
}
|
||||
}
|
||||
// 探针 ref 只为本条判据存在 —— 用完即删,不给仓库留垃圾(也不影响上面已收集的结论)
|
||||
for (const { ref } of remoteRefs) git(['update-ref', '-d', probeRef(ref)]);
|
||||
|
||||
assert.deepEqual(unresolved, [],
|
||||
'这几条远端 ref 的 tip **查不了可达历史**(`git log <sha> -- <路径>` 失败)——\n'
|
||||
'这几条远端 ref 的 tip **拿不到、也抓不回来** ——\n'
|
||||
+ ' ★ 按"不知道"处理,**不当绿**:**查不了 ≠ 干净**。\n'
|
||||
+ ' (常见成因:annotated tag 指向 tag 对象,或该对象本地没有且远端也不可达。)\n'
|
||||
+ ' ★ 真因不是"远端不可达"(sha 是从 `ls-remote` 拿的,**远端当然可达它**),\n'
|
||||
+ ' 而是"**本地没有这个对象**",且 `git log <sha>` 必须先有对象。\n'
|
||||
+ ' ★ 修法(**只 `git fetch` 不够** —— 它默认只抓 `refs/heads/*`,\n'
|
||||
+ ' 不在任何分支上的 tag 抓不下来):\n'
|
||||
+ ' git fetch --tags origin\n'
|
||||
+ ' 本条本来会自己精确抓那一条,这次没成 —— 所以是远端/网络/权限的问题,\n'
|
||||
+ ' 不是"少 fetch 了一下"。确认远端可达再跑。\n'
|
||||
+ ` ${unresolved.join('\n ')}`);
|
||||
|
||||
assert.deepEqual(published, [],
|
||||
@ -471,4 +520,12 @@ test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是
|
||||
+ ' 2) 历史清理(filter-repo)+ 强推,并与所有 clone 过的人对齐;\n'
|
||||
+ ' 3) 查清是哪次 push 绕过了 `.githooks/pre-push`(`--no-verify`?没装的机器?)。\n'
|
||||
+ ` 命中:\n ${published.join('\n ')}`);
|
||||
if (fetched.length > 0) {
|
||||
/*
|
||||
* 本次为了判定**临时抓过对象**,必须说出来:这是一条判据在对仓库做**写操作**,
|
||||
* 读者有权知道"绿是在什么代价/什么前提下拿到的"。不说 = 让判据偷偷改仓库。
|
||||
*/
|
||||
console.log(` (本条本次临时抓了 ${fetched.length} 条远端 ref 的对象:`
|
||||
+ fetched.map(s => s.split('(')[0]).join('、') + ')');
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user