@minonl/dia 0.0.2-beta.0 → 0.0.3
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 +763 -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 +8 -0
- package/package.json +3 -2
- package/packages/wt/bin/dia-wt +418 -9
- package/packages/wt/tests/adopt.test.sh +180 -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() {
|
|
@@ -440,7 +461,7 @@ cmd_prune_merged() {
|
|
|
440
461
|
local -a roots=()
|
|
441
462
|
[ -n "$dr" ] && [ -d "$dr/repos" ] && roots+=("$dr/repos")
|
|
442
463
|
[ -d "$rr" ] && roots+=("$rr")
|
|
443
|
-
[ ${#roots[@]} -gt 0 ] ||
|
|
464
|
+
[ ${#roots[@]} -gt 0 ] || { echo "usage: dia wt adopt --all(未找到仓根:${rr})" >&2; exit 2; }
|
|
444
465
|
local root sub
|
|
445
466
|
for root in "${roots[@]}"; do
|
|
446
467
|
for sub in "$root"/*; do
|
|
@@ -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
|
|
@@ -758,7 +779,7 @@ cmd_check() {
|
|
|
758
779
|
rr="${DIA_WT_REPOS_ROOT:-$HOME/repos/leyan}"
|
|
759
780
|
[ -n "$dr" ] && [ -d "$dr/repos" ] && roots+=("$dr/repos")
|
|
760
781
|
[ -d "$rr" ] && roots+=("$rr")
|
|
761
|
-
[ ${#roots[@]} -gt 0 ] ||
|
|
782
|
+
[ ${#roots[@]} -gt 0 ] || { echo "usage: dia wt adopt --all(未找到仓根:${rr})" >&2; exit 2; }
|
|
762
783
|
local root sub
|
|
763
784
|
for root in "${roots[@]}"; do
|
|
764
785
|
for sub in "$root"/*; do
|
|
@@ -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,385 @@ 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 all=0
|
|
1247
|
+
local -a orig=("$@")
|
|
1248
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
1249
|
+
--all) all=1; shift ;;
|
|
1250
|
+
--apply) mode="apply"; shift ;;
|
|
1251
|
+
--check) mode="check"; shift ;;
|
|
1252
|
+
--revert) mode="revert"; shift ;;
|
|
1253
|
+
--dry-run) mode="dry"; shift ;;
|
|
1254
|
+
--template) template="$2"; shift 2 ;;
|
|
1255
|
+
--default-branch) def="$2"; shift 2 ;;
|
|
1256
|
+
--json) json=1; shift ;;
|
|
1257
|
+
--force) echo "[dia-wt] --force 已忽略:adopt 幂等、无破坏性默认行为" >&2; shift ;;
|
|
1258
|
+
-h|--help) sed -n '2,6p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
1259
|
+
-*) die "未知参数:$1" ;;
|
|
1260
|
+
*) repo="$1"; shift ;;
|
|
1261
|
+
esac; done
|
|
1262
|
+
if [ "$all" = 1 ]; then
|
|
1263
|
+
[ -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
|
|
1284
|
+
[ ${#repos[@]} -gt 0 ] || { echo "usage: dia wt adopt --all(未匹配到任何主 checkout)" >&2; exit 2; }
|
|
1285
|
+
local -a pass=()
|
|
1286
|
+
local a
|
|
1287
|
+
for a in "${orig[@]}"; do [ "$a" = "--all" ] && continue; pass+=("$a"); done
|
|
1288
|
+
local okc=0 badc=0 r
|
|
1289
|
+
for r in "${repos[@]}"; do
|
|
1290
|
+
if cmd_adopt "$r" "${pass[@]}"; then okc=$((okc+1)); else badc=$((badc+1)); echo "FAIL $r" >&2; fi
|
|
1291
|
+
done
|
|
1292
|
+
[ "$json" = 1 ] && python3 -c 'import json,sys;print(json.dumps({"repos":int(sys.argv[1]),"ok":int(sys.argv[2]),"fail":int(sys.argv[3])},separators=(",",":")))' "${#repos[@]}" "$okc" "$badc"
|
|
1293
|
+
[ "$badc" = 0 ] || return 1
|
|
1294
|
+
return 0
|
|
1295
|
+
fi
|
|
1296
|
+
[ -n "$repo" ] || die "用法: dia wt adopt <repo>|--all [--apply|--check|--revert] [--template T] [--default-branch B] [--json]"
|
|
1297
|
+
[ -d "$repo" ] || die "目录不存在:$repo"
|
|
1298
|
+
git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || die "不是 git 仓库:$repo"
|
|
1299
|
+
|
|
1300
|
+
local common cfgdir cfg hooksdir prev
|
|
1301
|
+
common="$(wt_common_dir "$repo")" || die "无法解析 git-common-dir"
|
|
1302
|
+
cfgdir="$common/dia"; cfg="$cfgdir/config.json"; hooksdir="$cfgdir/hooks"
|
|
1303
|
+
prev="$(git -C "$repo" config --get core.hooksPath 2>/dev/null || true)"
|
|
1304
|
+
[ -n "$template" ] || template='{{parent}}/{{repo}}-worktree--{{feature}}'
|
|
1305
|
+
[ -n "$def" ] || def="$(wt_default_branch "$repo")"
|
|
1306
|
+
|
|
1307
|
+
if [ "$mode" = "revert" ]; then
|
|
1308
|
+
[ -f "$cfg" ] || die "未 adopt(缺 $cfg)"
|
|
1309
|
+
local saved
|
|
1310
|
+
saved="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg")"
|
|
1311
|
+
if [ "$prev" != "$hooksdir" ]; then
|
|
1312
|
+
echo "[dia-wt] hooksPath 已被改动($prev),跳过还原并保留 $cfgdir 备份" >&2
|
|
1313
|
+
return 1
|
|
1314
|
+
fi
|
|
1315
|
+
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
|
|
1316
|
+
rm -rf "$cfgdir"
|
|
1317
|
+
echo "reverted:$cfgdir"
|
|
1318
|
+
return 0
|
|
1319
|
+
fi
|
|
1320
|
+
|
|
1321
|
+
if [ "$mode" = "check" ]; then
|
|
1322
|
+
local bad=0
|
|
1323
|
+
[ -f "$cfg" ] || { echo "FAIL D01 未 adopt(缺 $cfg)"; bad=1; }
|
|
1324
|
+
# 策略 SSOT 是 config.json:默认分支以它为准(不从 remote 重猜,避免与已写入的钩子策略漂移)
|
|
1325
|
+
if [ -f "$cfg" ]; then
|
|
1326
|
+
local cfgDef; cfgDef="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
|
|
1327
|
+
[ -n "$cfgDef" ] && def="$cfgDef"
|
|
1328
|
+
fi
|
|
1329
|
+
case "$prev" in /*) ;; *) echo "FAIL D02 core.hooksPath 不是绝对路径($prev)"; bad=1 ;; esac
|
|
1330
|
+
for h in pre-commit pre-push post-checkout; do
|
|
1331
|
+
[ -x "$hooksdir/$h" ] || { echo "FAIL D02 缺 $h 钩子"; bad=1; }
|
|
1332
|
+
done
|
|
1333
|
+
if [ -n "$def" ] && [ -x "$hooksdir/pre-commit" ]; then
|
|
1334
|
+
grep -q "prot=\" main master develop dev $def \"" "$hooksdir/pre-commit" \
|
|
1335
|
+
|| { echo "FAIL D03 钩子未保护默认分支 $def"; bad=1; }
|
|
1336
|
+
if wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1337
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
|
|
1338
|
+
&& { echo "FAIL D03 守卫探针未拦截主 checkout 的 $def 提交"; bad=1; }
|
|
1339
|
+
fi
|
|
1340
|
+
fi
|
|
1341
|
+
if [ "$json" = 1 ]; then
|
|
1342
|
+
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"
|
|
1343
|
+
fi
|
|
1344
|
+
return $bad
|
|
1345
|
+
fi
|
|
1346
|
+
|
|
1347
|
+
local nwt
|
|
1348
|
+
nwt="$(git -C "$repo" worktree list --porcelain 2>/dev/null | grep -c '^worktree ' || true)"
|
|
1349
|
+
if [ "$mode" = "dry" ]; then
|
|
1350
|
+
echo "adopt plan:$repo"
|
|
1351
|
+
echo " common=${common}"
|
|
1352
|
+
echo " defaultBranch=${def:-(无法解析,需 --default-branch)}"
|
|
1353
|
+
echo " template=${template}"
|
|
1354
|
+
echo " worktrees=${nwt}"
|
|
1355
|
+
echo " hooksPath=${prev:-(未设置)} → ${hooksdir}"
|
|
1356
|
+
if [ "$json" = 1 ]; then
|
|
1357
|
+
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"
|
|
1358
|
+
fi
|
|
1359
|
+
return 0
|
|
1360
|
+
fi
|
|
1361
|
+
|
|
1362
|
+
[ -n "$def" ] || die "默认分支不可解析;请 --default-branch main"
|
|
1363
|
+
mkdir -p "$hooksdir"
|
|
1364
|
+
# 保留首次 adopt 记下的 hooksPrevious(重复 apply 时 prev 已是我们自己的 hooksdir)
|
|
1365
|
+
local savedPrev=""
|
|
1366
|
+
[ -f "$cfg" ] && savedPrev="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg" 2>/dev/null || true)"
|
|
1367
|
+
[ "$prev" = "$hooksdir" ] && prev="$savedPrev"
|
|
1368
|
+
# 钩子链目标:首次 adopt 前的 hooksPath(husky/.githooks/…);没有则用仓库默认 hooks 目录
|
|
1369
|
+
local chain="$prev"
|
|
1370
|
+
[ -n "$chain" ] || chain="$common/hooks"
|
|
1371
|
+
[ -f "$cfg" ] && cp "$cfg" "$cfgdir/config.json.bak.$(date +%s)"
|
|
1372
|
+
python3 - "$cfg" "$repo" "$def" "$template" "$hooksdir" "$prev" "$chain" <<'PY'
|
|
1373
|
+
import json, sys, datetime
|
|
1374
|
+
cfg, repo, br, tmpl, hooks, prev, chain = sys.argv[1:8]
|
|
1375
|
+
data = {
|
|
1376
|
+
"schema": 1, "repo": repo, "defaultBranch": br, "template": tmpl,
|
|
1377
|
+
"hooksPath": hooks, "hooksPrevious": prev or None, "hooksChain": chain,
|
|
1378
|
+
"adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
1379
|
+
}
|
|
1380
|
+
json.dump(data, open(cfg, "w"), ensure_ascii=False, indent=1)
|
|
1381
|
+
PY
|
|
1382
|
+
# 提示:simple-git-hooks / husky 的 postinstall 会重写 core.hooksPath 下的钩子,
|
|
1383
|
+
# 每次 `pnpm/npm install` 后可能把守卫冲掉 → 用 `dia wt doctor --fix` 或重跑 adopt --apply 修复。
|
|
1384
|
+
if grep -qs '"simple-git-hooks"\|"husky"\|lefthook' "$repo/package.json" "$repo/.lefthook.yml" 2>/dev/null; then
|
|
1385
|
+
echo "[dia-wt] 注意:该仓使用 hook manager(simple-git-hooks/husky/lefthook),install 后若守卫被重写,跑 dia wt doctor --fix" >&2
|
|
1386
|
+
fi
|
|
1387
|
+
wt_write_hook "$hooksdir" pre-commit "$def" "$chain"
|
|
1388
|
+
wt_write_hook "$hooksdir" pre-push "$def" "$chain"
|
|
1389
|
+
wt_write_hook "$hooksdir" post-checkout "$def" "$chain"
|
|
1390
|
+
git -C "$repo" config --local core.hooksPath "$hooksdir"
|
|
1391
|
+
echo "adopted:$repo(hooksPath=$hooksdir)"
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
cmd_doctor() {
|
|
1395
|
+
local json=0 all=0 fix=0 repos=()
|
|
1396
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
1397
|
+
--json) json=1; shift ;;
|
|
1398
|
+
--all) all=1; shift ;;
|
|
1399
|
+
--fix) fix=1; shift ;;
|
|
1400
|
+
--strict) shift ;;
|
|
1401
|
+
-*) die "未知参数:$1" ;;
|
|
1402
|
+
*) repos+=("$1"); shift ;;
|
|
1403
|
+
esac; done
|
|
1404
|
+
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)
|
|
1406
|
+
fi
|
|
1407
|
+
if [ ${#repos[@]} -eq 0 ]; then
|
|
1408
|
+
echo "usage: dia wt doctor <repo>... | --all(--all 未匹配到任何仓库)" >&2
|
|
1409
|
+
exit 2
|
|
1410
|
+
fi
|
|
1411
|
+
local fails=0
|
|
1412
|
+
for repo in "${repos[@]:-}"; do
|
|
1413
|
+
[ -n "$repo" ] || continue
|
|
1414
|
+
git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || continue
|
|
1415
|
+
local common cfgdir cfg hooksdir prev def
|
|
1416
|
+
common="$(wt_common_dir "$repo")"; cfgdir="$common/dia"; cfg="$cfgdir/config.json"; hooksdir="$cfgdir/hooks"
|
|
1417
|
+
prev="$(git -C "$repo" config --get core.hooksPath 2>/dev/null || true)"
|
|
1418
|
+
def=""
|
|
1419
|
+
if [ -f "$cfg" ]; then
|
|
1420
|
+
def="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
|
|
1421
|
+
fi
|
|
1422
|
+
[ -n "$def" ] || def="$(wt_default_branch "$repo")"
|
|
1423
|
+
# --fix:无损修复(重写三个守卫钩子 + hooksPath 绝对路径)。只对已 adopt 的仓生效。
|
|
1424
|
+
if [ "$fix" = 1 ] && [ -f "$cfg" ]; then
|
|
1425
|
+
local fixChain=""
|
|
1426
|
+
fixChain="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksChain") or "")' "$cfg" 2>/dev/null || true)"
|
|
1427
|
+
[ -n "$fixChain" ] || fixChain="$common/hooks"
|
|
1428
|
+
mkdir -p "$hooksdir"
|
|
1429
|
+
wt_write_hook "$hooksdir" pre-commit "$def" "$fixChain"
|
|
1430
|
+
wt_write_hook "$hooksdir" pre-push "$def" "$fixChain"
|
|
1431
|
+
wt_write_hook "$hooksdir" post-checkout "$def" "$fixChain"
|
|
1432
|
+
[ "$prev" = "$hooksdir" ] || git -C "$repo" config --local core.hooksPath "$hooksdir"
|
|
1433
|
+
prev="$hooksdir"
|
|
1434
|
+
[ "$json" = 0 ] && echo "FIX $repo 钩子已重装(chain=$fixChain)"
|
|
1435
|
+
fi
|
|
1436
|
+
local bad=0
|
|
1437
|
+
[ -f "$cfg" ] || { echo "FAIL D01 $repo 未 adopt"; bad=1; }
|
|
1438
|
+
case "$prev" in /*) ;; *) echo "FAIL D02 $repo hooksPath 非绝对($prev)"; bad=1 ;; esac
|
|
1439
|
+
for h in pre-commit pre-push post-checkout; do
|
|
1440
|
+
[ -x "$hooksdir/$h" ] || { echo "FAIL D02 $repo 缺 $h"; bad=1; }
|
|
1441
|
+
done
|
|
1442
|
+
if [ -n "$def" ] && [ -x "$hooksdir/pre-commit" ]; then
|
|
1443
|
+
# 静态:钩子里的 protected 列表必须包含 defaultBranch
|
|
1444
|
+
grep -q "prot=\" main master develop dev $def \"" "$hooksdir/pre-commit" \
|
|
1445
|
+
|| { echo "FAIL D03 $repo 钩子未保护默认分支 $def"; bad=1; }
|
|
1446
|
+
# 动态:主 checkout 当前就在 def 上时必须被拦
|
|
1447
|
+
if wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1448
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
|
|
1449
|
+
&& { echo "FAIL D03 $repo 守卫未拦截 $def 提交"; bad=1; }
|
|
1450
|
+
fi
|
|
1451
|
+
fi
|
|
1452
|
+
if wt_is_main_checkout "$repo"; then
|
|
1453
|
+
[ -z "$(git -C "$repo" status --porcelain 2>/dev/null)" ] || echo "WARN D04 $repo 主 checkout 有未提交改动"
|
|
1454
|
+
[ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ] || echo "WARN D04 $repo 主 checkout 不在 $def"
|
|
1455
|
+
fi
|
|
1456
|
+
local missing=0
|
|
1457
|
+
while IFS= read -r p; do [ -n "$p" ] && [ ! -d "$p" ] && { echo "FAIL D05 $repo worktree 路径缺失:$p"; missing=1; }; done \
|
|
1458
|
+
< <(git -C "$repo" -c core.quotePath=false worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
|
|
1459
|
+
[ "$missing" = 0 ] || bad=1
|
|
1460
|
+
[ "$(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; }
|
|
1461
|
+
[ -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; }
|
|
1462
|
+
[ -n "$def" ] || { echo "FAIL D11 $repo 默认分支不可解析"; bad=1; }
|
|
1463
|
+
if [ "$json" = 1 ]; then
|
|
1464
|
+
python3 -c 'import json,sys;print(json.dumps({"repo":sys.argv[1],"ok":sys.argv[2]=="0"},ensure_ascii=False))' "$repo" "$bad"
|
|
1465
|
+
fi
|
|
1466
|
+
[ "$bad" = 0 ] && [ "$json" = 0 ] && echo "ok $repo"
|
|
1467
|
+
[ "$bad" = 1 ] && fails=$((fails+1))
|
|
1468
|
+
done
|
|
1469
|
+
[ "$fails" = 0 ]
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
cmd_gate() {
|
|
1473
|
+
local repo="" feature=""
|
|
1474
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
1475
|
+
-*) shift ;;
|
|
1476
|
+
*) if [ -z "$repo" ]; then repo="$1"; elif [ -z "$feature" ]; then feature="$1"; fi; shift ;;
|
|
1477
|
+
esac; done
|
|
1478
|
+
[ -n "$repo" ] && [ -n "$feature" ] || die "用法:dia wt gate <repo> <feature>"
|
|
1479
|
+
echo "== check(R20)"; cmd_check "$repo" || { echo "gate FAIL: check"; return 1; }
|
|
1480
|
+
echo "== doctor"; cmd_doctor "$repo" || { echo "gate FAIL: doctor"; return 1; }
|
|
1481
|
+
local def; def="$(wt_default_branch "$repo")"
|
|
1482
|
+
local wt=""
|
|
1483
|
+
while IFS= read -r p; do
|
|
1484
|
+
[ -n "$p" ] || continue
|
|
1485
|
+
local br; br="$(git -C "$p" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
1486
|
+
if [ "$br" = "feat/$feature" ] || [ "$(basename "$p")" = "${repo##*/}-worktree--$feature" ]; then wt="$p"; break; fi
|
|
1487
|
+
done < <(git -C "$repo" -c core.quotePath=false worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
|
|
1488
|
+
[ -n "$wt" ] || { echo "gate FAIL: 找不到 feature=$feature 的 worktree"; return 1; }
|
|
1489
|
+
[ -z "$(git -C "$wt" status --porcelain)" ] || { echo "gate FAIL: worktree 有未提交改动"; return 1; }
|
|
1490
|
+
local ahead; ahead="$(git -C "$wt" rev-list --count "origin/${def}..HEAD" 2>/dev/null || echo 0)"
|
|
1491
|
+
[ "$ahead" -gt 0 ] || { echo "gate FAIL: 相对 origin/${def} 没有提交"; return 1; }
|
|
1492
|
+
echo "gate OK:$feature(ahead origin/${def} = $ahead)"
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
|
|
1096
1496
|
case "${1:-}" in
|
|
1097
1497
|
create) shift; cmd_create "$@" ;;
|
|
1098
1498
|
route) shift; cmd_route "$@" ;;
|
|
@@ -1101,6 +1501,9 @@ case "${1:-}" in
|
|
|
1101
1501
|
list) shift; cmd_list "$@" ;;
|
|
1102
1502
|
status) shift; cmd_status "$@" ;;
|
|
1103
1503
|
check) shift; cmd_check "$@" ;;
|
|
1504
|
+
adopt) shift; cmd_adopt "$@" ;;
|
|
1505
|
+
doctor) shift; cmd_doctor "$@" ;;
|
|
1506
|
+
gate) shift; cmd_gate "$@" ;;
|
|
1104
1507
|
clean) shift; cmd_clean "$@" ;;
|
|
1105
1508
|
prune-merged) shift; cmd_prune_merged "$@" ;;
|
|
1106
1509
|
""|help|-h|--help)
|
|
@@ -1114,6 +1517,12 @@ dia wt —— worktree 目录形式选型 + 创建(唯一实现)
|
|
|
1114
1517
|
dia wt status [repo-path] # ★ 清理前必查:唯一工作副本 + 改动重叠(只读)
|
|
1115
1518
|
dia wt check [<repo>...] [--all] [--deep] [--quiet]
|
|
1116
1519
|
# ★ 依赖体检(R20):软链/逃逸/悬空;--all 扫全部仓
|
|
1520
|
+
dia wt adopt <repo>|--all [--apply|--check|--revert] [--template T] [--default-branch B] [--json]
|
|
1521
|
+
# 配置路径模板 + 守卫钩子(默认 dry-run;--all 扫仓根)
|
|
1522
|
+
# 守卫含推送前敏感扫描(DIA_WT_GUARD_SCAN=0 可临时关闭)
|
|
1523
|
+
dia wt doctor [<repo>...] [--all] [--json] [--fix]
|
|
1524
|
+
# 巡检:配置/钩子/探针/主 checkout/worktree 卫生;--fix 重装守卫钩子
|
|
1525
|
+
dia wt gate <repo> <feature> # 提交 MR 前:check + doctor + 干净 + ahead>0
|
|
1117
1526
|
dia wt route <repo-path> # ★ 开工前必查:走 MR 还是直推(R1.7,配置驱动)
|
|
1118
1527
|
dia wt scope <repo|worktree> [--feature <slug>] [--session <id>] # ★ 产出作用域声明(设计 §4.5)
|
|
1119
1528
|
dia wt scope --show # 看与当前目录匹配的作用域声明
|