@lumy-pack/line-lore 0.0.4 → 0.0.6

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/index.cjs CHANGED
@@ -317,6 +317,51 @@ async function gitExec(args, options) {
317
317
  LineLoreErrorCode.GIT_COMMAND_FAILED
318
318
  );
319
319
  }
320
+ async function gitPipe(producerArgs, consumerArgs, options) {
321
+ const { cwd, timeout } = options ?? {};
322
+ const pipeArgs = [...producerArgs, "|", ...consumerArgs];
323
+ try {
324
+ const result = await (0, import_execa.execa)("git", producerArgs, {
325
+ cwd,
326
+ timeout,
327
+ reject: false
328
+ }).pipe("git", consumerArgs, { cwd, timeout, reject: false });
329
+ const exitCode = result.exitCode ?? 0;
330
+ if (exitCode !== 0) {
331
+ throw new LineLoreError(
332
+ LineLoreErrorCode.GIT_COMMAND_FAILED,
333
+ `git pipe failed with exit code ${exitCode}: ${result.stderr}`,
334
+ {
335
+ command: "git",
336
+ args: pipeArgs,
337
+ exitCode,
338
+ stderr: result.stderr,
339
+ cwd
340
+ }
341
+ );
342
+ }
343
+ return {
344
+ stdout: result.stdout,
345
+ stderr: result.stderr,
346
+ exitCode
347
+ };
348
+ } catch (error) {
349
+ if (error instanceof LineLoreError) throw error;
350
+ const isTimeout = error instanceof Error && "isTerminated" in error && error.timedOut === true;
351
+ if (isTimeout) {
352
+ throw new LineLoreError(
353
+ LineLoreErrorCode.GIT_TIMEOUT,
354
+ `git pipe timed out after ${timeout}ms`,
355
+ { command: "git", args: pipeArgs, timeout, cwd }
356
+ );
357
+ }
358
+ throw new LineLoreError(
359
+ LineLoreErrorCode.GIT_COMMAND_FAILED,
360
+ `git pipe failed: ${error instanceof Error ? error.message : String(error)}`,
361
+ { command: "git", args: pipeArgs, cwd }
362
+ );
363
+ }
364
+ }
320
365
  async function shellExec(command, args, options) {
321
366
  return execCommand(
322
367
  command,
@@ -338,14 +383,22 @@ var init_executor = __esm({
338
383
  // src/core/ancestry/ancestry.ts
339
384
  async function findMergeCommit(commitSha, options) {
340
385
  const ref = options?.ref ?? "HEAD";
386
+ const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
387
+ const startTime = Date.now();
341
388
  const firstParentResult = await findMergeCommitWithArgs(
342
389
  commitSha,
343
390
  ref,
344
391
  ["--first-parent"],
345
- options
392
+ { ...options, timeout: budget }
346
393
  );
347
394
  if (firstParentResult) return firstParentResult;
348
- return findMergeCommitWithArgs(commitSha, ref, [], options);
395
+ const elapsed = Date.now() - startTime;
396
+ const remaining = budget - elapsed;
397
+ if (remaining <= 0) return null;
398
+ return findMergeCommitWithArgs(commitSha, ref, [], {
399
+ ...options,
400
+ timeout: remaining
401
+ });
349
402
  }
350
403
  async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
351
404
  try {
@@ -393,15 +446,18 @@ function extractPRFromMergeMessage(subject) {
393
446
  if (squashMatch) return parseInt(squashMatch[1], 10);
394
447
  const glMatch = /!(\d+)\s*$/.exec(subject);
395
448
  if (glMatch) return parseInt(glMatch[1], 10);
449
+ const adoMatch = /Merged PR (\d+):/.exec(subject);
450
+ if (adoMatch) return parseInt(adoMatch[1], 10);
396
451
  return null;
397
452
  }
398
- var import_common_utils9;
453
+ var import_common_utils9, DEFAULT_ANCESTRY_TIMEOUT;
399
454
  var init_ancestry = __esm({
400
455
  "src/core/ancestry/ancestry.ts"() {
401
456
  "use strict";
402
457
  init_cjs_shims();
403
458
  import_common_utils9 = require("@winglet/common-utils");
404
459
  init_executor();
460
+ DEFAULT_ANCESTRY_TIMEOUT = 3e4;
405
461
  }
406
462
  });
407
463
 
@@ -437,14 +493,10 @@ async function computePatchId(commitSha, options) {
437
493
  const cached = await cache.get(commitSha);
438
494
  if (cached) return cached;
439
495
  try {
440
- const cwd = options?.cwd ?? ".";
441
- const result = await shellExec(
442
- "bash",
443
- [
444
- "-c",
445
- `git -C "${cwd}" diff "${commitSha}^..${commitSha}" | git patch-id --stable`
446
- ],
447
- { timeout: options?.timeout }
496
+ const result = await gitPipe(
497
+ ["diff", `${commitSha}^..${commitSha}`],
498
+ ["patch-id", "--stable"],
499
+ { cwd: options?.cwd, timeout: options?.timeout }
448
500
  );
449
501
  const patchId = result.stdout.trim().split(/\s+/)[0];
450
502
  if (!patchId) return null;
@@ -460,15 +512,18 @@ async function findPatchIdMatch(commitSha, options) {
460
512
  const targetPatchId = await computePatchId(commitSha, options);
461
513
  if (!targetPatchId) return null;
462
514
  try {
463
- const logResult = await gitExec(
464
- ["log", "--format=%H", `-${scanDepth}`, ref],
465
- { cwd: options?.cwd, timeout: options?.timeout }
515
+ const result = await gitPipe(
516
+ ["log", `-${scanDepth}`, "-p", ref],
517
+ ["patch-id", "--stable"],
518
+ { cwd: options?.cwd, timeout: options?.timeout ?? 6e4 }
466
519
  );
467
- const candidates = (0, import_common_utils10.filter)(logResult.stdout.trim().split("\n"), import_common_utils10.isTruthy);
468
- for (const candidateSha of candidates) {
469
- if (candidateSha === commitSha) continue;
470
- const candidatePatchId = await computePatchId(candidateSha, options);
471
- if (candidatePatchId && candidatePatchId === targetPatchId) {
520
+ const lines = (0, import_common_utils10.filter)(result.stdout.trim().split("\n"), import_common_utils10.isTruthy);
521
+ const cache = getCache(options?.repoId, options?.noCache);
522
+ for (const line of lines) {
523
+ const [patchId, candidateSha] = line.split(/\s+/);
524
+ if (!patchId || !candidateSha) continue;
525
+ await cache.set(candidateSha, patchId);
526
+ if (candidateSha !== commitSha && patchId === targetPatchId) {
472
527
  return { matchedSha: candidateSha, patchId: targetPatchId };
473
528
  }
474
529
  }
@@ -525,10 +580,40 @@ function getCache2(repoId, noCache) {
525
580
  }
526
581
  return cache;
527
582
  }
528
- async function lookupPR(commitSha, adapter, options) {
529
- const cache = getCache2(options?.repoId, options?.noCache);
583
+ function toCachedPR(pr) {
584
+ return {
585
+ number: pr.number,
586
+ title: pr.title,
587
+ author: pr.author,
588
+ url: pr.url,
589
+ mergeCommit: pr.mergeCommit,
590
+ baseBranch: pr.baseBranch,
591
+ mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0
592
+ };
593
+ }
594
+ function fromCachedPR(cached) {
595
+ let mergedAt;
596
+ if (cached.mergedAt != null) {
597
+ mergedAt = typeof cached.mergedAt === "number" ? new Date(cached.mergedAt).toISOString() : String(cached.mergedAt);
598
+ }
599
+ return {
600
+ number: cached.number,
601
+ title: cached.title,
602
+ author: cached.author,
603
+ url: cached.url,
604
+ mergeCommit: cached.mergeCommit,
605
+ baseBranch: cached.baseBranch,
606
+ mergedAt
607
+ };
608
+ }
609
+ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
610
+ const cache = getCache2(
611
+ options?.repoId,
612
+ options?.cacheOnly ? false : options?.noCache
613
+ );
530
614
  const cached = await cache.get(commitSha);
531
- if (cached) return cached;
615
+ if (cached) return fromCachedPR(cached);
616
+ if (options?.cacheOnly) return null;
532
617
  let mergeBasedPR = null;
533
618
  const mergeResult = await findMergeCommit(commitSha, options);
534
619
  if (mergeResult) {
@@ -551,39 +636,46 @@ async function lookupPR(commitSha, adapter, options) {
551
636
  };
552
637
  }
553
638
  if (!options?.deep || mergeBasedPR.mergedAt) {
554
- await cache.set(commitSha, mergeBasedPR);
639
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
555
640
  return mergeBasedPR;
556
641
  }
557
642
  }
558
643
  }
559
- const patchIdMatch = await findPatchIdMatch(commitSha, {
560
- ...options,
561
- scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
562
- });
563
- if (patchIdMatch) {
564
- const result = await lookupPR(patchIdMatch.matchedSha, adapter, options);
565
- if (result) {
566
- await cache.set(commitSha, result);
567
- return result;
568
- }
569
- }
570
644
  if (mergeBasedPR) {
571
- await cache.set(commitSha, mergeBasedPR);
645
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
572
646
  return mergeBasedPR;
573
647
  }
574
648
  if (adapter) {
575
649
  const prInfo = await adapter.getPRForCommit(commitSha);
576
650
  if (prInfo?.mergedAt) {
577
- await cache.set(commitSha, prInfo);
651
+ await cache.set(commitSha, toCachedPR(prInfo));
578
652
  return prInfo;
579
653
  }
580
654
  }
655
+ if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
656
+ const patchIdMatch = await findPatchIdMatch(commitSha, {
657
+ ...options,
658
+ scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
659
+ });
660
+ if (patchIdMatch) {
661
+ const result = await lookupPR(
662
+ patchIdMatch.matchedSha,
663
+ adapter,
664
+ options,
665
+ _recursionDepth + 1
666
+ );
667
+ if (result) {
668
+ await cache.set(commitSha, toCachedPR(result));
669
+ return result;
670
+ }
671
+ }
672
+ }
581
673
  return null;
582
674
  }
583
675
  function resetPRCache() {
584
676
  cacheRegistry2.clear();
585
677
  }
586
- var cacheRegistry2, DEEP_SCAN_DEPTH;
678
+ var cacheRegistry2, DEEP_SCAN_DEPTH, MAX_RECURSION_DEPTH;
587
679
  var init_pr_lookup = __esm({
588
680
  "src/core/pr-lookup/pr-lookup.ts"() {
589
681
  "use strict";
@@ -593,6 +685,7 @@ var init_pr_lookup = __esm({
593
685
  init_patch_id2();
594
686
  cacheRegistry2 = /* @__PURE__ */ new Map();
595
687
  DEEP_SCAN_DEPTH = 2e3;
688
+ MAX_RECURSION_DEPTH = 2;
596
689
  }
597
690
  });
598
691
 
@@ -861,6 +954,27 @@ function isVersionAtLeast(version, minVersion) {
861
954
  }
862
955
  return true;
863
956
  }
957
+ async function checkCloneStatus(options) {
958
+ let partialClone = false;
959
+ let shallow = false;
960
+ try {
961
+ const shallowResult = await gitExec(
962
+ ["rev-parse", "--is-shallow-repository"],
963
+ { cwd: options?.cwd }
964
+ );
965
+ shallow = shallowResult.stdout.trim() === "true";
966
+ } catch {
967
+ }
968
+ try {
969
+ const partialResult = await gitExec(
970
+ ["config", "--get", "extensions.partialclone"],
971
+ { cwd: options?.cwd }
972
+ );
973
+ partialClone = partialResult.stdout.trim().length > 0;
974
+ } catch {
975
+ }
976
+ return { partialClone, shallow };
977
+ }
864
978
  async function checkGitHealth(options) {
865
979
  const hints = [];
866
980
  let gitVersion = "0.0.0";
@@ -887,7 +1001,18 @@ async function checkGitHealth(options) {
887
1001
  `Upgrade git to ${BLOOM_FILTER_MIN_VERSION.join(".")}+ for bloom filter support (current: ${gitVersion}).`
888
1002
  );
889
1003
  }
890
- return { commitGraph, bloomFilter, gitVersion, hints };
1004
+ const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
1005
+ if (cloneStatus.partialClone) {
1006
+ hints.push(
1007
+ "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
1008
+ );
1009
+ }
1010
+ if (cloneStatus.shallow) {
1011
+ hints.push(
1012
+ "Shallow repository detected. Ancestry-path results may be incomplete."
1013
+ );
1014
+ }
1015
+ return { commitGraph, bloomFilter, gitVersion, hints, ...cloneStatus };
891
1016
  }
892
1017
 
893
1018
  // src/platform/index.ts
@@ -2111,7 +2236,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2111
2236
  }
2112
2237
  return { analyzed: blameResult.value, operatingLevel, warnings };
2113
2238
  }
2114
- async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId) {
2239
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2115
2240
  const nodes = [];
2116
2241
  const commitNode = {
2117
2242
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
@@ -2142,8 +2267,10 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2142
2267
  const prInfo = await lookupPR(targetSha, adapter, {
2143
2268
  ...execOptions,
2144
2269
  noCache: options.noCache,
2270
+ cacheOnly: options.cacheOnly,
2145
2271
  deep: featureFlags.deepTrace,
2146
- repoId
2272
+ repoId,
2273
+ skipPatchIdScan
2147
2274
  });
2148
2275
  if (prInfo) {
2149
2276
  nodes.push({
@@ -2160,11 +2287,19 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2160
2287
  }
2161
2288
  return nodes;
2162
2289
  }
2163
- async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId) {
2290
+ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2164
2291
  const results = await Promise.allSettled(
2165
2292
  (0, import_common_utils11.map)(
2166
2293
  analyzed,
2167
- (entry) => processEntry(entry, featureFlags, adapter, options, execOptions, repoId)
2294
+ (entry) => processEntry(
2295
+ entry,
2296
+ featureFlags,
2297
+ adapter,
2298
+ options,
2299
+ execOptions,
2300
+ repoId,
2301
+ skipPatchIdScan
2302
+ )
2168
2303
  )
2169
2304
  );
2170
2305
  return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
@@ -2195,14 +2330,36 @@ async function trace(options) {
2195
2330
  );
2196
2331
  const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
2197
2332
  const warnings = [...platform.warnings, ...blameAuth.warnings];
2333
+ if (options.cacheOnly && options.noCache) {
2334
+ warnings.push(
2335
+ "Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
2336
+ );
2337
+ }
2198
2338
  const featureFlags = computeFeatureFlags(operatingLevel, options);
2339
+ let cloneStatus = { partialClone: false, shallow: false };
2340
+ try {
2341
+ const result = await checkCloneStatus({ cwd: options.cwd });
2342
+ if (result) cloneStatus = result;
2343
+ } catch {
2344
+ }
2345
+ if (cloneStatus.partialClone) {
2346
+ warnings.push(
2347
+ "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
2348
+ );
2349
+ }
2350
+ if (cloneStatus.shallow) {
2351
+ warnings.push(
2352
+ "Shallow repository detected. Ancestry-path results may be incomplete."
2353
+ );
2354
+ }
2199
2355
  const nodes = await buildTraceNodes(
2200
2356
  blameAuth.analyzed,
2201
2357
  featureFlags,
2202
2358
  platform.adapter,
2203
2359
  options,
2204
2360
  execOptions,
2205
- repoId
2361
+ repoId,
2362
+ cloneStatus.partialClone || void 0
2206
2363
  );
2207
2364
  return { nodes, operatingLevel, featureFlags, warnings };
2208
2365
  }