@xcanwin/manyoyo 8.6.8 → 8.6.10

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.
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ // “改动”视图:在容器里对工作目录所在的 git 仓库跑只读命令(status / diff),按需执行、不轮询。
4
+ // 仓库内容和 git 配置都由容器内进程控制,按不可信输入处理:关掉会执行外部程序的配置
5
+ //(外部 diff、textconv、fsmonitor),不写 index(GIT_OPTIONAL_LOCKS=0),输出有上限。
6
+
7
+ const CHANGES_MAX_FILES = 500;
8
+ const DIFF_MAX_BYTES = 512 * 1024;
9
+ const GIT_TIMEOUT_SECONDS = 10;
10
+ const STATUS_MARK = '__MANYOYO_GIT_STATUS__';
11
+ const NUMSTAT_MARK = '__MANYOYO_GIT_NUMSTAT__';
12
+ const ROOT_MARK = '__MANYOYO_GIT_ROOT__';
13
+
14
+ function quote(value) {
15
+ return `'${String(value).replace(/'/g, `'"'"'`)}'`;
16
+ }
17
+
18
+ const GIT = `GIT_OPTIONAL_LOCKS=0 GIT_LITERAL_PATHSPECS=1 timeout ${GIT_TIMEOUT_SECONDS} git -c core.quotepath=off -c core.fsmonitor=false -c diff.external= -c color.ui=false`;
19
+
20
+ // 进到工作目录所在仓库的根目录;不是仓库时只输出一行 NOT_REPO
21
+ function enterRepo(workDir) {
22
+ return [
23
+ `cd -- ${quote(workDir || '/')} 2>/dev/null || { echo NOT_REPO; exit 0; }`,
24
+ `top="$(${GIT} rev-parse --show-toplevel 2>/dev/null)" || { echo NOT_REPO; exit 0; }`,
25
+ 'cd -- "$top" || { echo NOT_REPO; exit 0; }'
26
+ ].join('; ');
27
+ }
28
+
29
+ function buildGitChangesCommand(workDir) {
30
+ return [
31
+ enterRepo(workDir),
32
+ `printf '%s\\n%s\\n' ${ROOT_MARK} "$top"`,
33
+ `printf '%s\\n' ${STATUS_MARK}`,
34
+ `${GIT} status --porcelain=v1 -z --no-renames --untracked-files=all 2>/dev/null | head -c 1048576`,
35
+ `printf '\\n%s\\n' ${NUMSTAT_MARK}`,
36
+ `if ${GIT} rev-parse --verify -q HEAD >/dev/null 2>&1; then ${GIT} diff HEAD --numstat -z --no-renames --no-ext-diff --no-textconv 2>/dev/null | head -c 1048576; fi`
37
+ ].join('; ');
38
+ }
39
+
40
+ function sectionAfter(output, mark, nextMark) {
41
+ const start = output.indexOf(`${mark}\n`);
42
+ if (start < 0) return '';
43
+ const from = start + mark.length + 1;
44
+ const end = nextMark ? output.indexOf(`\n${nextMark}\n`, from) : -1;
45
+ return end < 0 ? output.slice(from) : output.slice(from, end);
46
+ }
47
+
48
+ function parseGitChangesOutput(output) {
49
+ const text = String(output || '');
50
+ if (!text.includes(ROOT_MARK)) {
51
+ return { repo: false, root: '', files: [], truncated: false };
52
+ }
53
+ const root = sectionAfter(text, ROOT_MARK, STATUS_MARK).split('\n')[0].trim();
54
+ const statusPart = sectionAfter(text, STATUS_MARK, NUMSTAT_MARK);
55
+ const numstatPart = sectionAfter(text, NUMSTAT_MARK, '');
56
+ const counts = new Map();
57
+ for (const record of numstatPart.split('\0')) {
58
+ const match = record.replace(/^\n/, '').match(/^(-|\d+)\t(-|\d+)\t(.+)$/s);
59
+ if (match) {
60
+ counts.set(match[3], {
61
+ added: match[1] === '-' ? null : Number(match[1]),
62
+ deleted: match[2] === '-' ? null : Number(match[2])
63
+ });
64
+ }
65
+ }
66
+ const files = [];
67
+ let truncated = false;
68
+ for (const record of statusPart.split('\0')) {
69
+ if (record.length < 4 || record[2] !== ' ') continue;
70
+ const code = record.slice(0, 2);
71
+ const filePath = record.slice(3);
72
+ if (!filePath || filePath.includes('\n')) continue;
73
+ if (files.length >= CHANGES_MAX_FILES) {
74
+ truncated = true;
75
+ break;
76
+ }
77
+ const count = counts.get(filePath) || { added: null, deleted: null };
78
+ files.push({ path: filePath, status: code === '??' ? 'untracked' : describeStatus(code), ...count });
79
+ }
80
+ return { repo: true, root, files, truncated };
81
+ }
82
+
83
+ function describeStatus(code) {
84
+ if (code.includes('D')) return 'deleted';
85
+ if (code.includes('A')) return 'added';
86
+ if (code.includes('U')) return 'conflicted';
87
+ return 'modified';
88
+ }
89
+
90
+ function isAcceptableDiffPath(filePath) {
91
+ return typeof filePath === 'string'
92
+ && filePath.length > 0
93
+ && filePath.length <= 4096
94
+ && !filePath.startsWith('-')
95
+ && !filePath.includes('\0')
96
+ && !filePath.includes('\n');
97
+ }
98
+
99
+ function buildGitDiffCommand(workDir, filePath) {
100
+ return [
101
+ enterRepo(workDir),
102
+ `printf '%s\\n' ${STATUS_MARK}`,
103
+ `if ${GIT} rev-parse --verify -q HEAD >/dev/null 2>&1; then base=HEAD; else base=; fi`,
104
+ `${GIT} diff $base --no-ext-diff --no-textconv --no-color --no-renames -- ${quote(filePath)} 2>/dev/null | head -c ${DIFF_MAX_BYTES + 1}`
105
+ ].join('; ');
106
+ }
107
+
108
+ function parseGitDiffOutput(output) {
109
+ const text = String(output || '');
110
+ if (!text.includes(STATUS_MARK)) {
111
+ return { repo: false, diff: '', truncated: false };
112
+ }
113
+ const body = sectionAfter(text, STATUS_MARK, '');
114
+ const truncated = Buffer.byteLength(body, 'utf8') > DIFF_MAX_BYTES;
115
+ return {
116
+ repo: true,
117
+ diff: truncated ? Buffer.from(body, 'utf8').subarray(0, DIFF_MAX_BYTES).toString('utf8') : body,
118
+ truncated
119
+ };
120
+ }
121
+
122
+ module.exports = {
123
+ CHANGES_MAX_FILES,
124
+ DIFF_MAX_BYTES,
125
+ buildGitChangesCommand,
126
+ parseGitChangesOutput,
127
+ buildGitDiffCommand,
128
+ parseGitDiffOutput,
129
+ isAcceptableDiffPath
130
+ };