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.pwsh.js
CHANGED
|
@@ -1,778 +1,537 @@
|
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
function
|
|
87
|
-
return [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
'
|
|
107
|
-
].join(
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
'$
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
"$
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"$
|
|
208
|
-
|
|
209
|
-
'$
|
|
210
|
-
|
|
211
|
-
"
|
|
212
|
-
|
|
213
|
-
"
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
"$
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
'}',
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
"$
|
|
243
|
-
"
|
|
244
|
-
|
|
245
|
-
"$
|
|
246
|
-
"if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
"
|
|
264
|
-
'
|
|
265
|
-
"
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"$
|
|
280
|
-
'$git
|
|
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
|
-
"Write-Output
|
|
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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
"$
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
'
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"$
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
"
|
|
496
|
-
"Write-Output '
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
' if (($hp.Count -ge 2) -and [int64]::TryParse($hp[0], [ref]$a) -and [int64]::TryParse($hp[1], [ref]$b)) {',
|
|
539
|
-
' $age = [DateTimeOffset]::Now.ToUnixTimeSeconds() - $b',
|
|
540
|
-
' if (($a -gt 0) -and ($a -ne ' + String(process.pid) + ') -and ($age -ge 0) -and ($age -lt ' + HEARTBEAT_TTL_S + ')) {',
|
|
541
|
-
' if (Get-Process -Id $a -ErrorAction SilentlyContinue) {',
|
|
542
|
-
" Write-Output ('CLEANUP_OTHER_INSTANCE ' + $a)",
|
|
543
|
-
' exit 0',
|
|
544
|
-
' }',
|
|
545
|
-
' }',
|
|
546
|
-
' }',
|
|
547
|
-
' }',
|
|
548
|
-
'}',
|
|
549
|
-
// —— 第 2 级保护:新锁(有 git 操作可能正在进行)——
|
|
550
|
-
"$cutoff = (Get-Date).AddMinutes(-" + STALE_LOCK_MIN + ')',
|
|
551
|
-
'$fresh = $false',
|
|
552
|
-
"foreach ($n in @('index.lock','config.lock','HEAD.lock','gc.pid','packed-refs.lock','shallow.lock')) {",
|
|
553
|
-
' $lp = Join-Path $g $n',
|
|
554
|
-
' if (Test-Path -LiteralPath $lp -PathType Leaf) {',
|
|
555
|
-
' if ((Get-Item -LiteralPath $lp).LastWriteTime -gt $cutoff) { $fresh = $true; break }',
|
|
556
|
-
' }',
|
|
557
|
-
'}',
|
|
558
|
-
'if (-not $fresh) {',
|
|
559
|
-
" $fl = @(Get-ChildItem -LiteralPath (Join-Path $g 'refs') -Recurse -File -Filter '*.lock' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt $cutoff })",
|
|
560
|
-
' if ($fl.Count -gt 0) { $fresh = $true }',
|
|
561
|
-
'}',
|
|
562
|
-
'if ($fresh) {',
|
|
563
|
-
" Write-Output 'CLEANUP_SKIPPED_FRESH_LOCK'",
|
|
564
|
-
' exit 0',
|
|
565
|
-
'}',
|
|
566
|
-
// —— 保护未命中:原有清扫(杀孤儿 + 清 stale 锁)——
|
|
567
|
-
// 标记用变量拼接而非字面量:本脚本进程的命令行(-Command 全文)只有
|
|
568
|
-
// 未展开的 '$g',Where-Object 不会匹配到自己
|
|
569
|
-
"$marker = '--git-dir=' + $g",
|
|
570
|
-
"Get-CimInstance Win32_Process -Filter 'CommandLine IS NOT NULL' | Where-Object { $_.CommandLine.Contains($marker) } | ForEach-Object {",
|
|
571
|
-
' & taskkill /T /F /PID $_.ProcessId | Out-Null',
|
|
572
|
-
'}',
|
|
573
|
-
// 锁清单:index.lock 是 add/checkout 的持久锁,其余是 gc/tag/pack 链路
|
|
574
|
-
// 可能残留的;refs 下的 per-ref 锁用递归兜底(能走到这里说明锁已陈旧)
|
|
575
|
-
"foreach ($n in @('index.lock','config.lock','HEAD.lock','gc.pid','packed-refs.lock','shallow.lock')) {",
|
|
576
|
-
' Remove-Item -LiteralPath (Join-Path $g $n) -Force',
|
|
577
|
-
'}',
|
|
578
|
-
"Get-ChildItem -LiteralPath (Join-Path $g 'refs') -Recurse -File -Filter '*.lock' | Remove-Item -Force",
|
|
579
|
-
"Write-Output 'CLEANUP_DONE'"
|
|
580
|
-
].join('\n')
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// 删除指定快照 tag(会话已删联动清理用)。best-effort:个别 tag 已不存在时
|
|
584
|
-
// git 非零退出,但其余 tag 已被删除——所以显式 exit 0 吞掉退出码,
|
|
585
|
-
// 残留的由下一次清理幂等地收尾;JS 侧无论脚本结果都会同步索引。
|
|
586
|
-
export function purgeTagsScript(store, gitExe, tags) {
|
|
587
|
-
return [
|
|
588
|
-
"$ErrorActionPreference = 'Stop'",
|
|
589
|
-
'$git = ' + psq(gitExe),
|
|
590
|
-
'$g = ' + psq(store.git),
|
|
591
|
-
'& $git --git-dir=$g tag -d ' + tags.map((t) => psq(t)).join(' '),
|
|
592
|
-
"Write-Output 'PURGE_DONE'",
|
|
593
|
-
'exit 0'
|
|
594
|
-
].join('\n')
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// 任意长度文本写入(index.json / exclude.txt / lineage.json / root.txt 共用,
|
|
598
|
-
// PF-2):stdin 传全文 + 单进程落盘——旧实现按 base64 20000 字符分块内联
|
|
599
|
-
// (规避 -Command 单 argv 元素的 32767 上限),每块一条 PowerShell 进程,
|
|
600
|
-
// 索引几百条时 saveIndex = 6+ 条进程,而它在每条消息快照后、每次删除、每次
|
|
601
|
-
// init 都全量重写。stdin 不经命令行,长度上限与编码坑天然消失。
|
|
602
|
-
// 读取手法是探针(tests/probe/stdin-write.test.js,2026-08-29)钉死的形态:
|
|
603
|
-
// [Console]::In.ReadToEnd() 在 PS 5.1 按输入代码页(中文机器 GBK)解码
|
|
604
|
-
// UTF-8 字节必挂——必须走 OpenStandardInput 读原始字节再显式 UTF8 解码,
|
|
605
|
-
// 与代码页无关;落盘必须用 .NET WriteAllText 无 BOM 重载(PS 5.1 的
|
|
606
|
-
// Set-Content -Encoding utf8 必带 BOM)。目录创建归调用方(writeExclude 的
|
|
607
|
-
// mkdirScript 兜底 / index.json 的父目录在建仓时已存在)。
|
|
608
|
-
export function fileWriteStdinCmd(file) {
|
|
609
|
-
return [
|
|
610
|
-
"$ErrorActionPreference = 'Stop'",
|
|
611
|
-
'$tmp = ' + psq(file),
|
|
612
|
-
'$stream = [Console]::OpenStandardInput()',
|
|
613
|
-
'$ms = New-Object System.IO.MemoryStream',
|
|
614
|
-
'$buf = New-Object byte[] 8192',
|
|
615
|
-
'while (($n = $stream.Read($buf, 0, $buf.Length)) -gt 0) { $ms.Write($buf, 0, $n) }',
|
|
616
|
-
'$text = [Text.UTF8Encoding]::new($false).GetString($ms.ToArray())',
|
|
617
|
-
'[IO.File]::WriteAllText($tmp, $text, [Text.UTF8Encoding]::new($false))'
|
|
618
|
-
].join('\n')
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// 原子 rename(H2):同卷 move 是 O(1) 元数据操作,把「已完整写完的 tmp」
|
|
622
|
-
// 一步替换成目标文件,杜绝分块写中途崩溃留下的截断 JSON;也用于 loadIndex
|
|
623
|
-
// 把损坏索引改名 .corrupt-<ts> 保留现场(见 snapshots.js quarantineCorruptIndex)。
|
|
624
|
-
export function renameFileCmd(src, dst) {
|
|
625
|
-
return "$ErrorActionPreference = 'Stop'\nMove-Item -Force -LiteralPath " + psq(src) + ' -Destination ' + psq(dst)
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// 显式 -Encoding UTF8:写侧(writeTextViaShell base64 解码落盘)产出的是
|
|
629
|
-
// 无 BOM UTF-8,PS 7 默认即按 UTF-8 读,但 PS 5.1 兜底(pwsh-local 解析链
|
|
630
|
-
// 降级)按 ANSI 活动代码页解码——中文 root 乱码 → JSON.parse 失败 → 误走
|
|
631
|
-
// H2 隔离分支。与 excludeReadCmd 等同文件其他读取处的既有写法对齐。
|
|
632
|
-
export function indexReadCmd(dir) {
|
|
633
|
-
return 'Get-Content -LiteralPath ' + psq(dir + '\\index.json') + ' -Raw -Encoding UTF8 -ErrorAction SilentlyContinue'
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// fork lineage 读取(F1):lineage.json 记录 childId↔parentId 撤回链,
|
|
637
|
-
// 与 index.json 同层、原子写(writeTextViaShell)。文件不存在时输出空串。
|
|
638
|
-
export function lineageReadCmd(dir) {
|
|
639
|
-
return 'Get-Content -LiteralPath ' + psq(dir + '\\lineage.json') + ' -Raw -Encoding UTF8 -ErrorAction SilentlyContinue'
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// 旧版项目内 blobs 目录清理(仅 home 存储可用时调用,见 store.js cleanupLegacy)。
|
|
643
|
-
// -ErrorAction SilentlyContinue(PF-5):目标不存在是常态(极早期版本才有),
|
|
644
|
-
// 不容错的话 Remove-Item 抛错 → cleanupLegacy 永远走不到「成功」分支,
|
|
645
|
-
// legacyCleaned 标记失效,每次 init 都白跑一条进程。
|
|
646
|
-
export function legacyRmScript(path) {
|
|
647
|
-
return 'Remove-Item -Recurse -Force -LiteralPath ' + psq(path) + ' -ErrorAction SilentlyContinue'
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// exclude.txt 原文读取(设置页编辑用):-Raw 保留换行与空行结构,让用户
|
|
651
|
-
// 看到的就是落盘原文;文件不存在时 SilentlyContinue 输出空串,JS 侧按
|
|
652
|
-
// 「尚未配置」处理——设置页在快照存储刚建好、exclude.txt 还没写过时也会打开。
|
|
653
|
-
export function excludeReadCmd(file) {
|
|
654
|
-
return 'Get-Content -LiteralPath ' + psq(file) + ' -Raw -Encoding UTF8 -ErrorAction SilentlyContinue'
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// 批量读全部 exclude 文件(PF-8,一条脚本替代每文件一条进程——设置页
|
|
658
|
-
// 排除配置首开 4-6 条进程链里的大头)。内容按 base64 单行输出:exclude.txt
|
|
659
|
-
// 是用户可编辑的任意文本(可含空行/注释/任意字符串),逐行定界会被内容
|
|
660
|
-
// 行打乱,base64 天然免疫;也顺带规避 PS 5.1 下中文内容的代码页转码。
|
|
661
|
-
// 文件不存在输出空段(JS 侧按「尚未配置」处理,与 excludeReadCmd 一致)。
|
|
662
|
-
// 输出协议(JS 侧 parseExcludeDump 解析):
|
|
663
|
-
// EXCLBEGIN <文件路径> / <base64 单行,可为空> / EXCLEND
|
|
664
|
-
export function excludeDumpScript(files) {
|
|
665
|
-
const lines = ["$ErrorActionPreference = 'SilentlyContinue'"]
|
|
666
|
-
for (const f of files || []) {
|
|
667
|
-
const q = psq(f)
|
|
668
|
-
lines.push(
|
|
669
|
-
"Write-Output ('EXCLBEGIN ' + " + q + ')',
|
|
670
|
-
'if (Test-Path -LiteralPath ' + q + ' -PathType Leaf) { Write-Output ([Convert]::ToBase64String([IO.File]::ReadAllBytes(' + q + '))) }',
|
|
671
|
-
"Write-Output 'EXCLEND'"
|
|
672
|
-
)
|
|
673
|
-
}
|
|
674
|
-
return lines.join('\n')
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// 目录存在探测:输出定长 YES/NO 标记(与 posix 版逐字同语义),
|
|
678
|
-
// JS 侧统一按 'YES' 判定,不依赖退出码——runShell 对非零退出直接抛错。
|
|
679
|
-
export function dirExistsScript(dir) {
|
|
680
|
-
return "if (Test-Path -LiteralPath " + psq(dir) + " -PathType Container) { Write-Output 'YES' } else { Write-Output 'NO' }"
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// 影子仓库磁盘占用(设置页快照管理卡片用):git 自带的 count-objects -v
|
|
684
|
-
// 输出含 size-pack(KiB 单位,pack 文件总大小),足够向用户展示量级,
|
|
685
|
-
// 不需要逐文件累加的慢扫描。
|
|
686
|
-
export function countObjectsScript(store, gitExe) {
|
|
687
|
-
return [
|
|
688
|
-
'$git = ' + psq(gitExe),
|
|
689
|
-
'$g = ' + psq(store.git),
|
|
690
|
-
'& $git --git-dir=$g count-objects -v'
|
|
691
|
-
].join('\n')
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// 目录总大小(字节):.NET 手动栈遍历(PF-3,容错语义同 oversizeBlock——
|
|
695
|
-
// 逐目录 try/catch 跳过不可访问目录)替代 Get-ChildItem -Recurse 逐文件
|
|
696
|
-
// 管道求和,GB 级快照库从秒级降到亚秒。$sum 初始化 [long]0 防溢出,目录
|
|
697
|
-
// 为空/全跳过时输出 0(旧实现的 .Sum 为 null,JS 侧 parseInt||0 兜底等价)。
|
|
698
|
-
export function diskUsageScript(dir) {
|
|
699
|
-
const q = psq(dir)
|
|
700
|
-
return [
|
|
701
|
-
'$usageStack = [System.Collections.Generic.Stack[string]]::new()',
|
|
702
|
-
'$usageStack.Push(' + q + ')',
|
|
703
|
-
'$sum = [long]0',
|
|
704
|
-
'while ($usageStack.Count -gt 0) {',
|
|
705
|
-
' $dir = $usageStack.Pop()',
|
|
706
|
-
' try {',
|
|
707
|
-
' $di = [System.IO.DirectoryInfo]::new($dir)',
|
|
708
|
-
' foreach ($f in $di.EnumerateFiles()) { $sum += $f.Length }',
|
|
709
|
-
' foreach ($d in $di.EnumerateDirectories()) { $usageStack.Push($d.FullName) }',
|
|
710
|
-
' } catch {}',
|
|
711
|
-
'}',
|
|
712
|
-
'Write-Output $sum'
|
|
713
|
-
].join('\n')
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// 列目录下所有一级子目录全路径:manage/list 枚举 home 容器下的所有
|
|
717
|
-
// 哈希子目录用(每个子目录是一个工作区的 store)。SilentlyContinue
|
|
718
|
-
// 容忍个别不可读条目。输出每行一个全路径,JS 侧按换行拆分。
|
|
719
|
-
export function listSubdirsScript(dir) {
|
|
720
|
-
return "Get-ChildItem -LiteralPath " + psq(dir) + " -Directory -ErrorAction SilentlyContinue | ForEach-Object { Write-Output $_.FullName }"
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// 批量 dump 全部 store 的元数据:一条 shell 拿「容器下所有子目录 +
|
|
724
|
-
// 额外降级目录」的 root.txt、index.json 与 lineage.json 全文。为什么批量:
|
|
725
|
-
// 旧实现每个目录 2 条 shell(读 index + 读 root.txt)串行跑,20 个目录就是
|
|
726
|
-
// 40 次 PowerShell 冷启动(每次 0.3-1s)——快照管理列表 20 秒级慢的
|
|
727
|
-
// 根因。PF-4 起段内再带 lineage.json(manage lineage 原本对每个 root 串行
|
|
728
|
-
// 一条进程,并入后零新增)。输出定界格式(JS 侧 parseStoresDump 状态机解析):
|
|
729
|
-
// ==DIR <目录>
|
|
730
|
-
// ROOT <工作区路径或空>
|
|
731
|
-
// INDEXBEGIN / index.json 原文 / INDEXEND
|
|
732
|
-
// LINEAGEBEGIN / lineage.json 原文 / LINEAGEEND
|
|
733
|
-
// 标记行不会与内容混淆:root 路径不含换行(Windows 非法字符),JSON
|
|
734
|
-
// 单行以 [ 或 { 起头、内部路径同样不含换行。
|
|
735
|
-
export function storesDumpScript(container, extraDirs) {
|
|
736
|
-
const lines = [
|
|
737
|
-
"$ErrorActionPreference = 'Stop'",
|
|
738
|
-
'$dirs = @()'
|
|
739
|
-
]
|
|
740
|
-
if (container) {
|
|
741
|
-
lines.push('$base = ' + psq(container))
|
|
742
|
-
lines.push("$dirs += @(Get-ChildItem -LiteralPath $base -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName })")
|
|
743
|
-
}
|
|
744
|
-
for (const d of extraDirs || []) lines.push('$dirs += ' + psq(d))
|
|
745
|
-
lines.push(
|
|
746
|
-
'foreach ($d in $dirs) {',
|
|
747
|
-
' if (-not $d) { continue }',
|
|
748
|
-
' if (-not (Test-Path -LiteralPath $d -PathType Container)) { continue }',
|
|
749
|
-
' Write-Output ("==DIR " + $d)',
|
|
750
|
-
" $rt = Join-Path $d 'root.txt'",
|
|
751
|
-
" if (Test-Path -LiteralPath $rt -PathType Leaf) {",
|
|
752
|
-
" $rv = (Get-Content -LiteralPath $rt -Raw -Encoding UTF8 -ErrorAction SilentlyContinue)",
|
|
753
|
-
// 先规整成单行再拼接:root 是单行路径,但用户手改文件可能带 CRLF,
|
|
754
|
-
// 直接拼会把标记结构打乱
|
|
755
|
-
" if ($rv) { $rv = $rv.Trim() }",
|
|
756
|
-
" Write-Output ('ROOT ' + $rv)",
|
|
757
|
-
' } else {',
|
|
758
|
-
" Write-Output 'ROOT '",
|
|
759
|
-
' }',
|
|
760
|
-
" Write-Output 'INDEXBEGIN'",
|
|
761
|
-
" $ix = Join-Path $d 'index.json'",
|
|
762
|
-
" if (Test-Path -LiteralPath $ix -PathType Leaf) {",
|
|
763
|
-
' $j = Get-Content -LiteralPath $ix -Raw -Encoding UTF8 -ErrorAction SilentlyContinue',
|
|
764
|
-
' if ($j) { Write-Output $j.TrimEnd() }',
|
|
765
|
-
' }',
|
|
766
|
-
" Write-Output 'INDEXEND'",
|
|
767
|
-
" Write-Output 'LINEAGEBEGIN'",
|
|
768
|
-
" $lg = Join-Path $d 'lineage.json'",
|
|
769
|
-
" if (Test-Path -LiteralPath $lg -PathType Leaf) {",
|
|
770
|
-
' $l = Get-Content -LiteralPath $lg -Raw -Encoding UTF8 -ErrorAction SilentlyContinue',
|
|
771
|
-
' if ($l) { Write-Output $l.TrimEnd() }',
|
|
772
|
-
' }',
|
|
773
|
-
" Write-Output 'LINEAGEEND'",
|
|
774
|
-
'}',
|
|
775
|
-
'exit 0'
|
|
776
|
-
)
|
|
777
|
-
return lines.join('\n')
|
|
778
|
-
}
|
|
1
|
+
function psq(value) {
|
|
2
|
+
return "'" + String(value).replace(/'/g, "''") + "'";
|
|
3
|
+
}
|
|
4
|
+
const UTF8_PRELUDE = "$OutputEncoding = [Text.UTF8Encoding]::new($false)\ntry { [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) } catch {}";
|
|
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 --stage | Where-Object { $_ -like '160000*' } | ForEach-Object {",
|
|
15
|
+
' $p = ($_ -split "`t")[1]',
|
|
16
|
+
" & $git --literal-pathspecs --git-dir=$g update-index --force-remove -- $p",
|
|
17
|
+
"}"
|
|
18
|
+
].join("\n");
|
|
19
|
+
}
|
|
20
|
+
function oversizeBlock(maxBytes) {
|
|
21
|
+
return [
|
|
22
|
+
"$oversizeStack = [System.Collections.Generic.Stack[string]]::new()",
|
|
23
|
+
"$oversizeStack.Push($root)",
|
|
24
|
+
"$oversizeRel = [System.Collections.Generic.List[string]]::new()",
|
|
25
|
+
"while ($oversizeStack.Count -gt 0) {",
|
|
26
|
+
" $dir = $oversizeStack.Pop()",
|
|
27
|
+
" try {",
|
|
28
|
+
" $di = [System.IO.DirectoryInfo]::new($dir)",
|
|
29
|
+
" foreach ($f in $di.EnumerateFiles()) {",
|
|
30
|
+
" if ($f.Length -gt " + String(maxBytes || MAX_FILE_BYTES) + ") {",
|
|
31
|
+
" $oversizeRel.Add($f.FullName.Substring($root.Length + 1).Replace('\\','/'))",
|
|
32
|
+
" }",
|
|
33
|
+
" }",
|
|
34
|
+
" foreach ($d in $di.EnumerateDirectories()) { $oversizeStack.Push($d.FullName) }",
|
|
35
|
+
" } catch {}",
|
|
36
|
+
"}",
|
|
37
|
+
"for ($i = 0; $i -lt $oversizeRel.Count; $i += 100) {",
|
|
38
|
+
" $batch = $oversizeRel.GetRange($i, [Math]::Min(100, $oversizeRel.Count - $i))",
|
|
39
|
+
" & $git --literal-pathspecs --git-dir=$g update-index --force-remove -- $batch",
|
|
40
|
+
"}"
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
function excludeSyncBlock(excludeFile, base) {
|
|
44
|
+
const baseList = (Array.isArray(base) && base.length ? base : [".git", "node_modules/", ".dsh-recall-snapshots/", "dsh-recall-snapshots/"]).map(psq).join(",");
|
|
45
|
+
return [
|
|
46
|
+
"$exFile = " + psq(excludeFile),
|
|
47
|
+
"$userPats = @()",
|
|
48
|
+
"if (Test-Path -LiteralPath $exFile) { $userPats = @(Get-Content -LiteralPath $exFile -Encoding UTF8 -ErrorAction SilentlyContinue | Where-Object { $t = $_.Trim(); $t -and -not $t.StartsWith('#') }) }",
|
|
49
|
+
"$lines = @('') + @(" + baseList + ") + $userPats",
|
|
50
|
+
"$exc = Join-Path $g 'info\\exclude'",
|
|
51
|
+
"$excOld = @(Get-Content -LiteralPath $exc -Encoding UTF8 -ErrorAction SilentlyContinue)",
|
|
52
|
+
"$same = ($excOld.Count -eq $lines.Count)",
|
|
53
|
+
"if ($same) {",
|
|
54
|
+
" for ($i = 0; $i -lt $lines.Count; $i++) {",
|
|
55
|
+
" if ($excOld[$i] -ne $lines[$i]) { $same = $false; break }",
|
|
56
|
+
" }",
|
|
57
|
+
"}",
|
|
58
|
+
"if (-not $same) {",
|
|
59
|
+
" Set-Content -LiteralPath $exc -Value $lines -Encoding utf8",
|
|
60
|
+
" $hit = @(& $git -c core.quotePath=false --literal-pathspecs --git-dir=$g ls-files -i -c --exclude-from=$exc | Where-Object { $_ })",
|
|
61
|
+
" for ($i = 0; $i -lt $hit.Count; $i += 100) {",
|
|
62
|
+
" $batch = @($hit[$i..([Math]::Min($i + 99, $hit.Count - 1))])",
|
|
63
|
+
" & $git --literal-pathspecs --git-dir=$g update-index --force-remove -- $batch",
|
|
64
|
+
" }",
|
|
65
|
+
"}"
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
function heartbeatBlock() {
|
|
69
|
+
return [
|
|
70
|
+
"$hbf = Join-Path (Split-Path -Parent (Split-Path -Parent $g)) 'heartbeat'",
|
|
71
|
+
"Set-Content -LiteralPath $hbf -Value ('" + String(process.pid) + " ' + [DateTimeOffset]::Now.ToUnixTimeSeconds()) -Encoding ascii -ErrorAction SilentlyContinue"
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
function resolveGitScript() {
|
|
75
|
+
return [
|
|
76
|
+
"$candidates = @()",
|
|
77
|
+
"$g = (Get-Command git -ErrorAction SilentlyContinue).Source",
|
|
78
|
+
"if ($g) { $candidates += $g }",
|
|
79
|
+
"if (${env:ProgramFiles}) { $candidates += (Join-Path ${env:ProgramFiles} 'Git\\cmd\\git.exe') }",
|
|
80
|
+
"if (${env:ProgramFiles(x86)}) { $candidates += (Join-Path ${env:ProgramFiles(x86)} 'Git\\cmd\\git.exe') }",
|
|
81
|
+
"if (${env:LocalAppData}) { $candidates += (Join-Path ${env:LocalAppData} 'Programs\\Git\\cmd\\git.exe') }",
|
|
82
|
+
"$g = $candidates | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) } | Select-Object -First 1",
|
|
83
|
+
"if ($g) { Write-Output $g }"
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
function homeDirScript(root, envHome) {
|
|
87
|
+
return [
|
|
88
|
+
"$r = " + psq(root),
|
|
89
|
+
"$h = if ($env:DSH_HOME) { $env:DSH_HOME } elseif (" + psq(envHome) + ") { " + psq(envHome) + ' } else { Join-Path $env:USERPROFILE ".dsh" }',
|
|
90
|
+
"$sha = [Security.Cryptography.SHA256]::Create()",
|
|
91
|
+
"$hex = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($r))) -replace '-','').ToLower()",
|
|
92
|
+
"Write-Output (Join-Path $h ('dsh-recall-snapshots\\' + $hex))"
|
|
93
|
+
].join("\n");
|
|
94
|
+
}
|
|
95
|
+
function mkdirScript(dir) {
|
|
96
|
+
return "New-Item -ItemType Directory -Force -Path " + psq(dir) + " | Out-Null";
|
|
97
|
+
}
|
|
98
|
+
function migrateScript(src, dst) {
|
|
99
|
+
return [
|
|
100
|
+
"$ErrorActionPreference = 'Stop'",
|
|
101
|
+
"$src = " + psq(src),
|
|
102
|
+
"$dst = " + psq(dst),
|
|
103
|
+
"if (Test-Path -LiteralPath (Join-Path $src 'git')) { Move-Item -LiteralPath (Join-Path $src 'git') -Destination (Join-Path $dst 'git') -Force }",
|
|
104
|
+
"if (Test-Path -LiteralPath (Join-Path $src 'index.json')) { Move-Item -LiteralPath (Join-Path $src 'index.json') -Destination (Join-Path $dst 'index.json') -Force }",
|
|
105
|
+
"Remove-Item -Recurse -Force -LiteralPath $src -ErrorAction SilentlyContinue",
|
|
106
|
+
"Write-Output 'MIGRATE_OK'"
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
function ensureGitScript(store, gitExe, base) {
|
|
110
|
+
return [
|
|
111
|
+
"$ErrorActionPreference = 'Stop'",
|
|
112
|
+
"$git = " + psq(gitExe),
|
|
113
|
+
"$repo = " + psq(store.repo),
|
|
114
|
+
"$g = " + psq(store.git),
|
|
115
|
+
heartbeatBlock(),
|
|
116
|
+
"if (-not (Test-Path -LiteralPath $g)) {",
|
|
117
|
+
" & $git init $repo | Out-Null",
|
|
118
|
+
"}",
|
|
119
|
+
"& $git --git-dir=$g config core.longpaths true",
|
|
120
|
+
"& $git --git-dir=$g config core.autocrlf false",
|
|
121
|
+
"& $git --git-dir=$g config advice.addEmbeddedRepo false",
|
|
122
|
+
"$attrDir = Join-Path $g 'info'",
|
|
123
|
+
"Set-Content -LiteralPath (Join-Path $attrDir 'attributes') -Value '" + FIDELITY_ATTRS + "' -Encoding ascii",
|
|
124
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
125
|
+
"$stamp = Join-Path $g 'gc.stamp'",
|
|
126
|
+
"if (Test-Path -LiteralPath $stamp) { Write-Output ('GIT_OK ' + [String](Get-Content -LiteralPath $stamp -TotalCount 1 -ErrorAction SilentlyContinue)) } else { Write-Output 'GIT_OK' }"
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
129
|
+
function attrsMigrateBlock() {
|
|
130
|
+
return [
|
|
131
|
+
"$migStamp = Join-Path $g 'attrs-v1.stamp'",
|
|
132
|
+
"if (-not (Test-Path -LiteralPath $migStamp)) {",
|
|
133
|
+
" & $git --git-dir=$g --work-tree=$root add --renormalize --ignore-errors -- ':(top)'",
|
|
134
|
+
" if ($LASTEXITCODE -le 1) { Set-Content -LiteralPath $migStamp -Value 1 -Encoding ascii -ErrorAction SilentlyContinue }",
|
|
135
|
+
"}"
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
function snapshotScript(root, store, gitExe, messageId, base) {
|
|
139
|
+
return [
|
|
140
|
+
"$ErrorActionPreference = 'Stop'",
|
|
141
|
+
"$git = " + psq(gitExe),
|
|
142
|
+
"$g = " + psq(store.git),
|
|
143
|
+
"$root = " + psq(root),
|
|
144
|
+
heartbeatBlock(),
|
|
145
|
+
dropGitlinksBlock(),
|
|
146
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
147
|
+
attrsMigrateBlock(),
|
|
148
|
+
// fail-open add(issue #7 加固):--ignore-errors 让「个别路径无法索引」
|
|
149
|
+
// (无提交的嵌入式仓库、不可读文件等)以退出码 1 结束但索引照常落盘,
|
|
150
|
+
// 快照缺个别路径可接受,好过整条快照 fatal。退出码 ≥2 才是真 fatal
|
|
151
|
+
// (磁盘满、index.lock 等),必须显式 throw:pwsh 对原生命令非零退出
|
|
152
|
+
// 不抛(EAP 不作用于 native),不检查就会带着未更新的旧索引走完
|
|
153
|
+
// write-tree/commit/tag,产出「空树假成功」快照(实测 PS 5.1/pwsh 7
|
|
154
|
+
// 均如此)。add 输出临时降到 Continue 再 2>&1 捕获:合并进管道会把
|
|
155
|
+
// native stderr 包装成 ErrorRecord,EAP=Stop 下直接抛 NativeCommandError,
|
|
156
|
+
// 而 PS 5.1 的 SilentlyContinue 会把合并流里的记录整个丢弃(实测 LOG
|
|
157
|
+
// 为空)——Continue 是两个版本下唯一都能拿到 stderr 文本的取值。捕获
|
|
158
|
+
// 后按 "unable to index file 'X'" 提取被跳过的路径,以 SNAP_SKIP 行
|
|
159
|
+
// 回传 JS 侧做用户可见提示。
|
|
160
|
+
"$ErrorActionPreference = 'Continue'",
|
|
161
|
+
'$addLog = (@(& $git --git-dir=$g --work-tree=$root add -A --ignore-errors 2>&1) | ForEach-Object { [string]$_ }) -join "`n"',
|
|
162
|
+
"$addRc = $LASTEXITCODE",
|
|
163
|
+
"$ErrorActionPreference = 'Stop'",
|
|
164
|
+
'if ($addRc -ge 2) { throw ("git add fatal (exit " + $addRc + "): " + $addLog) }',
|
|
165
|
+
`foreach ($m in [regex]::Matches($addLog, "unable to index file '([^']+)'") ) { Write-Output ('SNAP_SKIP ' + $m.Groups[1].Value) }`,
|
|
166
|
+
dropGitlinksBlock(),
|
|
167
|
+
oversizeBlock(store.maxFileBytes),
|
|
168
|
+
"$tree = (& $git --git-dir=$g --work-tree=$root write-tree).Trim()",
|
|
169
|
+
"$commit = (& $git --git-dir=$g -c user.name=dsh-recall -c user.email=recall@dsh.local commit-tree $tree -m ('snapshot ' + " + psq(messageId) + ")).Trim()",
|
|
170
|
+
"& $git --git-dir=$g tag -f " + psq("snap-" + messageId) + " $commit | Out-Null",
|
|
171
|
+
// PF-1:TREE 行随 SNAP_OK 回传 add -A 之后的 index 树指纹——execute 用它与
|
|
172
|
+
// preview 时的指纹比对即可判定「预览后文件是否变化」(STALE),免掉 execute
|
|
173
|
+
// 侧整条重复 diff。安全快照(pre-rollback)同样输出,比对点见 routes-core。
|
|
174
|
+
"Write-Output ('TREE ' + $tree)",
|
|
175
|
+
"Write-Output 'SNAP_OK'"
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
function diffScript(root, store, gitExe, tag, base, maxChanges) {
|
|
179
|
+
const take = Math.max(1, Math.trunc(Number(maxChanges) || 500));
|
|
180
|
+
return [
|
|
181
|
+
"$ErrorActionPreference = 'Stop'",
|
|
182
|
+
"$git = " + psq(gitExe),
|
|
183
|
+
"$g = " + psq(store.git),
|
|
184
|
+
"$root = " + psq(root),
|
|
185
|
+
dropGitlinksBlock(),
|
|
186
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
187
|
+
// fail-open add:语义同 snapshotScript(--ignore-errors 跳过无法索引的
|
|
188
|
+
// 路径、≥2 显式 throw 防「旧索引假成功」);此处不提取 SNAP_SKIP——
|
|
189
|
+
// 被跳过的路径不进索引,diff 天然不显示、rollback 的删除清单来自当前
|
|
190
|
+
// 索引也天然不会误删它们
|
|
191
|
+
"& $git --git-dir=$g --work-tree=$root add -A --ignore-errors",
|
|
192
|
+
'if ($LASTEXITCODE -ge 2) { throw ("git add fatal (exit " + $LASTEXITCODE + ")") }',
|
|
193
|
+
dropGitlinksBlock(),
|
|
194
|
+
oversizeBlock(store.maxFileBytes),
|
|
195
|
+
"$curOut = & $git -c core.quotePath=false --git-dir=$g --work-tree=$root ls-files --stage",
|
|
196
|
+
// 旧 tag 的树里可能仍有 gitlink(修复前留下的),从目标侧一并剔除,
|
|
197
|
+
// 否则 diff 会报出“恢复 dsh-recall-plugin”这类幻影条目
|
|
198
|
+
"$targetOut = @(& $git -c core.quotePath=false --git-dir=$g ls-tree -r " + psq(tag) + " | Where-Object { -not $_.StartsWith('160000') })",
|
|
199
|
+
"$curMap = @{}",
|
|
200
|
+
"foreach ($r in @($curOut)) {",
|
|
201
|
+
" if (-not $r) { continue }",
|
|
202
|
+
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
203
|
+
' $sha = ($r.Substring(0, $tab) -split " ")[1]',
|
|
204
|
+
" $curMap[$path] = $sha",
|
|
205
|
+
"}",
|
|
206
|
+
"$targetMap = @{}",
|
|
207
|
+
"foreach ($r in @($targetOut)) {",
|
|
208
|
+
" if (-not $r) { continue }",
|
|
209
|
+
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
210
|
+
' $sha = ($r.Substring(0, $tab) -split " ")[2]',
|
|
211
|
+
" $targetMap[$path] = $sha",
|
|
212
|
+
"}",
|
|
213
|
+
"$result = @()",
|
|
214
|
+
"foreach ($k in $curMap.Keys) {",
|
|
215
|
+
' if (-not $targetMap.ContainsKey($k)) { $result += [pscustomobject]@{ rel = $k; kind = "added" } }',
|
|
216
|
+
' elseif ($targetMap[$k] -ne $curMap[$k]) { $result += [pscustomobject]@{ rel = $k; kind = "modified" } }',
|
|
217
|
+
"}",
|
|
218
|
+
"foreach ($k in $targetMap.Keys) {",
|
|
219
|
+
' if (-not $curMap.ContainsKey($k)) { $result += [pscustomobject]@{ rel = $k; kind = "restored" } }',
|
|
220
|
+
"}",
|
|
221
|
+
"$sorted = @($result | Sort-Object rel)",
|
|
222
|
+
"Write-Output ('TOTAL ' + $sorted.Count)",
|
|
223
|
+
"Write-Output (ConvertTo-Json -InputObject @($sorted | Select-Object -First " + take + ") -Depth 3 -Compress)",
|
|
224
|
+
"$tree = (& $git --git-dir=$g --work-tree=$root write-tree).Trim()",
|
|
225
|
+
"Write-Output ('TREE ' + $tree)"
|
|
226
|
+
].join("\n");
|
|
227
|
+
}
|
|
228
|
+
function rollbackScript(root, store, gitExe, tag, base) {
|
|
229
|
+
return [
|
|
230
|
+
"$ErrorActionPreference = 'Stop'",
|
|
231
|
+
"$git = " + psq(gitExe),
|
|
232
|
+
"$g = " + psq(store.git),
|
|
233
|
+
"$root = " + psq(root),
|
|
234
|
+
dropGitlinksBlock(),
|
|
235
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
236
|
+
// fail-open add:语义同 snapshotScript 同款注释(diff/rollback 复用)
|
|
237
|
+
"& $git --git-dir=$g --work-tree=$root add -A --ignore-errors",
|
|
238
|
+
'if ($LASTEXITCODE -ge 2) { throw ("git add fatal (exit " + $LASTEXITCODE + ")") }',
|
|
239
|
+
dropGitlinksBlock(),
|
|
240
|
+
oversizeBlock(store.maxFileBytes),
|
|
241
|
+
// 同 diffScript:-z 的 NUL 输出会被 PowerShell 捕获丢弃,改为逐行 + quotePath=false
|
|
242
|
+
"$curOut = & $git -c core.quotePath=false --git-dir=$g --work-tree=$root ls-files --stage",
|
|
243
|
+
"$targetOut = @(& $git -c core.quotePath=false --git-dir=$g ls-tree -r " + psq(tag) + " | Where-Object { -not $_.StartsWith('160000') })",
|
|
244
|
+
"$targetMap = @{}",
|
|
245
|
+
"foreach ($r in @($targetOut)) {",
|
|
246
|
+
" if (-not $r) { continue }",
|
|
247
|
+
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
248
|
+
" $targetMap[$path] = $true",
|
|
249
|
+
"}",
|
|
250
|
+
"$restored = $targetMap.Count",
|
|
251
|
+
"if ($restored -gt 0) {",
|
|
252
|
+
" $zip = " + psq(store.dir + "\\restore-tmp.zip"),
|
|
253
|
+
" & $git --git-dir=$g archive --format=zip --output=$zip " + psq(tag),
|
|
254
|
+
" Expand-Archive -LiteralPath $zip -DestinationPath $root -Force",
|
|
255
|
+
" Remove-Item -LiteralPath $zip -Force",
|
|
256
|
+
"}",
|
|
257
|
+
// 删除失败语义与 POSIX 版对齐(F-G2):本侧 EAP=Stop 下 Remove-Item
|
|
258
|
+
// 失败直接抛终止错误、pwsh 以非零码退出;POSIX 侧 rm 在 set -e 的 if
|
|
259
|
+
// 条件里失败不会自动终止,须显式 exit 1(见 scripts.posix.js rollbackScript)。
|
|
260
|
+
// 任何一侧半回退都不许报 ROLLBACK_OK——假成功会让救援永不触发(H1)。
|
|
261
|
+
"$deleted = 0",
|
|
262
|
+
"foreach ($r in @($curOut)) {",
|
|
263
|
+
" if (-not $r) { continue }",
|
|
264
|
+
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
265
|
+
" if (-not $targetMap.ContainsKey($path)) {",
|
|
266
|
+
" $full = Join-Path $root ($path.Replace('/','\\'))",
|
|
267
|
+
" if (Test-Path -LiteralPath $full) { Remove-Item -LiteralPath $full -Force; $deleted++ }",
|
|
268
|
+
" }",
|
|
269
|
+
"}",
|
|
270
|
+
"Write-Output ('ROLLBACK_OK ' + $deleted + ' ' + $restored)"
|
|
271
|
+
].join("\n");
|
|
272
|
+
}
|
|
273
|
+
function rescueScript(root, store, gitExe, tag) {
|
|
274
|
+
return [
|
|
275
|
+
"$ErrorActionPreference = 'Stop'",
|
|
276
|
+
"$git = " + psq(gitExe),
|
|
277
|
+
"$g = " + psq(store.git),
|
|
278
|
+
"$root = " + psq(root),
|
|
279
|
+
"& $git --git-dir=$g --work-tree=$root reset --hard " + psq(tag),
|
|
280
|
+
'if ($LASTEXITCODE -ne 0) { throw ("git reset --hard failed (exit " + $LASTEXITCODE + ")") }',
|
|
281
|
+
"Write-Output 'RESCUE_OK'"
|
|
282
|
+
].join("\n");
|
|
283
|
+
}
|
|
284
|
+
function listTagsScript(store, gitExe) {
|
|
285
|
+
return [
|
|
286
|
+
"$ErrorActionPreference = 'Stop'",
|
|
287
|
+
"$git = " + psq(gitExe),
|
|
288
|
+
"$g = " + psq(store.git),
|
|
289
|
+
// 仅创建过 store 目录、尚未产生过快照时没有 git/.git;把它视为
|
|
290
|
+
// 空快照仓库而非错误,全部删除仍可顺便清空其陈旧 index.json。
|
|
291
|
+
"if (-not (Test-Path -LiteralPath $g -PathType Container)) { exit 0 }",
|
|
292
|
+
'& $git --git-dir=$g tag -l "snap-*"'
|
|
293
|
+
].join("\n");
|
|
294
|
+
}
|
|
295
|
+
function listTagsWithTimeScript(store, gitExe) {
|
|
296
|
+
return [
|
|
297
|
+
"$ErrorActionPreference = 'Stop'",
|
|
298
|
+
"$git = " + psq(gitExe),
|
|
299
|
+
"$g = " + psq(store.git),
|
|
300
|
+
"if (-not (Test-Path -LiteralPath $g -PathType Container)) { exit 0 }",
|
|
301
|
+
'& $git --git-dir=$g for-each-ref --format="%(refname:short) %(creatordate:unix)" "refs/tags/snap-*"'
|
|
302
|
+
].join("\n");
|
|
303
|
+
}
|
|
304
|
+
function gcScript(store, gitExe) {
|
|
305
|
+
return [
|
|
306
|
+
"$ErrorActionPreference = 'Stop'",
|
|
307
|
+
"$git = " + psq(gitExe),
|
|
308
|
+
"$g = " + psq(store.git),
|
|
309
|
+
"& $git --git-dir=$g gc --quiet --prune=now",
|
|
310
|
+
"Set-Content -LiteralPath (Join-Path $g 'gc.stamp') -Value ([DateTimeOffset]::UtcNow.ToUnixTimeSeconds().ToString()) -Encoding ascii",
|
|
311
|
+
"Write-Output 'GC_OK'"
|
|
312
|
+
].join("\n");
|
|
313
|
+
}
|
|
314
|
+
function pruneScript(store, gitExe) {
|
|
315
|
+
return [
|
|
316
|
+
"$ErrorActionPreference = 'Stop'",
|
|
317
|
+
"$git = " + psq(gitExe),
|
|
318
|
+
"$g = " + psq(store.git),
|
|
319
|
+
"& $git --git-dir=$g prune",
|
|
320
|
+
"Write-Output 'PRUNE_OK'"
|
|
321
|
+
].join("\n");
|
|
322
|
+
}
|
|
323
|
+
function killOrphansScript(gitDir) {
|
|
324
|
+
return [
|
|
325
|
+
"# RECALL_CLEANUP",
|
|
326
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
327
|
+
"$g = " + psq(gitDir),
|
|
328
|
+
// —— 第 1 级保护:另一活实例心跳(store.dir 是 git-dir 上两级)——
|
|
329
|
+
"$hbf = Join-Path (Split-Path -Parent (Split-Path -Parent $g)) 'heartbeat'",
|
|
330
|
+
"if (Test-Path -LiteralPath $hbf -PathType Leaf) {",
|
|
331
|
+
" $hl = Get-Content -LiteralPath $hbf -TotalCount 1",
|
|
332
|
+
" if ($hl) {",
|
|
333
|
+
" $hp = ('' + $hl).Trim() -split '\\s+'",
|
|
334
|
+
" $a = [int64]0; $b = [int64]0",
|
|
335
|
+
" if (($hp.Count -ge 2) -and [int64]::TryParse($hp[0], [ref]$a) -and [int64]::TryParse($hp[1], [ref]$b)) {",
|
|
336
|
+
" $age = [DateTimeOffset]::Now.ToUnixTimeSeconds() - $b",
|
|
337
|
+
" if (($a -gt 0) -and ($a -ne " + String(process.pid) + ") -and ($age -ge 0) -and ($age -lt " + HEARTBEAT_TTL_S + ")) {",
|
|
338
|
+
" if (Get-Process -Id $a -ErrorAction SilentlyContinue) {",
|
|
339
|
+
" Write-Output ('CLEANUP_OTHER_INSTANCE ' + $a)",
|
|
340
|
+
" exit 0",
|
|
341
|
+
" }",
|
|
342
|
+
" }",
|
|
343
|
+
" }",
|
|
344
|
+
" }",
|
|
345
|
+
"}",
|
|
346
|
+
// —— 第 2 级保护:新锁(有 git 操作可能正在进行)——
|
|
347
|
+
"$cutoff = (Get-Date).AddMinutes(-" + STALE_LOCK_MIN + ")",
|
|
348
|
+
"$fresh = $false",
|
|
349
|
+
"foreach ($n in @('index.lock','config.lock','HEAD.lock','gc.pid','packed-refs.lock','shallow.lock')) {",
|
|
350
|
+
" $lp = Join-Path $g $n",
|
|
351
|
+
" if (Test-Path -LiteralPath $lp -PathType Leaf) {",
|
|
352
|
+
" if ((Get-Item -LiteralPath $lp).LastWriteTime -gt $cutoff) { $fresh = $true; break }",
|
|
353
|
+
" }",
|
|
354
|
+
"}",
|
|
355
|
+
"if (-not $fresh) {",
|
|
356
|
+
" $fl = @(Get-ChildItem -LiteralPath (Join-Path $g 'refs') -Recurse -File -Filter '*.lock' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt $cutoff })",
|
|
357
|
+
" if ($fl.Count -gt 0) { $fresh = $true }",
|
|
358
|
+
"}",
|
|
359
|
+
"if ($fresh) {",
|
|
360
|
+
" Write-Output 'CLEANUP_SKIPPED_FRESH_LOCK'",
|
|
361
|
+
" exit 0",
|
|
362
|
+
"}",
|
|
363
|
+
// —— 保护未命中:原有清扫(杀孤儿 + 清 stale 锁)——
|
|
364
|
+
// 标记用变量拼接而非字面量:本脚本进程的命令行(-Command 全文)只有
|
|
365
|
+
// 未展开的 '$g',Where-Object 不会匹配到自己
|
|
366
|
+
"$marker = '--git-dir=' + $g",
|
|
367
|
+
"Get-CimInstance Win32_Process -Filter 'CommandLine IS NOT NULL' | Where-Object { $_.CommandLine.Contains($marker) } | ForEach-Object {",
|
|
368
|
+
" & taskkill /T /F /PID $_.ProcessId | Out-Null",
|
|
369
|
+
"}",
|
|
370
|
+
// 锁清单:index.lock 是 add/checkout 的持久锁,其余是 gc/tag/pack 链路
|
|
371
|
+
// 可能残留的;refs 下的 per-ref 锁用递归兜底(能走到这里说明锁已陈旧)
|
|
372
|
+
"foreach ($n in @('index.lock','config.lock','HEAD.lock','gc.pid','packed-refs.lock','shallow.lock')) {",
|
|
373
|
+
" Remove-Item -LiteralPath (Join-Path $g $n) -Force",
|
|
374
|
+
"}",
|
|
375
|
+
"Get-ChildItem -LiteralPath (Join-Path $g 'refs') -Recurse -File -Filter '*.lock' | Remove-Item -Force",
|
|
376
|
+
"Write-Output 'CLEANUP_DONE'"
|
|
377
|
+
].join("\n");
|
|
378
|
+
}
|
|
379
|
+
function purgeTagsScript(store, gitExe, tags) {
|
|
380
|
+
return [
|
|
381
|
+
"$ErrorActionPreference = 'Stop'",
|
|
382
|
+
"$git = " + psq(gitExe),
|
|
383
|
+
"$g = " + psq(store.git),
|
|
384
|
+
"& $git --git-dir=$g tag -d " + tags.map((t) => psq(t)).join(" "),
|
|
385
|
+
"Write-Output 'PURGE_DONE'",
|
|
386
|
+
"exit 0"
|
|
387
|
+
].join("\n");
|
|
388
|
+
}
|
|
389
|
+
function fileWriteStdinCmd(file) {
|
|
390
|
+
return [
|
|
391
|
+
"$ErrorActionPreference = 'Stop'",
|
|
392
|
+
"$tmp = " + psq(file),
|
|
393
|
+
"$stream = [Console]::OpenStandardInput()",
|
|
394
|
+
"$ms = New-Object System.IO.MemoryStream",
|
|
395
|
+
"$buf = New-Object byte[] 8192",
|
|
396
|
+
"while (($n = $stream.Read($buf, 0, $buf.Length)) -gt 0) { $ms.Write($buf, 0, $n) }",
|
|
397
|
+
"$text = [Text.UTF8Encoding]::new($false).GetString($ms.ToArray())",
|
|
398
|
+
"[IO.File]::WriteAllText($tmp, $text, [Text.UTF8Encoding]::new($false))"
|
|
399
|
+
].join("\n");
|
|
400
|
+
}
|
|
401
|
+
function renameFileCmd(src, dst) {
|
|
402
|
+
return "$ErrorActionPreference = 'Stop'\nMove-Item -Force -LiteralPath " + psq(src) + " -Destination " + psq(dst);
|
|
403
|
+
}
|
|
404
|
+
function indexReadCmd(dir) {
|
|
405
|
+
return "Get-Content -LiteralPath " + psq(dir + "\\index.json") + " -Raw -Encoding UTF8 -ErrorAction SilentlyContinue";
|
|
406
|
+
}
|
|
407
|
+
function lineageReadCmd(dir) {
|
|
408
|
+
return "Get-Content -LiteralPath " + psq(dir + "\\lineage.json") + " -Raw -Encoding UTF8 -ErrorAction SilentlyContinue";
|
|
409
|
+
}
|
|
410
|
+
function legacyRmScript(path) {
|
|
411
|
+
return "Remove-Item -Recurse -Force -LiteralPath " + psq(path) + " -ErrorAction SilentlyContinue";
|
|
412
|
+
}
|
|
413
|
+
function excludeReadCmd(file) {
|
|
414
|
+
return "Get-Content -LiteralPath " + psq(file) + " -Raw -Encoding UTF8 -ErrorAction SilentlyContinue";
|
|
415
|
+
}
|
|
416
|
+
function excludeDumpScript(files) {
|
|
417
|
+
const lines = ["$ErrorActionPreference = 'SilentlyContinue'"];
|
|
418
|
+
for (const f of files || []) {
|
|
419
|
+
const q = psq(f);
|
|
420
|
+
lines.push(
|
|
421
|
+
"Write-Output ('EXCLBEGIN ' + " + q + ")",
|
|
422
|
+
"if (Test-Path -LiteralPath " + q + " -PathType Leaf) { Write-Output ([Convert]::ToBase64String([IO.File]::ReadAllBytes(" + q + "))) }",
|
|
423
|
+
"Write-Output 'EXCLEND'"
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return lines.join("\n");
|
|
427
|
+
}
|
|
428
|
+
function dirExistsScript(dir) {
|
|
429
|
+
return "if (Test-Path -LiteralPath " + psq(dir) + " -PathType Container) { Write-Output 'YES' } else { Write-Output 'NO' }";
|
|
430
|
+
}
|
|
431
|
+
function countObjectsScript(store, gitExe) {
|
|
432
|
+
return [
|
|
433
|
+
"$git = " + psq(gitExe),
|
|
434
|
+
"$g = " + psq(store.git),
|
|
435
|
+
"& $git --git-dir=$g count-objects -v"
|
|
436
|
+
].join("\n");
|
|
437
|
+
}
|
|
438
|
+
function diskUsageScript(dir) {
|
|
439
|
+
const q = psq(dir);
|
|
440
|
+
return [
|
|
441
|
+
"$usageStack = [System.Collections.Generic.Stack[string]]::new()",
|
|
442
|
+
"$usageStack.Push(" + q + ")",
|
|
443
|
+
"$sum = [long]0",
|
|
444
|
+
"while ($usageStack.Count -gt 0) {",
|
|
445
|
+
" $dir = $usageStack.Pop()",
|
|
446
|
+
" try {",
|
|
447
|
+
" $di = [System.IO.DirectoryInfo]::new($dir)",
|
|
448
|
+
" foreach ($f in $di.EnumerateFiles()) { $sum += $f.Length }",
|
|
449
|
+
" foreach ($d in $di.EnumerateDirectories()) { $usageStack.Push($d.FullName) }",
|
|
450
|
+
" } catch {}",
|
|
451
|
+
"}",
|
|
452
|
+
"Write-Output $sum"
|
|
453
|
+
].join("\n");
|
|
454
|
+
}
|
|
455
|
+
function listSubdirsScript(dir) {
|
|
456
|
+
return "Get-ChildItem -LiteralPath " + psq(dir) + " -Directory -ErrorAction SilentlyContinue | ForEach-Object { Write-Output $_.FullName }";
|
|
457
|
+
}
|
|
458
|
+
function storesDumpScript(container, extraDirs) {
|
|
459
|
+
const lines = [
|
|
460
|
+
"$ErrorActionPreference = 'Stop'",
|
|
461
|
+
"$dirs = @()"
|
|
462
|
+
];
|
|
463
|
+
if (container) {
|
|
464
|
+
lines.push("$base = " + psq(container));
|
|
465
|
+
lines.push("$dirs += @(Get-ChildItem -LiteralPath $base -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName })");
|
|
466
|
+
}
|
|
467
|
+
for (const d of extraDirs || []) lines.push("$dirs += " + psq(d));
|
|
468
|
+
lines.push(
|
|
469
|
+
"foreach ($d in $dirs) {",
|
|
470
|
+
" if (-not $d) { continue }",
|
|
471
|
+
" if (-not (Test-Path -LiteralPath $d -PathType Container)) { continue }",
|
|
472
|
+
' Write-Output ("==DIR " + $d)',
|
|
473
|
+
" $rt = Join-Path $d 'root.txt'",
|
|
474
|
+
" if (Test-Path -LiteralPath $rt -PathType Leaf) {",
|
|
475
|
+
" $rv = (Get-Content -LiteralPath $rt -Raw -Encoding UTF8 -ErrorAction SilentlyContinue)",
|
|
476
|
+
// 先规整成单行再拼接:root 是单行路径,但用户手改文件可能带 CRLF,
|
|
477
|
+
// 直接拼会把标记结构打乱
|
|
478
|
+
" if ($rv) { $rv = $rv.Trim() }",
|
|
479
|
+
" Write-Output ('ROOT ' + $rv)",
|
|
480
|
+
" } else {",
|
|
481
|
+
" Write-Output 'ROOT '",
|
|
482
|
+
" }",
|
|
483
|
+
" Write-Output 'INDEXBEGIN'",
|
|
484
|
+
" $ix = Join-Path $d 'index.json'",
|
|
485
|
+
" if (Test-Path -LiteralPath $ix -PathType Leaf) {",
|
|
486
|
+
" $j = Get-Content -LiteralPath $ix -Raw -Encoding UTF8 -ErrorAction SilentlyContinue",
|
|
487
|
+
" if ($j) { Write-Output $j.TrimEnd() }",
|
|
488
|
+
" }",
|
|
489
|
+
" Write-Output 'INDEXEND'",
|
|
490
|
+
" Write-Output 'LINEAGEBEGIN'",
|
|
491
|
+
" $lg = Join-Path $d 'lineage.json'",
|
|
492
|
+
" if (Test-Path -LiteralPath $lg -PathType Leaf) {",
|
|
493
|
+
" $l = Get-Content -LiteralPath $lg -Raw -Encoding UTF8 -ErrorAction SilentlyContinue",
|
|
494
|
+
" if ($l) { Write-Output $l.TrimEnd() }",
|
|
495
|
+
" }",
|
|
496
|
+
" Write-Output 'LINEAGEEND'",
|
|
497
|
+
"}",
|
|
498
|
+
"exit 0"
|
|
499
|
+
);
|
|
500
|
+
return lines.join("\n");
|
|
501
|
+
}
|
|
502
|
+
export {
|
|
503
|
+
FIDELITY_ATTRS,
|
|
504
|
+
HEARTBEAT_TTL_S,
|
|
505
|
+
MAX_FILE_BYTES,
|
|
506
|
+
STALE_LOCK_MIN,
|
|
507
|
+
UTF8_PRELUDE,
|
|
508
|
+
countObjectsScript,
|
|
509
|
+
diffScript,
|
|
510
|
+
dirExistsScript,
|
|
511
|
+
diskUsageScript,
|
|
512
|
+
ensureGitScript,
|
|
513
|
+
excludeDumpScript,
|
|
514
|
+
excludeReadCmd,
|
|
515
|
+
fileWriteStdinCmd,
|
|
516
|
+
gcScript,
|
|
517
|
+
homeDirScript,
|
|
518
|
+
indexReadCmd,
|
|
519
|
+
killOrphansScript,
|
|
520
|
+
legacyRmScript,
|
|
521
|
+
lineageReadCmd,
|
|
522
|
+
listSubdirsScript,
|
|
523
|
+
listTagsScript,
|
|
524
|
+
listTagsWithTimeScript,
|
|
525
|
+
migrateScript,
|
|
526
|
+
mkdirScript,
|
|
527
|
+
pruneScript,
|
|
528
|
+
psq,
|
|
529
|
+
purgeTagsScript,
|
|
530
|
+
renameFileCmd,
|
|
531
|
+
rescueScript,
|
|
532
|
+
resolveGitScript,
|
|
533
|
+
rollbackScript,
|
|
534
|
+
snapshotScript,
|
|
535
|
+
storesDumpScript,
|
|
536
|
+
stripBom
|
|
537
|
+
};
|