@minonl/dia 0.0.3 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -215,6 +215,31 @@ JS
215
215
  }
216
216
 
217
217
  # adopt 写入的路径模板(<git-common-dir>/dia/config.json 的 template);未 adopt → 空
218
+ # 仓根集合:<data>/repos + DIA_WT_REPOS_ROOTS(冒号分隔;默认 ~/repos + ~/.yan-agent/repos)
219
+ wt_repo_roots() {
220
+ local dr r
221
+ local -a out=()
222
+ dr="$(dia_wt_data_root 2>/dev/null || true)"
223
+ [ -n "$dr" ] && [ -d "$dr/repos" ] && out+=("$dr/repos")
224
+ local rr="${DIA_WT_REPOS_ROOTS:-${DIA_WT_REPOS_ROOT:-$HOME/repos:$HOME/.yan-agent/repos}}"
225
+ while IFS= read -r r; do [ -n "$r" ] && [ -d "$r" ] && out+=("$r"); done < <(printf '%s\n' "$rr" | tr ':' '\n')
226
+ [ ${#out[@]} -gt 0 ] && printf '%s\n' "${out[@]}"
227
+ return 0
228
+ }
229
+ # 所有主 checkout(git-dir == common-dir);跳过 *-worktree--*
230
+ wt_discover_main_checkouts() {
231
+ local root sub gd cd_
232
+ while IFS= read -r root; do
233
+ while IFS= read -r -d '' gd; do
234
+ sub="$(dirname "$gd")"
235
+ case "$(basename "$sub")" in *-worktree--*) continue ;; esac
236
+ cd_="$(git -C "$sub" rev-parse --git-common-dir 2>/dev/null || true)"
237
+ gd="$(cd "$sub" 2>/dev/null && cd "$gd" 2>/dev/null && pwd -P)"
238
+ cd_="$(cd "$sub" 2>/dev/null && cd "$cd_" 2>/dev/null && pwd -P)"
239
+ [ -n "$gd" ] && [ "$gd" = "$cd_" ] && printf '%s\n' "$sub"
240
+ done < <(find "$root" -maxdepth 3 -name .git -type d -print0 2>/dev/null)
241
+ done < <(wt_repo_roots)
242
+ }
218
243
  wt_repo_template() {
219
244
  local repo="$1" common cfg
220
245
  common="$(wt_common_dir "$repo" 2>/dev/null)" || return 1
@@ -939,7 +964,7 @@ cmd_clean() {
939
964
  }
940
965
 
941
966
  # ═══════════════════════════════════════════════════════════════════════════
942
- # route —— 【开工前必查】这次改动该走 MR 还是直推 main?(R1.7)
967
+ # route —— 【开工前必查】合并通道是哪一种?(R1.7:所有仓都走 MR,仅合并通道不同)
943
968
  #
944
969
  # 为什么需要(2026-10-12 复发教训):R20 管「别污染 main checkout」(隔离);R1.7 管「要不要评审」——
945
970
  # **两件事独立**。agent 做对了 A(隔离),却错误推导出 B(要 MR):在内部仓走 MR 全套打扰同事;
@@ -948,13 +973,13 @@ cmd_clean() {
948
973
  # 为什么改成**配置驱动**(2026-10-16 用户指正):
949
974
  # 原先把仓名硬编码在本脚本里(`yan-agent|personal-agent` 内部仓、`example-repo|*-qa|…` 业务仓)⇒
950
975
  # ① 「哪些仓是我的/业务仓」是**环境事实**,换个人就全错;② 它含「**我是谁**」成分 ——
951
- # `fed/yan-agent` 对本人是自持(直推)、对同事是别人的仓(该 MR)⇒ **同一仓在不同人机器上结论不同**;
976
+ # 仓归属因人而异(本人的自持仓 / 别人的仓)⇒ **同一仓在不同人机器上合并通道不同**;
952
977
  # ③ 每加一个仓都要改共享配方库 ⇒ 漂移源。⇒ **规则留在本脚本,数据只落本地**。
953
978
  #
954
979
  # 判定顺序(**每步都记录来源**、输出可审计;先否决后放行):
955
980
  # ① 本地配置 `mr` 名单命中 → 走 MR(显式否决,优先级最高)
956
- # ② 本地配置 `direct` 名单命中 → 直推
957
- # ③ remote 命名空间 ∈ `my_namespaces` → 直推("这是我的仓" = 可推导事实,不靠猜仓名)
981
+ # ② 本地配置 `direct` 名单命中 → 个人仓:MR + owner 自合
982
+ # ③ remote 命名空间 ∈ `my_namespaces` → 个人仓:MR + owner 自合("这是我的仓" = 可推导事实)
958
983
  # ④ 其余 / 信息不足 → **走 MR**(安全侧兜底)
959
984
  #
960
985
  # 配置读取(**数据根为准**,可与个人仓基线合并;三个名单键取**并集**:本地只能增、不能删):
@@ -966,7 +991,7 @@ cmd_clean() {
966
991
  # 只在输出里给「推测 + 如何确认」,由人核对(glab auth status)后写进配置。
967
992
  #
968
993
  # 用法:dia wt route <repo-path>
969
- # 退出码:0 = 直推;1 = 走 MR(含默认兜底);2 = 信息不足(按 MR 处理,需补配置/remote)
994
+ # 退出码:0 = MR + owner 自合(个人仓);1 = MR + LGTM + bot(团队仓);2 = 信息不足(按团队仓处理)
970
995
  # ═══════════════════════════════════════════════════════════════════════════
971
996
 
972
997
  # ── remote 命名空间:`<host>/<ns>/<repo>` 里的 `<ns>`(无 remote → 空)────────
@@ -1065,46 +1090,45 @@ case "$name" in *-worktree--*) name="${name%%-worktree--*}"; repo="$(dirname "$r
1065
1090
  verdict=mr; why="命名空间「${ns}」不在 my_namespaces(安全侧默认 MR)"
1066
1091
  fi
1067
1092
 
1068
- echo "── 仓库路由判定(R1.7 · 配置驱动,不再硬编码仓名)──"
1093
+ echo "── 仓库路由判定(R1.7 · 配置驱动:所有仓走 MR,仅合并通道不同)──"
1069
1094
  echo " 仓库 : $name${ns:+(命名空间 ${ns})}"
1070
1095
  printf ' 判定来源 : %s\n' "$(printf '%s' "$cfg" | node -e 'let s="";process.stdin.on("data",d=>{s+=d}).on("end",()=>{const d=JSON.parse(s);const srcs=(d.sources&&d.sources.length)?d.sources:["(无配置文件)"];console.log(srcs.join("、")+(d.errors.length?" ⚠️ "+d.errors.join(";"):""))})')"
1071
1096
 
1072
1097
  case "$verdict" in
1073
- direct)
1098
+ direct|personal)
1074
1099
  cat <<EOS
1075
- 结论 : ✅ **直推 main**(不建 MR)|依据:$why
1100
+ 结论 : ✅ **MR + owner 自合**(个人仓)|依据:$why
1076
1101
  正确姿势:
1077
- git worktree add <平级路径> -b <branch> origin/main
1078
- # ... 开发 + 自测 ...
1079
- git push origin HEAD:main # 快进推 main,不建 MR
1080
- ⚠️ 「worktree 隔离」≠「要评审」——两者独立:
1081
- · worktree = 别污染 main checkout(R20)
1082
- · 免 MR = 我自持的仓、自己主导,review 为辅(R1.7)
1083
- ⚠️ yan-agent main 拒绝 force push(pre-receive hook)→ 先 rebase 成快进
1102
+ git worktree add <平级路径> -b feat/<feature> origin/main
1103
+ # ... 开发 + 自测 + rebase(落后必须为 0)...
1104
+ git push origin HEAD:feat/<feature>
1105
+ glab mr create ... --source-branch feat/<feature> --target-branch main --yes
1106
+ # pipeline 绿 → owner 直接合(无需 LGTM、不发 @bot mergeme)
1107
+ glab mr merge <iid> --yes --remove-source-branch
1108
+ 提示 : 一律走 MR(2026-11-05 起没有「直推 main」);owner 自合省的是 LGTM,不是留痕
1084
1109
  EOS
1085
1110
  exit 0 ;;
1086
1111
  mr)
