seendiff 0.0.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/README.md +124 -0
- package/bin/seendiff.js +9 -0
- package/package.json +38 -0
- package/src/cli.js +216 -0
- package/src/git.js +615 -0
- package/src/highlight.js +220 -0
- package/src/server.js +580 -0
- package/src/store.js +128 -0
- package/src/theme-base.css +482 -0
- package/src/theme.js +14 -0
- package/src/walkthrough.js +338 -0
- package/static/fonts/JetBrainsMono.woff2 +0 -0
- package/static/fonts/LICENCE-UbuntuSansMono.txt +96 -0
- package/static/fonts/LICENSE-JetBrainsMono.txt +93 -0
- package/static/fonts/README.md +29 -0
- package/static/fonts/UbuntuSansMono.woff2 +0 -0
- package/static/index.html +3175 -0
package/src/git.js
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export const MERGE_GAP = 3;
|
|
7
|
+
|
|
8
|
+
export const SECTION_MIN = 12;
|
|
9
|
+
export const SECTION_MAX = 48;
|
|
10
|
+
export const BASE_CANDIDATES = ["origin/master", "origin/main", "master", "main"];
|
|
11
|
+
export const WHOLE_FILE_ID = "*";
|
|
12
|
+
|
|
13
|
+
export class GitError extends Error { }
|
|
14
|
+
|
|
15
|
+
function universalNewlines(s) {
|
|
16
|
+
return s.replace(/\r\n|\r/g, "\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function git(repo, ...args) {
|
|
20
|
+
const proc = spawnSync(
|
|
21
|
+
"git",
|
|
22
|
+
["-C", repo, "-c", "core.quotePath=false", ...args],
|
|
23
|
+
{ encoding: "utf8", maxBuffer: 1024 * 1024 * 512 }
|
|
24
|
+
);
|
|
25
|
+
if (proc.error) throw new GitError(`git ${args.join(" ")} failed: ${proc.error.message}`);
|
|
26
|
+
if (proc.status !== 0) {
|
|
27
|
+
throw new GitError(`git ${args.join(" ")} failed: ${(proc.stderr || "").trim()}`);
|
|
28
|
+
}
|
|
29
|
+
return universalNewlines(proc.stdout);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function gitQuiet(repo, ...args) {
|
|
33
|
+
return spawnSync("git", ["-C", repo, ...args], { encoding: "utf8" });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function repoRoot(cwd = ".") {
|
|
37
|
+
try {
|
|
38
|
+
return git(cwd, "rev-parse", "--show-toplevel").trim();
|
|
39
|
+
} catch {
|
|
40
|
+
throw new GitError(`not inside a git repository: ${cwd}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveBase(repo, base = null) {
|
|
45
|
+
const candidates = base ? [base] : BASE_CANDIDATES;
|
|
46
|
+
for (const cand of candidates) {
|
|
47
|
+
const proc = gitQuiet(repo, "rev-parse", "--verify", "--quiet", `${cand}^{commit}`);
|
|
48
|
+
if (proc.status === 0) return cand;
|
|
49
|
+
}
|
|
50
|
+
throw new GitError("no base ref found; tried: " + candidates.join(", "));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function mergeBase(repo, baseRef) {
|
|
54
|
+
return git(repo, "merge-base", baseRef, "HEAD").trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function revSha(repo, ref = "HEAD") {
|
|
58
|
+
return git(repo, "rev-parse", ref).trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function commitDate(repo, sha) {
|
|
62
|
+
return git(repo, "show", "-s", "--format=%cI", sha).trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function branchName(repo) {
|
|
66
|
+
const proc = gitQuiet(repo, "symbolic-ref", "--short", "-q", "HEAD");
|
|
67
|
+
return proc.status === 0 ? proc.stdout.trim() : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function dirtyAndConflicted(repo) {
|
|
71
|
+
const out = git(repo, "status", "--porcelain", "--untracked-files=no");
|
|
72
|
+
const dirty = [];
|
|
73
|
+
const conflicted = [];
|
|
74
|
+
for (const line of out.split("\n")) {
|
|
75
|
+
if (line.length < 4) continue;
|
|
76
|
+
const xy = line.slice(0, 2);
|
|
77
|
+
let p = line.slice(3);
|
|
78
|
+
if (p.includes(" -> ")) p = p.split(" -> ", 2)[1];
|
|
79
|
+
if (xy.includes("U") || xy === "AA" || xy === "DD") conflicted.push(p);
|
|
80
|
+
else dirty.push(p);
|
|
81
|
+
}
|
|
82
|
+
return [dirty, conflicted];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function fetch(repo) {
|
|
86
|
+
git(repo, "fetch", "--quiet");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function showFile(repo, sha, filePath) {
|
|
90
|
+
return git(repo, "show", `${sha}:${filePath}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function newLo(run) {
|
|
94
|
+
return run.tgtStart;
|
|
95
|
+
}
|
|
96
|
+
function newHi(run) {
|
|
97
|
+
return run.tgtLen ? run.tgtStart + run.tgtLen - 1 : run.tgtStart;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function blockNewStart(b) {
|
|
101
|
+
return Math.max(1, newLo(b.runs[0]));
|
|
102
|
+
}
|
|
103
|
+
function blockNewEnd(b) {
|
|
104
|
+
return Math.max(1, newHi(b.runs[b.runs.length - 1]));
|
|
105
|
+
}
|
|
106
|
+
function blockAdded(b) {
|
|
107
|
+
let n = 0;
|
|
108
|
+
for (const r of b.runs) for (const [t] of r.lines) if (t === "+") n++;
|
|
109
|
+
return n;
|
|
110
|
+
}
|
|
111
|
+
function blockRemoved(b) {
|
|
112
|
+
let n = 0;
|
|
113
|
+
for (const r of b.runs) for (const [t] of r.lines) if (t === "-") n++;
|
|
114
|
+
return n;
|
|
115
|
+
}
|
|
116
|
+
function blockKind(b) {
|
|
117
|
+
const added = blockAdded(b);
|
|
118
|
+
const removed = blockRemoved(b);
|
|
119
|
+
if (added && removed) return "mixed";
|
|
120
|
+
return added ? "add" : "del";
|
|
121
|
+
}
|
|
122
|
+
function blockBody(b) {
|
|
123
|
+
return b.runs.map((r) => r.lines.map(([t, text]) => t + text).join("")).join("");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function decorateBlock(b) {
|
|
127
|
+
return {
|
|
128
|
+
...b,
|
|
129
|
+
newStart: b.runs.length ? blockNewStart(b) : 1,
|
|
130
|
+
newEnd: b.runs.length ? blockNewEnd(b) : 1,
|
|
131
|
+
added: blockAdded(b),
|
|
132
|
+
removed: blockRemoved(b),
|
|
133
|
+
kind: b.runs.length ? blockKind(b) : "meta",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function fileStatus(pf) {
|
|
138
|
+
if (pf.isBinary) return "binary";
|
|
139
|
+
if (pf.isAdded) return "added";
|
|
140
|
+
if (pf.isRemoved) return "deleted";
|
|
141
|
+
if (pf.isRename) return "renamed";
|
|
142
|
+
return "modified";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function stripAB(p) {
|
|
146
|
+
if (p.startsWith("a/") || p.startsWith("b/")) return p.slice(2);
|
|
147
|
+
return p;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const DEF_RE =
|
|
151
|
+
/^(@\w|#\s*(define|include|pragma)\b|(export|default|public|private|protected|internal|static|final|abstract|pub|async|inline|extern)\s+)*(def|class|fn|func|function|impl|struct|enum|trait|interface|namespace|module|mod|package|type|template|const|let|var)\b/;
|
|
152
|
+
|
|
153
|
+
function seamScore(lines, i) {
|
|
154
|
+
const [prevType, prevText] = lines[i - 1];
|
|
155
|
+
const [curType, curText] = lines[i];
|
|
156
|
+
let score = 0;
|
|
157
|
+
if (curType !== prevType) score += 4;
|
|
158
|
+
if (!prevText.trim()) score += 2;
|
|
159
|
+
const body = curText.replace(/[\r\n]+$/, "");
|
|
160
|
+
const stripped = body.replace(/^\s+/, "");
|
|
161
|
+
if (stripped) {
|
|
162
|
+
if (body.length - stripped.length === 0) score += 2;
|
|
163
|
+
if (DEF_RE.test(stripped)) score += 3;
|
|
164
|
+
}
|
|
165
|
+
return score;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cutPoints(lines, lo, hi) {
|
|
169
|
+
const cuts = [];
|
|
170
|
+
let start = 0;
|
|
171
|
+
const n = lines.length;
|
|
172
|
+
while (n - start > hi) {
|
|
173
|
+
const lower = start + lo;
|
|
174
|
+
let upper = Math.min(start + hi, n - lo);
|
|
175
|
+
if (upper < lower) upper = Math.min(start + hi, n - 1);
|
|
176
|
+
if (upper < lower) break;
|
|
177
|
+
let best = lower;
|
|
178
|
+
let bestScore = seamScore(lines, lower);
|
|
179
|
+
for (let i = lower + 1; i <= upper; i++) {
|
|
180
|
+
const s = seamScore(lines, i);
|
|
181
|
+
if (s >= bestScore) {
|
|
182
|
+
bestScore = s;
|
|
183
|
+
best = i;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
cuts.push(best);
|
|
187
|
+
start = best;
|
|
188
|
+
}
|
|
189
|
+
return cuts;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function splitRun(run, lo, hi) {
|
|
193
|
+
const cuts = cutPoints(run.lines, lo, hi);
|
|
194
|
+
if (!cuts.length) return [run];
|
|
195
|
+
const subs = [];
|
|
196
|
+
const bounds = [0, ...cuts, run.lines.length];
|
|
197
|
+
let dels = 0;
|
|
198
|
+
let adds = 0;
|
|
199
|
+
for (let i = 0; i < bounds.length - 1; i++) {
|
|
200
|
+
const loI = bounds[i];
|
|
201
|
+
const hiI = bounds[i + 1];
|
|
202
|
+
const seg = run.lines.slice(loI, hiI);
|
|
203
|
+
const nd = seg.filter(([t]) => t === "-").length;
|
|
204
|
+
const na = seg.length - nd;
|
|
205
|
+
subs.push({
|
|
206
|
+
srcStart: run.srcStart + dels,
|
|
207
|
+
srcLen: nd,
|
|
208
|
+
tgtStart: run.tgtStart + adds,
|
|
209
|
+
tgtLen: na,
|
|
210
|
+
lines: seg,
|
|
211
|
+
});
|
|
212
|
+
dels += nd;
|
|
213
|
+
adds += na;
|
|
214
|
+
}
|
|
215
|
+
return subs;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function changedCount(runs) {
|
|
219
|
+
return runs.reduce((n, r) => n + r.lines.length, 0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function splitBlocks(blocks, lo = SECTION_MIN, hi = SECTION_MAX) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const b of blocks) {
|
|
225
|
+
if (changedCount(b.runs) <= hi) {
|
|
226
|
+
out.push(b);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
let group = [];
|
|
230
|
+
for (const run of b.runs) {
|
|
231
|
+
if (run.lines.length > hi) {
|
|
232
|
+
if (group.length) {
|
|
233
|
+
out.push({ runs: group, hunkId: "" });
|
|
234
|
+
group = [];
|
|
235
|
+
}
|
|
236
|
+
for (const sub of splitRun(run, lo, hi)) out.push({ runs: [sub], hunkId: "" });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (group.length && changedCount(group) + run.lines.length > hi) {
|
|
240
|
+
out.push({ runs: group, hunkId: "" });
|
|
241
|
+
group = [];
|
|
242
|
+
}
|
|
243
|
+
group.push(run);
|
|
244
|
+
}
|
|
245
|
+
if (group.length) out.push({ runs: group, hunkId: "" });
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function groupBlocks(hunks) {
|
|
251
|
+
const blocks = [];
|
|
252
|
+
for (const run of hunks) {
|
|
253
|
+
if (blocks.length) {
|
|
254
|
+
const last = blocks[blocks.length - 1];
|
|
255
|
+
const lastRun = last.runs[last.runs.length - 1];
|
|
256
|
+
if (newLo(run) - newHi(lastRun) - 1 <= MERGE_GAP) {
|
|
257
|
+
last.runs.push(run);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
blocks.push({ runs: [run], hunkId: "" });
|
|
262
|
+
}
|
|
263
|
+
return blocks;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function assignIds(blocks) {
|
|
267
|
+
const seen = new Map();
|
|
268
|
+
for (const b of blocks) {
|
|
269
|
+
const h = createHash("sha256").update(blockBody(b), "utf8").digest("hex").slice(0, 16);
|
|
270
|
+
const n = (seen.get(h) || 0) + 1;
|
|
271
|
+
seen.set(h, n);
|
|
272
|
+
b.hunkId = n === 1 ? h : `${h}-${n}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const SUBPROJECT = "Subproject commit ";
|
|
277
|
+
|
|
278
|
+
const RE_DIFF_GIT = /^diff --git (?:"?a\/(.+?)"?) (?:"?b\/(.+?)"?)$/;
|
|
279
|
+
const RE_HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
280
|
+
|
|
281
|
+
function parseUnifiedDiff(text) {
|
|
282
|
+
const lines = text.split("\n");
|
|
283
|
+
const files = [];
|
|
284
|
+
let i = 0;
|
|
285
|
+
let cur = null;
|
|
286
|
+
|
|
287
|
+
function push() {
|
|
288
|
+
if (cur) files.push(cur);
|
|
289
|
+
cur = null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
while (i < lines.length) {
|
|
293
|
+
const line = lines[i];
|
|
294
|
+
const m = RE_DIFF_GIT.exec(line);
|
|
295
|
+
if (m) {
|
|
296
|
+
push();
|
|
297
|
+
cur = {
|
|
298
|
+
sourceFile: m[1],
|
|
299
|
+
targetFile: m[2],
|
|
300
|
+
path: m[2],
|
|
301
|
+
isBinary: false,
|
|
302
|
+
isAdded: false,
|
|
303
|
+
isRemoved: false,
|
|
304
|
+
isRename: false,
|
|
305
|
+
added: 0,
|
|
306
|
+
removed: 0,
|
|
307
|
+
hunks: [],
|
|
308
|
+
};
|
|
309
|
+
i++;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (!cur) {
|
|
313
|
+
i++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (line.startsWith("new file mode")) {
|
|
317
|
+
cur.isAdded = true;
|
|
318
|
+
i++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (line.startsWith("deleted file mode")) {
|
|
322
|
+
cur.isRemoved = true;
|
|
323
|
+
i++;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (line.startsWith("rename from ")) {
|
|
327
|
+
cur.isRename = true;
|
|
328
|
+
cur.sourceFile = line.slice("rename from ".length);
|
|
329
|
+
i++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (line.startsWith("rename to ")) {
|
|
333
|
+
cur.isRename = true;
|
|
334
|
+
cur.targetFile = line.slice("rename to ".length);
|
|
335
|
+
cur.path = cur.targetFile;
|
|
336
|
+
i++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (line.startsWith("copy from ")) {
|
|
340
|
+
cur.isRename = true;
|
|
341
|
+
cur.sourceFile = line.slice("copy from ".length);
|
|
342
|
+
i++;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (line.startsWith("copy to ")) {
|
|
346
|
+
cur.isRename = true;
|
|
347
|
+
cur.targetFile = line.slice("copy to ".length);
|
|
348
|
+
cur.path = cur.targetFile;
|
|
349
|
+
i++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (line.startsWith("Binary files") || line.startsWith("GIT binary patch")) {
|
|
353
|
+
cur.isBinary = true;
|
|
354
|
+
i++;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (line.startsWith("--- ") || line.startsWith("+++ ")) {
|
|
358
|
+
i++;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const hm = RE_HUNK.exec(line);
|
|
362
|
+
if (hm) {
|
|
363
|
+
const srcStart = parseInt(hm[1], 10);
|
|
364
|
+
const srcLen = hm[2] !== undefined ? parseInt(hm[2], 10) : 1;
|
|
365
|
+
const tgtStart = parseInt(hm[3], 10);
|
|
366
|
+
const tgtLen = hm[4] !== undefined ? parseInt(hm[4], 10) : 1;
|
|
367
|
+
i++;
|
|
368
|
+
const hunkLines = [];
|
|
369
|
+
while (i < lines.length) {
|
|
370
|
+
const hl = lines[i];
|
|
371
|
+
if (hl.startsWith("diff --git") || RE_HUNK.test(hl)) break;
|
|
372
|
+
if (hl.startsWith("\")) {
|
|
373
|
+
i++;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (hl.startsWith("+")) {
|
|
377
|
+
hunkLines.push(["+", hl.slice(1) + "\n"]);
|
|
378
|
+
cur.added++;
|
|
379
|
+
} else if (hl.startsWith("-")) {
|
|
380
|
+
hunkLines.push(["-", hl.slice(1) + "\n"]);
|
|
381
|
+
cur.removed++;
|
|
382
|
+
} else if (hl === "") {
|
|
383
|
+
i++;
|
|
384
|
+
continue;
|
|
385
|
+
} else {
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
i++;
|
|
389
|
+
}
|
|
390
|
+
cur.hunks.push({ srcStart, srcLen, tgtStart, tgtLen, lines: hunkLines });
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
i++;
|
|
394
|
+
}
|
|
395
|
+
push();
|
|
396
|
+
return files;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function identityDiff(repo, mbSha, rev = "HEAD", sectionMin = SECTION_MIN, sectionMax = SECTION_MAX) {
|
|
400
|
+
const out = git(
|
|
401
|
+
repo,
|
|
402
|
+
"diff",
|
|
403
|
+
"--no-ext-diff",
|
|
404
|
+
"--no-color",
|
|
405
|
+
"--unified=0",
|
|
406
|
+
"--find-renames",
|
|
407
|
+
mbSha,
|
|
408
|
+
rev,
|
|
409
|
+
"--"
|
|
410
|
+
);
|
|
411
|
+
const files = [];
|
|
412
|
+
for (const pf of parseUnifiedDiff(out)) {
|
|
413
|
+
const status = fileStatus(pf);
|
|
414
|
+
const runs = pf.hunks.map((h) => ({
|
|
415
|
+
srcStart: h.srcStart,
|
|
416
|
+
srcLen: h.srcLen,
|
|
417
|
+
tgtStart: h.tgtStart,
|
|
418
|
+
tgtLen: h.tgtLen,
|
|
419
|
+
lines: h.lines,
|
|
420
|
+
}));
|
|
421
|
+
let blocks = splitBlocks(groupBlocks(runs), sectionMin, sectionMax);
|
|
422
|
+
assignIds(blocks);
|
|
423
|
+
const fd = {
|
|
424
|
+
runs,
|
|
425
|
+
path: stripAB(pf.path),
|
|
426
|
+
oldPath: pf.isRename ? stripAB(pf.sourceFile) : null,
|
|
427
|
+
status,
|
|
428
|
+
added: pf.added || 0,
|
|
429
|
+
removed: pf.removed || 0,
|
|
430
|
+
blocks,
|
|
431
|
+
sub: null,
|
|
432
|
+
};
|
|
433
|
+
if (!fd.blocks.length) {
|
|
434
|
+
if (fd.status === "modified") {
|
|
435
|
+
fd.status = pf.isRename ? "renamed" : "meta";
|
|
436
|
+
}
|
|
437
|
+
fd.blocks = [{ runs: [], hunkId: WHOLE_FILE_ID }];
|
|
438
|
+
}
|
|
439
|
+
const changed = fd.blocks.flatMap((b) => b.runs.flatMap((r) => r.lines.map(([, text]) => text)));
|
|
440
|
+
if (changed.length && changed.every((t) => t.startsWith(SUBPROJECT))) {
|
|
441
|
+
fd.status = "submodule";
|
|
442
|
+
}
|
|
443
|
+
files.push(fd);
|
|
444
|
+
}
|
|
445
|
+
return files;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function pointerShas(fd) {
|
|
449
|
+
let oldSha = null;
|
|
450
|
+
let newSha = null;
|
|
451
|
+
for (const b of fd.blocks) {
|
|
452
|
+
for (const r of b.runs) {
|
|
453
|
+
for (const [t, text] of r.lines) {
|
|
454
|
+
const sha = text.slice(SUBPROJECT.length).trim().replace(/-dirty$/, "");
|
|
455
|
+
if (t === "-") oldSha = sha;
|
|
456
|
+
else newSha = sha;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return [oldSha, newSha];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function expandSubmodules(repo, files, sectionMin = SECTION_MIN, sectionMax = SECTION_MAX) {
|
|
464
|
+
const out = [];
|
|
465
|
+
for (const fd of files) {
|
|
466
|
+
out.push(fd);
|
|
467
|
+
if (fd.status !== "submodule") continue;
|
|
468
|
+
const [oldSha, newSha] = pointerShas(fd);
|
|
469
|
+
const subRepo = path.join(repo, fd.path);
|
|
470
|
+
if (!oldSha || !newSha) continue;
|
|
471
|
+
try {
|
|
472
|
+
if (!statSync(subRepo).isDirectory()) continue;
|
|
473
|
+
} catch {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
let inner;
|
|
477
|
+
try {
|
|
478
|
+
inner = identityDiff(subRepo, oldSha, newSha, sectionMin, sectionMax);
|
|
479
|
+
} catch (e) {
|
|
480
|
+
if (e instanceof GitError) continue;
|
|
481
|
+
throw e;
|
|
482
|
+
}
|
|
483
|
+
for (const ifd of inner) {
|
|
484
|
+
ifd.sub = { repo: subRepo, old: oldSha, new: newSha, rel: ifd.path, oldRel: ifd.oldPath };
|
|
485
|
+
ifd.path = `${fd.path}/${ifd.path}`;
|
|
486
|
+
if (ifd.oldPath) ifd.oldPath = `${fd.path}/${ifd.oldPath}`;
|
|
487
|
+
out.push(ifd);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export const WORKTREE_MAX_BYTES = 5_000_000;
|
|
494
|
+
|
|
495
|
+
export function worktreeRows(repo, filePath) {
|
|
496
|
+
const full = path.join(repo, filePath);
|
|
497
|
+
let raw;
|
|
498
|
+
try {
|
|
499
|
+
raw = readFileSync(full);
|
|
500
|
+
} catch (e) {
|
|
501
|
+
throw new GitError(`cannot read ${filePath}: ${e.message}`);
|
|
502
|
+
}
|
|
503
|
+
if (raw.length > WORKTREE_MAX_BYTES) {
|
|
504
|
+
throw new GitError(`${filePath} too large (${Math.floor(raw.length / 1024)} kB)`);
|
|
505
|
+
}
|
|
506
|
+
if (raw.subarray(0, 8192).includes(0)) {
|
|
507
|
+
throw new GitError(`${filePath} is binary`);
|
|
508
|
+
}
|
|
509
|
+
const lines = splitText(raw.toString("utf8"));
|
|
510
|
+
return lines.map((line, i) => ({ old: i + 1, new: i + 1, type: "context", text: line, hunkId: null }));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function splitText(text) {
|
|
514
|
+
const lines = text.split("\n");
|
|
515
|
+
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
516
|
+
return lines;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function rowsFromRuns(runs, newText, filePath) {
|
|
520
|
+
const newLines = splitText(newText);
|
|
521
|
+
const rows = [];
|
|
522
|
+
let oldI = 1;
|
|
523
|
+
let newI = 1;
|
|
524
|
+
for (const r of runs) {
|
|
525
|
+
const ctxEnd = r.tgtLen ? r.tgtStart - 1 : r.tgtStart;
|
|
526
|
+
if (ctxEnd > newLines.length || ctxEnd < newI - 1) {
|
|
527
|
+
throw new GitError(
|
|
528
|
+
`${filePath}: hunk at new line ${r.tgtStart} does not fit the file (${newLines.length} lines) — it changed under us`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
while (newI <= ctxEnd) {
|
|
532
|
+
rows.push({ old: oldI, new: newI, type: "context", text: newLines[newI - 1], hunkId: null });
|
|
533
|
+
oldI++;
|
|
534
|
+
newI++;
|
|
535
|
+
}
|
|
536
|
+
const dels = r.lines.filter(([k]) => k === "-").map(([, t]) => t);
|
|
537
|
+
const adds = r.lines.filter(([k]) => k === "+").map(([, t]) => t);
|
|
538
|
+
dels.forEach((t, i) => rows.push({ old: r.srcStart + i, new: null, type: "del", text: t.replace(/\n$/, ""), hunkId: null }));
|
|
539
|
+
adds.forEach((t, i) => rows.push({ old: null, new: r.tgtStart + i, type: "add", text: t.replace(/\n$/, ""), hunkId: null }));
|
|
540
|
+
if (r.srcLen) oldI = r.srcStart + r.srcLen;
|
|
541
|
+
if (r.tgtLen) newI = r.tgtStart + r.tgtLen;
|
|
542
|
+
}
|
|
543
|
+
while (newI <= newLines.length) {
|
|
544
|
+
rows.push({ old: oldI, new: newI, type: "context", text: newLines[newI - 1], hunkId: null });
|
|
545
|
+
oldI++;
|
|
546
|
+
newI++;
|
|
547
|
+
}
|
|
548
|
+
return rows;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function tagRows(rows, blocks) {
|
|
552
|
+
const byNew = new Map();
|
|
553
|
+
const byOld = new Map();
|
|
554
|
+
for (const b of blocks) {
|
|
555
|
+
for (const r of b.runs) {
|
|
556
|
+
for (let n = r.tgtStart; n < r.tgtStart + r.tgtLen; n++) byNew.set(n, b.hunkId);
|
|
557
|
+
for (let n = r.srcStart; n < r.srcStart + r.srcLen; n++) byOld.set(n, b.hunkId);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
for (const row of rows) {
|
|
561
|
+
if (row.type === "add") row.hunkId = byNew.get(row.new) ?? null;
|
|
562
|
+
else if (row.type === "del") row.hunkId = byOld.get(row.old) ?? null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function verifyTagging(rows, blocks, filePath) {
|
|
567
|
+
const untagged = rows.filter((r) => r.type !== "context" && r.hunkId == null);
|
|
568
|
+
if (untagged.length) {
|
|
569
|
+
const r = untagged[0];
|
|
570
|
+
throw new Error(
|
|
571
|
+
`${filePath}: ${untagged.length} changed row(s) not covered by any block, first at old=${r.old} new=${r.new} '${r.type}'`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const taggedIds = new Set(rows.filter((r) => r.hunkId).map((r) => r.hunkId));
|
|
575
|
+
const empty = blocks.filter((b) => b.runs.length && !taggedIds.has(b.hunkId)).map((b) => b.hunkId);
|
|
576
|
+
if (empty.length) {
|
|
577
|
+
throw new Error(`${filePath}: block(s) ${empty} matched no rows`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function displayRows(repo, mbSha, fd) {
|
|
582
|
+
const gitRepo = fd.sub ? fd.sub.repo : repo;
|
|
583
|
+
const base = fd.sub ? fd.sub.old : mbSha;
|
|
584
|
+
const rev = fd.sub ? fd.sub.new : "HEAD";
|
|
585
|
+
const rel = fd.sub ? fd.sub.rel : fd.path;
|
|
586
|
+
|
|
587
|
+
let rows;
|
|
588
|
+
if (fd.status === "submodule") {
|
|
589
|
+
rows = rowsFromRuns(fd.runs, "", fd.path);
|
|
590
|
+
} else if (fd.status === "deleted") {
|
|
591
|
+
const lines = splitText(showFile(gitRepo, base, rel));
|
|
592
|
+
rows = lines.map((line, i) => ({ old: i + 1, new: null, type: "del", text: line, hunkId: null }));
|
|
593
|
+
} else if (fd.status !== "binary" && fd.status !== "meta" && fd.runs.length) {
|
|
594
|
+
rows = rowsFromRuns(fd.runs, showFile(gitRepo, rev, rel), fd.path);
|
|
595
|
+
} else {
|
|
596
|
+
let text;
|
|
597
|
+
try {
|
|
598
|
+
text = showFile(gitRepo, rev, rel);
|
|
599
|
+
} catch (e) {
|
|
600
|
+
if (e instanceof GitError) return [];
|
|
601
|
+
throw e;
|
|
602
|
+
}
|
|
603
|
+
return splitText(text).map((line, i) => ({ old: i + 1, new: i + 1, type: "context", text: line, hunkId: null }));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
tagRows(rows, fd.blocks);
|
|
607
|
+
try {
|
|
608
|
+
verifyTagging(rows, fd.blocks, fd.path);
|
|
609
|
+
} catch (e) {
|
|
610
|
+
process.stderr.write(
|
|
611
|
+
`seendiff: tagging invariant broken, showing ${fd.path} without block marks — please report: ${e.message}\n`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return rows;
|
|
615
|
+
}
|