dsh-recall-plugin 2.3.1 → 2.3.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/CHANGELOG.md +30 -0
- package/README.en.md +4 -4
- package/README.md +8 -9
- package/lib/client.js +625 -476
- package/lib/config.js +53 -96
- package/lib/diagnostics.js +27 -52
- package/lib/dump-parse.js +79 -61
- package/lib/errors.js +41 -55
- package/lib/index.js +345 -446
- package/lib/maintenance.js +204 -275
- package/lib/routes-core.js +89 -140
- package/lib/routes-manage.js +571 -624
- package/lib/scripts.posix.js +452 -638
- package/lib/scripts.pwsh.js +537 -778
- package/lib/session-info.js +42 -57
- package/lib/snapshots.js +400 -588
- package/lib/store.js +364 -563
- package/package.json +7 -3
package/lib/scripts.posix.js
CHANGED
|
@@ -1,638 +1,452 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* -
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
function
|
|
83
|
-
return [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
'
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'
|
|
151
|
-
'exit
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return [
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
'
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
'
|
|
206
|
-
'git=
|
|
207
|
-
'
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
'if [
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
'"$git" --git-dir="$g"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return [
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
'
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
'
|
|
281
|
-
'
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
'
|
|
300
|
-
'
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
'
|
|
308
|
-
'
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
'
|
|
314
|
-
'
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
'
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
'
|
|
399
|
-
'
|
|
400
|
-
'
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
'echo
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
'
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
'echo PRUNE_OK'
|
|
454
|
-
].join('\n')
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// 失败后的孤儿进程清扫 + stale 锁清理(issue #7 兜底 + issue #11 根因治理)。
|
|
458
|
-
// 三级出口(M3):
|
|
459
|
-
// 1. CLEANUP_OTHER_INSTANCE <pid>——心跳文件(宿主 PID + epoch 秒)显示另一
|
|
460
|
-
// 个存活实例正在使用同一快照库:直接让路,不杀进程、不动锁。这是两个
|
|
461
|
-
// DSH 实例并发互踩(一方清扫误杀另一方活跃 git → 对方也失败 → 循环)的
|
|
462
|
-
// 根治;心跳由 ensureGit/snapshotScript 随操作刷新,TTL 外视为失效。
|
|
463
|
-
// 2. CLEANUP_SKIPPED_FRESH_LOCK——存在 STALE_LOCK_MIN 分钟内的新锁(疑似有
|
|
464
|
-
// git 操作正在进行):同样让路,锁陈旧后下次失败自然进入第 3 级。
|
|
465
|
-
// 3. CLEANUP_DONE——两级保护均未命中,按原有行为清孤儿进程与 stale 锁。
|
|
466
|
-
// POSIX 版差异:pgrep -f 按扩展正则匹配整条命令行——路径元字符([、+ 等)
|
|
467
|
-
// 会让模式失配,清扫静默失效,属安全降级(等价于本兜底加入前的行为);
|
|
468
|
-
// 标记在运行期拼接,本脚本自身命令行里只有未展开的 $g 字面量,不会自杀。
|
|
469
|
-
// 全程无 set -e + 逐步容错:清扫自身的失败不能抛。mtime 判定用 find -mmin
|
|
470
|
-
// 而非 stat:GNU/BSD stat 参数不同(macOS bash 3.2 约束),find 的 -mmin
|
|
471
|
-
// 两平台语义一致。
|
|
472
|
-
export function killOrphansScript(gitDir) {
|
|
473
|
-
return [
|
|
474
|
-
'# RECALL_CLEANUP',
|
|
475
|
-
'g=' + psq(gitDir),
|
|
476
|
-
'hbf="$(dirname "$(dirname "$g")")/heartbeat"',
|
|
477
|
-
'if [ -f "$hbf" ]; then',
|
|
478
|
-
' hbl=$(head -n1 "$hbf" 2>/dev/null | tr -d \'\\r\')',
|
|
479
|
-
' hbp=${hbl%% *}',
|
|
480
|
-
' hbt=${hbl#* }',
|
|
481
|
-
" case \"$hbp\" in ''|*[!0-9]*) hbp='' ;; esac",
|
|
482
|
-
" case \"$hbt\" in ''|*[!0-9]*) hbt=0 ;; esac",
|
|
483
|
-
' hbage=$(( $(date +%s) - hbt ))',
|
|
484
|
-
' if [ -n "$hbp" ] && [ "$hbp" != ' + psq(String(process.pid)) + ' ] && [ "$hbage" -ge 0 ] && [ "$hbage" -lt ' + HEARTBEAT_TTL_S + ' ]; then',
|
|
485
|
-
' if kill -0 "$hbp" 2>/dev/null; then',
|
|
486
|
-
' echo "CLEANUP_OTHER_INSTANCE $hbp"',
|
|
487
|
-
' exit 0',
|
|
488
|
-
' fi',
|
|
489
|
-
' fi',
|
|
490
|
-
'fi',
|
|
491
|
-
"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)',
|
|
492
|
-
'if [ -n "$fresh" ]; then',
|
|
493
|
-
' echo CLEANUP_SKIPPED_FRESH_LOCK',
|
|
494
|
-
' exit 0',
|
|
495
|
-
'fi',
|
|
496
|
-
'marker="--git-dir=$g"',
|
|
497
|
-
'for p in $(pgrep -f -- "$marker" 2>/dev/null); do',
|
|
498
|
-
' [ "$p" = "$$" ] && continue',
|
|
499
|
-
' kill "$p" 2>/dev/null || true',
|
|
500
|
-
'done',
|
|
501
|
-
// 锁清单对齐 pwsh 版:index.lock 是 add/checkout 持久锁,其余是
|
|
502
|
-
// gc/tag/pack 链路残留;refs 下 per-ref 锁用 find 兜底(只删陈旧锁,
|
|
503
|
-
// 新锁已被上方分级保护拦下)
|
|
504
|
-
'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',
|
|
505
|
-
'find "$g/refs" -type f -name "*.lock" -mmin +' + STALE_LOCK_MIN + ' -delete 2>/dev/null || true',
|
|
506
|
-
'echo CLEANUP_DONE'
|
|
507
|
-
].join('\n')
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// 删除指定快照 tag(会话已删联动清理用):tag -d 对不存在 tag 非零退出,
|
|
511
|
-
// || true 吞掉——best-effort,残留 tag 由下次清理幂等收尾(同 pwsh 版)
|
|
512
|
-
export function purgeTagsScript(store, gitExe, tags) {
|
|
513
|
-
return [
|
|
514
|
-
'git=' + psq(gitExe),
|
|
515
|
-
'g=' + psq(store.git),
|
|
516
|
-
'"$git" --git-dir="$g" tag -d ' + tags.map((t) => psq(t)).join(' ') + ' >/dev/null 2>&1 || true',
|
|
517
|
-
'echo PURGE_DONE'
|
|
518
|
-
].join('\n')
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// 原子 rename(H2,语义见 pwsh 版同款注释):mv -f 同卷 move 为 O(1) 元
|
|
522
|
-
// 数据操作;也用于 loadIndex 把损坏索引改名 .corrupt-<ts> 保留现场。
|
|
523
|
-
export function renameFileCmd(src, dst) {
|
|
524
|
-
return 'mv -f -- ' + psq(src) + ' ' + psq(dst)
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// 任意长度文本写入(PF-2,语义见 pwsh 版同名注释):stdin 传全文 + 单进程
|
|
528
|
-
// 落盘。POSIX 原本就直写 stdin(cat > tmp 此前内联在 store.js,PF-2 起迁进
|
|
529
|
-
// 模板统一走同名导出),bash 无编码/长度问题,模板本体就是这条 cat。
|
|
530
|
-
export function fileWriteStdinCmd(file) {
|
|
531
|
-
return 'cat > ' + psq(file)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// 索引读取(写入走 stdin:见 snapshots.js saveIndex 的 POSIX 分支,
|
|
535
|
-
// 不经命令行传参,天然没有 32767/128KB argv 上限问题)
|
|
536
|
-
export function indexReadCmd(dir) {
|
|
537
|
-
return 'cat ' + psq(dir + '/index.json') + ' 2>/dev/null || true'
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// fork lineage 读取(F1,语义见 pwsh 版同款注释):lineage.json 与 index.json
|
|
541
|
-
// 同层、原子写;缺失文件输出空串。
|
|
542
|
-
export function lineageReadCmd(dir) {
|
|
543
|
-
return 'cat ' + psq(dir + '/lineage.json') + ' 2>/dev/null || true'
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// 旧版项目内 blobs 目录清理(仅 home 存储可用时调用)
|
|
547
|
-
export function legacyRmScript(path) {
|
|
548
|
-
return 'rm -rf -- ' + psq(path)
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// exclude.txt 原文读取(设置页编辑用):缺失文件 cat 报错走 2>/dev/null ||
|
|
552
|
-
// true 吞掉输出空串——与 pwsh 版同语义,按「尚未配置」处理;写入不走
|
|
553
|
-
// 模板函数,调用方直接 cat > file + stdin(同 saveIndex 的 POSIX 分支)。
|
|
554
|
-
export function excludeReadCmd(file) {
|
|
555
|
-
return 'cat ' + psq(file) + ' 2>/dev/null || true'
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// 批量读全部 exclude 文件(PF-8,语义见 pwsh 版同注释):内容 base64 单行
|
|
559
|
-
// 输出(任意文本免疫定界混淆)。GNU base64 默认 76 字符折行、BSD(macOS)
|
|
560
|
-
// 不折行且无 -w——统一 base64 | tr -d '\n' 兼容两侧;读失败输出空段。
|
|
561
|
-
export function excludeDumpScript(files) {
|
|
562
|
-
const lines = []
|
|
563
|
-
for (const f of files || []) {
|
|
564
|
-
const q = psq(f)
|
|
565
|
-
lines.push(
|
|
566
|
-
"printf 'EXCLBEGIN %s\\n' " + q,
|
|
567
|
-
'if [ -f ' + q + ' ]; then base64 ' + q + " 2>/dev/null | tr -d '\\n'; fi",
|
|
568
|
-
"echo 'EXCLEND'"
|
|
569
|
-
)
|
|
570
|
-
}
|
|
571
|
-
return lines.join('\n')
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// 目录存在探测:YES/NO 定长标记与 pwsh 版逐字同语义(容器路径本身在
|
|
575
|
-
// JS 侧解析,POSIX 不需要 homeContainerScript 的 shell 版)。
|
|
576
|
-
export function dirExistsScript(dir) {
|
|
577
|
-
return '[ -d ' + psq(dir) + ' ] && echo YES || echo NO'
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// 影子仓库磁盘占用(设置页快照管理卡片用,语义同 pwsh 版)
|
|
581
|
-
export function countObjectsScript(store, gitExe) {
|
|
582
|
-
return [
|
|
583
|
-
'git=' + psq(gitExe),
|
|
584
|
-
'g=' + psq(store.git),
|
|
585
|
-
'"$git" --git-dir="$g" count-objects -v'
|
|
586
|
-
].join('\n')
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// 目录总大小(字节):du -sk 取 KiB 再 ×1024;macOS/BSD du 与 GNU du
|
|
590
|
-
// 对 -sk 的输出格式一致("大小<TAB>路径"),awk 取首列最稳。
|
|
591
|
-
export function diskUsageScript(dir) {
|
|
592
|
-
return 'du -sk ' + psq(dir) + ' 2>/dev/null | awk \'{print $1 * 1024}\''
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// 列目录下所有一级子目录全路径:manage/list 枚举 home 容器下的所有
|
|
596
|
-
// 哈希子目录用(每个子目录是一个工作区的 store)。find -maxdepth 1
|
|
597
|
-
// 限定深度避免递归,2>/dev/null 容忍个别不可读条目。
|
|
598
|
-
export function listSubdirsScript(dir) {
|
|
599
|
-
return 'find ' + psq(dir) + ' -maxdepth 1 -mindepth 1 -type d 2>/dev/null'
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// 批量 dump 全部 store 元数据(与 pwsh 版 storesDumpScript 同格式、
|
|
603
|
-
// 同语义,见其注释):一条 shell 拿全部目录的 root.txt + index.json +
|
|
604
|
-
// lineage.json(PF-4)。
|
|
605
|
-
// bash 3.2 兼容:数组 + += 均可用,glob 无匹配时字面量经 [ -d ] 过滤。
|
|
606
|
-
// root.txt 经 tr 去掉可能的 CRLF 再拼单行,防标记结构被打乱。
|
|
607
|
-
export function storesDumpScript(container, extraDirs) {
|
|
608
|
-
const lines = ['set -e', 'dirs=()']
|
|
609
|
-
if (container) {
|
|
610
|
-
lines.push('base=' + psq(container))
|
|
611
|
-
lines.push('if [ -d "$base" ]; then')
|
|
612
|
-
lines.push(' for d in "$base"/*/; do')
|
|
613
|
-
// if 而不是 [ ] && :glob 无匹配时条件为假,&& 链返回非零会触发 set -e
|
|
614
|
-
lines.push(' if [ -d "$d" ]; then dirs+=("${d%/}"); fi')
|
|
615
|
-
lines.push(' done')
|
|
616
|
-
lines.push('fi')
|
|
617
|
-
}
|
|
618
|
-
for (const d of extraDirs || []) lines.push('dirs+=(' + psq(d) + ')')
|
|
619
|
-
lines.push(
|
|
620
|
-
'for d in "${dirs[@]}"; do',
|
|
621
|
-
' [ -d "$d" ] || continue',
|
|
622
|
-
' echo "==DIR $d"',
|
|
623
|
-
' if [ -f "$d/root.txt" ]; then',
|
|
624
|
-
' printf "ROOT %s\\n" "$(cat "$d/root.txt" 2>/dev/null | tr -d \'\\r\\n\')"',
|
|
625
|
-
' else',
|
|
626
|
-
' echo "ROOT "',
|
|
627
|
-
' fi',
|
|
628
|
-
' echo INDEXBEGIN',
|
|
629
|
-
' cat "$d/index.json" 2>/dev/null',
|
|
630
|
-
' echo INDEXEND',
|
|
631
|
-
' echo LINEAGEBEGIN',
|
|
632
|
-
' cat "$d/lineage.json" 2>/dev/null',
|
|
633
|
-
' echo LINEAGEEND',
|
|
634
|
-
'done',
|
|
635
|
-
'exit 0'
|
|
636
|
-
)
|
|
637
|
-
return lines.join('\n')
|
|
638
|
-
}
|
|
1
|
+
function psq(value) {
|
|
2
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
3
|
+
}
|
|
4
|
+
const UTF8_PRELUDE = "export LC_ALL=C";
|
|
5
|
+
const MAX_FILE_BYTES = 104857600;
|
|
6
|
+
const STALE_LOCK_MIN = 5;
|
|
7
|
+
const HEARTBEAT_TTL_S = 900;
|
|
8
|
+
const FIDELITY_ATTRS = "* -text -filter -ident -export-ignore -export-subst -working-tree-encoding";
|
|
9
|
+
function stripBom(text) {
|
|
10
|
+
return text.replace(/^\uFEFF/, "");
|
|
11
|
+
}
|
|
12
|
+
function dropGitlinksBlock() {
|
|
13
|
+
return [
|
|
14
|
+
`"$git" --git-dir="$g" ls-files -z --stage | while IFS= read -r -d '' e; do`,
|
|
15
|
+
' case "$e" in',
|
|
16
|
+
` 160000\\ *) p=\${e#*$'\\t'}; "$git" --literal-pathspecs --git-dir="$g" update-index --force-remove -- "$p" ;;`,
|
|
17
|
+
" esac",
|
|
18
|
+
"done"
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
function oversizeBlock(maxBytes) {
|
|
22
|
+
return [
|
|
23
|
+
'find "$root" -type f -size +' + String(maxBytes || MAX_FILE_BYTES) + "c -print0 2>/dev/null | while IFS= read -r -d '' f; do",
|
|
24
|
+
` printf '%s\\0' "\${f#"$root"/}"`,
|
|
25
|
+
'done | xargs -0 "$git" --literal-pathspecs --git-dir="$g" update-index --force-remove -- 2>/dev/null || true'
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
function excludeSyncBlock(excludeFile, base) {
|
|
29
|
+
const baseList = Array.isArray(base) && base.length ? base : [".git", "node_modules/", ".dsh-recall-snapshots/", "dsh-recall-snapshots/"];
|
|
30
|
+
const baseLines = baseList.join("\n") + "\n";
|
|
31
|
+
return [
|
|
32
|
+
"ex_file=" + psq(excludeFile),
|
|
33
|
+
'exc="$g/info/exclude"',
|
|
34
|
+
'user_pats=""',
|
|
35
|
+
'if [ -f "$ex_file" ]; then',
|
|
36
|
+
' while IFS= read -r line || [ -n "$line" ]; do',
|
|
37
|
+
" t=${line%$'\\r'}",
|
|
38
|
+
' t="${t#"${t%%[![:space:]]*}"}"; t="${t%"${t##*[![:space:]]}"}"',
|
|
39
|
+
' if [ -z "$t" ]; then continue; fi',
|
|
40
|
+
' case "$t" in \\#*) continue ;; esac',
|
|
41
|
+
' user_pats="$user_pats$t\\n"',
|
|
42
|
+
' done < "$ex_file"',
|
|
43
|
+
"fi",
|
|
44
|
+
"new_exc=$(printf '\\n" + baseLines.replace(/\\/g, "\\\\").replace(/%/g, "%%") + `%b' "$user_pats")`,
|
|
45
|
+
'old_exc=$(cat "$exc" 2>/dev/null || true)',
|
|
46
|
+
'if [ "$new_exc" != "$old_exc" ]; then',
|
|
47
|
+
` printf '%s\\n' "$new_exc" > "$exc"`,
|
|
48
|
+
' "$git" -c core.quotePath=false --literal-pathspecs --git-dir="$g" ls-files -i -c --exclude-from="$exc" -z 2>/dev/null | xargs -0 "$git" --literal-pathspecs --git-dir="$g" update-index --force-remove -- 2>/dev/null || true',
|
|
49
|
+
"fi"
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
function heartbeatBlock() {
|
|
53
|
+
return [
|
|
54
|
+
'hbf="$(dirname "$(dirname "$g")")/heartbeat"',
|
|
55
|
+
"printf '%s %s\\n' " + psq(String(process.pid)) + ' "$(date +%s)" > "$hbf" 2>/dev/null || true'
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
function resolveGitScript() {
|
|
59
|
+
return [
|
|
60
|
+
"p=$(command -v git 2>/dev/null || true)",
|
|
61
|
+
`[ -n "$p" ] && printf '%s\\n' "$p"`,
|
|
62
|
+
"exit 0"
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
function probeHomeScript() {
|
|
66
|
+
return `printf '%s' "\${DSH_HOME:-}"`;
|
|
67
|
+
}
|
|
68
|
+
function mkdirScript(dir) {
|
|
69
|
+
return "mkdir -p -- " + psq(dir);
|
|
70
|
+
}
|
|
71
|
+
function migrateScript(src, dst) {
|
|
72
|
+
return [
|
|
73
|
+
"set -e",
|
|
74
|
+
"src=" + psq(src),
|
|
75
|
+
"dst=" + psq(dst),
|
|
76
|
+
'if [ -e "$src/git" ]; then mv -f "$src/git" "$dst/git"; fi',
|
|
77
|
+
'if [ -e "$src/index.json" ]; then mv -f "$src/index.json" "$dst/index.json"; fi',
|
|
78
|
+
'rm -rf -- "$src"',
|
|
79
|
+
"echo MIGRATE_OK"
|
|
80
|
+
].join("\n");
|
|
81
|
+
}
|
|
82
|
+
function legacyHomeMigrateScript(homedir) {
|
|
83
|
+
return [
|
|
84
|
+
"old=" + psq(homedir + "/dsh-recall-snapshots"),
|
|
85
|
+
"new=" + psq(homedir + "/.dsh/dsh-recall-snapshots"),
|
|
86
|
+
'if [ -d "$old" ] && [ ! -d "$new" ]; then',
|
|
87
|
+
' if mkdir -p -- "$(dirname "$new")" && mv -f -- "$old" "$new"; then echo MIGRATE_OK',
|
|
88
|
+
" else echo MIGRATE_FAIL; fi",
|
|
89
|
+
'elif [ -d "$old" ]; then echo BOTH_PRESENT',
|
|
90
|
+
"else echo OLD_ABSENT; fi"
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
function ensureGitScript(store, gitExe, base) {
|
|
94
|
+
return [
|
|
95
|
+
"set -e",
|
|
96
|
+
"git=" + psq(gitExe),
|
|
97
|
+
"repo=" + psq(store.repo),
|
|
98
|
+
"g=" + psq(store.git),
|
|
99
|
+
heartbeatBlock(),
|
|
100
|
+
// 冷启动首消息快照与启动预热(或双实例)并发时,两个 git init 在空
|
|
101
|
+
// repo 目录同跑,输家报 fatal: cannot mkdir <git>: File exists——窗口
|
|
102
|
+
// 极小但首条消息的快照会因此丢失。git init 幂等:失败后 HEAD 已出现
|
|
103
|
+
// 即同伴建成,视同成功继续;HEAD 也没有才是真失败,带错误退出(诊断
|
|
104
|
+
// 不丢)。检查 HEAD 而非目录存在:半截目录也会被 init 补齐。pwsh 版
|
|
105
|
+
// 无需同款改动——native 非零退出不抛(I14),输家继续跑 config 时
|
|
106
|
+
// 同伴已建好 repo,竞态天然容忍,真失败由快照 add 的显式检查兜底。
|
|
107
|
+
'if [ ! -f "$g/HEAD" ]; then',
|
|
108
|
+
' init_log=$("$git" init "$repo" 2>&1) || {',
|
|
109
|
+
` if [ ! -f "$g/HEAD" ]; then printf '%s\\n' "$init_log" >&2; exit 1; fi`,
|
|
110
|
+
" }",
|
|
111
|
+
"fi",
|
|
112
|
+
'"$git" --git-dir="$g" config core.longpaths true',
|
|
113
|
+
'"$git" --git-dir="$g" config core.autocrlf false',
|
|
114
|
+
'"$git" --git-dir="$g" config advice.addEmbeddedRepo false',
|
|
115
|
+
// 属性固化(issue #12,内容见 FIDELITY_ATTRS):info/ 由 git init 自带
|
|
116
|
+
// (info/exclude 模板,excludeSyncBlock 同样依赖),无需建目录;每次
|
|
117
|
+
// ensureGit 重写幂等,存量仓库升级后首次 init 自然补上。
|
|
118
|
+
"printf " + psq(FIDELITY_ATTRS + "\n") + ' > "$g/info/attributes"',
|
|
119
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
120
|
+
'stamp="$g/gc.stamp"',
|
|
121
|
+
`if [ -f "$stamp" ]; then printf 'GIT_OK %s\\n' "$(head -n1 "$stamp" 2>/dev/null)"; else echo GIT_OK; fi`
|
|
122
|
+
].join("\n");
|
|
123
|
+
}
|
|
124
|
+
function attrsMigrateBlock() {
|
|
125
|
+
return [
|
|
126
|
+
'mig_stamp="$g/attrs-v1.stamp"',
|
|
127
|
+
'if [ ! -f "$mig_stamp" ]; then',
|
|
128
|
+
" migrc=0",
|
|
129
|
+
` "$git" --git-dir="$g" --work-tree="$root" add --renormalize --ignore-errors -- ':(top)' >/dev/null 2>&1 || migrc=$?`,
|
|
130
|
+
` if [ "$migrc" -le 1 ]; then printf '1\\n' > "$mig_stamp" 2>/dev/null || true; fi`,
|
|
131
|
+
"fi"
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
function snapshotScript(root, store, gitExe, messageId, base) {
|
|
135
|
+
return [
|
|
136
|
+
"set -e",
|
|
137
|
+
"git=" + psq(gitExe),
|
|
138
|
+
"g=" + psq(store.git),
|
|
139
|
+
"root=" + psq(root),
|
|
140
|
+
heartbeatBlock(),
|
|
141
|
+
dropGitlinksBlock(),
|
|
142
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
143
|
+
attrsMigrateBlock(),
|
|
144
|
+
// fail-open add(issue #7 加固,语义见 pwsh 版同款注释):--ignore-errors
|
|
145
|
+
// 下「无法索引的路径」以退出码 1 结束但索引已落盘;≥2 才是真 fatal,
|
|
146
|
+
// 显式退出让 runShell 抛错(set -e 对 add 非零本会终止,但 || rc=$?
|
|
147
|
+
// 捕获后必须自检,否则 tolerated/fatal 无法区分)。stderr 合并进变量
|
|
148
|
+
// 供 fatal 时带回诊断与 SNAP_SKIP 提取。
|
|
149
|
+
"addrc=0",
|
|
150
|
+
'add_log=$("$git" --git-dir="$g" --work-tree="$root" add -A --ignore-errors 2>&1) || addrc=$?',
|
|
151
|
+
`if [ "$addrc" -ge 2 ]; then printf '%s\\n' "$add_log" >&2; exit "$addrc"; fi`,
|
|
152
|
+
`printf '%s\\n' "$add_log" | sed -n "s/^error: unable to index file '\\(.*\\)'$/\\1/p" | sort -u | while IFS= read -r sk; do`,
|
|
153
|
+
// 循环体用 if/fi 而非 && 列表:&& 列表条件为假时整条管道退出码为 1,
|
|
154
|
+
// set -e 会把脚本杀掉(if 语句天然豁免)
|
|
155
|
+
` if [ -n "$sk" ]; then printf 'SNAP_SKIP %s\\n' "$sk"; fi`,
|
|
156
|
+
"done",
|
|
157
|
+
dropGitlinksBlock(),
|
|
158
|
+
oversizeBlock(store.maxFileBytes),
|
|
159
|
+
'tree=$("$git" --git-dir="$g" --work-tree="$root" write-tree)',
|
|
160
|
+
'commit=$("$git" --git-dir="$g" -c user.name=dsh-recall -c user.email=recall@dsh.local commit-tree "$tree" -m ' + psq("snapshot " + messageId) + ")",
|
|
161
|
+
'"$git" --git-dir="$g" tag -f ' + psq("snap-" + messageId) + ' "$commit" >/dev/null',
|
|
162
|
+
// PF-1:TREE 行随 SNAP_OK 回传 add -A 之后的 index 树指纹(语义见 pwsh 版
|
|
163
|
+
// 同名注释)——execute 与 preview 指纹比对判 STALE,免整条重复 diff
|
|
164
|
+
'echo "TREE $tree"',
|
|
165
|
+
"echo SNAP_OK"
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
function collectListsBlock(store, gitExe, root, tag, base) {
|
|
169
|
+
return [
|
|
170
|
+
"git=" + psq(gitExe),
|
|
171
|
+
"g=" + psq(store.git),
|
|
172
|
+
"root=" + psq(root),
|
|
173
|
+
dropGitlinksBlock(),
|
|
174
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
175
|
+
// fail-open add(语义见 snapshotScript 同款注释):diff/rollback 的当前
|
|
176
|
+
// 清单来自这次 add 后的索引——被跳过的路径不进清单,diff 不显示、
|
|
177
|
+
// rollback 删除清单也不会误删它们;≥2 显式退出防「旧索引假成功」
|
|
178
|
+
"addrc=0",
|
|
179
|
+
'"$git" --git-dir="$g" --work-tree="$root" add -A --ignore-errors || addrc=$?',
|
|
180
|
+
'[ "$addrc" -le 1 ] || exit "$addrc"',
|
|
181
|
+
dropGitlinksBlock(),
|
|
182
|
+
oversizeBlock(store.maxFileBytes),
|
|
183
|
+
"tmpc=" + psq(store.dir + "/diff-cur.$$"),
|
|
184
|
+
"tmpt=" + psq(store.dir + "/diff-tgt.$$"),
|
|
185
|
+
`"$git" -c core.quotePath=false --git-dir="$g" --work-tree="$root" ls-files --stage | grep -v '^160000 ' > "$tmpc" || true`,
|
|
186
|
+
'"$git" -c core.quotePath=false --git-dir="$g" ls-tree -r ' + psq(tag) + ` | grep -v '^160000 ' > "$tmpt" || true`
|
|
187
|
+
].join("\n");
|
|
188
|
+
}
|
|
189
|
+
function diffScript(root, store, gitExe, tag, base) {
|
|
190
|
+
return [
|
|
191
|
+
"set -e -o pipefail",
|
|
192
|
+
collectListsBlock(store, gitExe, root, tag, base),
|
|
193
|
+
`trap 'rm -f "$tmpc" "$tmpt"' EXIT`,
|
|
194
|
+
"awk -F'\\t' -v OFS='\\t' '",
|
|
195
|
+
" FNR==1 { fidx++ }",
|
|
196
|
+
' fidx==1 { split($1, a, " "); cur[$2]=a[2]; next }',
|
|
197
|
+
' { split($1, a, " "); tgt[$2]=a[3] }',
|
|
198
|
+
" END {",
|
|
199
|
+
" for (p in cur) {",
|
|
200
|
+
' if (p in tgt) { if (tgt[p] != cur[p]) print "modified", p }',
|
|
201
|
+
' else print "added", p',
|
|
202
|
+
" }",
|
|
203
|
+
' for (p in tgt) if (!(p in cur)) print "restored", p',
|
|
204
|
+
" }",
|
|
205
|
+
`' "$tmpc" "$tmpt" | sort -t$'\\t' -k2,2`,
|
|
206
|
+
'tree=$("$git" --git-dir="$g" --work-tree="$root" write-tree)',
|
|
207
|
+
'echo "TREE $tree"',
|
|
208
|
+
"exit 0"
|
|
209
|
+
].join("\n");
|
|
210
|
+
}
|
|
211
|
+
function rollbackScript(root, store, gitExe, tag, base) {
|
|
212
|
+
return [
|
|
213
|
+
"set -e -o pipefail",
|
|
214
|
+
collectListsBlock(store, gitExe, root, tag, base),
|
|
215
|
+
`trap 'rm -f "$tmpc" "$tmpt"' EXIT`,
|
|
216
|
+
`restored=$(wc -l < "$tmpt" | tr -d ' ')`,
|
|
217
|
+
'if [ "$restored" -gt 0 ]; then',
|
|
218
|
+
// -m(--touch):解包不恢复归档成员的 mtime(文件 mtime = 解包时刻)。
|
|
219
|
+
// 必须如此:tar 默认保留归档内 mtime,而快照→篡改→回滚常在数秒内
|
|
220
|
+
// 完成,恢复出的 mtime 可能与 index 里旧条目的 stat 记录碰撞,下一次
|
|
221
|
+
// add -A 的 stat 缓存误判「未变更」跳过 re-hash——工作区内容与快照
|
|
222
|
+
// 从此脱钩(实测解包出篡改前内容的间歇性失败)。Windows 版的
|
|
223
|
+
// Expand-Archive 天然把 mtime 设为解包时刻,无此问题;-m 让 tar 对齐。
|
|
224
|
+
' "$git" --git-dir="$g" archive ' + psq(tag) + ' | tar -x -m -C "$root"',
|
|
225
|
+
"fi",
|
|
226
|
+
"tmpd=" + psq(store.dir + "/diff-del.$$"),
|
|
227
|
+
`trap 'rm -f "$tmpc" "$tmpt" "$tmpd"' EXIT`,
|
|
228
|
+
"awk -F'\\t' '",
|
|
229
|
+
" FNR==1 { fidx++ }",
|
|
230
|
+
" fidx==1 { cur[$2]=1; next }",
|
|
231
|
+
" { tgt[$2]=1 }",
|
|
232
|
+
" END { for (p in cur) if (!(p in tgt)) print p }",
|
|
233
|
+
`' "$tmpc" "$tmpt" > "$tmpd"`,
|
|
234
|
+
"deleted=0",
|
|
235
|
+
"while IFS= read -r p; do",
|
|
236
|
+
// 循环体禁裸 && 链(AGENTS.md 已知坑同款规矩):&& 列表条件为假时整条
|
|
237
|
+
// 退出码为 1,set -e 会把脚本杀掉;if 语句天然豁免。rm 失败必须响亮
|
|
238
|
+
// exit 1——半回退假成功会让救援永不触发(F-G2)
|
|
239
|
+
' if [ -z "$p" ]; then continue; fi',
|
|
240
|
+
' if rm -f -- "$root/$p"; then deleted=$((deleted + 1)); else echo "RM_FAILED $p" >&2; exit 1; fi',
|
|
241
|
+
'done < "$tmpd"',
|
|
242
|
+
'echo "ROLLBACK_OK $deleted $restored"'
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
function rescueScript(root, store, gitExe, tag) {
|
|
246
|
+
return [
|
|
247
|
+
"set -e",
|
|
248
|
+
"git=" + psq(gitExe),
|
|
249
|
+
"g=" + psq(store.git),
|
|
250
|
+
"root=" + psq(root),
|
|
251
|
+
'"$git" --git-dir="$g" --work-tree="$root" reset --hard ' + psq(tag),
|
|
252
|
+
"echo RESCUE_OK"
|
|
253
|
+
].join("\n");
|
|
254
|
+
}
|
|
255
|
+
function listTagsScript(store, gitExe) {
|
|
256
|
+
return [
|
|
257
|
+
"set -e",
|
|
258
|
+
"git=" + psq(gitExe),
|
|
259
|
+
"g=" + psq(store.git),
|
|
260
|
+
// 仅创建过 store 目录、尚未产生过快照时没有 git/.git;把它视为
|
|
261
|
+
// 空快照仓库而非错误,全部删除仍可顺便清空其陈旧 index.json。
|
|
262
|
+
'[ -d "$g" ] || exit 0',
|
|
263
|
+
`"$git" --git-dir="$g" tag -l 'snap-*'`
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
266
|
+
function listTagsWithTimeScript(store, gitExe) {
|
|
267
|
+
return [
|
|
268
|
+
"set -e",
|
|
269
|
+
"git=" + psq(gitExe),
|
|
270
|
+
"g=" + psq(store.git),
|
|
271
|
+
'[ -d "$g" ] || exit 0',
|
|
272
|
+
`"$git" --git-dir="$g" for-each-ref --format='%(refname:short) %(creatordate:unix)' 'refs/tags/snap-*'`
|
|
273
|
+
].join("\n");
|
|
274
|
+
}
|
|
275
|
+
function gcScript(store, gitExe) {
|
|
276
|
+
return [
|
|
277
|
+
"set -e",
|
|
278
|
+
"git=" + psq(gitExe),
|
|
279
|
+
"g=" + psq(store.git),
|
|
280
|
+
'"$git" --git-dir="$g" gc --quiet --prune=now',
|
|
281
|
+
'date +%s > "$g/gc.stamp"',
|
|
282
|
+
"echo GC_OK"
|
|
283
|
+
].join("\n");
|
|
284
|
+
}
|
|
285
|
+
function pruneScript(store, gitExe) {
|
|
286
|
+
return [
|
|
287
|
+
"set -e",
|
|
288
|
+
"git=" + psq(gitExe),
|
|
289
|
+
"g=" + psq(store.git),
|
|
290
|
+
'"$git" --git-dir="$g" prune',
|
|
291
|
+
"echo PRUNE_OK"
|
|
292
|
+
].join("\n");
|
|
293
|
+
}
|
|
294
|
+
function killOrphansScript(gitDir) {
|
|
295
|
+
return [
|
|
296
|
+
"# RECALL_CLEANUP",
|
|
297
|
+
"g=" + psq(gitDir),
|
|
298
|
+
'hbf="$(dirname "$(dirname "$g")")/heartbeat"',
|
|
299
|
+
'if [ -f "$hbf" ]; then',
|
|
300
|
+
` hbl=$(head -n1 "$hbf" 2>/dev/null | tr -d '\\r')`,
|
|
301
|
+
" hbp=${hbl%% *}",
|
|
302
|
+
" hbt=${hbl#* }",
|
|
303
|
+
` case "$hbp" in ''|*[!0-9]*) hbp='' ;; esac`,
|
|
304
|
+
` case "$hbt" in ''|*[!0-9]*) hbt=0 ;; esac`,
|
|
305
|
+
" hbage=$(( $(date +%s) - hbt ))",
|
|
306
|
+
' if [ -n "$hbp" ] && [ "$hbp" != ' + psq(String(process.pid)) + ' ] && [ "$hbage" -ge 0 ] && [ "$hbage" -lt ' + HEARTBEAT_TTL_S + " ]; then",
|
|
307
|
+
' if kill -0 "$hbp" 2>/dev/null; then',
|
|
308
|
+
' echo "CLEANUP_OTHER_INSTANCE $hbp"',
|
|
309
|
+
" exit 0",
|
|
310
|
+
" fi",
|
|
311
|
+
" fi",
|
|
312
|
+
"fi",
|
|
313
|
+
`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)",
|
|
314
|
+
'if [ -n "$fresh" ]; then',
|
|
315
|
+
" echo CLEANUP_SKIPPED_FRESH_LOCK",
|
|
316
|
+
" exit 0",
|
|
317
|
+
"fi",
|
|
318
|
+
'marker="--git-dir=$g"',
|
|
319
|
+
'for p in $(pgrep -f -- "$marker" 2>/dev/null); do',
|
|
320
|
+
' [ "$p" = "$$" ] && continue',
|
|
321
|
+
' kill "$p" 2>/dev/null || true',
|
|
322
|
+
"done",
|
|
323
|
+
// 锁清单对齐 pwsh 版:index.lock 是 add/checkout 持久锁,其余是
|
|
324
|
+
// gc/tag/pack 链路残留;refs 下 per-ref 锁用 find 兜底(只删陈旧锁,
|
|
325
|
+
// 新锁已被上方分级保护拦下)
|
|
326
|
+
'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',
|
|
327
|
+
'find "$g/refs" -type f -name "*.lock" -mmin +' + STALE_LOCK_MIN + " -delete 2>/dev/null || true",
|
|
328
|
+
"echo CLEANUP_DONE"
|
|
329
|
+
].join("\n");
|
|
330
|
+
}
|
|
331
|
+
function purgeTagsScript(store, gitExe, tags) {
|
|
332
|
+
return [
|
|
333
|
+
"git=" + psq(gitExe),
|
|
334
|
+
"g=" + psq(store.git),
|
|
335
|
+
'"$git" --git-dir="$g" tag -d ' + tags.map((t) => psq(t)).join(" ") + " >/dev/null 2>&1 || true",
|
|
336
|
+
"echo PURGE_DONE"
|
|
337
|
+
].join("\n");
|
|
338
|
+
}
|
|
339
|
+
function renameFileCmd(src, dst) {
|
|
340
|
+
return "mv -f -- " + psq(src) + " " + psq(dst);
|
|
341
|
+
}
|
|
342
|
+
function fileWriteStdinCmd(file) {
|
|
343
|
+
return "cat > " + psq(file);
|
|
344
|
+
}
|
|
345
|
+
function indexReadCmd(dir) {
|
|
346
|
+
return "cat " + psq(dir + "/index.json") + " 2>/dev/null || true";
|
|
347
|
+
}
|
|
348
|
+
function lineageReadCmd(dir) {
|
|
349
|
+
return "cat " + psq(dir + "/lineage.json") + " 2>/dev/null || true";
|
|
350
|
+
}
|
|
351
|
+
function legacyRmScript(path) {
|
|
352
|
+
return "rm -rf -- " + psq(path);
|
|
353
|
+
}
|
|
354
|
+
function excludeReadCmd(file) {
|
|
355
|
+
return "cat " + psq(file) + " 2>/dev/null || true";
|
|
356
|
+
}
|
|
357
|
+
function excludeDumpScript(files) {
|
|
358
|
+
const lines = [];
|
|
359
|
+
for (const f of files || []) {
|
|
360
|
+
const q = psq(f);
|
|
361
|
+
lines.push(
|
|
362
|
+
"printf 'EXCLBEGIN %s\\n' " + q,
|
|
363
|
+
"if [ -f " + q + " ]; then base64 " + q + " 2>/dev/null | tr -d '\\n'; fi",
|
|
364
|
+
"echo 'EXCLEND'"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return lines.join("\n");
|
|
368
|
+
}
|
|
369
|
+
function dirExistsScript(dir) {
|
|
370
|
+
return "[ -d " + psq(dir) + " ] && echo YES || echo NO";
|
|
371
|
+
}
|
|
372
|
+
function countObjectsScript(store, gitExe) {
|
|
373
|
+
return [
|
|
374
|
+
"git=" + psq(gitExe),
|
|
375
|
+
"g=" + psq(store.git),
|
|
376
|
+
'"$git" --git-dir="$g" count-objects -v'
|
|
377
|
+
].join("\n");
|
|
378
|
+
}
|
|
379
|
+
function diskUsageScript(dir) {
|
|
380
|
+
return "du -sk " + psq(dir) + " 2>/dev/null | awk '{print $1 * 1024}'";
|
|
381
|
+
}
|
|
382
|
+
function listSubdirsScript(dir) {
|
|
383
|
+
return "find " + psq(dir) + " -maxdepth 1 -mindepth 1 -type d 2>/dev/null";
|
|
384
|
+
}
|
|
385
|
+
function storesDumpScript(container, extraDirs) {
|
|
386
|
+
const lines = ["set -e", "dirs=()"];
|
|
387
|
+
if (container) {
|
|
388
|
+
lines.push("base=" + psq(container));
|
|
389
|
+
lines.push('if [ -d "$base" ]; then');
|
|
390
|
+
lines.push(' for d in "$base"/*/; do');
|
|
391
|
+
lines.push(' if [ -d "$d" ]; then dirs+=("${d%/}"); fi');
|
|
392
|
+
lines.push(" done");
|
|
393
|
+
lines.push("fi");
|
|
394
|
+
}
|
|
395
|
+
for (const d of extraDirs || []) lines.push("dirs+=(" + psq(d) + ")");
|
|
396
|
+
lines.push(
|
|
397
|
+
'for d in "${dirs[@]}"; do',
|
|
398
|
+
' [ -d "$d" ] || continue',
|
|
399
|
+
' echo "==DIR $d"',
|
|
400
|
+
' if [ -f "$d/root.txt" ]; then',
|
|
401
|
+
` printf "ROOT %s\\n" "$(cat "$d/root.txt" 2>/dev/null | tr -d '\\r\\n')"`,
|
|
402
|
+
" else",
|
|
403
|
+
' echo "ROOT "',
|
|
404
|
+
" fi",
|
|
405
|
+
" echo INDEXBEGIN",
|
|
406
|
+
' cat "$d/index.json" 2>/dev/null',
|
|
407
|
+
" echo INDEXEND",
|
|
408
|
+
" echo LINEAGEBEGIN",
|
|
409
|
+
' cat "$d/lineage.json" 2>/dev/null',
|
|
410
|
+
" echo LINEAGEEND",
|
|
411
|
+
"done",
|
|
412
|
+
"exit 0"
|
|
413
|
+
);
|
|
414
|
+
return lines.join("\n");
|
|
415
|
+
}
|
|
416
|
+
export {
|
|
417
|
+
FIDELITY_ATTRS,
|
|
418
|
+
HEARTBEAT_TTL_S,
|
|
419
|
+
MAX_FILE_BYTES,
|
|
420
|
+
STALE_LOCK_MIN,
|
|
421
|
+
UTF8_PRELUDE,
|
|
422
|
+
countObjectsScript,
|
|
423
|
+
diffScript,
|
|
424
|
+
dirExistsScript,
|
|
425
|
+
diskUsageScript,
|
|
426
|
+
ensureGitScript,
|
|
427
|
+
excludeDumpScript,
|
|
428
|
+
excludeReadCmd,
|
|
429
|
+
fileWriteStdinCmd,
|
|
430
|
+
gcScript,
|
|
431
|
+
indexReadCmd,
|
|
432
|
+
killOrphansScript,
|
|
433
|
+
legacyHomeMigrateScript,
|
|
434
|
+
legacyRmScript,
|
|
435
|
+
lineageReadCmd,
|
|
436
|
+
listSubdirsScript,
|
|
437
|
+
listTagsScript,
|
|
438
|
+
listTagsWithTimeScript,
|
|
439
|
+
migrateScript,
|
|
440
|
+
mkdirScript,
|
|
441
|
+
probeHomeScript,
|
|
442
|
+
pruneScript,
|
|
443
|
+
psq,
|
|
444
|
+
purgeTagsScript,
|
|
445
|
+
renameFileCmd,
|
|
446
|
+
rescueScript,
|
|
447
|
+
resolveGitScript,
|
|
448
|
+
rollbackScript,
|
|
449
|
+
snapshotScript,
|
|
450
|
+
storesDumpScript,
|
|
451
|
+
stripBom
|
|
452
|
+
};
|