@monotykamary/localterm-server 2.0.3 → 2.0.5
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/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -5
- package/dist/constants.js.map +1 -1
- package/dist/git-diff.d.ts +3 -1
- package/dist/git-diff.d.ts.map +1 -1
- package/dist/git-diff.js +245 -271
- package/dist/git-diff.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/schemas.d.ts +13 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +11 -0
- package/dist/schemas.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/reconcile-file-stats.d.ts +7 -0
- package/dist/utils/reconcile-file-stats.d.ts.map +1 -0
- package/dist/utils/reconcile-file-stats.js +45 -0
- package/dist/utils/reconcile-file-stats.js.map +1 -0
- package/package.json +1 -1
package/dist/git-diff.js
CHANGED
|
@@ -3,10 +3,10 @@ import fsPromises from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Octokit } from "@octokit/rest";
|
|
5
5
|
import { openRepository } from "es-git";
|
|
6
|
-
import { computePatchFromContents } from "./utils/compute-patch.js";
|
|
7
6
|
import { memoBy } from "./utils/memo-by.js";
|
|
7
|
+
import { computePatchFromContents } from "./utils/compute-patch.js";
|
|
8
8
|
import { resolveGithubToken } from "./utils/resolve-github-token.js";
|
|
9
|
-
import { GIT_BINARY_SNIFF_BYTES,
|
|
9
|
+
import { GIT_BINARY_SNIFF_BYTES, GIT_CACHE_TTL_MS, GIT_EMPTY_TREE_HASH, GIT_MAX_BRANCHES, GIT_MAX_PATCH_BYTES_PER_FILE, GIT_MAX_TOTAL_PATCH_BYTES, GIT_MAX_UNTRACKED_FILE_BYTES, GIT_MAX_UNTRACKED_FILES, } from "./constants.js";
|
|
10
10
|
const WORKING_OPTIONS = { mode: "working" };
|
|
11
11
|
const EMPTY_SUMMARY = {
|
|
12
12
|
isRepo: false,
|
|
@@ -16,8 +16,37 @@ const EMPTY_SUMMARY = {
|
|
|
16
16
|
binaries: 0,
|
|
17
17
|
branch: null,
|
|
18
18
|
};
|
|
19
|
-
const GIT_EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
20
19
|
const ZERO_OID = "0000000000000000000000000000000000000000";
|
|
20
|
+
// Nested so a cwd can hold more than one comparison (the working-tree summary
|
|
21
|
+
// is pushed on git-dirty while the viewer may be open in branch mode).
|
|
22
|
+
const diffCacheByCwd = new Map();
|
|
23
|
+
const comparisonKey = (mode, base) => `${mode}:${base ?? ""}`;
|
|
24
|
+
const readDiffCache = (cwd, mode, base) => {
|
|
25
|
+
const byComparison = diffCacheByCwd.get(cwd);
|
|
26
|
+
if (!byComparison)
|
|
27
|
+
return null;
|
|
28
|
+
const entry = byComparison.get(comparisonKey(mode, base));
|
|
29
|
+
if (!entry)
|
|
30
|
+
return null;
|
|
31
|
+
if (Date.now() - entry.builtAt > GIT_CACHE_TTL_MS) {
|
|
32
|
+
byComparison.delete(comparisonKey(mode, base));
|
|
33
|
+
if (byComparison.size === 0)
|
|
34
|
+
diffCacheByCwd.delete(cwd);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return entry;
|
|
38
|
+
};
|
|
39
|
+
const writeDiffCache = (cwd, mode, base, cache) => {
|
|
40
|
+
let byComparison = diffCacheByCwd.get(cwd);
|
|
41
|
+
if (!byComparison) {
|
|
42
|
+
byComparison = new Map();
|
|
43
|
+
diffCacheByCwd.set(cwd, byComparison);
|
|
44
|
+
}
|
|
45
|
+
byComparison.set(comparisonKey(mode, base), cache);
|
|
46
|
+
};
|
|
47
|
+
export const invalidateGitDiffCache = (cwd) => {
|
|
48
|
+
diffCacheByCwd.delete(cwd);
|
|
49
|
+
};
|
|
21
50
|
const collectIterator = (iterable) => {
|
|
22
51
|
const result = [];
|
|
23
52
|
for (const item of iterable)
|
|
@@ -238,10 +267,7 @@ const readWorkingTreeFile = (r, filePath, maxBytes = GIT_MAX_UNTRACKED_FILE_BYTE
|
|
|
238
267
|
return null;
|
|
239
268
|
}
|
|
240
269
|
};
|
|
241
|
-
const
|
|
242
|
-
const stats = diff.stats();
|
|
243
|
-
const totalInsertions = Number(stats.insertions);
|
|
244
|
-
const totalDeletions = Number(stats.deletions);
|
|
270
|
+
const collectDeltaInfos = (diff) => {
|
|
245
271
|
const deltas = collectIterator(diff.deltas());
|
|
246
272
|
const result = [];
|
|
247
273
|
for (const delta of deltas) {
|
|
@@ -250,97 +276,41 @@ const collectDeltaMeta = (r, diff, isWorkingTree) => {
|
|
|
250
276
|
continue;
|
|
251
277
|
const newFile = delta.newFile();
|
|
252
278
|
const oldFile = delta.oldFile();
|
|
253
|
-
const isBinary = newFile.isBinary();
|
|
254
279
|
result.push({
|
|
255
280
|
path: newFile.path(),
|
|
256
281
|
oldPath: status === "renamed" ? oldFile.path() : null,
|
|
257
282
|
status,
|
|
258
|
-
|
|
259
|
-
deletions: 0,
|
|
260
|
-
binary: isBinary,
|
|
283
|
+
binary: newFile.isBinary(),
|
|
261
284
|
oldId: oldFile.id(),
|
|
262
285
|
newId: newFile.id(),
|
|
263
|
-
isWorkingTree,
|
|
264
286
|
});
|
|
265
287
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const newContent = d.isWorkingTree
|
|
289
|
-
? readWorkingTreeFile(r, d.path, GIT_MAX_TOTAL_PATCH_BYTES)
|
|
290
|
-
: d.newId === ZERO_OID || d.newId === ""
|
|
291
|
-
? d.status === "deleted"
|
|
292
|
-
? null
|
|
293
|
-
: readBlobContent(r, d.newId)
|
|
294
|
-
: readBlobContent(r, d.newId);
|
|
295
|
-
const aPath = d.oldPath ?? d.path;
|
|
296
|
-
const patchResult = computePatchFromContents(oldContent, newContent, aPath, d.path, null, null, d.oldId === ZERO_OID ? null : d.oldId, d.newId === ZERO_OID ? null : d.newId, d.status === "renamed");
|
|
297
|
-
d.additions = patchResult.additions;
|
|
298
|
-
d.deletions = patchResult.deletions;
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
// Fallback: proportional split of aggregate totals
|
|
302
|
-
const perFileAdd = Math.floor(totalInsertions / nonBinary.length);
|
|
303
|
-
d.additions = perFileAdd;
|
|
304
|
-
d.deletions = Math.floor(totalDeletions / nonBinary.length);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// Verify totals match and redistribute remainder if needed
|
|
308
|
-
let computedAdds = 0;
|
|
309
|
-
let computedDels = 0;
|
|
310
|
-
for (const d of result) {
|
|
311
|
-
if (!d.binary) {
|
|
312
|
-
computedAdds += d.additions;
|
|
313
|
-
computedDels += d.deletions;
|
|
314
|
-
}
|
|
288
|
+
return result;
|
|
289
|
+
};
|
|
290
|
+
const buildDeltaPatch = (r, delta) => {
|
|
291
|
+
if (delta.binary)
|
|
292
|
+
return { patchText: null, additions: 0, deletions: 0 };
|
|
293
|
+
// All diff passes use diffTreeToWorkdirWithIndex, so the new side is the
|
|
294
|
+
// working tree — read it straight from disk rather than the delta's blob id
|
|
295
|
+
// (which points at the index/HEAD blob for unstaged edits and would read as
|
|
296
|
+
// unchanged vs the old side → zero counts). The old side is the base tree's
|
|
297
|
+
// blob.
|
|
298
|
+
const oldContent = delta.status === "added" ? null : readBlobContent(r, delta.oldId);
|
|
299
|
+
const newContent = delta.status === "deleted"
|
|
300
|
+
? null
|
|
301
|
+
: readWorkingTreeFile(r, delta.path, GIT_MAX_TOTAL_PATCH_BYTES);
|
|
302
|
+
const aPath = delta.oldPath ?? delta.path;
|
|
303
|
+
try {
|
|
304
|
+
const patchResult = computePatchFromContents(oldContent, newContent, aPath, delta.path, null, null, delta.oldId === ZERO_OID ? null : delta.oldId, delta.newId === ZERO_OID ? null : delta.newId, delta.status === "renamed");
|
|
305
|
+
return {
|
|
306
|
+
patchText: patchResult.patchText || null,
|
|
307
|
+
additions: patchResult.additions,
|
|
308
|
+
deletions: patchResult.deletions,
|
|
309
|
+
};
|
|
315
310
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (addDelta !== 0 || delDelta !== 0) {
|
|
319
|
-
const sorted = result.filter((d) => !d.binary);
|
|
320
|
-
let remainingAdd = addDelta;
|
|
321
|
-
let remainingDel = delDelta;
|
|
322
|
-
for (const d of sorted) {
|
|
323
|
-
if (remainingAdd > 0) {
|
|
324
|
-
d.additions += 1;
|
|
325
|
-
remainingAdd -= 1;
|
|
326
|
-
}
|
|
327
|
-
else if (remainingAdd < 0) {
|
|
328
|
-
d.additions -= 1;
|
|
329
|
-
remainingAdd += 1;
|
|
330
|
-
}
|
|
331
|
-
if (remainingDel > 0) {
|
|
332
|
-
d.deletions += 1;
|
|
333
|
-
remainingDel -= 1;
|
|
334
|
-
}
|
|
335
|
-
else if (remainingDel < 0) {
|
|
336
|
-
d.deletions -= 1;
|
|
337
|
-
remainingDel += 1;
|
|
338
|
-
}
|
|
339
|
-
if (remainingAdd === 0 && remainingDel === 0)
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
311
|
+
catch {
|
|
312
|
+
return { patchText: null, additions: 0, deletions: 0 };
|
|
342
313
|
}
|
|
343
|
-
return result;
|
|
344
314
|
};
|
|
345
315
|
const buildBaseTree = (r, baseRef) => {
|
|
346
316
|
try {
|
|
@@ -363,17 +333,22 @@ const buildBaseTree = (r, baseRef) => {
|
|
|
363
333
|
}
|
|
364
334
|
}
|
|
365
335
|
};
|
|
366
|
-
|
|
367
|
-
|
|
336
|
+
// One full diff pass for `(cwd)` against `baseRef`: walks the tree diff, runs
|
|
337
|
+
// rename detection, then builds per-file metadata + patch text in a single
|
|
338
|
+
// loop (one jsdiff per file, used for both counts and patch — the old code
|
|
339
|
+
// ran jsdiff twice per file). Untracked files are folded in from the working
|
|
340
|
+
// tree with synthesized patches. Yields to the event loop between batches so a
|
|
341
|
+
// large branch diff never blocks the WS terminal during this one pass.
|
|
342
|
+
const buildDiffCache = async (r, baseRef) => {
|
|
368
343
|
const baseTree = buildBaseTree(r, baseRef);
|
|
369
344
|
if (!baseTree)
|
|
370
|
-
return
|
|
345
|
+
return null;
|
|
371
346
|
let diff;
|
|
372
347
|
try {
|
|
373
348
|
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
374
349
|
}
|
|
375
350
|
catch {
|
|
376
|
-
return
|
|
351
|
+
return null;
|
|
377
352
|
}
|
|
378
353
|
try {
|
|
379
354
|
diff.findSimilar({ renames: true });
|
|
@@ -381,16 +356,137 @@ const computeTrackedDeltas = async (r, baseRef) => {
|
|
|
381
356
|
catch {
|
|
382
357
|
// Rename detection failed
|
|
383
358
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
359
|
+
// Prime libgit2's lazy per-delta binary flag: DiffFile.isBinary() returns
|
|
360
|
+
// false until the diff's stats have been materialized, which loads content
|
|
361
|
+
// and sniffs for NUL bytes. Without this, binary files read as text and get
|
|
362
|
+
// a junk patch synthesized from utf8-decoded blob bytes.
|
|
363
|
+
void diff.stats();
|
|
364
|
+
const deltaInfos = collectDeltaInfos(diff);
|
|
365
|
+
const untracked = await collectUntrackedFiles(r);
|
|
366
|
+
const fileMeta = [];
|
|
367
|
+
const filePatchByPath = new Map();
|
|
368
|
+
const fileBinaryByPath = new Map();
|
|
369
|
+
const filePatchOmittedByPath = new Map();
|
|
370
|
+
let totalPatchBytes = 0;
|
|
371
|
+
let additions = 0;
|
|
372
|
+
let deletions = 0;
|
|
373
|
+
let binaries = 0;
|
|
374
|
+
const YIELD_EVERY = 200;
|
|
375
|
+
for (let index = 0; index < deltaInfos.length; index += 1) {
|
|
376
|
+
if (index > 0 && index % YIELD_EVERY === 0)
|
|
377
|
+
await yieldToEventLoop();
|
|
378
|
+
const delta = deltaInfos[index];
|
|
379
|
+
let patchText = null;
|
|
380
|
+
let patchOmitted = false;
|
|
381
|
+
let fileAdditions;
|
|
382
|
+
let fileDeletions;
|
|
383
|
+
if (delta.binary) {
|
|
384
|
+
fileAdditions = 0;
|
|
385
|
+
fileDeletions = 0;
|
|
386
|
+
binaries += 1;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
const patchResult = buildDeltaPatch(r, delta);
|
|
390
|
+
fileAdditions = patchResult.additions;
|
|
391
|
+
fileDeletions = patchResult.deletions;
|
|
392
|
+
patchText = patchResult.patchText;
|
|
393
|
+
if (patchText !== null) {
|
|
394
|
+
if (patchText.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
395
|
+
totalPatchBytes + patchText.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
396
|
+
patchText = null;
|
|
397
|
+
patchOmitted = true;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
totalPatchBytes += patchText.length;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
patchOmitted = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
additions += fileAdditions;
|
|
408
|
+
deletions += fileDeletions;
|
|
409
|
+
fileMeta.push({
|
|
410
|
+
path: delta.path,
|
|
411
|
+
oldPath: delta.oldPath,
|
|
412
|
+
status: delta.status,
|
|
413
|
+
additions: fileAdditions,
|
|
414
|
+
deletions: fileDeletions,
|
|
415
|
+
binary: delta.binary,
|
|
416
|
+
});
|
|
417
|
+
filePatchByPath.set(delta.path, patchText);
|
|
418
|
+
fileBinaryByPath.set(delta.path, delta.binary);
|
|
419
|
+
filePatchOmittedByPath.set(delta.path, patchOmitted);
|
|
420
|
+
}
|
|
421
|
+
for (const file of untracked) {
|
|
422
|
+
const patch = file.binary
|
|
423
|
+
? null
|
|
424
|
+
: file.truncated || file.content === null
|
|
425
|
+
? null
|
|
426
|
+
: buildUntrackedPatch(file.content);
|
|
427
|
+
let patchOmitted = file.truncated;
|
|
428
|
+
if (patch !== null) {
|
|
429
|
+
if (patch.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
430
|
+
totalPatchBytes + patch.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
431
|
+
patchOmitted = true;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
totalPatchBytes += patch.length;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const fileAdditions = file.binary ? 0 : file.lines;
|
|
438
|
+
if (file.binary)
|
|
439
|
+
binaries += 1;
|
|
440
|
+
additions += fileAdditions;
|
|
441
|
+
fileMeta.push({
|
|
442
|
+
path: file.path,
|
|
443
|
+
oldPath: null,
|
|
444
|
+
status: "untracked",
|
|
445
|
+
additions: fileAdditions,
|
|
446
|
+
deletions: 0,
|
|
447
|
+
binary: file.binary,
|
|
448
|
+
});
|
|
449
|
+
filePatchByPath.set(file.path, patchOmitted ? null : patch);
|
|
450
|
+
fileBinaryByPath.set(file.path, file.binary);
|
|
451
|
+
filePatchOmittedByPath.set(file.path, patchOmitted);
|
|
452
|
+
}
|
|
453
|
+
const summary = {
|
|
454
|
+
isRepo: true,
|
|
455
|
+
files: fileMeta.length,
|
|
456
|
+
additions,
|
|
457
|
+
deletions,
|
|
458
|
+
binaries,
|
|
459
|
+
branch: getCurrentBranch(r),
|
|
460
|
+
};
|
|
461
|
+
return {
|
|
462
|
+
summary,
|
|
463
|
+
fileMeta,
|
|
464
|
+
filePatchByPath,
|
|
465
|
+
fileBinaryByPath,
|
|
466
|
+
filePatchOmittedByPath,
|
|
467
|
+
builtAt: Date.now(),
|
|
468
|
+
};
|
|
393
469
|
};
|
|
470
|
+
const yieldToEventLoop = () => new Promise((resolve) => {
|
|
471
|
+
setImmediate(resolve);
|
|
472
|
+
});
|
|
473
|
+
const ensureDiffCache = async (r, options) => {
|
|
474
|
+
// Read the cache before resolving the base ref — that resolution does
|
|
475
|
+
// libgit2 work (revparseSingle + getMergeBase) on every call, so for the
|
|
476
|
+
// per-file patch endpoint (where the cache is warm on nearly every
|
|
477
|
+
// request) checking first keeps it a pure map lookup.
|
|
478
|
+
const cached = readDiffCache(r.cwd, options.mode, options.base ?? null);
|
|
479
|
+
if (cached)
|
|
480
|
+
return cached;
|
|
481
|
+
const baseRef = resolveEffectiveBaseRef(r, options);
|
|
482
|
+
if (baseRef === null)
|
|
483
|
+
return null;
|
|
484
|
+
const cache = await buildDiffCache(r, baseRef);
|
|
485
|
+
if (cache)
|
|
486
|
+
writeDiffCache(r.cwd, options.mode, options.base ?? null, cache);
|
|
487
|
+
return cache;
|
|
488
|
+
};
|
|
489
|
+
export const splitPatchByFile = (raw) => raw.split(/^(?=diff --git )/m).filter((chunk) => chunk.startsWith("diff --git "));
|
|
394
490
|
export const parseNumstatZ = (raw) => {
|
|
395
491
|
const tokens = raw.split("\0");
|
|
396
492
|
const entries = [];
|
|
@@ -450,6 +546,13 @@ export const getGitDiffSummary = async (cwd, options = WORKING_OPTIONS) => {
|
|
|
450
546
|
const r = await openRepo(cwd);
|
|
451
547
|
if (!r)
|
|
452
548
|
return EMPTY_SUMMARY;
|
|
549
|
+
// Summary is pushed on every git-dirty signal (per-keystroke during edits),
|
|
550
|
+
// so it must stay cheap even when the full diff cache is cold. Read only the
|
|
551
|
+
// aggregate stats from the tree diff — no per-file jsdiff — unless a cache
|
|
552
|
+
// is already warm, in which case reuse its per-file summary.
|
|
553
|
+
const cached = readDiffCache(r.cwd, options.mode, options.base ?? null);
|
|
554
|
+
if (cached)
|
|
555
|
+
return cached.summary;
|
|
453
556
|
try {
|
|
454
557
|
const baseRef = resolveEffectiveBaseRef(r, options);
|
|
455
558
|
if (baseRef === null)
|
|
@@ -498,191 +601,60 @@ export const getGitDiffSummary = async (cwd, options = WORKING_OPTIONS) => {
|
|
|
498
601
|
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
499
602
|
}
|
|
500
603
|
};
|
|
501
|
-
const buildFilePatch = (r, delta) => {
|
|
502
|
-
if (delta.binary)
|
|
503
|
-
return null;
|
|
504
|
-
const oldContent = delta.status === "added"
|
|
505
|
-
? null
|
|
506
|
-
: delta.isWorkingTree
|
|
507
|
-
? readBlobContent(r, delta.oldId)
|
|
508
|
-
: readBlobContent(r, delta.oldId);
|
|
509
|
-
const newContent = delta.status === "deleted"
|
|
510
|
-
? null
|
|
511
|
-
: delta.isWorkingTree
|
|
512
|
-
? readWorkingTreeFile(r, delta.path, GIT_MAX_TOTAL_PATCH_BYTES)
|
|
513
|
-
: delta.newId === ZERO_OID
|
|
514
|
-
? readWorkingTreeFile(r, delta.path, GIT_MAX_TOTAL_PATCH_BYTES)
|
|
515
|
-
: readBlobContent(r, delta.newId);
|
|
516
|
-
const aPath = delta.oldPath ?? delta.path;
|
|
517
|
-
const result = computePatchFromContents(oldContent, newContent, aPath, delta.path, null, null, delta.oldId === ZERO_OID ? null : delta.oldId, delta.newId === ZERO_OID ? null : delta.newId, delta.status === "renamed");
|
|
518
|
-
return result.patchText || null;
|
|
519
|
-
};
|
|
520
604
|
export const getGitDiff = async (cwd, options = WORKING_OPTIONS) => {
|
|
521
605
|
const r = await openRepo(cwd);
|
|
522
606
|
if (!r)
|
|
523
607
|
return { isRepo: false, files: [] };
|
|
524
|
-
const
|
|
525
|
-
if (
|
|
526
|
-
return { isRepo: true, files: [] };
|
|
527
|
-
const baseTree = buildBaseTree(r, baseRef);
|
|
528
|
-
if (!baseTree)
|
|
529
|
-
return { isRepo: true, files: [] };
|
|
530
|
-
let diff;
|
|
531
|
-
try {
|
|
532
|
-
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
533
|
-
}
|
|
534
|
-
catch {
|
|
608
|
+
const cache = await ensureDiffCache(r, options);
|
|
609
|
+
if (!cache)
|
|
535
610
|
return { isRepo: true, files: [] };
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
let patch = null;
|
|
547
|
-
let patchOmitted = false;
|
|
548
|
-
if (entry.binary) {
|
|
549
|
-
patch = null;
|
|
550
|
-
}
|
|
551
|
-
else {
|
|
552
|
-
try {
|
|
553
|
-
patch = buildFilePatch(r, entry);
|
|
554
|
-
}
|
|
555
|
-
catch {
|
|
556
|
-
patchOmitted = true;
|
|
557
|
-
}
|
|
558
|
-
if (patch !== null) {
|
|
559
|
-
if (patch.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
560
|
-
totalPatchBytes + patch.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
561
|
-
patch = null;
|
|
562
|
-
patchOmitted = true;
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
totalPatchBytes += patch.length;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
else if (!patchOmitted) {
|
|
569
|
-
patchOmitted = true;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
return {
|
|
573
|
-
path: entry.path,
|
|
574
|
-
oldPath: entry.oldPath,
|
|
575
|
-
status: entry.status,
|
|
576
|
-
additions: entry.additions,
|
|
577
|
-
deletions: entry.deletions,
|
|
578
|
-
binary: entry.binary,
|
|
579
|
-
patch,
|
|
580
|
-
patchOmitted,
|
|
581
|
-
};
|
|
582
|
-
});
|
|
583
|
-
const untracked = await collectUntrackedFiles(r);
|
|
584
|
-
for (const file of untracked) {
|
|
585
|
-
const patch = file.binary
|
|
586
|
-
? null
|
|
587
|
-
: file.truncated || file.content === null
|
|
588
|
-
? null
|
|
589
|
-
: buildUntrackedPatch(file.content);
|
|
590
|
-
const entry = {
|
|
591
|
-
path: file.path,
|
|
592
|
-
oldPath: null,
|
|
593
|
-
status: "untracked",
|
|
594
|
-
additions: file.binary ? 0 : file.lines,
|
|
595
|
-
deletions: 0,
|
|
596
|
-
binary: file.binary,
|
|
597
|
-
patch,
|
|
598
|
-
patchOmitted: file.truncated,
|
|
599
|
-
};
|
|
600
|
-
if (entry.patch !== null) {
|
|
601
|
-
if (entry.patch.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
602
|
-
totalPatchBytes + entry.patch.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
603
|
-
entry.patch = null;
|
|
604
|
-
entry.patchOmitted = true;
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
totalPatchBytes += entry.patch.length;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
files.push(entry);
|
|
611
|
-
}
|
|
611
|
+
const files = cache.fileMeta.map((meta) => ({
|
|
612
|
+
path: meta.path,
|
|
613
|
+
oldPath: meta.oldPath,
|
|
614
|
+
status: meta.status,
|
|
615
|
+
additions: meta.additions,
|
|
616
|
+
deletions: meta.deletions,
|
|
617
|
+
binary: meta.binary,
|
|
618
|
+
patch: cache.filePatchByPath.get(meta.path) ?? null,
|
|
619
|
+
patchOmitted: cache.filePatchOmittedByPath.get(meta.path) ?? false,
|
|
620
|
+
}));
|
|
612
621
|
return { isRepo: true, files };
|
|
613
622
|
};
|
|
614
623
|
export const getGitDiffFiles = async (cwd, options = WORKING_OPTIONS) => {
|
|
615
624
|
const r = await openRepo(cwd);
|
|
616
625
|
if (!r)
|
|
617
626
|
return { isRepo: false, files: [] };
|
|
618
|
-
const
|
|
619
|
-
if (
|
|
627
|
+
const cache = await ensureDiffCache(r, options);
|
|
628
|
+
if (!cache)
|
|
620
629
|
return { isRepo: true, files: [] };
|
|
621
|
-
|
|
622
|
-
const files = trackedDeltas.map((entry) => ({
|
|
623
|
-
path: entry.path,
|
|
624
|
-
oldPath: entry.oldPath,
|
|
625
|
-
status: entry.status,
|
|
626
|
-
additions: entry.additions,
|
|
627
|
-
deletions: entry.deletions,
|
|
628
|
-
binary: entry.binary,
|
|
629
|
-
}));
|
|
630
|
-
const untracked = await collectUntrackedFiles(r);
|
|
631
|
-
for (const file of untracked) {
|
|
632
|
-
files.push({
|
|
633
|
-
path: file.path,
|
|
634
|
-
oldPath: null,
|
|
635
|
-
status: "untracked",
|
|
636
|
-
additions: file.binary ? 0 : file.lines,
|
|
637
|
-
deletions: 0,
|
|
638
|
-
binary: file.binary,
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
return { isRepo: true, files };
|
|
630
|
+
return { isRepo: true, files: cache.fileMeta };
|
|
642
631
|
};
|
|
643
632
|
export const getGitDiffFilePatch = async (cwd, requestedPath, options = WORKING_OPTIONS) => {
|
|
644
633
|
const empty = { patch: null, patchOmitted: false, binary: false };
|
|
645
634
|
const r = await openRepo(cwd);
|
|
646
635
|
if (!r)
|
|
647
636
|
return empty;
|
|
648
|
-
const
|
|
649
|
-
if (
|
|
650
|
-
return empty;
|
|
651
|
-
const baseTree = buildBaseTree(r, baseRef);
|
|
652
|
-
if (!baseTree)
|
|
653
|
-
return empty;
|
|
654
|
-
let diff;
|
|
655
|
-
try {
|
|
656
|
-
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
657
|
-
}
|
|
658
|
-
catch {
|
|
637
|
+
const cache = await ensureDiffCache(r, options);
|
|
638
|
+
if (!cache)
|
|
659
639
|
return empty;
|
|
640
|
+
// O(1) lookup: the full diff pass that the per-file patch needs was already
|
|
641
|
+
// computed once for this (cwd, mode, base) and cached. This was the O(N²)
|
|
642
|
+
// regression — each per-file request re-ran the whole-tree diff + a jsdiff
|
|
643
|
+
// for every file. Now it's a map lookup.
|
|
644
|
+
if (cache.filePatchByPath.has(requestedPath)) {
|
|
645
|
+
return {
|
|
646
|
+
patch: cache.filePatchByPath.get(requestedPath) ?? null,
|
|
647
|
+
patchOmitted: cache.filePatchOmittedByPath.get(requestedPath) ?? false,
|
|
648
|
+
binary: cache.fileBinaryByPath.get(requestedPath) ?? false,
|
|
649
|
+
};
|
|
660
650
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
const
|
|
668
|
-
const entry = deltaMeta.find((d) => d.path === requestedPath);
|
|
669
|
-
if (entry) {
|
|
670
|
-
if (entry.binary)
|
|
671
|
-
return { patch: null, patchOmitted: false, binary: true };
|
|
672
|
-
try {
|
|
673
|
-
const patch = buildFilePatch(r, entry);
|
|
674
|
-
if (patch === null)
|
|
675
|
-
return empty;
|
|
676
|
-
if (patch.length > GIT_MAX_PATCH_BYTES_PER_FILE) {
|
|
677
|
-
return { patch: null, patchOmitted: true, binary: false };
|
|
678
|
-
}
|
|
679
|
-
return { patch, patchOmitted: false, binary: false };
|
|
680
|
-
}
|
|
681
|
-
catch {
|
|
682
|
-
return { patch: null, patchOmitted: true, binary: false };
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
const absolutePath = path.join(cwd, requestedPath);
|
|
651
|
+
// An untracked path the cache didn't cover (created between the cache build
|
|
652
|
+
// and this request) falls back to synthesizing from the working tree.
|
|
653
|
+
return getGitDiffFilePatchFromWorkingTree(r, requestedPath);
|
|
654
|
+
};
|
|
655
|
+
const getGitDiffFilePatchFromWorkingTree = async (r, requestedPath) => {
|
|
656
|
+
const empty = { patch: null, patchOmitted: false, binary: false };
|
|
657
|
+
const absolutePath = path.join(r.cwd, requestedPath);
|
|
686
658
|
try {
|
|
687
659
|
const stat = fs.statSync(absolutePath);
|
|
688
660
|
if (!stat.isFile())
|
|
@@ -824,10 +796,6 @@ export const getGitBranchInfo = async (cwd) => {
|
|
|
824
796
|
};
|
|
825
797
|
}
|
|
826
798
|
const currentBranch = getCurrentBranch(r);
|
|
827
|
-
const pr = await Promise.race([
|
|
828
|
-
detectPr(r),
|
|
829
|
-
new Promise((resolve) => setTimeout(() => resolve(null), GIT_BRANCH_INFO_PR_TIMEOUT_MS)),
|
|
830
|
-
]);
|
|
831
799
|
const defaultBase = resolveDefaultBase(r);
|
|
832
800
|
const branchEntries = collectIterator(r.repo.branches());
|
|
833
801
|
const branchData = [];
|
|
@@ -856,7 +824,13 @@ export const getGitBranchInfo = async (cwd) => {
|
|
|
856
824
|
defaultBase: defaultBase?.ref ?? null,
|
|
857
825
|
defaultBaseSource: defaultBase?.source ?? null,
|
|
858
826
|
branches: branchData.map((b) => b.name),
|
|
859
|
-
pr,
|
|
827
|
+
pr: null,
|
|
860
828
|
};
|
|
861
829
|
};
|
|
830
|
+
export const getGitBranchPr = async (cwd) => {
|
|
831
|
+
const r = await openRepo(cwd);
|
|
832
|
+
if (!r)
|
|
833
|
+
return null;
|
|
834
|
+
return detectPr(r);
|
|
835
|
+
};
|
|
862
836
|
//# sourceMappingURL=git-diff.js.map
|