@minonl/dia 0.0.1 → 0.0.2
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.
- package/README.md +1 -0
- package/dist/cli.js +761 -398
- package/docs/cli.md +1 -0
- package/docs/deps.jsonl +7 -7
- package/docs/index.md +4 -2
- package/docs/kit-install.md +3 -3
- package/docs/migrations/data-root-2026-11.md +112 -0
- package/docs/research/h0/report.md +2 -2
- package/docs/research/h0/sample.json +2 -2
- package/docs/security/audits/2026-11-04.json +4 -4
- package/docs/security/audits/2026-11-05-independent.json +45 -0
- package/docs/security/credentials.md +1 -1
- package/docs/security/public-release.md +7 -1
- package/docs/security/snapshot-isolation.md +50 -0
- package/docs/status.md +9 -0
- package/docs/versioning.md +11 -0
- package/package.json +3 -2
- package/packages/wt/bin/dia-wt +360 -7
- package/packages/wt/tests/adopt.test.sh +151 -0
package/packages/wt/bin/dia-wt
CHANGED
|
@@ -118,7 +118,7 @@ dia_wt_data_root() {
|
|
|
118
118
|
|
|
119
119
|
# ── 授权推导的**输入**:产出 scope.json(设计 §4.5 D0–D6;消费方 = dsh 引擎 / P3 兜底插件)──
|
|
120
120
|
# 为什么在这里产出:迭代入口天然知道"本次任务仓库是哪个",而权限层不知道(这是越权写的根因之一)。
|
|
121
|
-
# ⚠️ 关键判定:**是否 git 仓库** —— 非 git 仓库(如 ~/repos/
|
|
121
|
+
# ⚠️ 关键判定:**是否 git 仓库** —— 非 git 仓库(如 ~/repos/example 根)**不派生** worktree 同级目录,
|
|
122
122
|
# 否则会把兄弟目录(repos/local 等)误算进作用域(用户 2026-10-15 明确)。
|
|
123
123
|
write_scope() { # write_scope <repo-or-worktree-path> [feature] [session]
|
|
124
124
|
local target="$1" feature="${2:-}" session="${3:-}"
|
|
@@ -214,8 +214,29 @@ console.log(` repo=${d.repo}| gitRepo=${d.gitRepo}| writable=` + (d.writab
|
|
|
214
214
|
JS
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
# adopt 写入的路径模板(<git-common-dir>/dia/config.json 的 template);未 adopt → 空
|
|
218
|
+
wt_repo_template() {
|
|
219
|
+
local repo="$1" common cfg
|
|
220
|
+
common="$(wt_common_dir "$repo" 2>/dev/null)" || return 1
|
|
221
|
+
cfg="$common/dia/config.json"
|
|
222
|
+
[ -f "$cfg" ] || return 1
|
|
223
|
+
python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("template") or "")' "$cfg" 2>/dev/null
|
|
224
|
+
}
|
|
225
|
+
wt_expand_template() {
|
|
226
|
+
local tmpl="$1" repo="$2" feature="$3"
|
|
227
|
+
local parent base
|
|
228
|
+
parent="$(cd "$(dirname "$repo")" && pwd)"; base="$(basename "$repo")"
|
|
229
|
+
printf '%s' "$tmpl" | sed -e "s|{{parent}}|$parent|g" -e "s|{{repo}}|$base|g" \
|
|
230
|
+
-e "s|{{wtRoot}}|$repo/.worktrees|g" -e "s|{{feature}}|$feature|g"
|
|
231
|
+
}
|
|
217
232
|
wt_path() {
|
|
218
|
-
local repo="$1" feature="$2" form="$3"
|
|
233
|
+
local repo="$1" feature="$2" form="$3" tmpl dest
|
|
234
|
+
# 模板优先(adopt 后策略即 SSOT);未 adopt 维持凭 form 推导(默认平级,零迁移)
|
|
235
|
+
if tmpl="$(wt_repo_template "$repo")" && [ -n "$tmpl" ]; then
|
|
236
|
+
dest="$(wt_expand_template "$tmpl" "$repo" "$feature")"
|
|
237
|
+
case "$dest" in /*) ;; *) dest="$repo/$dest" ;; esac
|
|
238
|
+
printf '%s' "$dest"; return 0
|
|
239
|
+
fi
|
|
219
240
|
local parent; parent="$(cd "$(dirname "$repo")" && pwd)"
|
|
220
241
|
local base; base="$(basename "$repo")"
|
|
221
242
|
if [ "$form" = hidden ]; then printf '%s/.worktrees/%s' "$repo" "$feature"
|
|
@@ -368,7 +389,7 @@ cmd_status() {
|
|
|
368
389
|
[ -n "$ST_FILES" ] && printf '%s\n' "$ST_FILES" | sed 's/^/ · /'
|
|
369
390
|
[ -n "$ST_FILES" ] && dirty_map="${dirty_map}${p}"$'\t'"$(printf '%s' "$ST_FILES" | tr '\n' ',')"$'\n'
|
|
370
391
|
fi
|
|
371
|
-
done < <(git -C "$repo" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
|
|
392
|
+
done < <(git -C "$repo" -c core.quotePath=false worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
|
|
372
393
|
|
|
373
394
|
# 重叠检测(设计 D6):**同一仓内**两个以上 worktree 改同一**仓内相对路径**。
|
|
374
395
|
# 不跨仓比较 —— 不同仓的同名相对路径属巧合,报出来是噪音。
|
|
@@ -415,7 +436,7 @@ cmd_list() {
|
|
|
415
436
|
# · 删前逐个打印「分支名 + 最后提交日期 + 标题」,可回溯。
|
|
416
437
|
#
|
|
417
438
|
# 用法:prune-merged [<repo>] [--all] [--apply] [--base <ref>] [--protect a,b] [--quiet]
|
|
418
|
-
# --all 扫已知仓根(数据根 repos/、$DIA_WT_REPOS_ROOT 默认 ~/repos/
|
|
439
|
+
# --all 扫已知仓根(数据根 repos/、$DIA_WT_REPOS_ROOT 默认 ~/repos/example)下的主 checkout
|
|
419
440
|
# 退出码:0 = 无待清理 / 已全部清理;1 = 仍有残留(dry-run 发现待清理时也是 1)
|
|
420
441
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
421
442
|
cmd_prune_merged() {
|
|
@@ -586,7 +607,7 @@ prune_one() {
|
|
|
586
607
|
# 判据不区分 → 一旦启用 GVS 全仓误报;判据不查 → 违规静默破坏。
|
|
587
608
|
#
|
|
588
609
|
# 用法:check [<repo>...] [--all] [--deep] [--quiet]
|
|
589
|
-
# --all 扫已知仓根(`$YAN_DATA_ROOT/repos`、`$DIA_WT_REPOS_ROOT`(默认 `~/repos/
|
|
610
|
+
# --all 扫已知仓根(`$YAN_DATA_ROOT/repos`、`$DIA_WT_REPOS_ROOT`(默认 `~/repos/example`))下的主 checkout
|
|
590
611
|
# (跳过 `*-worktree--*`:它们的 worktree 由主仓 `worktree list` 覆盖)+ 仓根杂散 node_modules 检测
|
|
591
612
|
# --deep 递归扫 node_modules 全树(默认只扫顶层;大仓全树需数秒)
|
|
592
613
|
# --quiet 只输出有问题的 checkout
|
|
@@ -939,7 +960,7 @@ cmd_clean() {
|
|
|
939
960
|
# 配置读取(**数据根为准**,可与个人仓基线合并;三个名单键取**并集**:本地只能增、不能删):
|
|
940
961
|
# $YAN_ROUTE_CONFIG 显式指定(最高优先,一般不用)
|
|
941
962
|
# $YAN_DATA_ROOT/route.json 本机主入口 —— **可以是软链**,指向个人仓里的版本化文件
|
|
942
|
-
# $PERSONAL_AGENT_DIR/config/yan-route.json 个人仓基线(跨设备可版本化,默认 ~/repos/
|
|
963
|
+
# $PERSONAL_AGENT_DIR/config/yan-route.json 个人仓基线(跨设备可版本化,默认 ~/repos/example/personal-agent)
|
|
943
964
|
# 格式:{ "my_namespaces": ["example-user"], "direct": ["yan-agent"], "mr": [] }
|
|
944
965
|
# ⚠️ `my_namespaces` 缺失时**绝不瞎猜**(不拿 git author 的 email 前缀自动当命名空间):
|
|
945
966
|
# 只在输出里给「推测 + 如何确认」,由人核对(glab auth status)后写进配置。
|
|
@@ -1015,7 +1036,7 @@ cmd_route() {
|
|
|
1015
1036
|
else
|
|
1016
1037
|
name="$(basename "$repo")"
|
|
1017
1038
|
fi
|
|
1018
|
-
|
|
1039
|
+
case "$name" in *-worktree--*) name="${name%%-worktree--*}"; repo="$(dirname "$repo")/$name" ;; esac
|
|
1019
1040
|
|
|
1020
1041
|
# ── 配置驱动判定(**不再硬编码仓名**;判定来源会被打印出来,可审计)──
|
|
1021
1042
|
local cfg ns=""
|
|
@@ -1093,6 +1114,330 @@ EOS
|
|
|
1093
1114
|
esac
|
|
1094
1115
|
}
|
|
1095
1116
|
|
|
1117
|
+
# ── adopt / doctor / gate(2026-11-05)──────────────────────────────────────
|
|
1118
|
+
# 设计:core/docs/compose/spec/dia-wt-adopt-design.md
|
|
1119
|
+
# 纪律靠闸不靠 chmod:主 checkout 上 pre-commit/pre-push 拒绝 protected 分支;
|
|
1120
|
+
# 配置落 <git-common-dir>/dia/config.json;默认模板与现网 -worktree-- 布局一致(零迁移)。
|
|
1121
|
+
|
|
1122
|
+
wt_common_dir() {
|
|
1123
|
+
local repo="$1" d
|
|
1124
|
+
d="$(git -C "$repo" rev-parse --git-common-dir 2>/dev/null)" || return 1
|
|
1125
|
+
case "$d" in /*) printf '%s\n' "$d" ;; *) ( cd "$repo" && cd "$d" 2>/dev/null && pwd -P ) ;; esac
|
|
1126
|
+
}
|
|
1127
|
+
wt_is_main_checkout() {
|
|
1128
|
+
local repo="$1" gd cd
|
|
1129
|
+
gd="$(git -C "$repo" rev-parse --git-dir 2>/dev/null)" || return 2
|
|
1130
|
+
cd="$(wt_common_dir "$repo")" || return 2
|
|
1131
|
+
case "$gd" in /*) ;; *) gd="$(cd "$repo" && cd "$gd" 2>/dev/null && pwd -P)" ;; esac
|
|
1132
|
+
[ "$gd" = "$cd" ]
|
|
1133
|
+
}
|
|
1134
|
+
wt_default_branch() {
|
|
1135
|
+
local repo="$1" b=""
|
|
1136
|
+
b="$(git -C "$repo" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')"
|
|
1137
|
+
# origin/HEAD 可能是**过期**的(远端分支已删,如 grayscale-open 指向 feat/init)→ 必须验证 ref 还在
|
|
1138
|
+
if [ -n "$b" ] && ! git -C "$repo" show-ref --verify --quiet "refs/remotes/origin/$b"; then b=""; fi
|
|
1139
|
+
[ -n "$b" ] || { git -C "$repo" show-ref --verify --quiet refs/heads/main && b="main"; }
|
|
1140
|
+
[ -n "$b" ] || { git -C "$repo" show-ref --verify --quiet refs/heads/master && b="master"; }
|
|
1141
|
+
[ -n "$b" ] || b="$(git -C "$repo" config --get dia.defaultBranch 2>/dev/null || true)"
|
|
1142
|
+
# 注意:**不回落到当前 HEAD**(在 feature 分支上 adopt 会把 feature 当默认分支)
|
|
1143
|
+
printf '%s' "$b"
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
wt_write_hook() {
|
|
1147
|
+
local hooksdir="$1" name="$2" extra="${3:-}" chain="${4:-}"
|
|
1148
|
+
cat > "$hooksdir/$name" <<'HOOK'
|
|
1149
|
+
#!/usr/bin/env bash
|
|
1150
|
+
# dia-wt guard(managed by `dia wt adopt`;勿手改)
|
|
1151
|
+
set -uo pipefail
|
|
1152
|
+
name="$(basename "$0")"
|
|
1153
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
1154
|
+
common="$(git rev-parse --git-common-dir 2>/dev/null || true)"
|
|
1155
|
+
case "$common" in /*) ;; *) common="$(cd "$top" && cd "$common" 2>/dev/null && pwd -P)" ;; esac
|
|
1156
|
+
gd="$(git rev-parse --git-dir 2>/dev/null || true)"
|
|
1157
|
+
case "$gd" in /*) ;; *) gd="$(cd "$top" && cd "$gd" 2>/dev/null && pwd -P)" ;; esac
|
|
1158
|
+
is_main=0; [ "$gd" = "$common" ] && is_main=1
|
|
1159
|
+
branch="$(git symbolic-ref --short HEAD 2>/dev/null || true)"
|
|
1160
|
+
prot=" main master develop dev "
|
|
1161
|
+
probe="${DIA_WT_GUARD_PROBE:-0}"
|
|
1162
|
+
wt_guard_scan() {
|
|
1163
|
+
local ls="$1" rs="$2" range="" base="" hits=""
|
|
1164
|
+
[ "${DIA_WT_GUARD_SCAN:-1}" = "1" ] || return 0
|
|
1165
|
+
if [ -n "$rs" ] && [ "$rs" != "0000000000000000000000000000000000000000" ] && git cat-file -e "$rs" 2>/dev/null; then
|
|
1166
|
+
range="$rs..$ls"
|
|
1167
|
+
else
|
|
1168
|
+
base="$(git merge-base "$ls" "origin/HEAD" 2>/dev/null || git merge-base "$ls" main 2>/dev/null || true)"
|
|
1169
|
+
if [ -n "$base" ]; then range="$base..$ls"; else range="$ls^..$ls"; fi
|
|
1170
|
+
fi
|
|
1171
|
+
hits="$(git diff -U0 "$range" -- . 2>/dev/null | grep -E '^\+' | grep -vE '^\+\+\+' \
|
|
1172
|
+
| 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)"
|
|
1174
|
+
if [ -n "$hits" ]; then
|
|
1175
|
+
echo "[dia-wt] 推送包含敏感内容(已阻断):" >&2
|
|
1176
|
+
printf '%s\n' "$hits" >&2
|
|
1177
|
+
echo "[dia-wt] 确认误报可临时关闭:DIA_WT_GUARD_SCAN=0 git push ..." >&2
|
|
1178
|
+
return 1
|
|
1179
|
+
fi
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
case "$name" in
|
|
1183
|
+
pre-commit)
|
|
1184
|
+
if [ "$probe" = 1 ]; then
|
|
1185
|
+
{ [ "$is_main" = 1 ] && [[ "$prot" == *" $branch "* ]]; } && exit 1 || exit 0
|
|
1186
|
+
fi
|
|
1187
|
+
if [ "$is_main" = 1 ] && [[ "$prot" == *" $branch "* ]]; then
|
|
1188
|
+
echo "[dia-wt] 主 checkout 禁止在 $branch 直接提交;请先:dia wt create <repo> <feature>" >&2
|
|
1189
|
+
exit 1
|
|
1190
|
+
fi ;;
|
|
1191
|
+
pre-push)
|
|
1192
|
+
if [ "$probe" = 1 ]; then
|
|
1193
|
+
[ "$is_main" = 1 ] && exit 1 || exit 0
|
|
1194
|
+
fi
|
|
1195
|
+
if [ "$is_main" = 1 ]; then
|
|
1196
|
+
echo "[dia-wt] 主 checkout 禁止直接 push;请走 worktree 分支 + MR" >&2
|
|
1197
|
+
exit 1
|
|
1198
|
+
fi
|
|
1199
|
+
while read -r _lr ls rr rs; do
|
|
1200
|
+
case "${rr:-}" in
|
|
1201
|
+
refs/heads/main|refs/heads/master|refs/heads/develop|refs/heads/dev)
|
|
1202
|
+
echo "[dia-wt] 禁止直接 push ${rr#refs/heads/};请走 MR" >&2; exit 1 ;;
|
|
1203
|
+
esac
|
|
1204
|
+
wt_guard_scan "$ls" "$rs" || exit 1
|
|
1205
|
+
done ;;
|
|
1206
|
+
post-checkout)
|
|
1207
|
+
[ "$probe" = 1 ] && exit 0
|
|
1208
|
+
if [ "$is_main" = 1 ] && [[ "$prot" != *" $branch "* ]]; then
|
|
1209
|
+
echo "[dia-wt] 提示:主 checkout 当前在 $branch(建议回到默认分支)" >&2
|
|
1210
|
+
fi ;;
|
|
1211
|
+
esac
|
|
1212
|
+
# 钩子链:原 hooksPath(husky / .githooks / simple-git-hooks)优先,其次仓库默认 hooks 目录
|
|
1213
|
+
chain="${DIA_WT_CHAIN_DIR:-}"
|
|
1214
|
+
case "$chain" in
|
|
1215
|
+
"") ;;
|
|
1216
|
+
/*) ;;
|
|
1217
|
+
*) chain="$top/$chain" ;;
|
|
1218
|
+
esac
|
|
1219
|
+
if [ -n "$chain" ] && [ -x "$chain/$name" ] && [ "$chain/$name" != "$0" ]; then exec "$chain/$name" "$@"; fi
|
|
1220
|
+
orig="$common/hooks/$name"
|
|
1221
|
+
if [ -x "$orig" ] && [ "$orig" != "$0" ]; then exec "$orig" "$@"; fi
|
|
1222
|
+
exit 0
|
|
1223
|
+
HOOK
|
|
1224
|
+
if [ -n "$chain" ]; then
|
|
1225
|
+
python3 - "$hooksdir/$name" "$chain" <<'PYCHAIN'
|
|
1226
|
+
import sys
|
|
1227
|
+
p, chain = sys.argv[1], sys.argv[2]
|
|
1228
|
+
s = open(p).read()
|
|
1229
|
+
s = s.replace('chain="${DIA_WT_CHAIN_DIR:-}"', 'chain="${DIA_WT_CHAIN_DIR:-%s}"' % chain.replace('\\', '\\\\').replace('"', '\\"'))
|
|
1230
|
+
open(p, "w").write(s)
|
|
1231
|
+
PYCHAIN
|
|
1232
|
+
fi
|
|
1233
|
+
if [ -n "$extra" ]; then
|
|
1234
|
+
python3 - "$hooksdir/$name" "$extra" <<'PYGT'
|
|
1235
|
+
import sys, re
|
|
1236
|
+
p, extra = sys.argv[1], sys.argv[2]
|
|
1237
|
+
s = open(p).read()
|
|
1238
|
+
s = re.sub(r'^prot=" main master develop dev "$', f'prot=" main master develop dev {extra} "', s, flags=re.M)
|
|
1239
|
+
open(p, "w").write(s)
|
|
1240
|
+
PYGT
|
|
1241
|
+
fi
|
|
1242
|
+
chmod +x "$hooksdir/$name"
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
cmd_adopt() {
|
|
1246
|
+
local repo="" mode="dry" template="" def="" json=0
|
|
1247
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
1248
|
+
--apply) mode="apply"; shift ;;
|
|
1249
|
+
--check) mode="check"; shift ;;
|
|
1250
|
+
--revert) mode="revert"; shift ;;
|
|
1251
|
+
--dry-run) mode="dry"; shift ;;
|
|
1252
|
+
--template) template="$2"; shift 2 ;;
|
|
1253
|
+
--default-branch) def="$2"; shift 2 ;;
|
|
1254
|
+
--json) json=1; shift ;;
|
|
1255
|
+
--force) echo "[dia-wt] --force 已忽略:adopt 幂等、无破坏性默认行为" >&2; shift ;;
|
|
1256
|
+
-h|--help) sed -n '2,6p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
1257
|
+
-*) die "未知参数:$1" ;;
|
|
1258
|
+
*) repo="$1"; shift ;;
|
|
1259
|
+
esac; done
|
|
1260
|
+
[ -n "$repo" ] || die "用法: dia wt adopt <repo> [--apply|--check|--revert] [--template T] [--default-branch B] [--json]"
|
|
1261
|
+
[ -d "$repo" ] || die "目录不存在:$repo"
|
|
1262
|
+
git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || die "不是 git 仓库:$repo"
|
|
1263
|
+
|
|
1264
|
+
local common cfgdir cfg hooksdir prev
|
|
1265
|
+
common="$(wt_common_dir "$repo")" || die "无法解析 git-common-dir"
|
|
1266
|
+
cfgdir="$common/dia"; cfg="$cfgdir/config.json"; hooksdir="$cfgdir/hooks"
|
|
1267
|
+
prev="$(git -C "$repo" config --get core.hooksPath 2>/dev/null || true)"
|
|
1268
|
+
[ -n "$template" ] || template='{{parent}}/{{repo}}-worktree--{{feature}}'
|
|
1269
|
+
[ -n "$def" ] || def="$(wt_default_branch "$repo")"
|
|
1270
|
+
|
|
1271
|
+
if [ "$mode" = "revert" ]; then
|
|
1272
|
+
[ -f "$cfg" ] || die "未 adopt(缺 $cfg)"
|
|
1273
|
+
local saved
|
|
1274
|
+
saved="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg")"
|
|
1275
|
+
if [ "$prev" != "$hooksdir" ]; then
|
|
1276
|
+
echo "[dia-wt] hooksPath 已被改动($prev),跳过还原并保留 $cfgdir 备份" >&2
|
|
1277
|
+
return 1
|
|
1278
|
+
fi
|
|
1279
|
+
if [ -n "$saved" ]; then git -C "$repo" config --local core.hooksPath "$saved"; else git -C "$repo" config --local --unset core.hooksPath 2>/dev/null || true; fi
|
|
1280
|
+
rm -rf "$cfgdir"
|
|
1281
|
+
echo "reverted:$cfgdir"
|
|
1282
|
+
return 0
|
|
1283
|
+
fi
|
|
1284
|
+
|
|
1285
|
+
if [ "$mode" = "check" ]; then
|
|
1286
|
+
local bad=0
|
|
1287
|
+
[ -f "$cfg" ] || { echo "FAIL D01 未 adopt(缺 $cfg)"; bad=1; }
|
|
1288
|
+
# 策略 SSOT 是 config.json:默认分支以它为准(不从 remote 重猜,避免与已写入的钩子策略漂移)
|
|
1289
|
+
if [ -f "$cfg" ]; then
|
|
1290
|
+
local cfgDef; cfgDef="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
|
|
1291
|
+
[ -n "$cfgDef" ] && def="$cfgDef"
|
|
1292
|
+
fi
|
|
1293
|
+
case "$prev" in /*) ;; *) echo "FAIL D02 core.hooksPath 不是绝对路径($prev)"; bad=1 ;; esac
|
|
1294
|
+
for h in pre-commit pre-push post-checkout; do
|
|
1295
|
+
[ -x "$hooksdir/$h" ] || { echo "FAIL D02 缺 $h 钩子"; bad=1; }
|
|
1296
|
+
done
|
|
1297
|
+
if [ -n "$def" ] && [ -x "$hooksdir/pre-commit" ]; then
|
|
1298
|
+
grep -q "prot=\" main master develop dev $def \"" "$hooksdir/pre-commit" \
|
|
1299
|
+
|| { echo "FAIL D03 钩子未保护默认分支 $def"; bad=1; }
|
|
1300
|
+
if wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1301
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
|
|
1302
|
+
&& { echo "FAIL D03 守卫探针未拦截主 checkout 的 $def 提交"; bad=1; }
|
|
1303
|
+
fi
|
|
1304
|
+
fi
|
|
1305
|
+
if [ "$json" = 1 ]; then
|
|
1306
|
+
python3 -c 'import json,sys;print(json.dumps({"repo":sys.argv[1],"ok":sys.argv[2]=="0","defaultBranch":sys.argv[3]},ensure_ascii=False,separators=(",",":")))' "$repo" "$bad" "$def"
|
|
1307
|
+
fi
|
|
1308
|
+
return $bad
|
|
1309
|
+
fi
|
|
1310
|
+
|
|
1311
|
+
local nwt
|
|
1312
|
+
nwt="$(git -C "$repo" worktree list --porcelain 2>/dev/null | grep -c '^worktree ' || true)"
|
|
1313
|
+
if [ "$mode" = "dry" ]; then
|
|
1314
|
+
echo "adopt plan:$repo"
|
|
1315
|
+
echo " common=${common}"
|
|
1316
|
+
echo " defaultBranch=${def:-(无法解析,需 --default-branch)}"
|
|
1317
|
+
echo " template=${template}"
|
|
1318
|
+
echo " worktrees=${nwt}"
|
|
1319
|
+
echo " hooksPath=${prev:-(未设置)} → ${hooksdir}"
|
|
1320
|
+
if [ "$json" = 1 ]; then
|
|
1321
|
+
python3 -c 'import json,sys;print(json.dumps({"repo":sys.argv[1],"common":sys.argv[2],"defaultBranch":sys.argv[3],"template":sys.argv[4],"worktrees":int(sys.argv[5]),"hooksPath":sys.argv[6]},ensure_ascii=False,separators=(",",":")))' "$repo" "$common" "$def" "$template" "$nwt" "$prev"
|
|
1322
|
+
fi
|
|
1323
|
+
return 0
|
|
1324
|
+
fi
|
|
1325
|
+
|
|
1326
|
+
[ -n "$def" ] || die "默认分支不可解析;请 --default-branch main"
|
|
1327
|
+
mkdir -p "$hooksdir"
|
|
1328
|
+
# 保留首次 adopt 记下的 hooksPrevious(重复 apply 时 prev 已是我们自己的 hooksdir)
|
|
1329
|
+
local savedPrev=""
|
|
1330
|
+
[ -f "$cfg" ] && savedPrev="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg" 2>/dev/null || true)"
|
|
1331
|
+
[ "$prev" = "$hooksdir" ] && prev="$savedPrev"
|
|
1332
|
+
# 钩子链目标:首次 adopt 前的 hooksPath(husky/.githooks/…);没有则用仓库默认 hooks 目录
|
|
1333
|
+
local chain="$prev"
|
|
1334
|
+
[ -n "$chain" ] || chain="$common/hooks"
|
|
1335
|
+
[ -f "$cfg" ] && cp "$cfg" "$cfgdir/config.json.bak.$(date +%s)"
|
|
1336
|
+
python3 - "$cfg" "$repo" "$def" "$template" "$hooksdir" "$prev" "$chain" <<'PY'
|
|
1337
|
+
import json, sys, datetime
|
|
1338
|
+
cfg, repo, br, tmpl, hooks, prev, chain = sys.argv[1:8]
|
|
1339
|
+
data = {
|
|
1340
|
+
"schema": 1, "repo": repo, "defaultBranch": br, "template": tmpl,
|
|
1341
|
+
"hooksPath": hooks, "hooksPrevious": prev or None, "hooksChain": chain,
|
|
1342
|
+
"adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
1343
|
+
}
|
|
1344
|
+
json.dump(data, open(cfg, "w"), ensure_ascii=False, indent=1)
|
|
1345
|
+
PY
|
|
1346
|
+
wt_write_hook "$hooksdir" pre-commit "$def" "$chain"
|
|
1347
|
+
wt_write_hook "$hooksdir" pre-push "$def" "$chain"
|
|
1348
|
+
wt_write_hook "$hooksdir" post-checkout "$def" "$chain"
|
|
1349
|
+
git -C "$repo" config --local core.hooksPath "$hooksdir"
|
|
1350
|
+
echo "adopted:$repo(hooksPath=$hooksdir)"
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
cmd_doctor() {
|
|
1354
|
+
local json=0 all=0 repos=()
|
|
1355
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
1356
|
+
--json) json=1; shift ;;
|
|
1357
|
+
--all) all=1; shift ;;
|
|
1358
|
+
--strict|--fix) shift ;;
|
|
1359
|
+
-*) die "未知参数:$1" ;;
|
|
1360
|
+
*) repos+=("$1"); shift ;;
|
|
1361
|
+
esac; done
|
|
1362
|
+
if [ "$all" = 1 ] && [ ${#repos[@]} -eq 0 ]; then
|
|
1363
|
+
while IFS= read -r -d '' d; do repos+=("$(dirname "$d")"); done < <(find "${HOME}/repos" -maxdepth 3 -name .git -type d -print0 2>/dev/null)
|
|
1364
|
+
fi
|
|
1365
|
+
if [ ${#repos[@]} -eq 0 ]; then
|
|
1366
|
+
echo "usage: dia wt doctor <repo>... | --all(--all 未匹配到任何仓库)" >&2
|
|
1367
|
+
exit 2
|
|
1368
|
+
fi
|
|
1369
|
+
local fails=0
|
|
1370
|
+
for repo in "${repos[@]:-}"; do
|
|
1371
|
+
[ -n "$repo" ] || continue
|
|
1372
|
+
git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || continue
|
|
1373
|
+
local common cfgdir cfg hooksdir prev def
|
|
1374
|
+
common="$(wt_common_dir "$repo")"; cfgdir="$common/dia"; cfg="$cfgdir/config.json"; hooksdir="$cfgdir/hooks"
|
|
1375
|
+
prev="$(git -C "$repo" config --get core.hooksPath 2>/dev/null || true)"
|
|
1376
|
+
def=""
|
|
1377
|
+
if [ -f "$cfg" ]; then
|
|
1378
|
+
def="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
|
|
1379
|
+
fi
|
|
1380
|
+
[ -n "$def" ] || def="$(wt_default_branch "$repo")"
|
|
1381
|
+
local bad=0
|
|
1382
|
+
[ -f "$cfg" ] || { echo "FAIL D01 $repo 未 adopt"; bad=1; }
|
|
1383
|
+
case "$prev" in /*) ;; *) echo "FAIL D02 $repo hooksPath 非绝对($prev)"; bad=1 ;; esac
|
|
1384
|
+
for h in pre-commit pre-push post-checkout; do
|
|
1385
|
+
[ -x "$hooksdir/$h" ] || { echo "FAIL D02 $repo 缺 $h"; bad=1; }
|
|
1386
|
+
done
|
|
1387
|
+
if [ -n "$def" ] && [ -x "$hooksdir/pre-commit" ]; then
|
|
1388
|
+
# 静态:钩子里的 protected 列表必须包含 defaultBranch
|
|
1389
|
+
grep -q "prot=\" main master develop dev $def \"" "$hooksdir/pre-commit" \
|
|
1390
|
+
|| { echo "FAIL D03 $repo 钩子未保护默认分支 $def"; bad=1; }
|
|
1391
|
+
# 动态:主 checkout 当前就在 def 上时必须被拦
|
|
1392
|
+
if wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1393
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
|
|
1394
|
+
&& { echo "FAIL D03 $repo 守卫未拦截 $def 提交"; bad=1; }
|
|
1395
|
+
fi
|
|
1396
|
+
fi
|
|
1397
|
+
if wt_is_main_checkout "$repo"; then
|
|
1398
|
+
[ -z "$(git -C "$repo" status --porcelain 2>/dev/null)" ] || echo "WARN D04 $repo 主 checkout 有未提交改动"
|
|
1399
|
+
[ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ] || echo "WARN D04 $repo 主 checkout 不在 $def"
|
|
1400
|
+
fi
|
|
1401
|
+
local missing=0
|
|
1402
|
+
while IFS= read -r p; do [ -n "$p" ] && [ ! -d "$p" ] && { echo "FAIL D05 $repo worktree 路径缺失:$p"; missing=1; }; done \
|
|
1403
|
+
< <(git -C "$repo" -c core.quotePath=false worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
|
|
1404
|
+
[ "$missing" = 0 ] || bad=1
|
|
1405
|
+
[ "$(git -C "$repo" -c core.quotePath=false worktree list --porcelain 2>/dev/null | grep -cE '^(prunable|locked)')" = 0 ] || { echo "FAIL D06 $repo 有 prunable/locked worktree"; bad=1; }
|
|
1406
|
+
[ -z "$(git -C "$repo" -c core.quotePath=false worktree list --porcelain 2>/dev/null | sed -n 's/^branch refs\/heads\///p' | sort | uniq -d)" ] || { echo "FAIL D07 $repo 分支被多个 worktree 占用"; bad=1; }
|
|
1407
|
+
[ -n "$def" ] || { echo "FAIL D11 $repo 默认分支不可解析"; bad=1; }
|
|
1408
|
+
if [ "$json" = 1 ]; then
|
|
1409
|
+
python3 -c 'import json,sys;print(json.dumps({"repo":sys.argv[1],"ok":sys.argv[2]=="0"},ensure_ascii=False))' "$repo" "$bad"
|
|
1410
|
+
fi
|
|
1411
|
+
[ "$bad" = 0 ] && [ "$json" = 0 ] && echo "ok $repo"
|
|
1412
|
+
[ "$bad" = 1 ] && fails=$((fails+1))
|
|
1413
|
+
done
|
|
1414
|
+
[ "$fails" = 0 ]
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
cmd_gate() {
|
|
1418
|
+
local repo="" feature=""
|
|
1419
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
1420
|
+
-*) shift ;;
|
|
1421
|
+
*) if [ -z "$repo" ]; then repo="$1"; elif [ -z "$feature" ]; then feature="$1"; fi; shift ;;
|
|
1422
|
+
esac; done
|
|
1423
|
+
[ -n "$repo" ] && [ -n "$feature" ] || die "用法:dia wt gate <repo> <feature>"
|
|
1424
|
+
echo "== check(R20)"; cmd_check "$repo" || { echo "gate FAIL: check"; return 1; }
|
|
1425
|
+
echo "== doctor"; cmd_doctor "$repo" || { echo "gate FAIL: doctor"; return 1; }
|
|
1426
|
+
local def; def="$(wt_default_branch "$repo")"
|
|
1427
|
+
local wt=""
|
|
1428
|
+
while IFS= read -r p; do
|
|
1429
|
+
[ -n "$p" ] || continue
|
|
1430
|
+
local br; br="$(git -C "$p" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
1431
|
+
if [ "$br" = "feat/$feature" ] || [ "$(basename "$p")" = "${repo##*/}-worktree--$feature" ]; then wt="$p"; break; fi
|
|
1432
|
+
done < <(git -C "$repo" -c core.quotePath=false worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
|
|
1433
|
+
[ -n "$wt" ] || { echo "gate FAIL: 找不到 feature=$feature 的 worktree"; return 1; }
|
|
1434
|
+
[ -z "$(git -C "$wt" status --porcelain)" ] || { echo "gate FAIL: worktree 有未提交改动"; return 1; }
|
|
1435
|
+
local ahead; ahead="$(git -C "$wt" rev-list --count "origin/${def}..HEAD" 2>/dev/null || echo 0)"
|
|
1436
|
+
[ "$ahead" -gt 0 ] || { echo "gate FAIL: 相对 origin/${def} 没有提交"; return 1; }
|
|
1437
|
+
echo "gate OK:$feature(ahead origin/${def} = $ahead)"
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
|
|
1096
1441
|
case "${1:-}" in
|
|
1097
1442
|
create) shift; cmd_create "$@" ;;
|
|
1098
1443
|
route) shift; cmd_route "$@" ;;
|
|
@@ -1101,6 +1446,9 @@ case "${1:-}" in
|
|
|
1101
1446
|
list) shift; cmd_list "$@" ;;
|
|
1102
1447
|
status) shift; cmd_status "$@" ;;
|
|
1103
1448
|
check) shift; cmd_check "$@" ;;
|
|
1449
|
+
adopt) shift; cmd_adopt "$@" ;;
|
|
1450
|
+
doctor) shift; cmd_doctor "$@" ;;
|
|
1451
|
+
gate) shift; cmd_gate "$@" ;;
|
|
1104
1452
|
clean) shift; cmd_clean "$@" ;;
|
|
1105
1453
|
prune-merged) shift; cmd_prune_merged "$@" ;;
|
|
1106
1454
|
""|help|-h|--help)
|
|
@@ -1114,6 +1462,11 @@ dia wt —— worktree 目录形式选型 + 创建(唯一实现)
|
|
|
1114
1462
|
dia wt status [repo-path] # ★ 清理前必查:唯一工作副本 + 改动重叠(只读)
|
|
1115
1463
|
dia wt check [<repo>...] [--all] [--deep] [--quiet]
|
|
1116
1464
|
# ★ 依赖体检(R20):软链/逃逸/悬空;--all 扫全部仓
|
|
1465
|
+
dia wt adopt <repo> [--apply|--check|--revert] [--template T] [--default-branch B] [--json]
|
|
1466
|
+
# 配置路径模板 + 守卫钩子(默认 dry-run)
|
|
1467
|
+
# 守卫含推送前敏感扫描(DIA_WT_GUARD_SCAN=0 可临时关闭)
|
|
1468
|
+
dia wt doctor [<repo>...] [--all] [--json] # 巡检:配置/钩子/探针/主 checkout/worktree 卫生
|
|
1469
|
+
dia wt gate <repo> <feature> # 提交 MR 前:check + doctor + 干净 + ahead>0
|
|
1117
1470
|
dia wt route <repo-path> # ★ 开工前必查:走 MR 还是直推(R1.7,配置驱动)
|
|
1118
1471
|
dia wt scope <repo|worktree> [--feature <slug>] [--session <id>] # ★ 产出作用域声明(设计 §4.5)
|
|
1119
1472
|
dia wt scope --show # 看与当前目录匹配的作用域声明
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# adopt / doctor / gate 回归测试(2026-11-05)
|
|
3
|
+
# 覆盖:dry-run 计划、apply 幂等、check 通过、主 checkout 守卫、doctor、revert。
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
WT="$HERE/../bin/dia-wt"
|
|
7
|
+
TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
ok(){ echo " ✓ $1"; PASS=$((PASS+1)); }; bad(){ echo " ✗ $1"; FAIL=$((FAIL+1)); }
|
|
10
|
+
|
|
11
|
+
R="$TD/repo"; mkdir -p "$R"; git -C "$R" init -q -b main
|
|
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
|
|
14
|
+
|
|
15
|
+
out="$(bash "$WT" adopt "$R" --json)"
|
|
16
|
+
printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "dry-run 识别默认分支" || bad "dry-run: $out"
|
|
17
|
+
[ ! -f "$R/.git/dia/config.json" ] && ok "dry-run 不落盘" || bad "dry-run 落盘了"
|
|
18
|
+
|
|
19
|
+
bash "$WT" adopt "$R" --apply >/dev/null 2>&1 || bad "apply 失败"
|
|
20
|
+
[ -f "$R/.git/dia/config.json" ] && ok "config 已写" || bad "缺 config"
|
|
21
|
+
hp="$(git -C "$R" config --get core.hooksPath)"
|
|
22
|
+
case "$hp" in /*) ok "hooksPath 是绝对路径" ;; *) bad "hooksPath=$hp" ;; esac
|
|
23
|
+
[ -x "$R/.git/dia/hooks/pre-commit" ] && ok "pre-commit 可执行" || bad "缺 pre-commit"
|
|
24
|
+
|
|
25
|
+
md5_before="$(md5 -q "$R/.git/dia/config.json" 2>/dev/null || md5sum "$R/.git/dia/config.json" | cut -d' ' -f1)"
|
|
26
|
+
sleep 1
|
|
27
|
+
bash "$WT" adopt "$R" --apply >/dev/null 2>&1
|
|
28
|
+
md5_after="$(md5 -q "$R/.git/dia/config.json" 2>/dev/null || md5sum "$R/.git/dia/config.json" | cut -d' ' -f1)"
|
|
29
|
+
[ "$md5_before" != "$md5_after" ] && ok "重复 apply 会刷新时间戳(幂等语义:不重复建目录/钩子)" || bad "重复 apply 无变化"
|
|
30
|
+
|
|
31
|
+
bash "$WT" adopt "$R" --check >/dev/null 2>&1 && ok "check 通过" || bad "check 失败"
|
|
32
|
+
bash "$WT" doctor "$R" >/dev/null 2>&1 && ok "doctor 通过" || bad "doctor 失败"
|
|
33
|
+
|
|
34
|
+
echo b > "$R/b.txt"; git -C "$R" add b.txt
|
|
35
|
+
if git -C "$R" commit -qm should-block 2>/dev/null; then bad "主 checkout main 提交未被拦截"; else ok "主 checkout main 提交被拦截"; fi
|
|
36
|
+
git -C "$R" reset -q
|
|
37
|
+
|
|
38
|
+
# ── 推送前敏感扫描(PII/凭据形态)──
|
|
39
|
+
BR="$TD/remote.git"; git init -q --bare "$BR"
|
|
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
|
|
42
|
+
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
|
|
45
|
+
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
|
+
git -C "$TD/wt" push -q origin feat/scan 2>/dev/null && ok "干净推送放行" || bad "干净推送被拒"
|
|
48
|
+
|
|
49
|
+
bash "$WT" adopt "$R" --revert >/dev/null 2>&1 || bad "revert 失败"
|
|
50
|
+
[ -z "$(git -C "$R" config --get core.hooksPath || true)" ] && ok "revert 还原 hooksPath" || bad "revert 未还原"
|
|
51
|
+
[ ! -d "$R/.git/dia" ] && ok "revert 删除 dia 目录" || bad "dia 目录残留"
|
|
52
|
+
|
|
53
|
+
# ── 原 hooksPath 保留 + 二次 apply 不改写 hooksPrevious(review P2-7)──────────
|
|
54
|
+
R2="$TD/repo2"; mkdir -p "$R2" "$TD/orighooks"; git -C "$R2" init -q -b main
|
|
55
|
+
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
|
|
57
|
+
printf '#!/bin/sh\nexit 0\n' > "$TD/orighooks/pre-commit"; chmod +x "$TD/orighooks/pre-commit"
|
|
58
|
+
git -C "$R2" config --local core.hooksPath "$TD/orighooks"
|
|
59
|
+
bash "$WT" adopt "$R2" --apply >/dev/null 2>&1 || bad "R2 apply 失败"
|
|
60
|
+
hp1="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$R2/.git/dia/config.json")"
|
|
61
|
+
[ "$hp1" = "$TD/orighooks" ] && ok "首次 apply 记录 hooksPrevious" || bad "hooksPrevious=$hp1"
|
|
62
|
+
bash "$WT" adopt "$R2" --apply >/dev/null 2>&1
|
|
63
|
+
hp2="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$R2/.git/dia/config.json")"
|
|
64
|
+
[ "$hp2" = "$TD/orighooks" ] && ok "二次 apply 不覆盖 hooksPrevious" || bad "hooksPrevious 被改成 $hp2"
|
|
65
|
+
bash "$WT" adopt "$R2" --revert >/dev/null 2>&1 || bad "R2 revert 失败"
|
|
66
|
+
[ "$(git -C "$R2" config --get core.hooksPath)" = "$TD/orighooks" ] && ok "revert 还原原 hooksPath" || bad "revert 未还原原 hooksPath"
|
|
67
|
+
# hooksPath 被第三方改动 → revert 拒绝(不抢配置)
|
|
68
|
+
bash "$WT" adopt "$R2" --apply >/dev/null 2>&1
|
|
69
|
+
git -C "$R2" config --local core.hooksPath "$TD/orighooks"
|
|
70
|
+
if bash "$WT" adopt "$R2" --revert >/dev/null 2>&1; then bad "hooksPath 被改动后 revert 仍执行"; else ok "hooksPath 被改动后 revert 拒绝"; fi
|
|
71
|
+
[ -f "$R2/.git/dia/config.json" ] && ok "拒绝 revert 时保留 config 备份" || bad "拒绝 revert 却删了 config"
|
|
72
|
+
git -C "$R2" config --local core.hooksPath "$R2/.git/dia/hooks"; bash "$WT" adopt "$R2" --revert >/dev/null 2>&1
|
|
73
|
+
|
|
74
|
+
# ── 自定义默认分支受保护(review P1-1 + P1-2)────────────────────────────────
|
|
75
|
+
R3="$TD/repo3"; mkdir -p "$R3"; git -C "$R3" init -q -b trunk
|
|
76
|
+
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
|
|
78
|
+
bash "$WT" adopt "$R3" --apply --default-branch trunk >/dev/null 2>&1 || bad "自定义分支 apply 失败"
|
|
79
|
+
bash "$WT" adopt "$R3" --check >/dev/null 2>&1 && ok "自定义默认分支 check 通过" || bad "自定义默认分支 check 失败"
|
|
80
|
+
bash "$WT" doctor "$R3" >/dev/null 2>&1 && ok "自定义默认分支 doctor 通过" || bad "自定义默认分支 doctor 失败"
|
|
81
|
+
echo b > "$R3/b.txt"; git -C "$R3" add b.txt
|
|
82
|
+
if git -C "$R3" commit -qm should-block 2>/dev/null; then bad "trunk 提交未被拦截"; else ok "自定义默认分支 trunk 提交被拦截"; fi
|
|
83
|
+
git -C "$R3" reset -q
|
|
84
|
+
|
|
85
|
+
# ── doctor:未 adopt / 无参数(review P1-4)──────────────────────────────────
|
|
86
|
+
bash "$WT" doctor "$R3" >/dev/null 2>&1 || true
|
|
87
|
+
R4="$TD/repo4"; mkdir -p "$R4"; git -C "$R4" init -q -b main
|
|
88
|
+
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
|
|
90
|
+
if bash "$WT" doctor "$R4" >/dev/null 2>&1; then bad "未 adopt 的仓 doctor 误报通过"; else ok "未 adopt 的仓 doctor 失败"; fi
|
|
91
|
+
bash "$WT" doctor >/dev/null 2>&1; [ $? = 2 ] && ok "doctor 无参数 exit 2" || bad "doctor 无参数未 exit 2"
|
|
92
|
+
|
|
93
|
+
# ── pre-push 守卫(主 checkout 拒绝 / worktree 放行)──────────────────────────
|
|
94
|
+
R5="$TD/repo5"; ORIGIN5="$TD/origin5.git"; git init -q --bare "$ORIGIN5"
|
|
95
|
+
mkdir -p "$R5"; git -C "$R5" init -q -b main
|
|
96
|
+
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
|
|
99
|
+
git -C "$R5" fetch -q origin
|
|
100
|
+
git -C "$R5" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
|
|
101
|
+
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
|
|
105
|
+
git -C "$R5" worktree add -q -b feat/x "$TD/repo5-worktree--x" main
|
|
106
|
+
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
|
|
108
|
+
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
|
+
|
|
110
|
+
# ── template 生效(P2-6):adopt 后 create 按 config.template 推导路径 ─────────
|
|
111
|
+
out="$(bash "$WT" create "$R5" t1 --dry-run 2>&1 || true)"
|
|
112
|
+
printf '%s' "$out" | grep -q "repo5-worktree--t1" && ok "默认模板 create 路径" || bad "默认模板 create: $out"
|
|
113
|
+
bash "$WT" adopt "$R5" --apply --template '{{parent}}/{{repo}}.wt.{{feature}}' >/dev/null 2>&1 || bad "template apply 失败"
|
|
114
|
+
out="$(bash "$WT" create "$R5" t1 --dry-run 2>&1 || true)"
|
|
115
|
+
printf '%s' "$out" | grep -q "repo5.wt.t1" && ok "自定义模板 create 路径生效" || bad "自定义模板: $out"
|
|
116
|
+
bash "$WT" adopt "$R5" --apply >/dev/null 2>&1
|
|
117
|
+
|
|
118
|
+
# ── 过期 origin/HEAD(远端分支已删)不误判默认分支 ──────────────────────────
|
|
119
|
+
R6="$TD/repo6"; mkdir -p "$R6"; git -C "$R6" init -q -b main
|
|
120
|
+
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
|
|
122
|
+
git -C "$R6" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/gone
|
|
123
|
+
out="$(bash "$WT" adopt "$R6" --json)"
|
|
124
|
+
printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "过期 origin/HEAD 回落到 main" || bad "过期 origin/HEAD: $out"
|
|
125
|
+
|
|
126
|
+
# ── 钩子链:原 hooksPath(.githooks / husky)不被吞掉 ─────────────────────────
|
|
127
|
+
R7="$TD/repo7"; mkdir -p "$R7/.githooks"; git -C "$R7" init -q -b main
|
|
128
|
+
git -C "$R7" config user.email t@example.com; git -C "$R7" config user.name t
|
|
129
|
+
printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R7/.githooks/pre-commit"
|
|
130
|
+
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
|
|
132
|
+
git -C "$R7" config --local core.hooksPath .githooks
|
|
133
|
+
bash "$WT" adopt "$R7" --apply >/dev/null 2>&1 || bad "R7 apply 失败"
|
|
134
|
+
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" \
|
|
135
|
+
&& ok "config 记录 hooksChain" || bad "hooksChain 未记录"
|
|
136
|
+
git -C "$R7" worktree add -q -b feat/y "$TD/repo7-worktree--y" main
|
|
137
|
+
echo b > "$TD/repo7-worktree--y/b.txt"; git -C "$TD/repo7-worktree--y" add b.txt
|
|
138
|
+
git -C "$TD/repo7-worktree--y" commit -qm feat-y >/dev/null 2>&1
|
|
139
|
+
[ -f "$TD/repo7-worktree--y/chain.log" ] && ok "原 pre-commit 被链式执行" || bad "原 pre-commit 未执行(钩子被吞)"
|
|
140
|
+
bash "$WT" adopt "$R7" --revert >/dev/null 2>&1
|
|
141
|
+
[ "$(git -C "$R7" config --get core.hooksPath)" = ".githooks" ] && ok "revert 还原原 hooksPath(相对)" || bad "revert 未还原 .githooks"
|
|
142
|
+
|
|
143
|
+
# ── gate:成功 / 失败(P1-3 精确匹配)────────────────────────────────────────
|
|
144
|
+
bash "$WT" gate "$R5" x >/dev/null 2>&1 && ok "gate 成功" || bad "gate 失败"
|
|
145
|
+
if bash "$WT" gate "$R5" xb >/dev/null 2>&1; then bad "gate 子串误匹配(x 命中 xb)"; else ok "gate 精确匹配 feature"; fi
|
|
146
|
+
echo dirty >> "$TD/repo5-worktree--x/b.txt"
|
|
147
|
+
if bash "$WT" gate "$R5" x >/dev/null 2>&1; then bad "worktree 脏时 gate 通过"; else ok "worktree 脏时 gate 失败"; fi
|
|
148
|
+
git -C "$TD/repo5-worktree--x" checkout -q -- b.txt
|
|
149
|
+
|
|
150
|
+
echo; echo "── PASS $PASS · FAIL $FAIL ──"
|
|
151
|
+
[ "$FAIL" = 0 ]
|