@lumy-pack/line-lore 0.0.6 → 0.0.8
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 +44 -20
- package/dist/cli.mjs +306 -95
- package/dist/core/ancestry/ancestry.d.ts +32 -1
- package/dist/core/ancestry/index.d.ts +1 -1
- package/dist/core/blame/blame.d.ts +2 -2
- package/dist/core/index.d.ts +1 -1
- package/dist/core/pr-lookup/index.d.ts +1 -0
- package/dist/core/pr-lookup/pr-lookup.d.ts +18 -2
- package/dist/index.cjs +292 -92
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +292 -92
- package/dist/platform/github/github-adapter.d.ts +3 -1
- package/dist/platform/gitlab/gitlab-adapter.d.ts +3 -1
- package/dist/types/blame.d.ts +2 -0
- package/dist/types/cache.d.ts +2 -0
- package/dist/types/git.d.ts +7 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform.d.ts +3 -1
- package/dist/types/trace.d.ts +3 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -367,26 +367,36 @@ var init_executor = __esm({
|
|
|
367
367
|
|
|
368
368
|
// src/core/ancestry/ancestry.ts
|
|
369
369
|
import { filter as filter4, isTruthy as isTruthy4 } from "@winglet/common-utils";
|
|
370
|
-
async function
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
370
|
+
async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
|
|
371
|
+
if (mergeResult.parentShas.length < 2) return true;
|
|
372
|
+
const firstParent = mergeResult.parentShas[0];
|
|
373
|
+
const branchParents = mergeResult.parentShas.slice(1);
|
|
374
|
+
const onMainline = await isAncestor(targetSha, firstParent, options);
|
|
375
|
+
if (onMainline === null) return false;
|
|
376
|
+
if (onMainline) return false;
|
|
377
|
+
for (const branchParent of branchParents) {
|
|
378
|
+
const onBranch = await isAncestor(targetSha, branchParent, options);
|
|
379
|
+
if (onBranch === null) return false;
|
|
380
|
+
if (onBranch) return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
async function isAncestor(commitA, commitB, options) {
|
|
385
|
+
try {
|
|
386
|
+
const result = await gitExec(
|
|
387
|
+
["merge-base", "--is-ancestor", commitA, commitB],
|
|
388
|
+
{
|
|
389
|
+
cwd: options?.cwd,
|
|
390
|
+
timeout: options?.timeout ?? 5e3,
|
|
391
|
+
allowExitCodes: [1]
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
return result.exitCode === 0;
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
388
398
|
}
|
|
389
|
-
async function
|
|
399
|
+
async function findMergeCommitsWithArgs(commitSha, ref, extraArgs, options) {
|
|
390
400
|
try {
|
|
391
401
|
const result = await gitExec(
|
|
392
402
|
[
|
|
@@ -402,10 +412,29 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
|
|
|
402
412
|
{ cwd: options?.cwd, timeout: options?.timeout }
|
|
403
413
|
);
|
|
404
414
|
const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
|
|
405
|
-
if (lines.length === 0) return
|
|
406
|
-
|
|
415
|
+
if (lines.length === 0) return [];
|
|
416
|
+
const verifiedCandidates = [];
|
|
417
|
+
const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
|
|
418
|
+
let attemptedCount = 0;
|
|
419
|
+
for (let i = 0; i < candidateCount; i++) {
|
|
420
|
+
const candidate = parseMergeLogLine(lines[i]);
|
|
421
|
+
if (!candidate) continue;
|
|
422
|
+
attemptedCount++;
|
|
423
|
+
const verified = await verifyMergeIntroducesCommit(
|
|
424
|
+
commitSha,
|
|
425
|
+
candidate,
|
|
426
|
+
options
|
|
427
|
+
);
|
|
428
|
+
if (verified) verifiedCandidates.push(candidate);
|
|
429
|
+
}
|
|
430
|
+
if (attemptedCount > 0 && verifiedCandidates.length === 0 && options?.warnings) {
|
|
431
|
+
options.warnings.push(
|
|
432
|
+
`ancestry: all ${attemptedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return verifiedCandidates;
|
|
407
436
|
} catch {
|
|
408
|
-
return
|
|
437
|
+
return [];
|
|
409
438
|
}
|
|
410
439
|
}
|
|
411
440
|
function parseMergeLogLine(line) {
|
|
@@ -425,23 +454,70 @@ function parseMergeLogLine(line) {
|
|
|
425
454
|
const subject = parts.slice(subjectStart).join(" ");
|
|
426
455
|
return { mergeCommitSha, parentShas, subject };
|
|
427
456
|
}
|
|
428
|
-
function
|
|
457
|
+
async function findMergeCommits(commitSha, options) {
|
|
458
|
+
const ref = options?.ref ?? "HEAD";
|
|
459
|
+
const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
|
|
460
|
+
const startTime = Date.now();
|
|
461
|
+
const results = [];
|
|
462
|
+
const seen = /* @__PURE__ */ new Set();
|
|
463
|
+
const pushUnique = (candidates) => {
|
|
464
|
+
for (const candidate of candidates) {
|
|
465
|
+
if (seen.has(candidate.mergeCommitSha)) continue;
|
|
466
|
+
seen.add(candidate.mergeCommitSha);
|
|
467
|
+
results.push(candidate);
|
|
468
|
+
if (results.length >= MAX_CANDIDATES) break;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const firstParent = await findMergeCommitsWithArgs(
|
|
472
|
+
commitSha,
|
|
473
|
+
ref,
|
|
474
|
+
["--first-parent"],
|
|
475
|
+
{ ...options, timeout: budget }
|
|
476
|
+
);
|
|
477
|
+
pushUnique(firstParent);
|
|
478
|
+
const elapsed = Date.now() - startTime;
|
|
479
|
+
const remaining = budget - elapsed;
|
|
480
|
+
if (remaining > 0 && results.length < MAX_CANDIDATES) {
|
|
481
|
+
const full = await findMergeCommitsWithArgs(commitSha, ref, [], {
|
|
482
|
+
...options,
|
|
483
|
+
timeout: remaining
|
|
484
|
+
});
|
|
485
|
+
pushUnique(full);
|
|
486
|
+
}
|
|
487
|
+
return results;
|
|
488
|
+
}
|
|
489
|
+
async function getCommitSubject(sha, options) {
|
|
490
|
+
try {
|
|
491
|
+
const result = await gitExec(["log", "-1", "--format=%s", sha], {
|
|
492
|
+
cwd: options?.cwd,
|
|
493
|
+
timeout: options?.timeout ?? 5e3
|
|
494
|
+
});
|
|
495
|
+
const subject = result.stdout.trim();
|
|
496
|
+
return subject || null;
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function extractPRFromMergeMessage(subject, platform) {
|
|
429
502
|
const ghMatch = /Merge pull request #(\d+)/.exec(subject);
|
|
430
503
|
if (ghMatch) return parseInt(ghMatch[1], 10);
|
|
431
504
|
const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
|
|
432
505
|
if (squashMatch) return parseInt(squashMatch[1], 10);
|
|
433
|
-
|
|
434
|
-
|
|
506
|
+
if (!platform || platform === "gitlab" || platform === "gitlab-self-hosted") {
|
|
507
|
+
const glMatch = /See merge request\s+\S*!(\d+)\s*$/.exec(subject);
|
|
508
|
+
if (glMatch) return parseInt(glMatch[1], 10);
|
|
509
|
+
}
|
|
435
510
|
const adoMatch = /Merged PR (\d+):/.exec(subject);
|
|
436
511
|
if (adoMatch) return parseInt(adoMatch[1], 10);
|
|
437
512
|
return null;
|
|
438
513
|
}
|
|
439
|
-
var DEFAULT_ANCESTRY_TIMEOUT;
|
|
514
|
+
var DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
|
|
440
515
|
var init_ancestry = __esm({
|
|
441
516
|
"src/core/ancestry/ancestry.ts"() {
|
|
442
517
|
"use strict";
|
|
443
518
|
init_executor();
|
|
444
519
|
DEFAULT_ANCESTRY_TIMEOUT = 3e4;
|
|
520
|
+
MAX_CANDIDATES = 10;
|
|
445
521
|
}
|
|
446
522
|
});
|
|
447
523
|
|
|
@@ -569,7 +645,8 @@ function toCachedPR(pr) {
|
|
|
569
645
|
url: pr.url,
|
|
570
646
|
mergeCommit: pr.mergeCommit,
|
|
571
647
|
baseBranch: pr.baseBranch,
|
|
572
|
-
mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0
|
|
648
|
+
mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
|
|
649
|
+
resolvedVia: pr.resolvedVia
|
|
573
650
|
};
|
|
574
651
|
}
|
|
575
652
|
function fromCachedPR(cached) {
|
|
@@ -584,7 +661,9 @@ function fromCachedPR(cached) {
|
|
|
584
661
|
url: cached.url,
|
|
585
662
|
mergeCommit: cached.mergeCommit,
|
|
586
663
|
baseBranch: cached.baseBranch,
|
|
587
|
-
mergedAt
|
|
664
|
+
mergedAt,
|
|
665
|
+
// Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
|
|
666
|
+
resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
|
|
588
667
|
};
|
|
589
668
|
}
|
|
590
669
|
async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
@@ -595,45 +674,84 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
|
595
674
|
const cached = await cache.get(commitSha);
|
|
596
675
|
if (cached) return fromCachedPR(cached);
|
|
597
676
|
if (options?.cacheOnly) return null;
|
|
677
|
+
const prSelectOptions = options?.preferredBase ? { preferredBase: options.preferredBase } : void 0;
|
|
678
|
+
if (adapter) {
|
|
679
|
+
const directPR = await adapter.getPRForCommit(commitSha, prSelectOptions);
|
|
680
|
+
if (directPR?.mergedAt) {
|
|
681
|
+
const result = { ...directPR, resolvedVia: "api" };
|
|
682
|
+
await cache.set(commitSha, toCachedPR(result));
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
598
686
|
let mergeBasedPR = null;
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
687
|
+
const mergeCandidates = await findMergeCommits(commitSha, options);
|
|
688
|
+
const hasAncestryMerges = mergeCandidates.length > 0;
|
|
689
|
+
for (const candidate of mergeCandidates) {
|
|
690
|
+
const prNumber = extractPRFromMergeMessage(
|
|
691
|
+
candidate.subject,
|
|
692
|
+
options?.platform
|
|
693
|
+
);
|
|
602
694
|
if (prNumber) {
|
|
603
695
|
if (adapter) {
|
|
604
|
-
const prInfo = await adapter.getPRForCommit(
|
|
696
|
+
const prInfo = await adapter.getPRForCommit(
|
|
697
|
+
candidate.mergeCommitSha,
|
|
698
|
+
prSelectOptions
|
|
699
|
+
);
|
|
605
700
|
if (prInfo?.mergedAt) {
|
|
606
|
-
mergeBasedPR = prInfo;
|
|
701
|
+
mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
|
|
607
702
|
}
|
|
608
703
|
}
|
|
609
704
|
if (!mergeBasedPR) {
|
|
610
705
|
mergeBasedPR = {
|
|
611
706
|
number: prNumber,
|
|
612
|
-
title:
|
|
707
|
+
title: candidate.subject,
|
|
613
708
|
author: "",
|
|
614
709
|
url: "",
|
|
615
|
-
mergeCommit:
|
|
616
|
-
baseBranch: ""
|
|
710
|
+
mergeCommit: candidate.mergeCommitSha,
|
|
711
|
+
baseBranch: "",
|
|
712
|
+
resolvedVia: "ancestry"
|
|
617
713
|
};
|
|
618
714
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
if (adapter) {
|
|
718
|
+
const mergeCommitPR = await adapter.getPRForCommit(
|
|
719
|
+
candidate.mergeCommitSha,
|
|
720
|
+
prSelectOptions
|
|
721
|
+
);
|
|
722
|
+
if (mergeCommitPR?.mergedAt) {
|
|
723
|
+
mergeBasedPR = { ...mergeCommitPR, resolvedVia: "ancestry" };
|
|
724
|
+
break;
|
|
622
725
|
}
|
|
623
726
|
}
|
|
624
727
|
}
|
|
625
728
|
if (mergeBasedPR) {
|
|
626
|
-
|
|
627
|
-
|
|
729
|
+
if (!options?.deep || mergeBasedPR.mergedAt) {
|
|
730
|
+
await cache.set(commitSha, toCachedPR(mergeBasedPR));
|
|
731
|
+
return mergeBasedPR;
|
|
732
|
+
}
|
|
628
733
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
734
|
+
const commitSubject = await getCommitSubject(commitSha, options);
|
|
735
|
+
if (commitSubject) {
|
|
736
|
+
const directPrNumber = extractPRFromMergeMessage(
|
|
737
|
+
commitSubject,
|
|
738
|
+
options?.platform
|
|
739
|
+
);
|
|
740
|
+
if (directPrNumber) {
|
|
741
|
+
const subjectPR = {
|
|
742
|
+
number: directPrNumber,
|
|
743
|
+
title: commitSubject,
|
|
744
|
+
author: "",
|
|
745
|
+
url: "",
|
|
746
|
+
mergeCommit: commitSha,
|
|
747
|
+
baseBranch: "",
|
|
748
|
+
resolvedVia: "message"
|
|
749
|
+
};
|
|
750
|
+
await cache.set(commitSha, toCachedPR(subjectPR));
|
|
751
|
+
return subjectPR;
|
|
634
752
|
}
|
|
635
753
|
}
|
|
636
|
-
if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
|
|
754
|
+
if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH && (!hasAncestryMerges || options?.deep)) {
|
|
637
755
|
const patchIdMatch = await findPatchIdMatch(commitSha, {
|
|
638
756
|
...options,
|
|
639
757
|
scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
|
|
@@ -651,6 +769,10 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
|
651
769
|
}
|
|
652
770
|
}
|
|
653
771
|
}
|
|
772
|
+
if (mergeBasedPR) {
|
|
773
|
+
await cache.set(commitSha, toCachedPR(mergeBasedPR));
|
|
774
|
+
return mergeBasedPR;
|
|
775
|
+
}
|
|
654
776
|
return null;
|
|
655
777
|
}
|
|
656
778
|
function resetPRCache() {
|
|
@@ -687,7 +809,7 @@ var VERSION;
|
|
|
687
809
|
var init_version = __esm({
|
|
688
810
|
"src/version.ts"() {
|
|
689
811
|
"use strict";
|
|
690
|
-
VERSION = "0.0.
|
|
812
|
+
VERSION = "0.0.8";
|
|
691
813
|
}
|
|
692
814
|
});
|
|
693
815
|
|
|
@@ -784,6 +906,7 @@ import { Command } from "commander";
|
|
|
784
906
|
|
|
785
907
|
// src/core/core.ts
|
|
786
908
|
import { createHash as createHash2 } from "crypto";
|
|
909
|
+
import { dirname, isAbsolute, relative } from "path";
|
|
787
910
|
import { map as map8 } from "@winglet/common-utils";
|
|
788
911
|
|
|
789
912
|
// src/ast/parser.ts
|
|
@@ -1062,7 +1185,7 @@ async function checkGitHealth(options) {
|
|
|
1062
1185
|
const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
|
|
1063
1186
|
if (cloneStatus.partialClone) {
|
|
1064
1187
|
hints.push(
|
|
1065
|
-
"Partial clone detected. Patch-ID scan (Strategy
|
|
1188
|
+
"Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
|
|
1066
1189
|
);
|
|
1067
1190
|
}
|
|
1068
1191
|
if (cloneStatus.shallow) {
|
|
@@ -1163,7 +1286,7 @@ var GitHubAdapter = class {
|
|
|
1163
1286
|
return { authenticated: false, hostname: this.hostname };
|
|
1164
1287
|
}
|
|
1165
1288
|
}
|
|
1166
|
-
async getPRForCommit(sha) {
|
|
1289
|
+
async getPRForCommit(sha, options) {
|
|
1167
1290
|
if (this.scheduler.isRateLimited()) return null;
|
|
1168
1291
|
try {
|
|
1169
1292
|
const result = await shellExec(
|
|
@@ -1180,18 +1303,20 @@ var GitHubAdapter = class {
|
|
|
1180
1303
|
);
|
|
1181
1304
|
const prs = JSON.parse(result.stdout);
|
|
1182
1305
|
if (!isArray(prs) || prs.length === 0) return null;
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1306
|
+
let data = prs[0];
|
|
1307
|
+
if (options?.preferredBase) {
|
|
1308
|
+
const preferred = prs.find(
|
|
1309
|
+
(pr) => pr.base === options.preferredBase
|
|
1310
|
+
);
|
|
1311
|
+
if (preferred) data = preferred;
|
|
1312
|
+
}
|
|
1188
1313
|
return {
|
|
1189
1314
|
number: data.number,
|
|
1190
1315
|
title: data.title ?? "",
|
|
1191
1316
|
author: data.user ?? "",
|
|
1192
1317
|
url: data.html_url ?? "",
|
|
1193
1318
|
mergeCommit: data.merge_commit_sha ?? sha,
|
|
1194
|
-
baseBranch: data.base ??
|
|
1319
|
+
baseBranch: data.base ?? "",
|
|
1195
1320
|
mergedAt: data.merged_at
|
|
1196
1321
|
};
|
|
1197
1322
|
} catch {
|
|
@@ -1368,7 +1493,7 @@ var GitLabAdapter = class {
|
|
|
1368
1493
|
return { authenticated: false, hostname: this.hostname };
|
|
1369
1494
|
}
|
|
1370
1495
|
}
|
|
1371
|
-
async getPRForCommit(sha) {
|
|
1496
|
+
async getPRForCommit(sha, options) {
|
|
1372
1497
|
if (this.scheduler.isRateLimited()) return null;
|
|
1373
1498
|
try {
|
|
1374
1499
|
const result = await shellExec(
|
|
@@ -1392,18 +1517,20 @@ var GitLabAdapter = class {
|
|
|
1392
1517
|
return aTime - bTime;
|
|
1393
1518
|
});
|
|
1394
1519
|
if (mergedMRs.length === 0) return null;
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1520
|
+
let mr = mergedMRs[0];
|
|
1521
|
+
if (options?.preferredBase) {
|
|
1522
|
+
const preferred = mergedMRs.find(
|
|
1523
|
+
(m) => m.target_branch === options.preferredBase
|
|
1524
|
+
);
|
|
1525
|
+
if (preferred) mr = preferred;
|
|
1526
|
+
}
|
|
1400
1527
|
return {
|
|
1401
1528
|
number: mr.iid,
|
|
1402
1529
|
title: mr.title ?? "",
|
|
1403
1530
|
author: mr.author?.username ?? "",
|
|
1404
1531
|
url: mr.web_url ?? "",
|
|
1405
1532
|
mergeCommit: mr.merge_commit_sha ?? sha,
|
|
1406
|
-
baseBranch: mr.target_branch ??
|
|
1533
|
+
baseBranch: mr.target_branch ?? "",
|
|
1407
1534
|
mergedAt: mr.merged_at
|
|
1408
1535
|
};
|
|
1409
1536
|
} catch {
|
|
@@ -1995,6 +2122,7 @@ function parsePorcelainOutput(output) {
|
|
|
1995
2122
|
}
|
|
1996
2123
|
let commitHash = headerMatch[1];
|
|
1997
2124
|
const originalLine = parseInt(headerMatch[2], 10);
|
|
2125
|
+
const finalLine = parseInt(headerMatch[3], 10) || 0;
|
|
1998
2126
|
const isBoundary = commitHash.startsWith("^");
|
|
1999
2127
|
if (isBoundary) {
|
|
2000
2128
|
commitHash = commitHash.slice(1).padStart(40, "0");
|
|
@@ -2038,6 +2166,7 @@ function parsePorcelainOutput(output) {
|
|
|
2038
2166
|
authorEmail: cleanEmail,
|
|
2039
2167
|
date,
|
|
2040
2168
|
lineContent,
|
|
2169
|
+
finalLine,
|
|
2041
2170
|
originalFile,
|
|
2042
2171
|
originalLine: originalFile ? originalLine : void 0
|
|
2043
2172
|
});
|
|
@@ -2048,10 +2177,8 @@ function parsePorcelainOutput(output) {
|
|
|
2048
2177
|
// src/core/blame/blame.ts
|
|
2049
2178
|
async function executeBlame(file, lineRange, options) {
|
|
2050
2179
|
const lineSpec = `${lineRange.start},${lineRange.end}`;
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
options
|
|
2054
|
-
);
|
|
2180
|
+
const args = options?.mode === "change" ? ["blame", "-w", "--porcelain", "-L", lineSpec, file] : ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file];
|
|
2181
|
+
const result = await gitExec(args, options);
|
|
2055
2182
|
return parsePorcelainOutput(result.stdout);
|
|
2056
2183
|
}
|
|
2057
2184
|
async function analyzeBlameResults(results, filePath, options) {
|
|
@@ -2180,6 +2307,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
|
|
|
2180
2307
|
|
|
2181
2308
|
// src/core/core.ts
|
|
2182
2309
|
init_pr_lookup2();
|
|
2310
|
+
function resolvedViaToTrackingMethod(resolvedVia) {
|
|
2311
|
+
switch (resolvedVia) {
|
|
2312
|
+
case "api":
|
|
2313
|
+
return "api";
|
|
2314
|
+
case "ancestry":
|
|
2315
|
+
return "ancestry-path";
|
|
2316
|
+
case "message":
|
|
2317
|
+
return "message-parse";
|
|
2318
|
+
case "patch-id":
|
|
2319
|
+
return "patch-id";
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function resolvedViaToConfidence(resolvedVia) {
|
|
2323
|
+
switch (resolvedVia) {
|
|
2324
|
+
case "api":
|
|
2325
|
+
case "ancestry":
|
|
2326
|
+
return "exact";
|
|
2327
|
+
case "message":
|
|
2328
|
+
case "patch-id":
|
|
2329
|
+
return "heuristic";
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2183
2332
|
function computeFeatureFlags(operatingLevel, options) {
|
|
2184
2333
|
return {
|
|
2185
2334
|
astDiff: isAstAvailable() && !options.noAst,
|
|
@@ -2197,6 +2346,22 @@ async function resolveRepoIdentity(cwd) {
|
|
|
2197
2346
|
return { host: "_local", owner: "_", repo: "_unknown" };
|
|
2198
2347
|
}
|
|
2199
2348
|
}
|
|
2349
|
+
async function resolveFileContext(file, cwd) {
|
|
2350
|
+
if (cwd || !isAbsolute(file)) return { file, cwd };
|
|
2351
|
+
const fileDir = dirname(file);
|
|
2352
|
+
try {
|
|
2353
|
+
const result = await gitExec(["rev-parse", "--show-toplevel"], {
|
|
2354
|
+
cwd: fileDir
|
|
2355
|
+
});
|
|
2356
|
+
const repoRoot = result.stdout.trim();
|
|
2357
|
+
return {
|
|
2358
|
+
file: relative(repoRoot, file),
|
|
2359
|
+
cwd: repoRoot
|
|
2360
|
+
};
|
|
2361
|
+
} catch {
|
|
2362
|
+
return { file, cwd };
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2200
2365
|
async function detectPlatform2(options) {
|
|
2201
2366
|
const warnings = [];
|
|
2202
2367
|
let adapter = null;
|
|
@@ -2220,9 +2385,10 @@ async function runBlameAndAuth(adapter, options, execOptions) {
|
|
|
2220
2385
|
const lineRange = parseLineRange(
|
|
2221
2386
|
options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
|
|
2222
2387
|
);
|
|
2223
|
-
const blameChain = executeBlame(options.file, lineRange,
|
|
2224
|
-
|
|
2225
|
-
|
|
2388
|
+
const blameChain = executeBlame(options.file, lineRange, {
|
|
2389
|
+
...execOptions,
|
|
2390
|
+
mode: options.mode
|
|
2391
|
+
}).then((results) => analyzeBlameResults(results, options.file, execOptions));
|
|
2226
2392
|
const [authResult, blameResult] = await Promise.allSettled([
|
|
2227
2393
|
adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
|
|
2228
2394
|
blameChain
|
|
@@ -2243,12 +2409,24 @@ async function runBlameAndAuth(adapter, options, execOptions) {
|
|
|
2243
2409
|
}
|
|
2244
2410
|
return { analyzed: blameResult.value, operatingLevel, warnings };
|
|
2245
2411
|
}
|
|
2246
|
-
|
|
2412
|
+
function resolveTraceMode(mode) {
|
|
2413
|
+
return mode ?? "origin";
|
|
2414
|
+
}
|
|
2415
|
+
function deduplicatedLookupPR(sha, adapter, options, inflight) {
|
|
2416
|
+
const existing = inflight.get(sha);
|
|
2417
|
+
if (existing) return existing;
|
|
2418
|
+
const promise = lookupPR(sha, adapter, options);
|
|
2419
|
+
inflight.set(sha, promise);
|
|
2420
|
+
promise.finally(() => inflight.delete(sha));
|
|
2421
|
+
return promise;
|
|
2422
|
+
}
|
|
2423
|
+
async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, inflightPR, skipPatchIdScan, preferredBase) {
|
|
2247
2424
|
const nodes = [];
|
|
2425
|
+
const traceMode = resolveTraceMode(options.mode);
|
|
2248
2426
|
const commitNode = {
|
|
2249
2427
|
type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
|
|
2250
2428
|
sha: entry.blame.commitHash,
|
|
2251
|
-
trackingMethod: "blame-CMw",
|
|
2429
|
+
trackingMethod: traceMode === "change" ? "blame" : "blame-CMw",
|
|
2252
2430
|
confidence: "exact",
|
|
2253
2431
|
note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
|
|
2254
2432
|
};
|
|
@@ -2270,21 +2448,24 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
|
|
|
2270
2448
|
}
|
|
2271
2449
|
}
|
|
2272
2450
|
const targetSha = nodes[nodes.length - 1].sha;
|
|
2451
|
+
const prLookupOptions = {
|
|
2452
|
+
...execOptions,
|
|
2453
|
+
noCache: options.noCache,
|
|
2454
|
+
cacheOnly: options.cacheOnly,
|
|
2455
|
+
deep: featureFlags.deepTrace,
|
|
2456
|
+
repoId,
|
|
2457
|
+
skipPatchIdScan,
|
|
2458
|
+
preferredBase,
|
|
2459
|
+
platform: adapter?.platform
|
|
2460
|
+
};
|
|
2273
2461
|
if (targetSha) {
|
|
2274
|
-
const prInfo = await
|
|
2275
|
-
...execOptions,
|
|
2276
|
-
noCache: options.noCache,
|
|
2277
|
-
cacheOnly: options.cacheOnly,
|
|
2278
|
-
deep: featureFlags.deepTrace,
|
|
2279
|
-
repoId,
|
|
2280
|
-
skipPatchIdScan
|
|
2281
|
-
});
|
|
2462
|
+
const prInfo = await deduplicatedLookupPR(targetSha, adapter, prLookupOptions, inflightPR);
|
|
2282
2463
|
if (prInfo) {
|
|
2283
2464
|
nodes.push({
|
|
2284
2465
|
type: "pull_request",
|
|
2285
2466
|
sha: prInfo.mergeCommit,
|
|
2286
|
-
trackingMethod: prInfo.
|
|
2287
|
-
confidence: prInfo.
|
|
2467
|
+
trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
|
|
2468
|
+
confidence: resolvedViaToConfidence(prInfo.resolvedVia),
|
|
2288
2469
|
prNumber: prInfo.number,
|
|
2289
2470
|
prUrl: prInfo.url || void 0,
|
|
2290
2471
|
prTitle: prInfo.title || void 0,
|
|
@@ -2294,7 +2475,8 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
|
|
|
2294
2475
|
}
|
|
2295
2476
|
return nodes;
|
|
2296
2477
|
}
|
|
2297
|
-
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
|
|
2478
|
+
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
|
|
2479
|
+
const inflightPR = /* @__PURE__ */ new Map();
|
|
2298
2480
|
const results = await Promise.allSettled(
|
|
2299
2481
|
map8(
|
|
2300
2482
|
analyzed,
|
|
@@ -2305,7 +2487,9 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
|
|
|
2305
2487
|
options,
|
|
2306
2488
|
execOptions,
|
|
2307
2489
|
repoId,
|
|
2308
|
-
|
|
2490
|
+
inflightPR,
|
|
2491
|
+
skipPatchIdScan,
|
|
2492
|
+
preferredBase
|
|
2309
2493
|
)
|
|
2310
2494
|
)
|
|
2311
2495
|
);
|
|
@@ -2313,13 +2497,16 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
|
|
|
2313
2497
|
}
|
|
2314
2498
|
var legacyCacheCleaned = false;
|
|
2315
2499
|
async function trace(options) {
|
|
2316
|
-
const
|
|
2500
|
+
const mode = resolveTraceMode(options.mode);
|
|
2501
|
+
const { file, cwd } = await resolveFileContext(options.file, options.cwd);
|
|
2502
|
+
const warnings = [];
|
|
2503
|
+
const execOptions = { cwd, warnings };
|
|
2317
2504
|
if (!legacyCacheCleaned) {
|
|
2318
2505
|
legacyCacheCleaned = true;
|
|
2319
2506
|
cleanupLegacyCache().catch(() => {
|
|
2320
2507
|
});
|
|
2321
2508
|
}
|
|
2322
|
-
const platform = await detectPlatform2(options);
|
|
2509
|
+
const platform = await detectPlatform2({ ...options, cwd });
|
|
2323
2510
|
let repoId;
|
|
2324
2511
|
if (platform.remote) {
|
|
2325
2512
|
repoId = {
|
|
@@ -2328,15 +2515,15 @@ async function trace(options) {
|
|
|
2328
2515
|
repo: platform.remote.repo
|
|
2329
2516
|
};
|
|
2330
2517
|
} else {
|
|
2331
|
-
repoId = await resolveRepoIdentity(
|
|
2518
|
+
repoId = await resolveRepoIdentity(cwd);
|
|
2332
2519
|
}
|
|
2333
2520
|
const blameAuth = await runBlameAndAuth(
|
|
2334
2521
|
platform.adapter,
|
|
2335
|
-
options,
|
|
2522
|
+
{ ...options, mode, file, cwd },
|
|
2336
2523
|
execOptions
|
|
2337
2524
|
);
|
|
2338
2525
|
const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
|
|
2339
|
-
|
|
2526
|
+
warnings.push(...platform.warnings, ...blameAuth.warnings);
|
|
2340
2527
|
if (options.cacheOnly && options.noCache) {
|
|
2341
2528
|
warnings.push(
|
|
2342
2529
|
"Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
|
|
@@ -2345,13 +2532,13 @@ async function trace(options) {
|
|
|
2345
2532
|
const featureFlags = computeFeatureFlags(operatingLevel, options);
|
|
2346
2533
|
let cloneStatus = { partialClone: false, shallow: false };
|
|
2347
2534
|
try {
|
|
2348
|
-
const result = await checkCloneStatus({ cwd
|
|
2535
|
+
const result = await checkCloneStatus({ cwd });
|
|
2349
2536
|
if (result) cloneStatus = result;
|
|
2350
2537
|
} catch {
|
|
2351
2538
|
}
|
|
2352
2539
|
if (cloneStatus.partialClone) {
|
|
2353
2540
|
warnings.push(
|
|
2354
|
-
"Partial clone detected. Patch-ID scan (Strategy
|
|
2541
|
+
"Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
|
|
2355
2542
|
);
|
|
2356
2543
|
}
|
|
2357
2544
|
if (cloneStatus.shallow) {
|
|
@@ -2359,14 +2546,27 @@ async function trace(options) {
|
|
|
2359
2546
|
"Shallow repository detected. Ancestry-path results may be incomplete."
|
|
2360
2547
|
);
|
|
2361
2548
|
}
|
|
2549
|
+
let preferredBase;
|
|
2550
|
+
try {
|
|
2551
|
+
const branchResult = await gitExec(
|
|
2552
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
2553
|
+
execOptions
|
|
2554
|
+
);
|
|
2555
|
+
const branch = branchResult.stdout.trim();
|
|
2556
|
+
if (branch && branch !== "HEAD") {
|
|
2557
|
+
preferredBase = branch;
|
|
2558
|
+
}
|
|
2559
|
+
} catch {
|
|
2560
|
+
}
|
|
2362
2561
|
const nodes = await buildTraceNodes(
|
|
2363
2562
|
blameAuth.analyzed,
|
|
2364
2563
|
featureFlags,
|
|
2365
2564
|
platform.adapter,
|
|
2366
|
-
options,
|
|
2565
|
+
{ ...options, mode, file, cwd },
|
|
2367
2566
|
execOptions,
|
|
2368
2567
|
repoId,
|
|
2369
|
-
cloneStatus.partialClone || void 0
|
|
2568
|
+
cloneStatus.partialClone || void 0,
|
|
2569
|
+
preferredBase
|
|
2370
2570
|
);
|
|
2371
2571
|
return { nodes, operatingLevel, featureFlags, warnings };
|
|
2372
2572
|
}
|
|
@@ -2573,7 +2773,11 @@ function formatNodeHuman(node) {
|
|
|
2573
2773
|
init_normalizer();
|
|
2574
2774
|
init_errors();
|
|
2575
2775
|
function registerTraceCommand(program2) {
|
|
2576
|
-
program2.command("trace <file>").description("Trace a file line to its originating PR").requiredOption("-L, --line <range>", 'Line number or range (e.g., "42" or "10,50")').option("--deep", "Enable deep trace for squash PRs").option("--no-ast", "Disable AST diff analysis").option("--no-cache", "Disable cache").option("--cache-only", "Return cached results only (no API calls)").option("--json", "Output in JSON format").option("-q, --quiet", "Output PR number only").option("--output <format>", "Output format: human, json, llm", "human").option("--no-color", "Disable colored output").action(async (file, opts) => {
|
|
2776
|
+
program2.command("trace <file>").description("Trace a file line to its originating or last-change PR").requiredOption("-L, --line <range>", 'Line number or range (e.g., "42" or "10,50")').option("--mode <mode>", "Trace mode: change (default) or origin (includes copy/move source)", "change").option("--deep", "Enable deep trace for squash PRs").option("--no-ast", "Disable AST diff analysis").option("--no-cache", "Disable cache").option("--cache-only", "Return cached results only (no API calls)").option("--json", "Output in JSON format").option("-q, --quiet", "Output PR number only").option("--output <format>", "Output format: human, json, llm", "human").option("--no-color", "Disable colored output").action(async (file, opts) => {
|
|
2777
|
+
const mode = opts.mode;
|
|
2778
|
+
if (mode !== "origin" && mode !== "change") {
|
|
2779
|
+
throw new Error(`Invalid trace mode: ${String(mode)}`);
|
|
2780
|
+
}
|
|
2577
2781
|
const lineStr = opts.line;
|
|
2578
2782
|
const parts = lineStr.split(",");
|
|
2579
2783
|
const line = parseInt(parts[0], 10);
|
|
@@ -2582,6 +2786,7 @@ function registerTraceCommand(program2) {
|
|
|
2582
2786
|
file,
|
|
2583
2787
|
line,
|
|
2584
2788
|
endLine,
|
|
2789
|
+
mode,
|
|
2585
2790
|
deep: opts.deep,
|
|
2586
2791
|
noAst: opts.ast === false,
|
|
2587
2792
|
noCache: opts.cache === false,
|
|
@@ -2622,7 +2827,7 @@ init_errors();
|
|
|
2622
2827
|
// src/utils/command-registry.ts
|
|
2623
2828
|
var TRACE_COMMAND = {
|
|
2624
2829
|
name: "trace",
|
|
2625
|
-
description: "Trace a file line to its originating PR",
|
|
2830
|
+
description: "Trace a file line to its originating or last-change PR",
|
|
2626
2831
|
usage: "line-lore trace <file> [options]",
|
|
2627
2832
|
arguments: [
|
|
2628
2833
|
{
|
|
@@ -2637,6 +2842,12 @@ var TRACE_COMMAND = {
|
|
|
2637
2842
|
description: 'Line number or range (e.g., "42" or "10,50")',
|
|
2638
2843
|
type: "string"
|
|
2639
2844
|
},
|
|
2845
|
+
{
|
|
2846
|
+
flag: "--mode <mode>",
|
|
2847
|
+
description: "Trace mode: origin or change",
|
|
2848
|
+
type: "string",
|
|
2849
|
+
default: "origin"
|
|
2850
|
+
},
|
|
2640
2851
|
{
|
|
2641
2852
|
flag: "--deep",
|
|
2642
2853
|
description: "Enable deep trace for squash PRs",
|