1087
1112
  cat <<EOS
1088
- 结论 : ✅ **走 MR**(+ LGTM + @bot mergeme)|依据:$why
1113
+ 结论 : ✅ **MR + LGTM + @bot mergeme**(组织/团队仓)|依据:$why
1089
1114
  正确姿势:
1090
- git worktree add <平级路径> -b <分支前缀>/<feat> origin/main
1115
+ git worktree add <平级路径> -b feat/<feature> origin/main
1091
1116
  # ... 开发 + 自测 + rebase(落后必须为 0)...
1092
- glab mr create ... && glab mr update <iid> --squash-before-merge
1093
- # 等 LGTM → @bot mergeme
1094
- 提示 : example-repo 的 docs/** 纯文档改动,用户说「直推」时可直接特权合并(R1.7 常态授权)
1117
+ glab mr create ... --source-branch feat/<feature> --target-branch main --yes
1118
+ # 等 LGTM → @bot mergeme(yan-mr wait 可一键盯完)
1095
1119
  EOS
1096
1120
  exit 1 ;;
1097
1121
  *)
1098
1122
  cat <<EOS
1099
- 结论 : ⚠️ **信息不足,按 MR 处理**(安全侧)|依据:$why
1123
+ 结论 : ⚠️ **信息不足,按「MR + LGTM + bot」处理**(安全侧)|依据:$why
1100
1124
  补齐判定(任选):
1101
1125
  1) 让仓有 origin remote ⇒ 命名空间可推导;
1102
1126
  2) 写配置(数据根为主入口,可与个人仓基线合并;名单取并集):
1103
1127
  \$YAN_DATA_ROOT/route.json ← 本机;**可以是软链**(指向个人仓版本化文件)
1104
1128
  \$PERSONAL_AGENT_DIR/config/yan-route.json ← 个人仓基线(跨设备)
1105
1129
  { "my_namespaces": ["<你的 gitlab 账号>"], "direct": ["<我自持的仓名>"], "mr": [] }
1130
+ (`direct` 现表示「个人仓 → owner 自合」;路径已不再直推)
1106
1131
  EOS
1107
- # 未配置命名空间时给「推测 + 如何确认」,但**绝不自动采用**(不瞎猜"我是谁")
1108
1132
  if [ -n "$ns" ] && [ -z "$_cfg_ns" ]; then
1109
1133
  local guess; guess="$(git -C "$repo" config --get user.email 2>/dev/null | sed 's/@.*//' || true)"
