@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/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, GIT_BRANCH_INFO_PR_TIMEOUT_MS, 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";
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 collectDeltaMeta = (r, diff, isWorkingTree) => {
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
- additions: 0,
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
- // Compute per-file additions/deletions from patch text.
267
- // es-git's DiffStats only gives aggregate totals; we generate
268
- // patches per-delta and count +/- lines from the structured diff.
269
- const nonBinary = result.filter((d) => !d.binary);
270
- if (nonBinary.length === 0)
271
- return result;
272
- if (nonBinary.length === 1) {
273
- nonBinary[0].additions = totalInsertions;
274
- nonBinary[0].deletions = totalDeletions;
275
- return result;
276
- }
277
- // Compute per-file stats from patch text. Each delta gets its own
278
- // patch generated from old vs new content, giving exact counts.
279
- for (const d of result) {
280
- if (d.binary)
281
- continue;
282
- try {
283
- const oldContent = d.oldId === ZERO_OID || d.oldId === ""
284
- ? d.status === "added"
285
- ? null
286
- : readBlobContent(r, d.oldId)
287
- : readBlobContent(r, d.oldId);
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
- const addDelta = totalInsertions - computedAdds;
317
- const delDelta = totalDeletions - computedDels;
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
- export const splitPatchByFile = (raw) => raw.split(/^(?=diff --git )/m).filter((chunk) => chunk.startsWith("diff --git "));
367
- const computeTrackedDeltas = async (r, baseRef) => {
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
- const deltaMeta = collectDeltaMeta(r, diff, true);
385
- return deltaMeta.map((d) => ({
386
- path: d.path,
387
- oldPath: d.oldPath,
388
- status: d.status,
389
- additions: d.additions,
390
- deletions: d.deletions,
391
- binary: d.binary,
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 baseRef = resolveEffectiveBaseRef(r, options);
525
- if (baseRef === null)
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
- try {
538
- diff.findSimilar({ renames: true });
539
- }
540
- catch {
541
- // Rename detection failed
542
- }
543
- const deltaMeta = collectDeltaMeta(r, diff, true);
544
- let totalPatchBytes = 0;
545
- const files = deltaMeta.map((entry) => {
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 baseRef = resolveEffectiveBaseRef(r, options);
619
- if (baseRef === null)
627
+ const cache = await ensureDiffCache(r, options);
628
+ if (!cache)
620
629
  return { isRepo: true, files: [] };
621
- const trackedDeltas = await computeTrackedDeltas(r, baseRef);
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 baseRef = resolveEffectiveBaseRef(r, options);
649
- if (baseRef === null)
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
- try {
662
- diff.findSimilar({ renames: true });
663
- }
664
- catch {
665
- // Rename detection failed
666
- }
667
- const deltaMeta = collectDeltaMeta(r, diff, true);
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