dsh-recall-plugin 2.0.0 → 2.1.1
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/CHANGELOG.md +63 -29
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/lib/client.js +1459 -1479
- package/lib/diagnostics.js +59 -0
- package/lib/errors.js +73 -0
- package/lib/index.js +426 -1129
- package/lib/routes-core.js +162 -0
- package/lib/routes-manage.js +541 -0
- package/lib/scripts.posix.js +195 -27
- package/lib/scripts.pwsh.js +194 -33
- package/lib/session-info.js +71 -0
- package/lib/snapshots.js +243 -39
- package/lib/store.js +157 -19
- package/package.json +65 -61
package/lib/scripts.posix.js
CHANGED
|
@@ -30,6 +30,27 @@ export const UTF8_PRELUDE = 'export LC_ALL=C'
|
|
|
30
30
|
// 实际生效值以 store.maxFileBytes(用户 config 可调)经 oversizeBlock 注入为准
|
|
31
31
|
export const MAX_FILE_BYTES = 104857600
|
|
32
32
|
|
|
33
|
+
// 失败清扫的 stale 锁阈值(分钟,M3):锁文件 mtime 超过该值才视为残留可清;
|
|
34
|
+
// 更新的锁视为「有 git 操作正在进行」让路。本插件单条快照/回退的超时是
|
|
35
|
+
// 10 分钟,阈值取其一半,保证本方超时遗留的锁(≥10 分钟)一定能被清。
|
|
36
|
+
// 与 pwsh 版必须同值(scripts-contract 钉);属内部安全策略常量,与
|
|
37
|
+
// snapshots.js 的 FUSE_AFTER 同类,不走 Config。
|
|
38
|
+
export const STALE_LOCK_MIN = 5
|
|
39
|
+
|
|
40
|
+
// 心跳文件有效窗口(秒,M3):超过该时长的心跳视为失效实例(崩溃/重启
|
|
41
|
+
// 遗留或长期空闲),不再阻止清扫。心跳随 ensureGit/快照刷新,活动中的
|
|
42
|
+
// 实例窗口内必然有新心跳;长 git 操作(≤10 分钟)期间心跳写于操作开头,
|
|
43
|
+
// 仍在窗口内。
|
|
44
|
+
export const HEARTBEAT_TTL_S = 900
|
|
45
|
+
|
|
46
|
+
// 影子仓库 info/attributes 固化内容(issue #12 字节保真):与 pwsh 版同名
|
|
47
|
+
// 常量逐字同值(scripts-contract 钉),完整语义与逐项动机见 scripts.pwsh.js
|
|
48
|
+
// 同名常量注释。一句话:git archive/add 都会应用快照树里项目自己的
|
|
49
|
+
// .gitattributes(text=auto + 缺省 core.eol=native 的转换,仓库级
|
|
50
|
+
// autocrlf=false 挡不住),info/attributes 优先级最高、对全部路径一票否决,
|
|
51
|
+
// 快照 capture/restore 两侧逐字节保真。
|
|
52
|
+
export const FIDELITY_ATTRS = '* -text -filter -ident -export-ignore -export-subst -working-tree-encoding'
|
|
53
|
+
|
|
33
54
|
// bash/cat 输出无 BOM;保留同名导出维持两套模板接口一致(幂等无害)
|
|
34
55
|
export function stripBom(text) {
|
|
35
56
|
return text.replace(/^\uFEFF/, '')
|
|
@@ -83,18 +104,31 @@ function excludeSyncBlock(excludeFile, base) {
|
|
|
83
104
|
' while IFS= read -r line || [ -n "$line" ]; do',
|
|
84
105
|
" t=${line%$'\\r'}",
|
|
85
106
|
' t="${t#"${t%%[![:space:]]*}"}"; t="${t%"${t##*[![:space:]]}"}"',
|
|
86
|
-
' [ -z "$t" ]
|
|
107
|
+
' if [ -z "$t" ]; then continue; fi',
|
|
87
108
|
' case "$t" in \\#*) continue ;; esac',
|
|
88
109
|
' user_pats="$user_pats$t\\n"',
|
|
89
110
|
' done < "$ex_file"',
|
|
90
111
|
'fi',
|
|
91
112
|
"printf '\\n" + baseLines.replace(/\\/g, '\\\\').replace(/%/g, '%%') + "%b' \"$user_pats\" > \"$exc\"",
|
|
92
113
|
'"$git" -c core.quotePath=false --literal-pathspecs --git-dir="$g" ls-files -i -c --exclude-from="$exc" -z 2>/dev/null | while IFS= read -r -d \'\' p; do',
|
|
93
|
-
' [ -n "$p" ]
|
|
114
|
+
' if [ -n "$p" ]; then "$git" --literal-pathspecs --git-dir="$g" update-index --force-remove -- "$p"; fi',
|
|
94
115
|
'done || true'
|
|
95
116
|
].join('\n')
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
// 心跳写入(M3):随 git 操作顺手把「宿主 PID + epoch 秒」写进 store 目录
|
|
120
|
+
// (store.dir = git-dir 上两级),供对方实例的失败清扫判定「另一个 DSH 实例
|
|
121
|
+
// 正在使用此快照库」。PID 在模板生成期取宿主进程的 process.pid,无需调用方
|
|
122
|
+
// 传参。fail-open(|| true,set -e 下安全):心跳写失败绝不能连累快照本身
|
|
123
|
+
// ——最坏退化为无心跳,清扫只剩新锁分级保护。
|
|
124
|
+
// 依赖外层已定义的 $g;被 ensureGitScript / snapshotScript 复用。
|
|
125
|
+
function heartbeatBlock() {
|
|
126
|
+
return [
|
|
127
|
+
'hbf="$(dirname "$(dirname "$g")")/heartbeat"',
|
|
128
|
+
'printf \'%s %s\\n\' ' + psq(String(process.pid)) + ' "$(date +%s)" > "$hbf" 2>/dev/null || true'
|
|
129
|
+
].join('\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
98
132
|
// 解析 git 可执行文件路径:bash 从 PATH 找(POSIX 上 git 装了就在 PATH,
|
|
99
133
|
// 没有 Windows 那种四类安装位置的散装问题)
|
|
100
134
|
export function resolveGitScript() {
|
|
@@ -132,6 +166,25 @@ export function migrateScript(src, dst) {
|
|
|
132
166
|
].join('\n')
|
|
133
167
|
}
|
|
134
168
|
|
|
169
|
+
// 旧快照容器一次性迁移(POSIX 专属,I24 漂移修复的存量数据兜底,输出四态
|
|
170
|
+
// 由 store.js resolvePosixHomeBase 消费):仅当「旧容器存在且新容器不存在」
|
|
171
|
+
// 才整容器 mv——同卷(home 内部)rename 原子,无部分移动状态;容器级整移
|
|
172
|
+
// 自然带上根级 exclude.txt,语义无损。BOTH_PRESENT(双容器并存)/MIGRATE_FAIL
|
|
173
|
+
// 输出后不动任何数据。无 set -e(要靠分支输出状态而非中途退出);if 条件
|
|
174
|
+
// 内的 && 链豁免 I16 约束(该坑只针对循环体与裸列表)。非 git 命令,不适用
|
|
175
|
+
// g= 赋值 / RECALL_CLEANUP 哨兵约定。
|
|
176
|
+
export function legacyHomeMigrateScript(homedir) {
|
|
177
|
+
return [
|
|
178
|
+
'old=' + psq(homedir + '/dsh-recall-snapshots'),
|
|
179
|
+
'new=' + psq(homedir + '/.dsh/dsh-recall-snapshots'),
|
|
180
|
+
'if [ -d "$old" ] && [ ! -d "$new" ]; then',
|
|
181
|
+
' if mkdir -p -- "$(dirname "$new")" && mv -f -- "$old" "$new"; then echo MIGRATE_OK',
|
|
182
|
+
' else echo MIGRATE_FAIL; fi',
|
|
183
|
+
'elif [ -d "$old" ]; then echo BOTH_PRESENT',
|
|
184
|
+
'else echo OLD_ABSENT; fi'
|
|
185
|
+
].join('\n')
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
// 建立影子仓库 + 排除同步 + 回读 gc.stamp(语义与 pwsh 版一致,
|
|
136
189
|
// 见 scripts.pwsh.js 同名函数注释)
|
|
137
190
|
export function ensureGitScript(store, gitExe, base) {
|
|
@@ -140,16 +193,49 @@ export function ensureGitScript(store, gitExe, base) {
|
|
|
140
193
|
'git=' + psq(gitExe),
|
|
141
194
|
'repo=' + psq(store.repo),
|
|
142
195
|
'g=' + psq(store.git),
|
|
143
|
-
|
|
196
|
+
heartbeatBlock(),
|
|
197
|
+
// 冷启动首消息快照与启动预热(或双实例)并发时,两个 git init 在空
|
|
198
|
+
// repo 目录同跑,输家报 fatal: cannot mkdir <git>: File exists——窗口
|
|
199
|
+
// 极小但首条消息的快照会因此丢失。git init 幂等:失败后 HEAD 已出现
|
|
200
|
+
// 即同伴建成,视同成功继续;HEAD 也没有才是真失败,带错误退出(诊断
|
|
201
|
+
// 不丢)。检查 HEAD 而非目录存在:半截目录也会被 init 补齐。pwsh 版
|
|
202
|
+
// 无需同款改动——native 非零退出不抛(I14),输家继续跑 config 时
|
|
203
|
+
// 同伴已建好 repo,竞态天然容忍,真失败由快照 add 的显式检查兜底。
|
|
204
|
+
'if [ ! -f "$g/HEAD" ]; then',
|
|
205
|
+
' init_log=$("$git" init "$repo" 2>&1) || {',
|
|
206
|
+
' if [ ! -f "$g/HEAD" ]; then printf \'%s\\n\' "$init_log" >&2; exit 1; fi',
|
|
207
|
+
' }',
|
|
208
|
+
'fi',
|
|
144
209
|
'"$git" --git-dir="$g" config core.longpaths true',
|
|
145
210
|
'"$git" --git-dir="$g" config core.autocrlf false',
|
|
146
211
|
'"$git" --git-dir="$g" config advice.addEmbeddedRepo false',
|
|
212
|
+
// 属性固化(issue #12,内容见 FIDELITY_ATTRS):info/ 由 git init 自带
|
|
213
|
+
// (info/exclude 模板,excludeSyncBlock 同样依赖),无需建目录;每次
|
|
214
|
+
// ensureGit 重写幂等,存量仓库升级后首次 init 自然补上。
|
|
215
|
+
'printf ' + psq(FIDELITY_ATTRS + '\n') + ' > "$g/info/attributes"',
|
|
147
216
|
excludeSyncBlock(store.excludeFile, base),
|
|
148
217
|
'stamp="$g/gc.stamp"',
|
|
149
218
|
'if [ -f "$stamp" ]; then printf \'GIT_OK %s\\n\' "$(head -n1 "$stamp" 2>/dev/null)"; else echo GIT_OK; fi'
|
|
150
219
|
].join('\n')
|
|
151
220
|
}
|
|
152
221
|
|
|
222
|
+
// 存量归一化迁移(issue #12,语义与动机详见 pwsh 版 attrsMigrateBlock 注释):
|
|
223
|
+
// 属性固化后旧索引条目仍指向归一化 blob(stat 缓存时序依赖地跳过重哈希),
|
|
224
|
+
// --renormalize 按当前属性重哈希一次;无 pathspec 是空操作,必须带 ':(top)'
|
|
225
|
+
// 顶层魔法 pathspec。标记文件每仓库至多跑一次;失败(老 git 无该选项等)
|
|
226
|
+
// 只跳过标记下条消息重试,不连累快照主流程(set -e 下 || rc=$? 捕获)。
|
|
227
|
+
// 依赖外层已定义的 $git/$g/$root;仅 snapshotScript 使用。
|
|
228
|
+
function attrsMigrateBlock() {
|
|
229
|
+
return [
|
|
230
|
+
'mig_stamp="$g/attrs-v1.stamp"',
|
|
231
|
+
'if [ ! -f "$mig_stamp" ]; then',
|
|
232
|
+
' migrc=0',
|
|
233
|
+
' "$git" --git-dir="$g" --work-tree="$root" add --renormalize --ignore-errors -- \':(top)\' >/dev/null 2>&1 || migrc=$?',
|
|
234
|
+
' if [ "$migrc" -le 1 ]; then printf \'1\\n\' > "$mig_stamp" 2>/dev/null || true; fi',
|
|
235
|
+
'fi',
|
|
236
|
+
].join('\n')
|
|
237
|
+
}
|
|
238
|
+
|
|
153
239
|
// 快照:add -A → write-tree → commit-tree(孤儿提交)→ tag(语义同 pwsh 版)。
|
|
154
240
|
// tag -f:事件重放/重发会产生重复 messageId,裸 tag 对已存在 tag fatal
|
|
155
241
|
// 导致整条快照失败;-f 把 tag 指到最新提交,语义为「同一条消息取最新状态」。
|
|
@@ -159,8 +245,10 @@ export function snapshotScript(root, store, gitExe, messageId, base) {
|
|
|
159
245
|
'git=' + psq(gitExe),
|
|
160
246
|
'g=' + psq(store.git),
|
|
161
247
|
'root=' + psq(root),
|
|
248
|
+
heartbeatBlock(),
|
|
162
249
|
dropGitlinksBlock(),
|
|
163
250
|
excludeSyncBlock(store.excludeFile, base),
|
|
251
|
+
attrsMigrateBlock(),
|
|
164
252
|
// fail-open add(issue #7 加固,语义见 pwsh 版同款注释):--ignore-errors
|
|
165
253
|
// 下「无法索引的路径」以退出码 1 结束但索引已落盘;≥2 才是真 fatal,
|
|
166
254
|
// 显式退出让 runShell 抛错(set -e 对 add 非零本会终止,但 || rc=$?
|
|
@@ -240,6 +328,11 @@ export function diffScript(root, store, gitExe, tag, base) {
|
|
|
240
328
|
// 回退:archive | tar 直接管到工作区(无需 Windows 的 zip 中转),
|
|
241
329
|
// 空目标跳过;再删除「当前有、目标无」的文件(awk 求差集)。
|
|
242
330
|
// pipefail 保证 git archive 失败时整条非零退出。
|
|
331
|
+
// 删除侧失败必须响亮(F-G2):set -e 豁免 if 条件内的 rm 失败——若写成
|
|
332
|
+
// `rm ... && deleted++` 裸链,rm 失败(权限等)被静默跳过、脚本仍输出
|
|
333
|
+
// ROLLBACK_OK,半回退报成功、救援永不触发;改为 if/fi + 失败显式 exit 1,
|
|
334
|
+
// 与 pwsh 版「EAP=Stop 下 Remove-Item 抛终止错误」对齐「删除失败即失败」
|
|
335
|
+
// 的语义(见 scripts.pwsh.js rollbackScript 同位置注释)。
|
|
243
336
|
export function rollbackScript(root, store, gitExe, tag, base) {
|
|
244
337
|
return [
|
|
245
338
|
'set -e -o pipefail',
|
|
@@ -265,13 +358,31 @@ export function rollbackScript(root, store, gitExe, tag, base) {
|
|
|
265
358
|
"' \"$tmpc\" \"$tmpt\" > \"$tmpd\"",
|
|
266
359
|
'deleted=0',
|
|
267
360
|
'while IFS= read -r p; do',
|
|
268
|
-
|
|
269
|
-
|
|
361
|
+
// 循环体禁裸 && 链(AGENTS.md 已知坑同款规矩):&& 列表条件为假时整条
|
|
362
|
+
// 退出码为 1,set -e 会把脚本杀掉;if 语句天然豁免。rm 失败必须响亮
|
|
363
|
+
// exit 1——半回退假成功会让救援永不触发(F-G2)
|
|
364
|
+
' if [ -z "$p" ]; then continue; fi',
|
|
365
|
+
' if rm -f -- "$root/$p"; then deleted=$((deleted + 1)); else echo "RM_FAILED $p" >&2; exit 1; fi',
|
|
270
366
|
'done < "$tmpd"',
|
|
271
367
|
'echo "ROLLBACK_OK $deleted $restored"'
|
|
272
368
|
].join('\n')
|
|
273
369
|
}
|
|
274
370
|
|
|
371
|
+
// 回退失败救援(H1,语义见 pwsh 版同款注释):reset --hard 回安全快照。
|
|
372
|
+
// 入参 tag 是完整 tag 名(snap- 前缀由调用侧 rescueRollback 拼齐)。
|
|
373
|
+
// set -e 下 git 非零退出自然终止脚本,与 pwsh 版 $LASTEXITCODE 显式自检
|
|
374
|
+
// 对齐「失败即抛」语义。
|
|
375
|
+
export function rescueScript(root, store, gitExe, tag) {
|
|
376
|
+
return [
|
|
377
|
+
'set -e',
|
|
378
|
+
'git=' + psq(gitExe),
|
|
379
|
+
'g=' + psq(store.git),
|
|
380
|
+
'root=' + psq(root),
|
|
381
|
+
'"$git" --git-dir="$g" --work-tree="$root" reset --hard ' + psq(tag),
|
|
382
|
+
'echo RESCUE_OK'
|
|
383
|
+
].join('\n')
|
|
384
|
+
}
|
|
385
|
+
|
|
275
386
|
export function listTagsScript(store, gitExe) {
|
|
276
387
|
return [
|
|
277
388
|
'set -e',
|
|
@@ -284,6 +395,20 @@ export function listTagsScript(store, gitExe) {
|
|
|
284
395
|
].join('\n')
|
|
285
396
|
}
|
|
286
397
|
|
|
398
|
+
// 孤儿重建用 tag 清单(带 creatordate):rebuildOrphans 据此恢复快照
|
|
399
|
+
// 时间——只列 tag 名会让重建条目 time=0,管理列表时间前缀缺失、
|
|
400
|
+
// retention/limits 按「最旧」误清。lightweight tag 无 tag 对象,
|
|
401
|
+
// creatordate 即指向 commit 的提交日期。输出每行「<tag名> <秒级时间戳>」。
|
|
402
|
+
export function listTagsWithTimeScript(store, gitExe) {
|
|
403
|
+
return [
|
|
404
|
+
'set -e',
|
|
405
|
+
'git=' + psq(gitExe),
|
|
406
|
+
'g=' + psq(store.git),
|
|
407
|
+
'[ -d "$g" ] || exit 0',
|
|
408
|
+
'"$git" --git-dir="$g" for-each-ref --format=\'%(refname:short) %(creatordate:unix)\' \'refs/tags/snap-*\''
|
|
409
|
+
].join('\n')
|
|
410
|
+
}
|
|
411
|
+
|
|
287
412
|
// 定期 gc(语义同 pwsh 版);date +%s 写秒级时间戳,JS 侧 ×1000
|
|
288
413
|
export function gcScript(store, gitExe) {
|
|
289
414
|
return [
|
|
@@ -308,28 +433,59 @@ export function pruneScript(store, gitExe) {
|
|
|
308
433
|
].join('\n')
|
|
309
434
|
}
|
|
310
435
|
|
|
311
|
-
// 失败后的孤儿进程清扫 + stale 锁清理(issue #7
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
'
|
|
329
|
-
'
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
436
|
+
// 失败后的孤儿进程清扫 + stale 锁清理(issue #7 兜底 + issue #11 根因治理)。
|
|
437
|
+
// 三级出口(M3):
|
|
438
|
+
// 1. CLEANUP_OTHER_INSTANCE <pid>——心跳文件(宿主 PID + epoch 秒)显示另一
|
|
439
|
+
// 个存活实例正在使用同一快照库:直接让路,不杀进程、不动锁。这是两个
|
|
440
|
+
// DSH 实例并发互踩(一方清扫误杀另一方活跃 git → 对方也失败 → 循环)的
|
|
441
|
+
// 根治;心跳由 ensureGit/snapshotScript 随操作刷新,TTL 外视为失效。
|
|
442
|
+
// 2. CLEANUP_SKIPPED_FRESH_LOCK——存在 STALE_LOCK_MIN 分钟内的新锁(疑似有
|
|
443
|
+
// git 操作正在进行):同样让路,锁陈旧后下次失败自然进入第 3 级。
|
|
444
|
+
// 3. CLEANUP_DONE——两级保护均未命中,按原有行为清孤儿进程与 stale 锁。
|
|
445
|
+
// POSIX 版差异:pgrep -f 按扩展正则匹配整条命令行——路径元字符([、+ 等)
|
|
446
|
+
// 会让模式失配,清扫静默失效,属安全降级(等价于本兜底加入前的行为);
|
|
447
|
+
// 标记在运行期拼接,本脚本自身命令行里只有未展开的 $g 字面量,不会自杀。
|
|
448
|
+
// 全程无 set -e + 逐步容错:清扫自身的失败不能抛。mtime 判定用 find -mmin
|
|
449
|
+
// 而非 stat:GNU/BSD stat 参数不同(macOS bash 3.2 约束),find 的 -mmin
|
|
450
|
+
// 两平台语义一致。
|
|
451
|
+
export function killOrphansScript(gitDir) {
|
|
452
|
+
return [
|
|
453
|
+
'# RECALL_CLEANUP',
|
|
454
|
+
'g=' + psq(gitDir),
|
|
455
|
+
'hbf="$(dirname "$(dirname "$g")")/heartbeat"',
|
|
456
|
+
'if [ -f "$hbf" ]; then',
|
|
457
|
+
' hbl=$(head -n1 "$hbf" 2>/dev/null | tr -d \'\\r\')',
|
|
458
|
+
' hbp=${hbl%% *}',
|
|
459
|
+
' hbt=${hbl#* }',
|
|
460
|
+
" case \"$hbp\" in ''|*[!0-9]*) hbp='' ;; esac",
|
|
461
|
+
" case \"$hbt\" in ''|*[!0-9]*) hbt=0 ;; esac",
|
|
462
|
+
' hbage=$(( $(date +%s) - hbt ))',
|
|
463
|
+
' if [ -n "$hbp" ] && [ "$hbp" != ' + psq(String(process.pid)) + ' ] && [ "$hbage" -ge 0 ] && [ "$hbage" -lt ' + HEARTBEAT_TTL_S + ' ]; then',
|
|
464
|
+
' if kill -0 "$hbp" 2>/dev/null; then',
|
|
465
|
+
' echo "CLEANUP_OTHER_INSTANCE $hbp"',
|
|
466
|
+
' exit 0',
|
|
467
|
+
' fi',
|
|
468
|
+
' fi',
|
|
469
|
+
'fi',
|
|
470
|
+
"fresh=$(find \"$g\" -maxdepth 1 -type f \\( -name '*.lock' -o -name 'gc.pid' \\) -mmin -" + STALE_LOCK_MIN + " 2>/dev/null; find \"$g/refs\" -type f -name '*.lock' -mmin -" + STALE_LOCK_MIN + ' 2>/dev/null)',
|
|
471
|
+
'if [ -n "$fresh" ]; then',
|
|
472
|
+
' echo CLEANUP_SKIPPED_FRESH_LOCK',
|
|
473
|
+
' exit 0',
|
|
474
|
+
'fi',
|
|
475
|
+
'marker="--git-dir=$g"',
|
|
476
|
+
'for p in $(pgrep -f -- "$marker" 2>/dev/null); do',
|
|
477
|
+
' [ "$p" = "$$" ] && continue',
|
|
478
|
+
' kill "$p" 2>/dev/null || true',
|
|
479
|
+
'done',
|
|
480
|
+
// 锁清单对齐 pwsh 版:index.lock 是 add/checkout 持久锁,其余是
|
|
481
|
+
// gc/tag/pack 链路残留;refs 下 per-ref 锁用 find 兜底(只删陈旧锁,
|
|
482
|
+
// 新锁已被上方分级保护拦下)
|
|
483
|
+
'rm -f "$g/index.lock" "$g/config.lock" "$g/HEAD.lock" "$g/gc.pid" "$g/packed-refs.lock" "$g/shallow.lock" 2>/dev/null || true',
|
|
484
|
+
'find "$g/refs" -type f -name "*.lock" -mmin +' + STALE_LOCK_MIN + ' -delete 2>/dev/null || true',
|
|
485
|
+
'echo CLEANUP_DONE'
|
|
486
|
+
].join('\n')
|
|
487
|
+
}
|
|
488
|
+
|
|
333
489
|
// 删除指定快照 tag(会话已删联动清理用):tag -d 对不存在 tag 非零退出,
|
|
334
490
|
// || true 吞掉——best-effort,残留 tag 由下次清理幂等收尾(同 pwsh 版)
|
|
335
491
|
export function purgeTagsScript(store, gitExe, tags) {
|
|
@@ -341,12 +497,24 @@ export function purgeTagsScript(store, gitExe, tags) {
|
|
|
341
497
|
].join('\n')
|
|
342
498
|
}
|
|
343
499
|
|
|
500
|
+
// 原子 rename(H2,语义见 pwsh 版同款注释):mv -f 同卷 move 为 O(1) 元
|
|
501
|
+
// 数据操作;也用于 loadIndex 把损坏索引改名 .corrupt-<ts> 保留现场。
|
|
502
|
+
export function renameFileCmd(src, dst) {
|
|
503
|
+
return 'mv -f -- ' + psq(src) + ' ' + psq(dst)
|
|
504
|
+
}
|
|
505
|
+
|
|
344
506
|
// 索引读取(写入走 stdin:见 snapshots.js saveIndex 的 POSIX 分支,
|
|
345
507
|
// 不经命令行传参,天然没有 32767/128KB argv 上限问题)
|
|
346
508
|
export function indexReadCmd(dir) {
|
|
347
509
|
return 'cat ' + psq(dir + '/index.json') + ' 2>/dev/null || true'
|
|
348
510
|
}
|
|
349
511
|
|
|
512
|
+
// fork lineage 读取(F1,语义见 pwsh 版同款注释):lineage.json 与 index.json
|
|
513
|
+
// 同层、原子写;缺失文件输出空串。
|
|
514
|
+
export function lineageReadCmd(dir) {
|
|
515
|
+
return 'cat ' + psq(dir + '/lineage.json') + ' 2>/dev/null || true'
|
|
516
|
+
}
|
|
517
|
+
|
|
350
518
|
// 旧版项目内 blobs 目录清理(仅 home 存储可用时调用)
|
|
351
519
|
export function legacyRmScript(path) {
|
|
352
520
|
return 'rm -rf -- ' + psq(path)
|
package/lib/scripts.pwsh.js
CHANGED
|
@@ -27,6 +27,31 @@ export const UTF8_PRELUDE = '$OutputEncoding = [Text.UTF8Encoding]::new($false)\
|
|
|
27
27
|
// 生效值以 store.maxFileBytes(用户 config 可调)经 oversizeBlock 注入为准。
|
|
28
28
|
export const MAX_FILE_BYTES = 104857600
|
|
29
29
|
|
|
30
|
+
// 失败清扫的 stale 锁阈值(分钟,M3)与心跳有效窗口(秒,M3):语义、取值
|
|
31
|
+
// 理由与「不走 Config」的定性见 scripts.posix.js 同名常量注释,两侧必须
|
|
32
|
+
// 同值(scripts-contract 钉)。
|
|
33
|
+
export const STALE_LOCK_MIN = 5
|
|
34
|
+
export const HEARTBEAT_TTL_S = 900
|
|
35
|
+
|
|
36
|
+
// 影子仓库 info/attributes 固化内容(issue #12 字节保真,两套模板同值,
|
|
37
|
+
// scripts-contract 钉)。为什么必须:回退恢复走 git archive、捕获走 add,
|
|
38
|
+
// 两者都会应用「快照树里项目自己的 .gitattributes」——text=auto(极常见)
|
|
39
|
+
// + 缺省 core.eol=native(Windows 即 CRLF)会让 archive 把 LF 转 CRLF,
|
|
40
|
+
// 仓库级 core.autocrlf=false 挡不住(属性驱动的转换看 core.eol,不看
|
|
41
|
+
// autocrlf,实测)。info/attributes 是优先级最高的属性源,对全部路径
|
|
42
|
+
// 无条件关闭内容转换,capture/restore 两侧逐字节保真;也顺带治掉
|
|
43
|
+
// ensureGit 的 git config 写入静默失败(I14:pwsh 对 native 非零不抛)
|
|
44
|
+
// 后裸露 system autocrlf 的帮凶路径。逐项语义:
|
|
45
|
+
// -text 关闭全部 EOL 转换(含 autocrlf/text/eol 全部语义)
|
|
46
|
+
// -filter 关闭 clean/smudge 外部过滤命令(LFS 类,防内容被改写/存指针)
|
|
47
|
+
// -ident 关闭 $Id$ 展开
|
|
48
|
+
// -export-ignore 防归档静默丢文件(回退恢复缺文件零报错,实测)
|
|
49
|
+
// -export-subst 防归档内容 $Format:$ 替换
|
|
50
|
+
// -working-tree-encoding 防编码转码
|
|
51
|
+
// 快照是备份/还原不是 VCS:字节保真优先于用户在 .gitattributes 里声明的
|
|
52
|
+
// 任何转换策略——那是给 git 仓库的,不是给影子快照库的。
|
|
53
|
+
export const FIDELITY_ATTRS = '* -text -filter -ident -export-ignore -export-subst -working-tree-encoding'
|
|
54
|
+
|
|
30
55
|
// 去除 PS 5.1 Set-Content -Encoding utf8 写出的 BOM:JSON 解析前必须剥掉,
|
|
31
56
|
// 否则 JSON.parse 把 BOM 当正文首字符直接抛错。
|
|
32
57
|
export function stripBom(text) {
|
|
@@ -89,6 +114,18 @@ function excludeSyncBlock(excludeFile, base) {
|
|
|
89
114
|
].join('\n')
|
|
90
115
|
}
|
|
91
116
|
|
|
117
|
+
// 心跳写入(M3,语义见 scripts.posix.js 同名注释):Set-Content 用 ascii
|
|
118
|
+
// 编码——内容纯数字,关键是绝不能带 BOM(POSIX 侧按空白分词解析首字段,
|
|
119
|
+
// 5.1 的 utf8 默认编码会写 BOM 破坏首字段);-ErrorAction SilentlyContinue
|
|
120
|
+
// 让心跳写失败不连累外层 EAP=Stop 的快照主流程(与 POSIX 的 || true 对齐)。
|
|
121
|
+
// 依赖外层已定义的 $g;被 ensureGitScript / snapshotScript 复用。
|
|
122
|
+
function heartbeatBlock() {
|
|
123
|
+
return [
|
|
124
|
+
"$hbf = Join-Path (Split-Path -Parent (Split-Path -Parent $g)) 'heartbeat'",
|
|
125
|
+
"Set-Content -LiteralPath $hbf -Value ('" + String(process.pid) + " ' + [DateTimeOffset]::Now.ToUnixTimeSeconds()) -Encoding ascii -ErrorAction SilentlyContinue"
|
|
126
|
+
].join('\n')
|
|
127
|
+
}
|
|
128
|
+
|
|
92
129
|
// 解析 git 可执行文件路径:DSH 进程 PATH 可能不含 git,脚本里用绝对路径调用。
|
|
93
130
|
// 逐项判空再 Join-Path:个别 env 在特殊环境(32 位系统无 ProgramFiles(x86))
|
|
94
131
|
// 取到 null,EAP=Stop 下 Join-Path 抛错会让整个探测失败、误报 gitMissing。
|
|
@@ -142,6 +179,9 @@ export function migrateScript(src, dst) {
|
|
|
142
179
|
// autocrlf=true 时的 LF/CRLF stderr 警告;addEmbeddedRepo=false:嵌套仓库
|
|
143
180
|
// hint/warning 走 stderr,在 DSH shell(EAP=Stop)下会让整条脚本非零退出,
|
|
144
181
|
// 必须在仓库级配置里静默掉。
|
|
182
|
+
// info/attributes 固化(issue #12,内容见 FIDELITY_ATTRS):info/ 由 git init
|
|
183
|
+
// 自带(info/exclude 模板,excludeSyncBlock 同样依赖),无需建目录;每次
|
|
184
|
+
// ensureGit 重写幂等,存量仓库升级后首次 init 自然补上。
|
|
145
185
|
// 结尾回读 gc.stamp(maintenance.js 上次 gc 时间戳):让重启后的 gc 节流
|
|
146
186
|
// 不归零——没有它,天天重启 DSH 的用户每次开机第一条消息都会触发一次
|
|
147
187
|
// 全量 gc,纯浪费。
|
|
@@ -151,18 +191,42 @@ export function ensureGitScript(store, gitExe, base) {
|
|
|
151
191
|
'$git = ' + psq(gitExe),
|
|
152
192
|
'$repo = ' + psq(store.repo),
|
|
153
193
|
'$g = ' + psq(store.git),
|
|
194
|
+
heartbeatBlock(),
|
|
154
195
|
'if (-not (Test-Path -LiteralPath $g)) {',
|
|
155
196
|
' & $git init $repo | Out-Null',
|
|
156
197
|
'}',
|
|
157
198
|
'& $git --git-dir=$g config core.longpaths true',
|
|
158
199
|
'& $git --git-dir=$g config core.autocrlf false',
|
|
159
200
|
'& $git --git-dir=$g config advice.addEmbeddedRepo false',
|
|
201
|
+
"$attrDir = Join-Path $g 'info'",
|
|
202
|
+
"Set-Content -LiteralPath (Join-Path $attrDir 'attributes') -Value '" + FIDELITY_ATTRS + "' -Encoding ascii",
|
|
160
203
|
excludeSyncBlock(store.excludeFile, base),
|
|
161
204
|
"$stamp = Join-Path $g 'gc.stamp'",
|
|
162
205
|
"if (Test-Path -LiteralPath $stamp) { Write-Output ('GIT_OK ' + [String](Get-Content -LiteralPath $stamp -TotalCount 1 -ErrorAction SilentlyContinue)) } else { Write-Output 'GIT_OK' }"
|
|
163
206
|
].join('\n')
|
|
164
207
|
}
|
|
165
208
|
|
|
209
|
+
// 存量归一化迁移(issue #12):属性固化后,索引里的旧条目仍指向归一化
|
|
210
|
+
// blob 的哈希——stat 缓存让裸 add -A 时序依赖地跳过重哈希(racy 复查只在
|
|
211
|
+
// 「add 与文件同秒写入」时触发,实测不可靠),归一化残留会一直潜伏进新
|
|
212
|
+
// 快照。--renormalize 对全部跟踪文件按当前属性(无转换)重哈希一次;
|
|
213
|
+
// 无 pathspec 时它是空操作(实测「Nothing specified, nothing added」),
|
|
214
|
+
// 必须带 ':(top)' 顶层魔法 pathspec(cwd 无关),也不能加
|
|
215
|
+
// --literal-pathspecs(会废掉魔法解析)。标记文件保证每仓库至多跑一次;
|
|
216
|
+
// 失败(老 git 无该选项退出 129、偶发锁冲突等)只跳过标记、下条消息重试,
|
|
217
|
+
// 不 throw——迁移是 best-effort,不能让老 git 机器的快照从此全挂。
|
|
218
|
+
// 依赖外层已定义的 $git/$g/$root;仅 snapshotScript 使用(execute 的救援
|
|
219
|
+
// 安全快照在回退前必先跑一次快照,回退链路天然被覆盖)。
|
|
220
|
+
function attrsMigrateBlock() {
|
|
221
|
+
return [
|
|
222
|
+
"$migStamp = Join-Path $g 'attrs-v1.stamp'",
|
|
223
|
+
'if (-not (Test-Path -LiteralPath $migStamp)) {',
|
|
224
|
+
" & $git --git-dir=$g --work-tree=$root add --renormalize --ignore-errors -- ':(top)'",
|
|
225
|
+
' if ($LASTEXITCODE -le 1) { Set-Content -LiteralPath $migStamp -Value 1 -Encoding ascii -ErrorAction SilentlyContinue }',
|
|
226
|
+
'}',
|
|
227
|
+
].join('\n')
|
|
228
|
+
}
|
|
229
|
+
|
|
166
230
|
// 快照:git add -A 增量同步 index(.gitignore/exclude 语义由 git 统一处理),
|
|
167
231
|
// write-tree 生成树、commit-tree 生成无父孤儿提交、tag 保对象可达。
|
|
168
232
|
// 不做 parent 链、不修剪:像 TraeWork 一样保留全量历史,tag 永远可查。
|
|
@@ -175,8 +239,10 @@ export function snapshotScript(root, store, gitExe, messageId, base) {
|
|
|
175
239
|
'$git = ' + psq(gitExe),
|
|
176
240
|
'$g = ' + psq(store.git),
|
|
177
241
|
'$root = ' + psq(root),
|
|
242
|
+
heartbeatBlock(),
|
|
178
243
|
dropGitlinksBlock(),
|
|
179
244
|
excludeSyncBlock(store.excludeFile, base),
|
|
245
|
+
attrsMigrateBlock(),
|
|
180
246
|
// fail-open add(issue #7 加固):--ignore-errors 让「个别路径无法索引」
|
|
181
247
|
// (无提交的嵌入式仓库、不可读文件等)以退出码 1 结束但索引照常落盘,
|
|
182
248
|
// 快照缺个别路径可接受,好过整条快照 fatal。退出码 ≥2 才是真 fatal
|
|
@@ -296,6 +362,10 @@ export function rollbackScript(root, store, gitExe, tag, base) {
|
|
|
296
362
|
' Expand-Archive -LiteralPath $zip -DestinationPath $root -Force',
|
|
297
363
|
' Remove-Item -LiteralPath $zip -Force',
|
|
298
364
|
'}',
|
|
365
|
+
// 删除失败语义与 POSIX 版对齐(F-G2):本侧 EAP=Stop 下 Remove-Item
|
|
366
|
+
// 失败直接抛终止错误、pwsh 以非零码退出;POSIX 侧 rm 在 set -e 的 if
|
|
367
|
+
// 条件里失败不会自动终止,须显式 exit 1(见 scripts.posix.js rollbackScript)。
|
|
368
|
+
// 任何一侧半回退都不许报 ROLLBACK_OK——假成功会让救援永不触发(H1)。
|
|
299
369
|
'$deleted = 0',
|
|
300
370
|
'foreach ($r in @($curOut)) {',
|
|
301
371
|
' if (-not $r) { continue }',
|
|
@@ -309,6 +379,25 @@ export function rollbackScript(root, store, gitExe, tag, base) {
|
|
|
309
379
|
].join('\n')
|
|
310
380
|
}
|
|
311
381
|
|
|
382
|
+
// 回退失败救援(H1):rollback 脚本未输出 ROLLBACK_OK(工作区可能半回退)
|
|
383
|
+
// 时,用 execute 预先打下的安全快照把工作区 reset 回「回退前」状态。
|
|
384
|
+
// 入参 tag 是完整 tag 名(snap-pre-rollback-<ts>,snap- 前缀由调用侧
|
|
385
|
+
// rescueRollback 拼齐——snapshotScript 打 tag 无条件加前缀,本模板保持
|
|
386
|
+
// 通用只接受完整名)。命令与 rollbackScript 同款 --git-dir/--work-tree 形态;
|
|
387
|
+
// pwsh 对 native 非零退出不抛(EAP 不作用于 native),必须显式查
|
|
388
|
+
// $LASTEXITCODE 并 throw,否则救援失败被静默吞掉、工作区停在半回退状态。
|
|
389
|
+
export function rescueScript(root, store, gitExe, tag) {
|
|
390
|
+
return [
|
|
391
|
+
"$ErrorActionPreference = 'Stop'",
|
|
392
|
+
'$git = ' + psq(gitExe),
|
|
393
|
+
'$g = ' + psq(store.git),
|
|
394
|
+
'$root = ' + psq(root),
|
|
395
|
+
'& $git --git-dir=$g --work-tree=$root reset --hard ' + psq(tag),
|
|
396
|
+
'if ($LASTEXITCODE -ne 0) { throw ("git reset --hard failed (exit " + $LASTEXITCODE + ")") }',
|
|
397
|
+
"Write-Output 'RESCUE_OK'"
|
|
398
|
+
].join('\n')
|
|
399
|
+
}
|
|
400
|
+
|
|
312
401
|
export function listTagsScript(store, gitExe) {
|
|
313
402
|
return [
|
|
314
403
|
"$ErrorActionPreference = 'Stop'",
|
|
@@ -321,6 +410,20 @@ export function listTagsScript(store, gitExe) {
|
|
|
321
410
|
].join('\n')
|
|
322
411
|
}
|
|
323
412
|
|
|
413
|
+
// 孤儿重建用 tag 清单(带 creatordate):语义同 POSIX 版同名导出——
|
|
414
|
+
// rebuildOrphans 据此恢复快照时间,time=0 会让管理列表时间前缀缺失、
|
|
415
|
+
// retention/limits 按「最旧」误清。lightweight tag 的 creatordate 即
|
|
416
|
+
// 指向 commit 的提交日期。输出每行「<tag名> <秒级时间戳>」。
|
|
417
|
+
export function listTagsWithTimeScript(store, gitExe) {
|
|
418
|
+
return [
|
|
419
|
+
"$ErrorActionPreference = 'Stop'",
|
|
420
|
+
'$git = ' + psq(gitExe),
|
|
421
|
+
'$g = ' + psq(store.git),
|
|
422
|
+
'if (-not (Test-Path -LiteralPath $g -PathType Container)) { exit 0 }',
|
|
423
|
+
'& $git --git-dir=$g for-each-ref --format="%(refname:short) %(creatordate:unix)" "refs/tags/snap-*"'
|
|
424
|
+
].join('\n')
|
|
425
|
+
}
|
|
426
|
+
|
|
324
427
|
// 定期 gc:全量保留策略下对象只增不减,且默认 loose 存储(每对象一个
|
|
325
428
|
// 小文件,NTFS 最小簇 4KB)非常浪费;gc 压 pack + 跨版本 delta 通常省一半
|
|
326
429
|
// 以上。--prune=now 让「会话删除联动清理」删掉的 tag 立即真正释放空间
|
|
@@ -354,36 +457,73 @@ export function pruneScript(store, gitExe) {
|
|
|
354
457
|
].join('\n')
|
|
355
458
|
}
|
|
356
459
|
|
|
357
|
-
// 失败后的孤儿进程清扫 + stale 锁清理(issue #7 实测:超时被杀的 shell 留下
|
|
358
|
-
// 孤儿 git 继续持有 index.lock 30+ 分钟)。
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
//
|
|
373
|
-
"$
|
|
374
|
-
|
|
375
|
-
'
|
|
376
|
-
'
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
'
|
|
381
|
-
'
|
|
382
|
-
|
|
383
|
-
"Write-Output '
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
460
|
+
// 失败后的孤儿进程清扫 + stale 锁清理(issue #7 实测:超时被杀的 shell 留下
|
|
461
|
+
// 孤儿 git 继续持有 index.lock 30+ 分钟)。M3 三级出口(语义与动机见
|
|
462
|
+
// scripts.posix.js 同名函数注释):另一活实例心跳让路(CLEANUP_OTHER_INSTANCE)
|
|
463
|
+
// → 新锁让路(CLEANUP_SKIPPED_FRESH_LOCK)→ 原有清扫(CLEANUP_DONE)。
|
|
464
|
+
// DSH 的 subprocess 服务本身已做树级终止(taskkill /T /F),这里是竞态窗口
|
|
465
|
+
// 与旧版本漏网的兜底:按「命令行含 --git-dir=<本仓库>」定位孤儿——该标记只
|
|
466
|
+
// 出现在本插件派生的 git 进程参数里,编辑器等无关进程不会命中。
|
|
467
|
+
// 全程 SilentlyContinue + best-effort:调用点在 runShell 的失败路径上,
|
|
468
|
+
// 清扫自身再抛错只会掩盖原始错误。首行哨兵注释供 runShell 识别本脚本、
|
|
469
|
+
// 防止「清扫失败 → 再清扫」的递归。
|
|
470
|
+
export function killOrphansScript(gitDir) {
|
|
471
|
+
return [
|
|
472
|
+
'# RECALL_CLEANUP',
|
|
473
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
474
|
+
'$g = ' + psq(gitDir),
|
|
475
|
+
// —— 第 1 级保护:另一活实例心跳(store.dir 是 git-dir 上两级)——
|
|
476
|
+
"$hbf = Join-Path (Split-Path -Parent (Split-Path -Parent $g)) 'heartbeat'",
|
|
477
|
+
'if (Test-Path -LiteralPath $hbf -PathType Leaf) {',
|
|
478
|
+
' $hl = Get-Content -LiteralPath $hbf -TotalCount 1',
|
|
479
|
+
' if ($hl) {',
|
|
480
|
+
" $hp = ('' + $hl).Trim() -split '\\s+'",
|
|
481
|
+
' $a = [int64]0; $b = [int64]0',
|
|
482
|
+
' if (($hp.Count -ge 2) -and [int64]::TryParse($hp[0], [ref]$a) -and [int64]::TryParse($hp[1], [ref]$b)) {',
|
|
483
|
+
' $age = [DateTimeOffset]::Now.ToUnixTimeSeconds() - $b',
|
|
484
|
+
' if (($a -gt 0) -and ($a -ne ' + String(process.pid) + ') -and ($age -ge 0) -and ($age -lt ' + HEARTBEAT_TTL_S + ')) {',
|
|
485
|
+
' if (Get-Process -Id $a -ErrorAction SilentlyContinue) {',
|
|
486
|
+
" Write-Output ('CLEANUP_OTHER_INSTANCE ' + $a)",
|
|
487
|
+
' exit 0',
|
|
488
|
+
' }',
|
|
489
|
+
' }',
|
|
490
|
+
' }',
|
|
491
|
+
' }',
|
|
492
|
+
'}',
|
|
493
|
+
// —— 第 2 级保护:新锁(有 git 操作可能正在进行)——
|
|
494
|
+
"$cutoff = (Get-Date).AddMinutes(-" + STALE_LOCK_MIN + ')',
|
|
495
|
+
'$fresh = $false',
|
|
496
|
+
"foreach ($n in @('index.lock','config.lock','HEAD.lock','gc.pid','packed-refs.lock','shallow.lock')) {",
|
|
497
|
+
' $lp = Join-Path $g $n',
|
|
498
|
+
' if (Test-Path -LiteralPath $lp -PathType Leaf) {',
|
|
499
|
+
' if ((Get-Item -LiteralPath $lp).LastWriteTime -gt $cutoff) { $fresh = $true; break }',
|
|
500
|
+
' }',
|
|
501
|
+
'}',
|
|
502
|
+
'if (-not $fresh) {',
|
|
503
|
+
" $fl = @(Get-ChildItem -LiteralPath (Join-Path $g 'refs') -Recurse -File -Filter '*.lock' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt $cutoff })",
|
|
504
|
+
' if ($fl.Count -gt 0) { $fresh = $true }',
|
|
505
|
+
'}',
|
|
506
|
+
'if ($fresh) {',
|
|
507
|
+
" Write-Output 'CLEANUP_SKIPPED_FRESH_LOCK'",
|
|
508
|
+
' exit 0',
|
|
509
|
+
'}',
|
|
510
|
+
// —— 保护未命中:原有清扫(杀孤儿 + 清 stale 锁)——
|
|
511
|
+
// 标记用变量拼接而非字面量:本脚本进程的命令行(-Command 全文)只有
|
|
512
|
+
// 未展开的 '$g',Where-Object 不会匹配到自己
|
|
513
|
+
"$marker = '--git-dir=' + $g",
|
|
514
|
+
"Get-CimInstance Win32_Process -Filter 'CommandLine IS NOT NULL' | Where-Object { $_.CommandLine.Contains($marker) } | ForEach-Object {",
|
|
515
|
+
' & taskkill /T /F /PID $_.ProcessId | Out-Null',
|
|
516
|
+
'}',
|
|
517
|
+
// 锁清单:index.lock 是 add/checkout 的持久锁,其余是 gc/tag/pack 链路
|
|
518
|
+
// 可能残留的;refs 下的 per-ref 锁用递归兜底(能走到这里说明锁已陈旧)
|
|
519
|
+
"foreach ($n in @('index.lock','config.lock','HEAD.lock','gc.pid','packed-refs.lock','shallow.lock')) {",
|
|
520
|
+
' Remove-Item -LiteralPath (Join-Path $g $n) -Force',
|
|
521
|
+
'}',
|
|
522
|
+
"Get-ChildItem -LiteralPath (Join-Path $g 'refs') -Recurse -File -Filter '*.lock' | Remove-Item -Force",
|
|
523
|
+
"Write-Output 'CLEANUP_DONE'"
|
|
524
|
+
].join('\n')
|
|
525
|
+
}
|
|
526
|
+
|
|
387
527
|
// 删除指定快照 tag(会话已删联动清理用)。best-effort:个别 tag 已不存在时
|
|
388
528
|
// git 非零退出,但其余 tag 已被删除——所以显式 exit 0 吞掉退出码,
|
|
389
529
|
// 残留的由下一次清理幂等地收尾;JS 侧无论脚本结果都会同步索引。
|
|
@@ -406,14 +546,35 @@ export function purgeTagsScript(store, gitExe, tags) {
|
|
|
406
546
|
// 表达式,first 决定覆盖还是追加。目录创建归调用方(writeExclude 的
|
|
407
547
|
// mkdirScript 兜底 / index.json 的父目录在建仓时已存在)。
|
|
408
548
|
export function fileWriteCmd(file, piece, first) {
|
|
549
|
+
// EAP=Stop:Set-Content/Add-Content 是 cmdlet,失败(权限/磁盘满)在
|
|
550
|
+
// 默认 EAP=Continue 下只写 error stream、退出码仍 0,半截写入被静默吞掉。
|
|
551
|
+
// 显式 Stop 让失败抛终止错误,runShell 据此检测非零退出并抛错(H2)。
|
|
552
|
+
const prelude = "$ErrorActionPreference = 'Stop'\n"
|
|
409
553
|
if (first) {
|
|
410
|
-
return piece + 'Set-Content -LiteralPath ' + psq(file) + ' -Encoding utf8 -NoNewline'
|
|
554
|
+
return prelude + piece + 'Set-Content -LiteralPath ' + psq(file) + ' -Encoding utf8 -NoNewline'
|
|
411
555
|
}
|
|
412
|
-
return piece + 'Add-Content -LiteralPath ' + psq(file) + ' -Encoding utf8 -NoNewline'
|
|
556
|
+
return prelude + piece + 'Add-Content -LiteralPath ' + psq(file) + ' -Encoding utf8 -NoNewline'
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 原子 rename(H2):同卷 move 是 O(1) 元数据操作,把「已完整写完的 tmp」
|
|
560
|
+
// 一步替换成目标文件,杜绝分块写中途崩溃留下的截断 JSON;也用于 loadIndex
|
|
561
|
+
// 把损坏索引改名 .corrupt-<ts> 保留现场(见 snapshots.js quarantineCorruptIndex)。
|
|
562
|
+
export function renameFileCmd(src, dst) {
|
|
563
|
+
return "$ErrorActionPreference = 'Stop'\nMove-Item -Force -LiteralPath " + psq(src) + ' -Destination ' + psq(dst)
|
|
413
564
|
}
|
|
414
565
|
|
|
566
|
+
// 显式 -Encoding UTF8:写侧(writeTextViaShell base64 解码落盘)产出的是
|
|
567
|
+
// 无 BOM UTF-8,PS 7 默认即按 UTF-8 读,但 PS 5.1 兜底(pwsh-local 解析链
|
|
568
|
+
// 降级)按 ANSI 活动代码页解码——中文 root 乱码 → JSON.parse 失败 → 误走
|
|
569
|
+
// H2 隔离分支。与 excludeReadCmd 等同文件其他读取处的既有写法对齐。
|
|
415
570
|
export function indexReadCmd(dir) {
|
|
416
|
-
return 'Get-Content -LiteralPath ' + psq(dir + '\\index.json') + ' -Raw -ErrorAction SilentlyContinue'
|
|
571
|
+
return 'Get-Content -LiteralPath ' + psq(dir + '\\index.json') + ' -Raw -Encoding UTF8 -ErrorAction SilentlyContinue'
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// fork lineage 读取(F1):lineage.json 记录 childId↔parentId 撤回链,
|
|
575
|
+
// 与 index.json 同层、原子写(writeTextViaShell)。文件不存在时输出空串。
|
|
576
|
+
export function lineageReadCmd(dir) {
|
|
577
|
+
return 'Get-Content -LiteralPath ' + psq(dir + '\\lineage.json') + ' -Raw -Encoding UTF8 -ErrorAction SilentlyContinue'
|
|
417
578
|
}
|
|
418
579
|
|
|
419
580
|
// 旧版项目内 blobs 目录清理(仅 home 存储可用时调用,见 store.js cleanupLegacy)
|