1110
1134
  printf ' 💡 推测(**未采用**):origin 命名空间 =「%s」;%s\n' "$ns" "${guess:+你的 git user.email 前缀 =「${guess}」}"
@@ -1161,7 +1185,6 @@ prot=" main master develop dev "
1161
1185
  probe="${DIA_WT_GUARD_PROBE:-0}"
1162
1186
  wt_guard_scan() {
1163
1187
  local ls="$1" rs="$2" range="" base="" hits=""
1164
- [ "${DIA_WT_GUARD_SCAN:-1}" = "1" ] || return 0
1165
1188
  if [ -n "$rs" ] && [ "$rs" != "0000000000000000000000000000000000000000" ] && git cat-file -e "$rs" 2>/dev/null; then
1166
1189
  range="$rs..$ls"
1167
1190
  else
@@ -1170,11 +1193,11 @@ else
1170
1193
  fi
1171
1194
  hits="$(git diff -U0 "$range" -- . 2>/dev/null | grep -E '^\+' | grep -vE '^\+\+\+' \
1172
1195
  | grep -vE '/(Users|home)/(you|user|example|runner|shared|me)(/|$)' \
1173
- | grep -nE '(/Users/[A-Za-z0-9._-]+|/home/[A-Za-z0-9._-]+|-----BEGIN [A-Z ]*PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})' | head -5)"
1196
+ | grep -nE '(^|[+ "'\''`(=])/(Users|home)/[A-Za-z0-9._-]+|-----BEGIN [A-Z ]*PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}' | head -5)"
1174
1197
  if [ -n "$hits" ]; then
1175
1198
  echo "[dia-wt] 推送包含敏感内容(已阻断):" >&2
1176
1199
  printf '%s\n' "$hits" >&2
1177
- echo "[dia-wt] 确认误报可临时关闭:DIA_WT_GUARD_SCAN=0 git push ..." >&2
1200
+ echo "[dia-wt] 误报请修扫描规则(hooks 由 dia wt adopt 生成),不提供关闭开关" >&2
1178
1201
  return 1
1179
1202
  fi
1180
1203
  }
@@ -1261,26 +1284,8 @@ cmd_adopt() {
1261
1284
  esac; done
1262
1285
  if [ "$all" = 1 ]; then
1263
1286
  [ -z "$repo" ] || die "--all 不能与 <repo> 同时给(要么单个仓,要么全量)"
1264
- local -a roots=() repos=()
1265
- local dr rr
1266
- dr="$(dia_wt_data_root 2>/dev/null || true)"
1267
- rr="${DIA_WT_REPOS_ROOT:-$HOME/repos}"
1268
- [ -n "$dr" ] && [ -d "$dr/repos" ] && roots+=("$dr/repos")
1269
- [ -d "$rr" ] && roots+=("$rr")
1270
- [ ${#roots[@]} -gt 0 ] || { echo "usage: dia wt adopt --all(未找到仓根:${rr})" >&2; exit 2; }
1271
- local root sub gd cd_
1272
- for root in "${roots[@]}"; do
1273
- while IFS= read -r -d '' gd; do
1274
- sub="$(dirname "$gd")"
1275
- case "$(basename "$sub")" in *-worktree--*) continue ;; esac
1276
- cd_="$(git -C "$sub" rev-parse --git-common-dir 2>/dev/null || true)"
1277
- # 统一物理路径(macOS /tmp → /private/tmp),否则等式比较会假失败
1278
- gd="$(cd "$sub" 2>/dev/null && cd "$gd" 2>/dev/null && pwd -P)"
1279
- cd_="$(cd "$sub" 2>/dev/null && cd "$cd_" 2>/dev/null && pwd -P)"
1280
- [ -n "$gd" ] && [ "$gd" = "$cd_" ] || continue # 只处理主 checkout
1281
- repos+=("$sub")
1282
- done < <(find "$root" -maxdepth 3 -name .git -type d -print0 2>/dev/null)
1283
- done
1287
+ local -a repos=()
1288
+ while IFS= read -r r; do repos+=("$r"); done < <(wt_discover_main_checkouts)
1284
1289
  [ ${#repos[@]} -gt 0 ] || { echo "usage: dia wt adopt --all(未匹配到任何主 checkout)" >&2; exit 2; }
1285
1290
  local -a pass=()
1286
1291
  local a
@@ -1402,7 +1407,7 @@ cmd_doctor() {
1402
1407
  *) repos+=("$1"); shift ;;
1403
1408
  esac; done
1404
1409
  if [ "$all" = 1 ] && [ ${#repos[@]} -eq 0 ]; then
1405
- while IFS= read -r -d '' d; do repos+=("$(dirname "$d")"); done < <(find "${HOME}/repos" -maxdepth 3 -name .git -type d -print0 2>/dev/null)
1410
+ while IFS= read -r d; do repos+=("$d"); done < <(wt_discover_main_checkouts)
1406
1411
  fi
1407
1412
  if [ ${#repos[@]} -eq 0 ]; then
1408
1413
  echo "usage: dia wt doctor <repo>... | --all(--all 未匹配到任何仓库)" >&2
@@ -1519,11 +1524,11 @@ dia wt —— worktree 目录形式选型 + 创建(唯一实现)
1519
1524
  # ★ 依赖体检(R20):软链/逃逸/悬空;--all 扫全部仓
1520
1525
  dia wt adopt <repo>|--all [--apply|--check|--revert] [--template T] [--default-branch B] [--json]
1521
1526
  # 配置路径模板 + 守卫钩子(默认 dry-run;--all 扫仓根)
1522
- # 守卫含推送前敏感扫描(DIA_WT_GUARD_SCAN=0 可临时关闭)
1527
+ # 守卫含推送前敏感扫描(不提供关闭开关;误报修规则)
1523
1528
  dia wt doctor [<repo>...] [--all] [--json] [--fix]
1524
1529
  # 巡检:配置/钩子/探针/主 checkout/worktree 卫生;--fix 重装守卫钩子
1525
1530
  dia wt gate <repo> <feature> # 提交 MR 前:check + doctor + 干净 + ahead>0
1526
- dia wt route <repo-path> # ★ 开工前必查:走 MR 还是直推(R1.7,配置驱动)
1531
+ dia wt route <repo-path> # ★ 开工前必查:合并通道(owner 自合 / LGTM+bot;一律走 MR)
1527
1532
  dia wt scope <repo|worktree> [--feature <slug>] [--session <id>] # ★ 产出作用域声明(设计 §4.5)
1528
1533
  dia wt scope --show # 看与当前目录匹配的作用域声明
1529
1534
  dia wt scope --list # 列出全部作用域文件(按 worktree 分文件)
@@ -10,7 +10,7 @@ ok(){ echo " ✓ $1"; PASS=$((PASS+1)); }; bad(){ echo " ✗ $1"; FAIL=$((FAIL
10
10
 
11
11
  R="$TD/repo"; mkdir -p "$R"; git -C "$R" init -q -b main
12
12
  git -C "$R" config user.email t@example.com; git -C "$R" config user.name t
13
- echo a > "$R/a.txt"; git -C "$R" add -A; git -C "$R" commit -qm init --no-verify
13
+ echo a > "$R/a.txt"; git -C "$R" add -A; git -C "$R" commit -qm init
14
14
 
15
15
  out="$(bash "$WT" adopt "$R" --json)"
16
16
  printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "dry-run 识别默认分支" || bad "dry-run: $out"
@@ -38,14 +38,20 @@ git -C "$R" reset -q
38
38
  # ── 推送前敏感扫描(PII/凭据形态)──
39
39
  BR="$TD/remote.git"; git init -q --bare "$BR"
40
40
  git -C "$R" remote add origin "$BR" 2>/dev/null || true
41
- git -C "$R" push -q --no-verify -u origin main 2>/dev/null || true
41
+ git --git-dir="$BR" fetch -q "$R" main:main 2>/dev/null || true
42
+ git -C "$R" fetch -q origin 2>/dev/null || true
42
43
  git -C "$R" worktree add -q "$TD/wt" -b feat/scan 2>/dev/null || true
43
- echo "/Users/alice/secret" > "$TD/wt/bad.txt"
44
- git -C "$TD/wt" add -A && git -C "$TD/wt" commit -qm bad --no-verify
44
+ BAD_HOME="/Users"; echo "$BAD_HOME/alice/secret" > "$TD/wt/bad.txt"
45
+ git -C "$TD/wt" add -A && git -C "$TD/wt" commit -qm bad
45
46
  if git -C "$TD/wt" push -q origin feat/scan 2>/dev/null; then bad "敏感推送未被拦截"; else ok "敏感推送被拦截"; fi
46
- echo ok > "$TD/wt/bad.txt" && git -C "$TD/wt" add -A && git -C "$TD/wt" commit -qm fix --no-verify
47
+ echo ok > "$TD/wt/bad.txt" && git -C "$TD/wt" add -A && git -C "$TD/wt" commit -qm fix
47
48
  git -C "$TD/wt" push -q origin feat/scan 2>/dev/null && ok "干净推送放行" || bad "干净推送被拒"
48
49
 
50
+ # 误报回归:`$TD/home/kits/...` 这种相对片段不该被判成 /home/<user>
51
+ printf 'path=$TD/home/kits/dia-msg/dia.json\n' > "$TD/wt/vars.txt"
52
+ git -C "$TD/wt" add -A && git -C "$TD/wt" commit -qm vars
53
+ git -C "$TD/wt" push -q origin feat/scan 2>/dev/null && ok "相对片段 home/ 不误报" || bad "相对片段被误判为敏感路径"
54
+
49
55
  bash "$WT" adopt "$R" --revert >/dev/null 2>&1 || bad "revert 失败"
50
56
  [ -z "$(git -C "$R" config --get core.hooksPath || true)" ] && ok "revert 还原 hooksPath" || bad "revert 未还原"
51
57
  [ ! -d "$R/.git/dia" ] && ok "revert 删除 dia 目录" || bad "dia 目录残留"
@@ -53,7 +59,7 @@ bash "$WT" adopt "$R" --revert >/dev/null 2>&1 || bad "revert 失败"
53
59
  # ── 原 hooksPath 保留 + 二次 apply 不改写 hooksPrevious(review P2-7)──────────
54
60
  R2="$TD/repo2"; mkdir -p "$R2" "$TD/orighooks"; git -C "$R2" init -q -b main
55
61
  git -C "$R2" config user.email t@example.com; git -C "$R2" config user.name t
56
- echo a > "$R2/a.txt"; git -C "$R2" add -A; git -C "$R2" commit -qm init --no-verify
62
+ echo a > "$R2/a.txt"; git -C "$R2" add -A; git -C "$R2" commit -qm init
57
63
  printf '#!/bin/sh\nexit 0\n' > "$TD/orighooks/pre-commit"; chmod +x "$TD/orighooks/pre-commit"
58
64
  git -C "$R2" config --local core.hooksPath "$TD/orighooks"
59
65
  bash "$WT" adopt "$R2" --apply >/dev/null 2>&1 || bad "R2 apply 失败"
@@ -74,7 +80,7 @@ git -C "$R2" config --local core.hooksPath "$R2/.git/dia/hooks"; bash "$WT" adop
74
80
  # ── 自定义默认分支受保护(review P1-1 + P1-2)────────────────────────────────
75
81
  R3="$TD/repo3"; mkdir -p "$R3"; git -C "$R3" init -q -b trunk
76
82
  git -C "$R3" config user.email t@example.com; git -C "$R3" config user.name t
77
- echo a > "$R3/a.txt"; git -C "$R3" add -A; git -C "$R3" commit -qm init --no-verify
83
+ echo a > "$R3/a.txt"; git -C "$R3" add -A; git -C "$R3" commit -qm init
78
84
  bash "$WT" adopt "$R3" --apply --default-branch trunk >/dev/null 2>&1 || bad "自定义分支 apply 失败"
79
85
  bash "$WT" adopt "$R3" --check >/dev/null 2>&1 && ok "自定义默认分支 check 通过" || bad "自定义默认分支 check 失败"
80
86
  bash "$WT" doctor "$R3" >/dev/null 2>&1 && ok "自定义默认分支 doctor 通过" || bad "自定义默认分支 doctor 失败"
@@ -86,7 +92,7 @@ git -C "$R3" reset -q
86
92
  bash "$WT" doctor "$R3" >/dev/null 2>&1 || true
87
93
  R4="$TD/repo4"; mkdir -p "$R4"; git -C "$R4" init -q -b main
88
94
  git -C "$R4" config user.email t@example.com; git -C "$R4" config user.name t
89
- echo a > "$R4/a.txt"; git -C "$R4" add -A; git -C "$R4" commit -qm init --no-verify
95
+ echo a > "$R4/a.txt"; git -C "$R4" add -A; git -C "$R4" commit -qm init
90
96
  if bash "$WT" doctor "$R4" >/dev/null 2>&1; then bad "未 adopt 的仓 doctor 误报通过"; else ok "未 adopt 的仓 doctor 失败"; fi
91
97
  bash "$WT" doctor >/dev/null 2>&1; [ $? = 2 ] && ok "doctor 无参数 exit 2" || bad "doctor 无参数未 exit 2"
92
98
 
@@ -94,17 +100,18 @@ bash "$WT" doctor >/dev/null 2>&1; [ $? = 2 ] && ok "doctor 无参数 exit 2" ||
94
100
  R5="$TD/repo5"; ORIGIN5="$TD/origin5.git"; git init -q --bare "$ORIGIN5"
95
101
  mkdir -p "$R5"; git -C "$R5" init -q -b main
96
102
  git -C "$R5" config user.email t@example.com; git -C "$R5" config user.name t
97
- echo a > "$R5/a.txt"; git -C "$R5" add -A; git -C "$R5" commit -qm init --no-verify
98
- git -C "$R5" remote add origin "$ORIGIN5"; git -C "$R5" push -q --no-verify origin main
103
+ echo a > "$R5/a.txt"; git -C "$R5" add -A; git -C "$R5" commit -qm init
104
+ git -C "$R5" remote add origin "$ORIGIN5"; git -C "$R5" push -q origin main
99
105
  git -C "$R5" fetch -q origin
100
106
  git -C "$R5" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
101
107
  bash "$WT" adopt "$R5" --apply >/dev/null 2>&1 || bad "R5 apply 失败"
102
- echo x >> "$R5/a.txt"; git -C "$R5" commit -qam dirty-main --no-verify 2>/dev/null
103
- if git -C "$R5" push origin main >/dev/null 2>&1; then bad "主 checkout push 未被拦截"; else ok "主 checkout push 被拦截"; fi
104
- git -C "$R5" reset -q --hard origin/main
108
+ # 主 checkout 的 pre-push 拒绝的是「从这里发起的任何 push」(含 feature 分支),不需要脏提交
109
+ git -C "$R5" branch feat/push-probe origin/main
110
+ if git -C "$R5" push origin feat/push-probe >/dev/null 2>&1; then bad "主 checkout push 未被拦截"; else ok "主 checkout push 被拦截"; fi
111
+ git -C "$R5" branch -D feat/push-probe >/dev/null 2>&1 || true
105
112
  git -C "$R5" worktree add -q -b feat/x "$TD/repo5-worktree--x" main
106
113
  echo b > "$TD/repo5-worktree--x/b.txt"; git -C "$TD/repo5-worktree--x" add b.txt
107
- git -C "$TD/repo5-worktree--x" commit -qm feat-x --no-verify 2>/dev/null
114
+ git -C "$TD/repo5-worktree--x" commit -qm feat-x 2>/dev/null
108
115
  if git -C "$TD/repo5-worktree--x" push -q origin feat/x >/dev/null 2>&1; then ok "worktree push 放行"; else bad "worktree push 被误拦"; fi
109
116
 
110
117
  # ── template 生效(P2-6):adopt 后 create 按 config.template 推导路径 ─────────
@@ -118,7 +125,7 @@ bash "$WT" adopt "$R5" --apply >/dev/null 2>&1
118
125
  # ── 过期 origin/HEAD(远端分支已删)不误判默认分支 ──────────────────────────
119
126
  R6="$TD/repo6"; mkdir -p "$R6"; git -C "$R6" init -q -b main
120
127
  git -C "$R6" config user.email t@example.com; git -C "$R6" config user.name t
121
- echo a > "$R6/a.txt"; git -C "$R6" add -A; git -C "$R6" commit -qm init --no-verify
128
+ echo a > "$R6/a.txt"; git -C "$R6" add -A; git -C "$R6" commit -qm init
122
129
  git -C "$R6" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/gone
123
130
  out="$(bash "$WT" adopt "$R6" --json)"
124
131
  printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "过期 origin/HEAD 回落到 main" || bad "过期 origin/HEAD: $out"
@@ -128,7 +135,7 @@ R7="$TD/repo7"; mkdir -p "$R7/.githooks"; git -C "$R7" init -q -b main
128
135
  git -C "$R7" config user.email t@example.com; git -C "$R7" config user.name t
129
136
  printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R7/.githooks/pre-commit"
130
137
  chmod +x "$R7/.githooks/pre-commit"
131
- echo a > "$R7/a.txt"; git -C "$R7" add -A; git -C "$R7" commit -qm init --no-verify
138
+ echo a > "$R7/a.txt"; git -C "$R7" add -A; git -C "$R7" commit -qm init
132
139
  git -C "$R7" config --local core.hooksPath .githooks
133
140
  bash "$WT" adopt "$R7" --apply >/dev/null 2>&1 || bad "R7 apply 失败"
134
141
  python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));sys.exit(0 if d.get("hooksChain")==".githooks" else 1)' "$R7/.git/dia/config.json" \
@@ -145,7 +152,7 @@ AR="$TD/allroot"; mkdir -p "$AR/a" "$AR/b"
145
152
  for r in a b; do
146
153
  git -C "$AR/$r" init -q -b main
147
154
  git -C "$AR/$r" config user.email t@example.com; git -C "$AR/$r" config user.name t
148
- echo x > "$AR/$r/x.txt"; git -C "$AR/$r" add -A; git -C "$AR/$r" commit -qm init --no-verify
155
+ echo x > "$AR/$r/x.txt"; git -C "$AR/$r" add -A; git -C "$AR/$r" commit -qm init
149
156
  done
150
157
  git -C "$AR/a" worktree add -q -b feat/w "$AR/a-worktree--w" main
151
158
  out="$(DIA_WT_REPOS_ROOT="$AR" DIA_WT_DATA_ROOT="$TD/dr2" bash "$WT" adopt --all --apply --json 2>&1)"
@@ -154,13 +161,15 @@ for r in a b; do
154
161
  [ -n "$(git -C "$AR/$r" config --get core.hooksPath || true)" ] && ok "adopt --all: $r 已配 hooksPath" || bad "adopt --all: $r 未配置"
155
162
  done
156
163
  [ ! -d "$AR/a-worktree--w/.git/dia" ] && ok "adopt --all 跳过 worktree 目录" || bad "adopt --all 误改 worktree"
164
+ out="$(DIA_WT_REPOS_ROOT="$AR" DIA_WT_DATA_ROOT="$TD/dr4" bash "$WT" doctor --all --json 2>/dev/null)"
165
+ printf '%s' "$out" | grep -q "$AR/a" && ok "doctor --all 覆盖 DIA_WT_REPOS_ROOT" || bad "doctor --all 未覆盖自定义仓根"
157
166
  DIA_WT_REPOS_ROOT="$TD/empty-root" DIA_WT_DATA_ROOT="$TD/dr3" bash "$WT" adopt --all >/dev/null 2>&1
158
167
  [ $? = 2 ] && ok "adopt --all 空结果 exit 2" || bad "adopt --all 空结果未 exit 2"
159
168
 
160
169
  # ── doctor --fix:钩子被覆盖后能无损重建 ────────────────────────────────────
161
170
  R8="$TD/repo8"; mkdir -p "$R8"; git -C "$R8" init -q -b main
162
171
  git -C "$R8" config user.email t@example.com; git -C "$R8" config user.name t
163
- echo a > "$R8/a.txt"; git -C "$R8" add -A; git -C "$R8" commit -qm init --no-verify
172
+ echo a > "$R8/a.txt"; git -C "$R8" add -A; git -C "$R8" commit -qm init
164
173
  bash "$WT" adopt "$R8" --apply >/dev/null 2>&1
165
174
  rm -f "$R8/.git/dia/hooks/post-checkout"
166
175
  printf '#!/bin/sh\nexit 0\n' > "$R8/.git/dia/hooks/pre-commit"; chmod +x "$R8/.git/dia/hooks/pre-commit"
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # 覆盖(**为什么是这些**):
5
5
  # · 无配置时**绝不放行**:即便命名空间看着像我的,也只能走 MR(不瞎猜"我是谁")
6
- # · 命名空间推导:配置了 my_namespaces 后,我的仓自动直推
6
+ # · 命名空间推导:配置了 my_namespaces 后,我的仓 → MR + owner 自合(rc 0)
7
7
  # · 显式名单:direct 放行 / mr **否决优先**
8
8
  # · 个人仓基线 + 数据根本地**并集合并**;数据根配置**可以是软链**
9
9
  # · 空列表**不误命中**(`grep -qx ""` 匹配空行是这类脚本的经典假阳性)
@@ -45,13 +45,13 @@ echo "== 2. 无配置 + 命名空间看着像我的 ⇒ **仍 MR**(不瞎猜
45
45
  [ "$(run "$MINE")" = 1 ] && ok "退出码 1(未因命名空间相似而放行)" || { bad "应 1:不能瞎猜"; out; }
46
46
  see "不在 my_namespaces" && ok "说明未配置" || { bad "缺说明"; out; }
47
47
 
48
- echo "== 3. 配置 my_namespaces ⇒ 我的仓自动直推 =="
48
+ echo "== 3. 配置 my_namespaces ⇒ 我的仓 = MR + owner 自合 =="
49
49
  setcfg "$CFG" '{ "my_namespaces": ["example-user"] }'
50
50
  [ "$(run "$MINE")" = 0 ] && ok "退出码 0" || { bad "应 0"; out; }
51
51
  see "∈ my_namespaces" && ok "判定来源 = 命名空间" || { bad "来源不对"; out; }
52
52
  [ "$(run "$TEAM")" = 1 ] && ok "非我命名空间仍 MR" || { bad "应 1"; out; }
53
53
 
54
- echo "== 4. direct 名单 ⇒ 直推(覆盖默认)=="
54
+ echo "== 4. direct 名单 ⇒ owner 自合(覆盖默认)=="
55
55
  setcfg "$CFG" '{ "my_namespaces": ["example-user"], "direct": ["teamrepo"] }'
56
56
  [ "$(run "$TEAM")" = 0 ] && ok "退出码 0" || { bad "应 0"; out; }
57
57
  see "配置 direct 名单命中" && ok "判定来源 = 配置" || { bad "来源不对"; out; }
@@ -64,9 +64,9 @@ see "显式否决" && ok "给出否决理由" || { bad "缺理由"; out; }
64
64
  echo "== 6. 个人仓基线 + 数据根 ⇒ 并集合并(判定来源列出两个文件)=="
65
65
  setcfg "$CFG" '{ "direct": ["teamrepo"] }'
66
66
  setcfg "$BASE" '{ "my_namespaces": ["example-user"], "direct": ["myrepo"] }'
67
- [ "$(run "$TEAM")" = 0 ] && ok "本地新增生效(teamrepo 直推)" || { bad "本地配置未生效"; out; }
67
+ [ "$(run "$TEAM")" = 0 ] && ok "本地新增生效(teamrepo = owner 自合)" || { bad "本地配置未生效"; out; }
68
68
  see "route.json" && ok "来源含数据根文件" || { bad "缺来源"; out; }
69
- [ "$(run "$MINE")" = 0 ] && ok "基线条目保留(myrepo 直推)" || { bad "基线未生效"; out; }
69
+ [ "$(run "$MINE")" = 0 ] && ok "基线条目保留(myrepo = owner 自合)" || { bad "基线未生效"; out; }
70
70
  see "yan-route.json" && ok "来源含基线文件" || { bad "缺基线来源"; out; }
71
71
  [ "$(run "$NOREMOTE")" = 2 ] && ok "无 remote 的仓:信息不足(rc=2)" || { bad "应 2"; out; }
72
72
 
@@ -89,16 +89,16 @@ echo "== 10. \$DIA_WT_ROUTE_CONFIG 显式指定 ⇒ 最高优先(忽略其余
89
89
  setcfg "$CFG" '{ "direct": ["myrepo"] }'
90
90
  EX="$TD/explicit.json"; setcfg "$EX" '{ "direct": ["teamrepo"] }'
91
91
  env DIA_WT_ROUTE_CONFIG="$EX" bash "$WT" route "$TEAM" >"$TD/out.txt" 2>&1; RC=$?
92
- [ "$RC" = 0 ] && ok "显式配置生效(teamrepo 直推)" || { bad "显式配置未生效(rc=${RC})"; out; }
92
+ [ "$RC" = 0 ] && ok "显式配置生效(teamrepo = owner 自合)" || { bad "显式配置未生效(rc=${RC})"; out; }
93
93
  env DIA_WT_ROUTE_CONFIG="$EX" bash "$WT" route "$MINE" >"$TD/out.txt" 2>&1; RC=$?
94
- [ "$RC" = 1 ] && ok "其余配置被忽略(myrepo 未直推)" || { bad "应忽略数据根配置(rc=${RC})"; out; }
94
+ [ "$RC" = 1 ] && ok "其余配置被忽略(myrepo 走 LGTM+bot)" || { bad "应忽略数据根配置(rc=${RC})"; out; }
95
95
  rm -f "$EX"
96
96
 
97
97
  echo "== 11. worktree 路径输入 ⇒ 归主仓判定 =="
98
98
  setcfg "$CFG" '{ "direct": ["teamrepo"] }'
99
99
  git -C "$TEAM" worktree add -q "$TD/teamrepo-worktree--t1" -b t1 >/dev/null 2>&1
100
100
  RC="$(run "$TD/teamrepo-worktree--t1")"
101
- [ "$RC" = 0 ] && ok "worktree 归主仓 → 直推" || { bad "worktree 判定错(rc=${RC})"; out; }
101
+ [ "$RC" = 0 ] && ok "worktree 归主仓 → owner 自合" || { bad "worktree 判定错(rc=${RC})"; out; }
102
102
  see "teamrepo" && ok "报出主仓名" || { bad "未报主仓名"; out; }
103
103
 
104
104
  echo "== 12. 非 git 目录 ⇒ 信息不足(rc=2)=="