@worca/app 0.1.0 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +186 -364
- package/package.json +1 -2
- package/ui/public/app.js +4159 -964
- package/ui/public/diff-view.mjs +151 -0
- package/ui/public/index.html +366 -136
- package/ui/public/style.css +1114 -143
- package/ui/server.mjs +36 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// diff-view.mjs — pure unified-diff parsing for the History Diff tab.
|
|
2
|
+
// DOM-free on purpose (pattern: log-line.mjs / log-filter.mjs) so node:test can
|
|
3
|
+
// exercise it without jsdom. Input is the persisted diff-patch.patch artifact;
|
|
4
|
+
// workspace runs concatenate per-member patches, each prefixed with a
|
|
5
|
+
// "# <projectKey>" comment line (orchestrator.mjs:3443).
|
|
6
|
+
|
|
7
|
+
export const MAX_FILE_SECTION_BYTES = 500_000;
|
|
8
|
+
|
|
9
|
+
// One cheap pass: split the patch into per-file sections. Lines outside any
|
|
10
|
+
// "diff --git" section are ignored, EXCEPT "# <key>" markers, which set the
|
|
11
|
+
// project context for the sections that follow (workspace patches).
|
|
12
|
+
export function splitPatchSections(text) {
|
|
13
|
+
const out = [];
|
|
14
|
+
const lines = String(text || '').split('\n');
|
|
15
|
+
let project = null;
|
|
16
|
+
let cur = null;
|
|
17
|
+
const flush = () => {
|
|
18
|
+
if (!cur) return;
|
|
19
|
+
cur.raw = cur.rawLines.join('\n');
|
|
20
|
+
delete cur.rawLines;
|
|
21
|
+
out.push(cur);
|
|
22
|
+
cur = null;
|
|
23
|
+
};
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
if (line.startsWith('diff --git ')) {
|
|
26
|
+
flush();
|
|
27
|
+
cur = { project, path: null, oldPath: null, header: line, rawLines: [line] };
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// A marker either introduces the first section or terminates the current one.
|
|
31
|
+
// It can never collide with hunk content: adds start '+', deletes '-',
|
|
32
|
+
// context ' ', and "\ No newline" starts '\'.
|
|
33
|
+
if (/^# \S/.test(line)) {
|
|
34
|
+
flush();
|
|
35
|
+
project = line.slice(2).trim();
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!cur) continue;
|
|
39
|
+
|
|
40
|
+
cur.rawLines.push(line);
|
|
41
|
+
if (cur.oldPath == null && line.startsWith('--- ')) {
|
|
42
|
+
const p = stripSide(line.slice(4));
|
|
43
|
+
if (p) cur.oldPath = p;
|
|
44
|
+
} else if (cur.path == null && line.startsWith('+++ ')) {
|
|
45
|
+
const p = stripSide(line.slice(4));
|
|
46
|
+
if (p) cur.path = p;
|
|
47
|
+
} else if (line.startsWith('rename from ')) {
|
|
48
|
+
cur.oldPath = line.slice('rename from '.length).trim();
|
|
49
|
+
} else if (line.startsWith('rename to ')) {
|
|
50
|
+
cur.path = line.slice('rename to '.length).trim();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
flush();
|
|
54
|
+
for (const s of out) {
|
|
55
|
+
// Deleted files carry "+++ /dev/null" and new files "--- /dev/null"; mirror
|
|
56
|
+
// the surviving side. Binary and mode-only sections have NEITHER header, so
|
|
57
|
+
// fall back to the "diff --git a/X b/X" line — without this they keep
|
|
58
|
+
// path === null, drop out of patchIndex, and always render "(no textual
|
|
59
|
+
// diff for this file)" even when results lists them.
|
|
60
|
+
if (!s.path) s.path = s.oldPath || pathFromHeader(s.header);
|
|
61
|
+
if (!s.oldPath) s.oldPath = s.path;
|
|
62
|
+
delete s.header;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// "diff --git a/src/x.js b/src/x.js" -> "src/x.js". Split at the LAST " b/" so a
|
|
68
|
+
// path that itself contains " b/" still resolves. null when the shape is unusual
|
|
69
|
+
// (e.g. a C-quoted path — see the DESCOPED note on sectionKey).
|
|
70
|
+
function pathFromHeader(header) {
|
|
71
|
+
const rest = String(header || '').slice('diff --git '.length);
|
|
72
|
+
const i = rest.lastIndexOf(' b/');
|
|
73
|
+
if (i <= 0) return null;
|
|
74
|
+
const newSide = rest.slice(i + 3).trim();
|
|
75
|
+
return newSide || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// "a/src/x.js" -> "src/x.js"; "/dev/null" and "" -> null. Git terminates names
|
|
79
|
+
// with a tab when they need it, so cut at the first tab.
|
|
80
|
+
function stripSide(s) {
|
|
81
|
+
const t = String(s || '').split('\t')[0].trim();
|
|
82
|
+
if (!t || t === '/dev/null') return null;
|
|
83
|
+
return t.replace(/^[ab]\//, '');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse ONE section's hunks. Lazy per selected file — never called for the whole
|
|
87
|
+
// patch up front.
|
|
88
|
+
export function parseFileSection(raw) {
|
|
89
|
+
let text = String(raw || '');
|
|
90
|
+
let truncated = false;
|
|
91
|
+
if (text.length > MAX_FILE_SECTION_BYTES) {
|
|
92
|
+
const cut = text.lastIndexOf('\n', MAX_FILE_SECTION_BYTES);
|
|
93
|
+
if (cut > 0) {
|
|
94
|
+
text = text.slice(0, cut); // snapped to a line boundary
|
|
95
|
+
} else {
|
|
96
|
+
// One line longer than the whole cap: there is no newline to snap to, so
|
|
97
|
+
// cut mid-line and drop a trailing LONE HIGH SURROGATE (a '\n' can never
|
|
98
|
+
// sit inside a pair, which is why the snapped path needs no such guard).
|
|
99
|
+
text = text.slice(0, MAX_FILE_SECTION_BYTES);
|
|
100
|
+
const last = text.charCodeAt(text.length - 1);
|
|
101
|
+
if (last >= 0xd800 && last <= 0xdbff) text = text.slice(0, -1);
|
|
102
|
+
}
|
|
103
|
+
truncated = true;
|
|
104
|
+
}
|
|
105
|
+
const res = { binary: false, truncated, hunks: [] };
|
|
106
|
+
let hunk = null;
|
|
107
|
+
const rows = text.split('\n');
|
|
108
|
+
// A \n-terminated section yields a trailing '' from split (and the workspace
|
|
109
|
+
// '\n\n' member join leaves extras at each seam); without the pop each becomes
|
|
110
|
+
// a phantom empty context line. A GENUINE empty context line is the row ' '
|
|
111
|
+
// (one space), never '', so popping every trailing '' is safe.
|
|
112
|
+
while (rows.length && rows[rows.length - 1] === '') rows.pop();
|
|
113
|
+
for (const line of rows) {
|
|
114
|
+
if (line.startsWith('Binary files ') || line === 'GIT binary patch') {
|
|
115
|
+
res.binary = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (line.startsWith('@@')) {
|
|
119
|
+
hunk = { header: line, lines: [] };
|
|
120
|
+
res.hunks.push(hunk);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!hunk) continue; // still in the header block
|
|
124
|
+
if (line.startsWith('+')) hunk.lines.push({ kind: 'add', text: line.slice(1) });
|
|
125
|
+
else if (line.startsWith('-')) hunk.lines.push({ kind: 'del', text: line.slice(1) });
|
|
126
|
+
else if (line.startsWith('\\')) continue; // ""
|
|
127
|
+
else hunk.lines.push({ kind: 'ctx', text: line.startsWith(' ') ? line.slice(1) : line });
|
|
128
|
+
}
|
|
129
|
+
return res;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// DESCOPED, by design: git C-quotes unusual paths (non-ASCII / control chars /
|
|
133
|
+
// '"' / '\') as `--- "a/caf\303\251.md"`; those sections keep the quoted string
|
|
134
|
+
// as their path and won't match results paths, so the pane shows "(no textual
|
|
135
|
+
// diff for this file)" — graceful. A future unquoteGitPath() can lift this.
|
|
136
|
+
// The separator is NUL, not a space: a space would make
|
|
137
|
+
// sectionKey('a-11111111', 'x y.js') collide with sectionKey(null, 'a-11111111 x y.js').
|
|
138
|
+
// Unreachable in practice (a workspace patch always emits its marker before the
|
|
139
|
+
// first section, so no section in one has project === null) — but free to fix,
|
|
140
|
+
// and no test asserts the key's literal text.
|
|
141
|
+
export function sectionKey(project, path) {
|
|
142
|
+
return project ? `${project}\u0000${path}` : String(path || '');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function patchIndex(sections) {
|
|
146
|
+
const m = new Map();
|
|
147
|
+
for (const s of Array.isArray(sections) ? sections : []) {
|
|
148
|
+
if (s && s.path) m.set(sectionKey(s.project, s.path), s);
|
|
149
|
+
}
|
|
150
|
+
return m;
|
|
151
|
+
}
|