dsh-recall-plugin 1.2.2 → 1.4.0
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 +109 -0
- package/README.en.md +124 -118
- package/README.md +125 -119
- package/cordis.patch.yml +19 -11
- package/lib/client.js +780 -456
- package/lib/config.js +36 -0
- package/lib/index.js +541 -164
- package/lib/maintenance.js +120 -107
- package/lib/scripts.posix.js +366 -290
- package/lib/scripts.pwsh.js +432 -334
- package/lib/snapshots.js +214 -192
- package/lib/store.js +342 -249
- package/package.json +52 -57
package/lib/scripts.posix.js
CHANGED
|
@@ -1,290 +1,366 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-recall-plugin — bash 脚本模板(纯函数,无 ctx 依赖,POSIX 平台专用)
|
|
3
|
-
*
|
|
4
|
-
* 职责:Linux/macOS 下所有 shell 命令的 bash 脚本文本。与 scripts.pwsh.js
|
|
5
|
-
* 导出同名接口,由 store.js 按 process.platform 选择。
|
|
6
|
-
*
|
|
7
|
-
* 硬约束(写每一段前先过一遍):
|
|
8
|
-
* - macOS 系统 bash 是 3.2:禁用 declare -A / mapfile / ${var,,} 等 bash 4
|
|
9
|
-
* 特性;关联数组需求全部下沉给 awk(POSIX awk 自带),排序交给 sort。
|
|
10
|
-
* - 与 PowerShell 版不同,bash 按行解析时不存在「NUL 丢弃」问题,ls-files
|
|
11
|
-
* 可以用 -z——但 diff/回退的清单对比仍走临时文件 + awk(bash 3.2 无映射
|
|
12
|
-
* 结构),行内路径含 TAB/换行的极端情形与 Windows 版同为已知限制。
|
|
13
|
-
* - 文件名比较、哈希输入全部按字节处理(LC_ALL=C,见 POSIX_PRELUDE),
|
|
14
|
-
* 与 Node 侧 UTF-8 解码各司其职:bash 不转码,字节原样通过。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
// 单引号字面量转义:bash 单引号串里不能出现单引号,标准手法是
|
|
18
|
-
// 关闭引号 + \' 转义 + 重开('…'\''…'),杜绝变量展开与注入。
|
|
19
|
-
export function psq(value) {
|
|
20
|
-
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// POSIX 统一前导:LC_ALL=C 让 sort/awk 的路径排序按字节确定序(跨机器
|
|
24
|
-
// 一致),也避免部分环境 locale 缺失时 git/perl 打 warning。bash 本身
|
|
25
|
-
// 不转码 stdout,中文字节原样传给 Node 按 UTF-8 解码,无 Windows 侧
|
|
26
|
-
// 的代码页问题。
|
|
27
|
-
export const UTF8_PRELUDE = 'export LC_ALL=C'
|
|
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
|
-
'
|
|
87
|
-
'done
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// bash
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
export function
|
|
117
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-recall-plugin — bash 脚本模板(纯函数,无 ctx 依赖,POSIX 平台专用)
|
|
3
|
+
*
|
|
4
|
+
* 职责:Linux/macOS 下所有 shell 命令的 bash 脚本文本。与 scripts.pwsh.js
|
|
5
|
+
* 导出同名接口,由 store.js 按 process.platform 选择。
|
|
6
|
+
*
|
|
7
|
+
* 硬约束(写每一段前先过一遍):
|
|
8
|
+
* - macOS 系统 bash 是 3.2:禁用 declare -A / mapfile / ${var,,} 等 bash 4
|
|
9
|
+
* 特性;关联数组需求全部下沉给 awk(POSIX awk 自带),排序交给 sort。
|
|
10
|
+
* - 与 PowerShell 版不同,bash 按行解析时不存在「NUL 丢弃」问题,ls-files
|
|
11
|
+
* 可以用 -z——但 diff/回退的清单对比仍走临时文件 + awk(bash 3.2 无映射
|
|
12
|
+
* 结构),行内路径含 TAB/换行的极端情形与 Windows 版同为已知限制。
|
|
13
|
+
* - 文件名比较、哈希输入全部按字节处理(LC_ALL=C,见 POSIX_PRELUDE),
|
|
14
|
+
* 与 Node 侧 UTF-8 解码各司其职:bash 不转码,字节原样通过。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// 单引号字面量转义:bash 单引号串里不能出现单引号,标准手法是
|
|
18
|
+
// 关闭引号 + \' 转义 + 重开('…'\''…'),杜绝变量展开与注入。
|
|
19
|
+
export function psq(value) {
|
|
20
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// POSIX 统一前导:LC_ALL=C 让 sort/awk 的路径排序按字节确定序(跨机器
|
|
24
|
+
// 一致),也避免部分环境 locale 缺失时 git/perl 打 warning。bash 本身
|
|
25
|
+
// 不转码 stdout,中文字节原样传给 Node 按 UTF-8 解码,无 Windows 侧
|
|
26
|
+
// 的代码页问题。
|
|
27
|
+
export const UTF8_PRELUDE = 'export LC_ALL=C'
|
|
28
|
+
|
|
29
|
+
// 超大文件跳过阈值(字节),默认值与 config.js 的 maxFileBytes 一致;
|
|
30
|
+
// 实际生效值以 store.maxFileBytes(用户 config 可调)经 oversizeBlock 注入为准
|
|
31
|
+
export const MAX_FILE_BYTES = 104857600
|
|
32
|
+
|
|
33
|
+
// bash/cat 输出无 BOM;保留同名导出维持两套模板接口一致(幂等无害)
|
|
34
|
+
export function stripBom(text) {
|
|
35
|
+
return text.replace(/^\uFEFF/, '')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 嵌套 git 仓库(工作区里的子项目自带 .git)会被 add -A 记成 gitlink
|
|
39
|
+
// (160000);gitlink 残留在 index 时 add -A 会 fatal,且对文件回退毫无
|
|
40
|
+
// 意义——所以 add 前后各清一次,子仓库内容不进快照。
|
|
41
|
+
// 依赖外层脚本已定义的 $git/$g;被 snapshot/diff/rollback 三处复用。
|
|
42
|
+
function dropGitlinksBlock() {
|
|
43
|
+
return [
|
|
44
|
+
'"$git" --git-dir="$g" ls-files -z --stage | while IFS= read -r -d \'\' e; do',
|
|
45
|
+
' case "$e" in',
|
|
46
|
+
" 160000\\ *) p=${e#*$'\\t'}; \"$git\" --literal-pathspecs --git-dir=\"$g\" update-index --force-remove -- \"$p\" ;;",
|
|
47
|
+
' esac',
|
|
48
|
+
'done'
|
|
49
|
+
].join('\n')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 剔除超大文件:find -print0 + read -d '' 按字节安全遍历(文件名含换行
|
|
53
|
+
// 也不怕);2>/dev/null 容忍个别不可访问子目录(杀软锁定、异常 ACL),
|
|
54
|
+
// 漏看个别文件是 fail-open,可接受——与 pwsh 版同策略。
|
|
55
|
+
// 阈值按调用注入(store.maxFileBytes,config 可调),不读模块常量。
|
|
56
|
+
// 依赖外层已定义的 $git/$g/$root。
|
|
57
|
+
function oversizeBlock(maxBytes) {
|
|
58
|
+
return [
|
|
59
|
+
'find "$root" -type f -size +' + String(maxBytes || MAX_FILE_BYTES) + 'c -print0 2>/dev/null | while IFS= read -r -d \'\' f; do',
|
|
60
|
+
' rel=${f#"$root"/}',
|
|
61
|
+
' "$git" --literal-pathspecs --git-dir="$g" update-index --force-remove -- "$rel"',
|
|
62
|
+
'done'
|
|
63
|
+
].join('\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 用户自定义排除同步:基础排除表 + 用户 exclude.txt 合并重写 info/exclude,
|
|
67
|
+
// 再用 ls-files -i -c --exclude-from 找出「已被跟踪但命中排除」的条目清掉。
|
|
68
|
+
// 与 pwsh 版同语义:只用 --exclude-from,不引入项目 .gitignore(--exclude-standard)
|
|
69
|
+
// 的语义;放在 add -A 之前让排除先生效。read 循环里做 trim + 注释过滤,
|
|
70
|
+
// 兼容 Windows 上编辑带 CRLF 的 exclude.txt。
|
|
71
|
+
// base 基础排除表按调用注入(config.baseExcludes 可调),不硬编码。
|
|
72
|
+
// 依赖外层已定义的 $git/$g。
|
|
73
|
+
function excludeSyncBlock(excludeFile, base) {
|
|
74
|
+
const baseList = Array.isArray(base) && base.length ? base : ['.git', 'node_modules/', '.dsh-recall-snapshots/']
|
|
75
|
+
const baseLines = baseList.join('\n') + '\n'
|
|
76
|
+
return [
|
|
77
|
+
'ex_file=' + psq(excludeFile),
|
|
78
|
+
'exc="$g/info/exclude"',
|
|
79
|
+
'user_pats=""',
|
|
80
|
+
'if [ -f "$ex_file" ]; then',
|
|
81
|
+
' while IFS= read -r line || [ -n "$line" ]; do',
|
|
82
|
+
" t=${line%$'\\r'}",
|
|
83
|
+
' t="${t#"${t%%[![:space:]]*}"}"; t="${t%"${t##*[![:space:]]}"}"',
|
|
84
|
+
' [ -z "$t" ] && continue',
|
|
85
|
+
' case "$t" in \\#*) continue ;; esac',
|
|
86
|
+
' user_pats="$user_pats$t\\n"',
|
|
87
|
+
' done < "$ex_file"',
|
|
88
|
+
'fi',
|
|
89
|
+
"printf '\\n" + baseLines.replace(/\\/g, '\\\\').replace(/%/g, '%%') + "%b' \"$user_pats\" > \"$exc\"",
|
|
90
|
+
'"$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',
|
|
91
|
+
' [ -n "$p" ] && "$git" --literal-pathspecs --git-dir="$g" update-index --force-remove -- "$p"',
|
|
92
|
+
'done || true'
|
|
93
|
+
].join('\n')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 解析 git 可执行文件路径:bash 从 PATH 找(POSIX 上 git 装了就在 PATH,
|
|
97
|
+
// 没有 Windows 那种四类安装位置的散装问题)
|
|
98
|
+
export function resolveGitScript() {
|
|
99
|
+
return [
|
|
100
|
+
'p=$(command -v git 2>/dev/null || true)',
|
|
101
|
+
'[ -n "$p" ] && printf \'%s\\n\' "$p"',
|
|
102
|
+
'exit 0'
|
|
103
|
+
].join('\n')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 探测 bash 侧的 home 基底:只回显 bash env 里的 $DSH_HOME(可能为空)。
|
|
107
|
+
// 为什么不在这里回退 $HOME:DSH 的 bash 执行器会洗刷子进程的 DSH_* 变量
|
|
108
|
+
// (dsh-subprocess scrubbedParentEnv),用户导出的 DSH_HOME 在 bash 里
|
|
109
|
+
// 通常不可见——若在此回退 $HOME,Node 侧的字面量回退永远轮不到,
|
|
110
|
+
// 「DSH_HOME 指到哪、快照就存哪」会失效。优先级与 pwsh 版对齐:
|
|
111
|
+
// bash env 显式值 > Node 主进程 DSH_HOME > $HOME(os.homedir)。
|
|
112
|
+
export function probeHomeScript() {
|
|
113
|
+
return 'printf \'%s\' "${DSH_HOME:-}"'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function mkdirScript(dir) {
|
|
117
|
+
return 'mkdir -p -- ' + psq(dir)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 旧版迁移:把降级时代落在项目内的影子仓库整体搬回 home 并删源目录
|
|
121
|
+
export function migrateScript(src, dst) {
|
|
122
|
+
return [
|
|
123
|
+
'set -e',
|
|
124
|
+
'src=' + psq(src),
|
|
125
|
+
'dst=' + psq(dst),
|
|
126
|
+
'if [ -e "$src/git" ]; then mv -f "$src/git" "$dst/git"; fi',
|
|
127
|
+
'if [ -e "$src/index.json" ]; then mv -f "$src/index.json" "$dst/index.json"; fi',
|
|
128
|
+
'rm -rf -- "$src"',
|
|
129
|
+
'echo MIGRATE_OK'
|
|
130
|
+
].join('\n')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 建立影子仓库 + 排除同步 + 回读 gc.stamp(语义与 pwsh 版一致,
|
|
134
|
+
// 见 scripts.pwsh.js 同名函数注释)
|
|
135
|
+
export function ensureGitScript(store, gitExe, base) {
|
|
136
|
+
return [
|
|
137
|
+
'set -e',
|
|
138
|
+
'git=' + psq(gitExe),
|
|
139
|
+
'repo=' + psq(store.repo),
|
|
140
|
+
'g=' + psq(store.git),
|
|
141
|
+
'[ -d "$g" ] || "$git" init "$repo" >/dev/null',
|
|
142
|
+
'"$git" --git-dir="$g" config core.longpaths true',
|
|
143
|
+
'"$git" --git-dir="$g" config core.autocrlf false',
|
|
144
|
+
'"$git" --git-dir="$g" config advice.addEmbeddedRepo false',
|
|
145
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
146
|
+
'stamp="$g/gc.stamp"',
|
|
147
|
+
'if [ -f "$stamp" ]; then printf \'GIT_OK %s\\n\' "$(head -n1 "$stamp" 2>/dev/null)"; else echo GIT_OK; fi'
|
|
148
|
+
].join('\n')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 快照:add -A → write-tree → commit-tree(孤儿提交)→ tag(语义同 pwsh 版)。
|
|
152
|
+
// tag -f:事件重放/重发会产生重复 messageId,裸 tag 对已存在 tag fatal
|
|
153
|
+
// 导致整条快照失败;-f 把 tag 指到最新提交,语义为「同一条消息取最新状态」。
|
|
154
|
+
export function snapshotScript(root, store, gitExe, messageId, base) {
|
|
155
|
+
return [
|
|
156
|
+
'set -e',
|
|
157
|
+
'git=' + psq(gitExe),
|
|
158
|
+
'g=' + psq(store.git),
|
|
159
|
+
'root=' + psq(root),
|
|
160
|
+
dropGitlinksBlock(),
|
|
161
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
162
|
+
'"$git" --git-dir="$g" --work-tree="$root" add -A',
|
|
163
|
+
dropGitlinksBlock(),
|
|
164
|
+
oversizeBlock(store.maxFileBytes),
|
|
165
|
+
'tree=$("$git" --git-dir="$g" --work-tree="$root" write-tree)',
|
|
166
|
+
'commit=$("$git" --git-dir="$g" -c user.name=dsh-recall -c user.email=recall@dsh.local commit-tree "$tree" -m ' + psq('snapshot ' + messageId) + ')',
|
|
167
|
+
'"$git" --git-dir="$g" tag -f ' + psq('snap-' + messageId) + ' "$commit" >/dev/null',
|
|
168
|
+
'echo SNAP_OK'
|
|
169
|
+
].join('\n')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 当前清单/目标树清单落临时文件:两处复用(diff 与 rollback)。
|
|
173
|
+
// 关键前置链与 pwsh 版对齐——gitlink 清理 → 排除同步 → add -A → 再清
|
|
174
|
+
// gitlink → 超大剔除——少了 add -A 的话 ls-files 读到的还是上一次快照的
|
|
175
|
+
// 旧 index,「当前清单」永远等于目标 tag,diff 恒空(调试踩过的坑)。
|
|
176
|
+
// 行格式:cur 为「mode sha stage<TAB>path」(取 sha=a[2]),target 为
|
|
177
|
+
// 「mode type sha<TAB>path」(取 sha=a[3]);grep 滤掉 gitlink(160000)行,
|
|
178
|
+
// 无匹配时退出码 1,set -e 下统一 || true。
|
|
179
|
+
function collectListsBlock(store, gitExe, root, tag, base) {
|
|
180
|
+
return [
|
|
181
|
+
'git=' + psq(gitExe),
|
|
182
|
+
'g=' + psq(store.git),
|
|
183
|
+
'root=' + psq(root),
|
|
184
|
+
dropGitlinksBlock(),
|
|
185
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
186
|
+
'"$git" --git-dir="$g" --work-tree="$root" add -A',
|
|
187
|
+
dropGitlinksBlock(),
|
|
188
|
+
oversizeBlock(store.maxFileBytes),
|
|
189
|
+
'tmpc=' + psq(store.dir + '/diff-cur.$$'),
|
|
190
|
+
'tmpt=' + psq(store.dir + '/diff-tgt.$$'),
|
|
191
|
+
'"$git" -c core.quotePath=false --git-dir="$g" --work-tree="$root" ls-files --stage | grep -v \'^160000 \' > "$tmpc" || true',
|
|
192
|
+
'"$git" -c core.quotePath=false --git-dir="$g" ls-tree -r ' + psq(tag) + ' | grep -v \'^160000 \' > "$tmpt" || true'
|
|
193
|
+
].join('\n')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// diff:awk 一趟对比 cur/target(cur 侧 "mode sha<TAB>path" 取 a[2],
|
|
197
|
+
// target 侧 "mode type sha<TAB>path" 取 a[3]),输出 TSV「kind<TAB>path」
|
|
198
|
+
// 逐行打印,Node 侧解析(不在 bash 里拼 JSON——没有 jq 依赖、
|
|
199
|
+
// 转义路径的坑也一并消失)。sort -k2 按 path 确定序,与 pwsh 版对齐。
|
|
200
|
+
export function diffScript(root, store, gitExe, tag, base) {
|
|
201
|
+
return [
|
|
202
|
+
'set -e -o pipefail',
|
|
203
|
+
collectListsBlock(store, gitExe, root, tag, base),
|
|
204
|
+
"trap 'rm -f \"$tmpc\" \"$tmpt\"' EXIT",
|
|
205
|
+
'awk -F\'\\t\' -v OFS=\'\\t\' \'',
|
|
206
|
+
' FNR==1 { fidx++ }',
|
|
207
|
+
' fidx==1 { split($1, a, " "); cur[$2]=a[2]; next }',
|
|
208
|
+
' { split($1, a, " "); tgt[$2]=a[3] }',
|
|
209
|
+
' END {',
|
|
210
|
+
' for (p in cur) {',
|
|
211
|
+
' if (p in tgt) { if (tgt[p] != cur[p]) print "modified", p }',
|
|
212
|
+
' else print "added", p',
|
|
213
|
+
' }',
|
|
214
|
+
' for (p in tgt) if (!(p in cur)) print "restored", p',
|
|
215
|
+
' }',
|
|
216
|
+
"' \"$tmpc\" \"$tmpt\" | sort -t$'\\t' -k2,2",
|
|
217
|
+
'exit 0'
|
|
218
|
+
].join('\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 回退:archive | tar 直接管到工作区(无需 Windows 的 zip 中转),
|
|
222
|
+
// 空目标跳过;再删除「当前有、目标无」的文件(awk 求差集)。
|
|
223
|
+
// pipefail 保证 git archive 失败时整条非零退出。
|
|
224
|
+
export function rollbackScript(root, store, gitExe, tag, base) {
|
|
225
|
+
return [
|
|
226
|
+
'set -e -o pipefail',
|
|
227
|
+
collectListsBlock(store, gitExe, root, tag, base),
|
|
228
|
+
"trap 'rm -f \"$tmpc\" \"$tmpt\"' EXIT",
|
|
229
|
+
'restored=$(wc -l < "$tmpt" | tr -d \' \')',
|
|
230
|
+
'if [ "$restored" -gt 0 ]; then',
|
|
231
|
+
// -m(--touch):解包不恢复归档成员的 mtime(文件 mtime = 解包时刻)。
|
|
232
|
+
// 必须如此:tar 默认保留归档内 mtime,而快照→篡改→回滚常在数秒内
|
|
233
|
+
// 完成,恢复出的 mtime 可能与 index 里旧条目的 stat 记录碰撞,下一次
|
|
234
|
+
// add -A 的 stat 缓存误判「未变更」跳过 re-hash——工作区内容与快照
|
|
235
|
+
// 从此脱钩(实测解包出篡改前内容的间歇性失败)。Windows 版的
|
|
236
|
+
// Expand-Archive 天然把 mtime 设为解包时刻,无此问题;-m 让 tar 对齐。
|
|
237
|
+
' "$git" --git-dir="$g" archive ' + psq(tag) + ' | tar -x -m -C "$root"',
|
|
238
|
+
'fi',
|
|
239
|
+
'tmpd=' + psq(store.dir + '/diff-del.$$'),
|
|
240
|
+
"trap 'rm -f \"$tmpc\" \"$tmpt\" \"$tmpd\"' EXIT",
|
|
241
|
+
'awk -F\'\\t\' \'',
|
|
242
|
+
' FNR==1 { fidx++ }',
|
|
243
|
+
' fidx==1 { cur[$2]=1; next }',
|
|
244
|
+
' { tgt[$2]=1 }',
|
|
245
|
+
' END { for (p in cur) if (!(p in tgt)) print p }',
|
|
246
|
+
"' \"$tmpc\" \"$tmpt\" > \"$tmpd\"",
|
|
247
|
+
'deleted=0',
|
|
248
|
+
'while IFS= read -r p; do',
|
|
249
|
+
' [ -z "$p" ] && continue',
|
|
250
|
+
' rm -f -- "$root/$p" && deleted=$((deleted + 1))',
|
|
251
|
+
'done < "$tmpd"',
|
|
252
|
+
'echo "ROLLBACK_OK $deleted $restored"'
|
|
253
|
+
].join('\n')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function listTagsScript(store, gitExe) {
|
|
257
|
+
return [
|
|
258
|
+
'set -e',
|
|
259
|
+
'git=' + psq(gitExe),
|
|
260
|
+
'g=' + psq(store.git),
|
|
261
|
+
'"$git" --git-dir="$g" tag -l \'snap-*\''
|
|
262
|
+
].join('\n')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 定期 gc(语义同 pwsh 版);date +%s 写秒级时间戳,JS 侧 ×1000
|
|
266
|
+
export function gcScript(store, gitExe) {
|
|
267
|
+
return [
|
|
268
|
+
'set -e',
|
|
269
|
+
'git=' + psq(gitExe),
|
|
270
|
+
'g=' + psq(store.git),
|
|
271
|
+
'"$git" --git-dir="$g" gc --quiet --prune=now',
|
|
272
|
+
'date +%s > "$g/gc.stamp"',
|
|
273
|
+
'echo GC_OK'
|
|
274
|
+
].join('\n')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 删除指定快照 tag(会话已删联动清理用):tag -d 对不存在 tag 非零退出,
|
|
278
|
+
// || true 吞掉——best-effort,残留 tag 由下次清理幂等收尾(同 pwsh 版)
|
|
279
|
+
export function purgeTagsScript(store, gitExe, tags) {
|
|
280
|
+
return [
|
|
281
|
+
'git=' + psq(gitExe),
|
|
282
|
+
'g=' + psq(store.git),
|
|
283
|
+
'"$git" --git-dir="$g" tag -d ' + tags.map((t) => psq(t)).join(' ') + ' >/dev/null 2>&1 || true',
|
|
284
|
+
'echo PURGE_DONE'
|
|
285
|
+
].join('\n')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 索引读取(写入走 stdin:见 snapshots.js saveIndex 的 POSIX 分支,
|
|
289
|
+
// 不经命令行传参,天然没有 32767/128KB argv 上限问题)
|
|
290
|
+
export function indexReadCmd(dir) {
|
|
291
|
+
return 'cat ' + psq(dir + '/index.json') + ' 2>/dev/null || true'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 旧版项目内 blobs 目录清理(仅 home 存储可用时调用)
|
|
295
|
+
export function legacyRmScript(path) {
|
|
296
|
+
return 'rm -rf -- ' + psq(path)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// exclude.txt 原文读取(设置页编辑用):缺失文件 cat 报错走 2>/dev/null ||
|
|
300
|
+
// true 吞掉输出空串——与 pwsh 版同语义,按「尚未配置」处理;写入不走
|
|
301
|
+
// 模板函数,调用方直接 cat > file + stdin(同 saveIndex 的 POSIX 分支)。
|
|
302
|
+
export function excludeReadCmd(file) {
|
|
303
|
+
return 'cat ' + psq(file) + ' 2>/dev/null || true'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 目录存在探测:YES/NO 定长标记与 pwsh 版逐字同语义(容器路径本身在
|
|
307
|
+
// JS 侧解析,POSIX 不需要 homeContainerScript 的 shell 版)。
|
|
308
|
+
export function dirExistsScript(dir) {
|
|
309
|
+
return '[ -d ' + psq(dir) + ' ] && echo YES || echo NO'
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 影子仓库磁盘占用(设置页快照管理卡片用,语义同 pwsh 版)
|
|
313
|
+
export function countObjectsScript(store, gitExe) {
|
|
314
|
+
return [
|
|
315
|
+
'git=' + psq(gitExe),
|
|
316
|
+
'g=' + psq(store.git),
|
|
317
|
+
'"$git" --git-dir="$g" count-objects -v'
|
|
318
|
+
].join('\n')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 目录总大小(字节):du -sk 取 KiB 再 ×1024;macOS/BSD du 与 GNU du
|
|
322
|
+
// 对 -sk 的输出格式一致("大小<TAB>路径"),awk 取首列最稳。
|
|
323
|
+
export function diskUsageScript(dir) {
|
|
324
|
+
return 'du -sk ' + psq(dir) + ' 2>/dev/null | awk \'{print $1 * 1024}\''
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 列目录下所有一级子目录全路径:manage/list 枚举 home 容器下的所有
|
|
328
|
+
// 哈希子目录用(每个子目录是一个工作区的 store)。find -maxdepth 1
|
|
329
|
+
// 限定深度避免递归,2>/dev/null 容忍个别不可读条目。
|
|
330
|
+
export function listSubdirsScript(dir) {
|
|
331
|
+
return 'find ' + psq(dir) + ' -maxdepth 1 -mindepth 1 -type d 2>/dev/null'
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 批量 dump 全部 store 元数据(与 pwsh 版 storesDumpScript 同格式、
|
|
335
|
+
// 同语义,见其注释):一条 shell 拿全部目录的 root.txt + index.json。
|
|
336
|
+
// bash 3.2 兼容:数组 + += 均可用,glob 无匹配时字面量经 [ -d ] 过滤。
|
|
337
|
+
// root.txt 经 tr 去掉可能的 CRLF 再拼单行,防标记结构被打乱。
|
|
338
|
+
export function storesDumpScript(container, extraDirs) {
|
|
339
|
+
const lines = ['set -e', 'dirs=()']
|
|
340
|
+
if (container) {
|
|
341
|
+
lines.push('base=' + psq(container))
|
|
342
|
+
lines.push('if [ -d "$base" ]; then')
|
|
343
|
+
lines.push(' for d in "$base"/*/; do')
|
|
344
|
+
// if 而不是 [ ] && :glob 无匹配时条件为假,&& 链返回非零会触发 set -e
|
|
345
|
+
lines.push(' if [ -d "$d" ]; then dirs+=("${d%/}"); fi')
|
|
346
|
+
lines.push(' done')
|
|
347
|
+
lines.push('fi')
|
|
348
|
+
}
|
|
349
|
+
for (const d of extraDirs || []) lines.push('dirs+=(' + psq(d) + ')')
|
|
350
|
+
lines.push(
|
|
351
|
+
'for d in "${dirs[@]}"; do',
|
|
352
|
+
' [ -d "$d" ] || continue',
|
|
353
|
+
' echo "==DIR $d"',
|
|
354
|
+
' if [ -f "$d/root.txt" ]; then',
|
|
355
|
+
' printf "ROOT %s\\n" "$(cat "$d/root.txt" 2>/dev/null | tr -d \'\\r\\n\')"',
|
|
356
|
+
' else',
|
|
357
|
+
' echo "ROOT "',
|
|
358
|
+
' fi',
|
|
359
|
+
' echo INDEXBEGIN',
|
|
360
|
+
' cat "$d/index.json" 2>/dev/null',
|
|
361
|
+
' echo INDEXEND',
|
|
362
|
+
'done',
|
|
363
|
+
'exit 0'
|
|
364
|
+
)
|
|
365
|
+
return lines.join('\n')
|
|
366
|
+
}
|