@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/index.mjs
CHANGED
|
@@ -368,26 +368,36 @@ var init_executor = __esm({
|
|
|
368
368
|
|
|
369
369
|
// src/core/ancestry/ancestry.ts
|
|
370
370
|
import { filter as filter4, isTruthy as isTruthy4 } from "@winglet/common-utils";
|
|
371
|
-
async function
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
371
|
+
async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
|
|
372
|
+
if (mergeResult.parentShas.length < 2) return true;
|
|
373
|
+
const firstParent = mergeResult.parentShas[0];
|
|
374
|
+
const branchParents = mergeResult.parentShas.slice(1);
|
|
375
|
+
const onMainline = await isAncestor(targetSha, firstParent, options);
|
|
376
|
+
if (onMainline === null) return false;
|
|
377
|
+
if (onMainline) return false;
|
|
378
|
+
for (const branchParent of branchParents) {
|
|
379
|
+
const onBranch = await isAncestor(targetSha, branchParent, options);
|
|
380
|
+
if (onBranch === null) return false;
|
|
381
|
+
if (onBranch) return true;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
async function isAncestor(commitA, commitB, options) {
|
|
386
|
+
try {
|
|
387
|
+
const result = await gitExec(
|
|
388
|
+
["merge-base", "--is-ancestor", commitA, commitB],
|
|
389
|
+
{
|
|
390
|
+
cwd: options?.cwd,
|
|
391
|
+
timeout: options?.timeout ?? 5e3,
|
|
392
|
+
allowExitCodes: [1]
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
return result.exitCode === 0;
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
389
399
|
}
|
|
390
|
-
async function
|
|
400
|
+
async function findMergeCommitsWithArgs(commitSha, ref, extraArgs, options) {
|
|
391
401
|
try {
|
|
392
402
|
const result = await gitExec(
|
|
393
403
|
[
|
|
@@ -403,10 +413,29 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
|
|
|
403
413
|
{ cwd: options?.cwd, timeout: options?.timeout }
|
|
404
414
|
);
|
|
405
415
|
const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
|
|
406
|
-
if (lines.length === 0) return
|
|
407
|
-
|
|
416
|
+
if (lines.length === 0) return [];
|
|
417
|
+
const verifiedCandidates = [];
|
|
418
|
+
const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
|
|
419
|
+
let attemptedCount = 0;
|
|
420
|
+
for (let i = 0; i < candidateCount; i++) {
|
|
421
|
+
const candidate = parseMergeLogLine(lines[i]);
|
|
422
|
+
if (!candidate) continue;
|
|
423
|
+
attemptedCount++;
|
|
424
|
+
const verified = await verifyMergeIntroducesCommit(
|
|
425
|
+
commitSha,
|
|
426
|
+
candidate,
|
|
427
|
+
options
|
|
428
|
+
);
|
|
429
|
+
if (verified) verifiedCandidates.push(candidate);
|
|
430
|
+
}
|
|
431
|
+
if (attemptedCount > 0 && verifiedCandidates.length === 0 && options?.warnings) {
|
|
432
|
+
options.warnings.push(
|
|
433
|
+
`ancestry: all ${attemptedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return verifiedCandidates;
|
|
408
437
|
} catch {
|
|
409
|
-
return
|
|
438
|
+
return [];
|
|
410
439
|
}
|
|
411
440
|
}
|
|
412
441
|
function parseMergeLogLine(line) {
|
|
@@ -426,24 +455,71 @@ function parseMergeLogLine(line) {
|
|
|
426
455
|
const subject = parts.slice(subjectStart).join(" ");
|
|
427
456
|
return { mergeCommitSha, parentShas, subject };
|
|
428
457
|
}
|
|
429
|
-
function
|
|
458
|
+
async function findMergeCommits(commitSha, options) {
|
|
459
|
+
const ref = options?.ref ?? "HEAD";
|
|
460
|
+
const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
|
|
461
|
+
const startTime = Date.now();
|
|
462
|
+
const results = [];
|
|
463
|
+
const seen = /* @__PURE__ */ new Set();
|
|
464
|
+
const pushUnique = (candidates) => {
|
|
465
|
+
for (const candidate of candidates) {
|
|
466
|
+
if (seen.has(candidate.mergeCommitSha)) continue;
|
|
467
|
+
seen.add(candidate.mergeCommitSha);
|
|
468
|
+
results.push(candidate);
|
|
469
|
+
if (results.length >= MAX_CANDIDATES) break;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
const firstParent = await findMergeCommitsWithArgs(
|
|
473
|
+
commitSha,
|
|
474
|
+
ref,
|
|
475
|
+
["--first-parent"],
|
|
476
|
+
{ ...options, timeout: budget }
|
|
477
|
+
);
|
|
478
|
+
pushUnique(firstParent);
|
|
479
|
+
const elapsed = Date.now() - startTime;
|
|
480
|
+
const remaining = budget - elapsed;
|
|
481
|
+
if (remaining > 0 && results.length < MAX_CANDIDATES) {
|
|
482
|
+
const full = await findMergeCommitsWithArgs(commitSha, ref, [], {
|
|
483
|
+
...options,
|
|
484
|
+
timeout: remaining
|
|
485
|
+
});
|
|
486
|
+
pushUnique(full);
|
|
487
|
+
}
|
|
488
|
+
return results;
|
|
489
|
+
}
|
|
490
|
+
async function getCommitSubject(sha, options) {
|
|
491
|
+
try {
|
|
492
|
+
const result = await gitExec(["log", "-1", "--format=%s", sha], {
|
|
493
|
+
cwd: options?.cwd,
|
|
494
|
+
timeout: options?.timeout ?? 5e3
|
|
495
|
+
});
|
|
496
|
+
const subject = result.stdout.trim();
|
|
497
|
+
return subject || null;
|
|
498
|
+
} catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function extractPRFromMergeMessage(subject, platform) {
|
|
430
503
|
const ghMatch = /Merge pull request #(\d+)/.exec(subject);
|
|
431
504
|
if (ghMatch) return parseInt(ghMatch[1], 10);
|
|
432
505
|
const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
|
|
433
506
|
if (squashMatch) return parseInt(squashMatch[1], 10);
|
|
434
|
-
|
|
435
|
-
|
|
507
|
+
if (!platform || platform === "gitlab" || platform === "gitlab-self-hosted") {
|
|
508
|
+
const glMatch = /See merge request\s+\S*!(\d+)\s*$/.exec(subject);
|
|
509
|
+
if (glMatch) return parseInt(glMatch[1], 10);
|
|
510
|
+
}
|
|
436
511
|
const adoMatch = /Merged PR (\d+):/.exec(subject);
|
|
437
512
|
if (adoMatch) return parseInt(adoMatch[1], 10);
|
|
438
513
|
return null;
|
|
439
514
|
}
|
|
440
|
-
var DEFAULT_ANCESTRY_TIMEOUT;
|
|
515
|
+
var DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
|
|
441
516
|
var init_ancestry = __esm({
|
|
442
517
|
"src/core/ancestry/ancestry.ts"() {
|
|
443
518
|
"use strict";
|
|
444
519
|
init_esm_shims();
|
|
445
520
|
init_executor();
|
|
446
521
|
DEFAULT_ANCESTRY_TIMEOUT = 3e4;
|
|
522
|
+
MAX_CANDIDATES = 10;
|
|
447
523
|
}
|
|
448
524
|
});
|
|
449
525
|
|
|
@@ -574,7 +650,8 @@ function toCachedPR(pr) {
|
|
|
574
650
|
url: pr.url,
|
|
575
651
|
mergeCommit: pr.mergeCommit,
|
|
576
652
|
baseBranch: pr.baseBranch,
|
|
577
|
-
mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0
|
|
653
|
+
mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
|
|
654
|
+
resolvedVia: pr.resolvedVia
|
|
578
655
|
};
|
|
579
656
|
}
|
|
580
657
|
function fromCachedPR(cached) {
|
|
@@ -589,7 +666,9 @@ function fromCachedPR(cached) {
|
|
|
589
666
|
url: cached.url,
|
|
590
667
|
mergeCommit: cached.mergeCommit,
|
|
591
668
|
baseBranch: cached.baseBranch,
|
|
592
|
-
mergedAt
|
|
669
|
+
mergedAt,
|
|
670
|
+
// Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
|
|
671
|
+
resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
|
|
593
672
|
};
|
|
594
673
|
}
|
|
595
674
|
async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
@@ -600,45 +679,84 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
|
600
679
|
const cached = await cache.get(commitSha);
|
|
601
680
|
if (cached) return fromCachedPR(cached);
|
|
602
681
|
if (options?.cacheOnly) return null;
|
|
682
|
+
const prSelectOptions = options?.preferredBase ? { preferredBase: options.preferredBase } : void 0;
|
|
683
|
+
if (adapter) {
|
|
684
|
+
const directPR = await adapter.getPRForCommit(commitSha, prSelectOptions);
|
|
685
|
+
if (directPR?.mergedAt) {
|
|
686
|
+
const result = { ...directPR, resolvedVia: "api" };
|
|
687
|
+
await cache.set(commitSha, toCachedPR(result));
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
603
691
|
let mergeBasedPR = null;
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
692
|
+
const mergeCandidates = await findMergeCommits(commitSha, options);
|
|
693
|
+
const hasAncestryMerges = mergeCandidates.length > 0;
|
|
694
|
+
for (const candidate of mergeCandidates) {
|
|
695
|
+
const prNumber = extractPRFromMergeMessage(
|
|
696
|
+
candidate.subject,
|
|
697
|
+
options?.platform
|
|
698
|
+
);
|
|
607
699
|
if (prNumber) {
|
|
608
700
|
if (adapter) {
|
|
609
|
-
const prInfo = await adapter.getPRForCommit(
|
|
701
|
+
const prInfo = await adapter.getPRForCommit(
|
|
702
|
+
candidate.mergeCommitSha,
|
|
703
|
+
prSelectOptions
|
|
704
|
+
);
|
|
610
705
|
if (prInfo?.mergedAt) {
|
|
611
|
-
mergeBasedPR = prInfo;
|
|
706
|
+
mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
|
|
612
707
|
}
|
|
613
708
|
}
|
|
614
709
|
if (!mergeBasedPR) {
|
|
615
710
|
mergeBasedPR = {
|
|
616
711
|
number: prNumber,
|
|
617
|
-
title:
|
|
712
|
+
title: candidate.subject,
|
|
618
713
|
author: "",
|
|
619
714
|
url: "",
|
|
620
|
-
mergeCommit:
|
|
621
|
-
baseBranch: ""
|
|
715
|
+
mergeCommit: candidate.mergeCommitSha,
|
|
716
|
+
baseBranch: "",
|
|
717
|
+
resolvedVia: "ancestry"
|
|
622
718
|
};
|
|
623
719
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
if (adapter) {
|
|
723
|
+
const mergeCommitPR = await adapter.getPRForCommit(
|
|
724
|
+
candidate.mergeCommitSha,
|
|
725
|
+
prSelectOptions
|
|
726
|
+
);
|
|
727
|
+
if (mergeCommitPR?.mergedAt) {
|
|
728
|
+
mergeBasedPR = { ...mergeCommitPR, resolvedVia: "ancestry" };
|
|
729
|
+
break;
|
|
627
730
|
}
|
|
628
731
|
}
|
|
629
732
|
}
|
|
630
733
|
if (mergeBasedPR) {
|
|
631
|
-
|
|
632
|
-
|
|
734
|
+
if (!options?.deep || mergeBasedPR.mergedAt) {
|
|
735
|
+
await cache.set(commitSha, toCachedPR(mergeBasedPR));
|
|
736
|
+
return mergeBasedPR;
|
|
737
|
+
}
|
|
633
738
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
739
|
+
const commitSubject = await getCommitSubject(commitSha, options);
|
|
740
|
+
if (commitSubject) {
|
|
741
|
+
const directPrNumber = extractPRFromMergeMessage(
|
|
742
|
+
commitSubject,
|
|
743
|
+
options?.platform
|
|
744
|
+
);
|
|
745
|
+
if (directPrNumber) {
|
|
746
|
+
const subjectPR = {
|
|
747
|
+
number: directPrNumber,
|
|
748
|
+
title: commitSubject,
|
|
749
|
+
author: "",
|
|
750
|
+
url: "",
|
|
751
|
+
mergeCommit: commitSha,
|
|
752
|
+
baseBranch: "",
|
|
753
|
+
resolvedVia: "message"
|
|
754
|
+
};
|
|
755
|
+
await cache.set(commitSha, toCachedPR(subjectPR));
|
|
756
|
+
return subjectPR;
|
|
639
757
|
}
|
|
640
758
|
}
|
|
641
|
-
if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
|
|
759
|
+
if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH && (!hasAncestryMerges || options?.deep)) {
|
|
642
760
|
const patchIdMatch = await findPatchIdMatch(commitSha, {
|
|
643
761
|
...options,
|
|
644
762
|
scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
|
|
@@ -656,6 +774,10 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
|
656
774
|
}
|
|
657
775
|
}
|
|
658
776
|
}
|
|
777
|
+
if (mergeBasedPR) {
|
|
778
|
+
await cache.set(commitSha, toCachedPR(mergeBasedPR));
|
|
779
|
+
return mergeBasedPR;
|
|
780
|
+
}
|
|
659
781
|
return null;
|
|
660
782
|
}
|
|
661
783
|
function resetPRCache() {
|
|
@@ -696,6 +818,7 @@ init_errors();
|
|
|
696
818
|
// src/core/core.ts
|
|
697
819
|
init_esm_shims();
|
|
698
820
|
import { createHash as createHash2 } from "crypto";
|
|
821
|
+
import { dirname, isAbsolute, relative } from "path";
|
|
699
822
|
import { map as map8 } from "@winglet/common-utils";
|
|
700
823
|
|
|
701
824
|
// src/ast/index.ts
|
|
@@ -979,7 +1102,7 @@ async function checkGitHealth(options) {
|
|
|
979
1102
|
const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
|
|
980
1103
|
if (cloneStatus.partialClone) {
|
|
981
1104
|
hints.push(
|
|
982
|
-
"Partial clone detected. Patch-ID scan (Strategy
|
|
1105
|
+
"Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
|
|
983
1106
|
);
|
|
984
1107
|
}
|
|
985
1108
|
if (cloneStatus.shallow) {
|
|
@@ -1095,7 +1218,7 @@ var GitHubAdapter = class {
|
|
|
1095
1218
|
return { authenticated: false, hostname: this.hostname };
|
|
1096
1219
|
}
|
|
1097
1220
|
}
|
|
1098
|
-
async getPRForCommit(sha) {
|
|
1221
|
+
async getPRForCommit(sha, options) {
|
|
1099
1222
|
if (this.scheduler.isRateLimited()) return null;
|
|
1100
1223
|
try {
|
|
1101
1224
|
const result = await shellExec(
|
|
@@ -1112,18 +1235,20 @@ var GitHubAdapter = class {
|
|
|
1112
1235
|
);
|
|
1113
1236
|
const prs = JSON.parse(result.stdout);
|
|
1114
1237
|
if (!isArray(prs) || prs.length === 0) return null;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1238
|
+
let data = prs[0];
|
|
1239
|
+
if (options?.preferredBase) {
|
|
1240
|
+
const preferred = prs.find(
|
|
1241
|
+
(pr) => pr.base === options.preferredBase
|
|
1242
|
+
);
|
|
1243
|
+
if (preferred) data = preferred;
|
|
1244
|
+
}
|
|
1120
1245
|
return {
|
|
1121
1246
|
number: data.number,
|
|
1122
1247
|
title: data.title ?? "",
|
|
1123
1248
|
author: data.user ?? "",
|
|
1124
1249
|
url: data.html_url ?? "",
|
|
1125
1250
|
mergeCommit: data.merge_commit_sha ?? sha,
|
|
1126
|
-
baseBranch: data.base ??
|
|
1251
|
+
baseBranch: data.base ?? "",
|
|
1127
1252
|
mergedAt: data.merged_at
|
|
1128
1253
|
};
|
|
1129
1254
|
} catch {
|
|
@@ -1305,7 +1430,7 @@ var GitLabAdapter = class {
|
|
|
1305
1430
|
return { authenticated: false, hostname: this.hostname };
|
|
1306
1431
|
}
|
|
1307
1432
|
}
|
|
1308
|
-
async getPRForCommit(sha) {
|
|
1433
|
+
async getPRForCommit(sha, options) {
|
|
1309
1434
|
if (this.scheduler.isRateLimited()) return null;
|
|
1310
1435
|
try {
|
|
1311
1436
|
const result = await shellExec(
|
|
@@ -1329,18 +1454,20 @@ var GitLabAdapter = class {
|
|
|
1329
1454
|
return aTime - bTime;
|
|
1330
1455
|
});
|
|
1331
1456
|
if (mergedMRs.length === 0) return null;
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1457
|
+
let mr = mergedMRs[0];
|
|
1458
|
+
if (options?.preferredBase) {
|
|
1459
|
+
const preferred = mergedMRs.find(
|
|
1460
|
+
(m) => m.target_branch === options.preferredBase
|
|
1461
|
+
);
|
|
1462
|
+
if (preferred) mr = preferred;
|
|
1463
|
+
}
|
|
1337
1464
|
return {
|
|
1338
1465
|
number: mr.iid,
|
|
1339
1466
|
title: mr.title ?? "",
|
|
1340
1467
|
author: mr.author?.username ?? "",
|
|
1341
1468
|
url: mr.web_url ?? "",
|
|
1342
1469
|
mergeCommit: mr.merge_commit_sha ?? sha,
|
|
1343
|
-
baseBranch: mr.target_branch ??
|
|
1470
|
+
baseBranch: mr.target_branch ?? "",
|
|
1344
1471
|
mergedAt: mr.merged_at
|
|
1345
1472
|
};
|
|
1346
1473
|
} catch {
|
|
@@ -1959,6 +2086,7 @@ function parsePorcelainOutput(output) {
|
|
|
1959
2086
|
}
|
|
1960
2087
|
let commitHash = headerMatch[1];
|
|
1961
2088
|
const originalLine = parseInt(headerMatch[2], 10);
|
|
2089
|
+
const finalLine = parseInt(headerMatch[3], 10) || 0;
|
|
1962
2090
|
const isBoundary = commitHash.startsWith("^");
|
|
1963
2091
|
if (isBoundary) {
|
|
1964
2092
|
commitHash = commitHash.slice(1).padStart(40, "0");
|
|
@@ -2002,6 +2130,7 @@ function parsePorcelainOutput(output) {
|
|
|
2002
2130
|
authorEmail: cleanEmail,
|
|
2003
2131
|
date,
|
|
2004
2132
|
lineContent,
|
|
2133
|
+
finalLine,
|
|
2005
2134
|
originalFile,
|
|
2006
2135
|
originalLine: originalFile ? originalLine : void 0
|
|
2007
2136
|
});
|
|
@@ -2012,10 +2141,8 @@ function parsePorcelainOutput(output) {
|
|
|
2012
2141
|
// src/core/blame/blame.ts
|
|
2013
2142
|
async function executeBlame(file, lineRange, options) {
|
|
2014
2143
|
const lineSpec = `${lineRange.start},${lineRange.end}`;
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
options
|
|
2018
|
-
);
|
|
2144
|
+
const args = options?.mode === "change" ? ["blame", "-w", "--porcelain", "-L", lineSpec, file] : ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file];
|
|
2145
|
+
const result = await gitExec(args, options);
|
|
2019
2146
|
return parsePorcelainOutput(result.stdout);
|
|
2020
2147
|
}
|
|
2021
2148
|
async function analyzeBlameResults(results, filePath, options) {
|
|
@@ -2148,6 +2275,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
|
|
|
2148
2275
|
|
|
2149
2276
|
// src/core/core.ts
|
|
2150
2277
|
init_pr_lookup2();
|
|
2278
|
+
function resolvedViaToTrackingMethod(resolvedVia) {
|
|
2279
|
+
switch (resolvedVia) {
|
|
2280
|
+
case "api":
|
|
2281
|
+
return "api";
|
|
2282
|
+
case "ancestry":
|
|
2283
|
+
return "ancestry-path";
|
|
2284
|
+
case "message":
|
|
2285
|
+
return "message-parse";
|
|
2286
|
+
case "patch-id":
|
|
2287
|
+
return "patch-id";
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
function resolvedViaToConfidence(resolvedVia) {
|
|
2291
|
+
switch (resolvedVia) {
|
|
2292
|
+
case "api":
|
|
2293
|
+
case "ancestry":
|
|
2294
|
+
return "exact";
|
|
2295
|
+
case "message":
|
|
2296
|
+
case "patch-id":
|
|
2297
|
+
return "heuristic";
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2151
2300
|
function computeFeatureFlags(operatingLevel, options) {
|
|
2152
2301
|
return {
|
|
2153
2302
|
astDiff: isAstAvailable() && !options.noAst,
|
|
@@ -2165,6 +2314,22 @@ async function resolveRepoIdentity(cwd) {
|
|
|
2165
2314
|
return { host: "_local", owner: "_", repo: "_unknown" };
|
|
2166
2315
|
}
|
|
2167
2316
|
}
|
|
2317
|
+
async function resolveFileContext(file, cwd) {
|
|
2318
|
+
if (cwd || !isAbsolute(file)) return { file, cwd };
|
|
2319
|
+
const fileDir = dirname(file);
|
|
2320
|
+
try {
|
|
2321
|
+
const result = await gitExec(["rev-parse", "--show-toplevel"], {
|
|
2322
|
+
cwd: fileDir
|
|
2323
|
+
});
|
|
2324
|
+
const repoRoot = result.stdout.trim();
|
|
2325
|
+
return {
|
|
2326
|
+
file: relative(repoRoot, file),
|
|
2327
|
+
cwd: repoRoot
|
|
2328
|
+
};
|
|
2329
|
+
} catch {
|
|
2330
|
+
return { file, cwd };
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2168
2333
|
async function detectPlatform2(options) {
|
|
2169
2334
|
const warnings = [];
|
|
2170
2335
|
let adapter = null;
|
|
@@ -2188,9 +2353,10 @@ async function runBlameAndAuth(adapter, options, execOptions) {
|
|
|
2188
2353
|
const lineRange = parseLineRange(
|
|
2189
2354
|
options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
|
|
2190
2355
|
);
|
|
2191
|
-
const blameChain = executeBlame(options.file, lineRange,
|
|
2192
|
-
|
|
2193
|
-
|
|
2356
|
+
const blameChain = executeBlame(options.file, lineRange, {
|
|
2357
|
+
...execOptions,
|
|
2358
|
+
mode: options.mode
|
|
2359
|
+
}).then((results) => analyzeBlameResults(results, options.file, execOptions));
|
|
2194
2360
|
const [authResult, blameResult] = await Promise.allSettled([
|
|
2195
2361
|
adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
|
|
2196
2362
|
blameChain
|
|
@@ -2211,12 +2377,24 @@ async function runBlameAndAuth(adapter, options, execOptions) {
|
|
|
2211
2377
|
}
|
|
2212
2378
|
return { analyzed: blameResult.value, operatingLevel, warnings };
|
|
2213
2379
|
}
|
|
2214
|
-
|
|
2380
|
+
function resolveTraceMode(mode) {
|
|
2381
|
+
return mode ?? "origin";
|
|
2382
|
+
}
|
|
2383
|
+
function deduplicatedLookupPR(sha, adapter, options, inflight) {
|
|
2384
|
+
const existing = inflight.get(sha);
|
|
2385
|
+
if (existing) return existing;
|
|
2386
|
+
const promise = lookupPR(sha, adapter, options);
|
|
2387
|
+
inflight.set(sha, promise);
|
|
2388
|
+
promise.finally(() => inflight.delete(sha));
|
|
2389
|
+
return promise;
|
|
2390
|
+
}
|
|
2391
|
+
async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, inflightPR, skipPatchIdScan, preferredBase) {
|
|
2215
2392
|
const nodes = [];
|
|
2393
|
+
const traceMode = resolveTraceMode(options.mode);
|
|
2216
2394
|
const commitNode = {
|
|
2217
2395
|
type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
|
|
2218
2396
|
sha: entry.blame.commitHash,
|
|
2219
|
-
trackingMethod: "blame-CMw",
|
|
2397
|
+
trackingMethod: traceMode === "change" ? "blame" : "blame-CMw",
|
|
2220
2398
|
confidence: "exact",
|
|
2221
2399
|
note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
|
|
2222
2400
|
};
|
|
@@ -2238,21 +2416,24 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
|
|
|
2238
2416
|
}
|
|
2239
2417
|
}
|
|
2240
2418
|
const targetSha = nodes[nodes.length - 1].sha;
|
|
2419
|
+
const prLookupOptions = {
|
|
2420
|
+
...execOptions,
|
|
2421
|
+
noCache: options.noCache,
|
|
2422
|
+
cacheOnly: options.cacheOnly,
|
|
2423
|
+
deep: featureFlags.deepTrace,
|
|
2424
|
+
repoId,
|
|
2425
|
+
skipPatchIdScan,
|
|
2426
|
+
preferredBase,
|
|
2427
|
+
platform: adapter?.platform
|
|
2428
|
+
};
|
|
2241
2429
|
if (targetSha) {
|
|
2242
|
-
const prInfo = await
|
|
2243
|
-
...execOptions,
|
|
2244
|
-
noCache: options.noCache,
|
|
2245
|
-
cacheOnly: options.cacheOnly,
|
|
2246
|
-
deep: featureFlags.deepTrace,
|
|
2247
|
-
repoId,
|
|
2248
|
-
skipPatchIdScan
|
|
2249
|
-
});
|
|
2430
|
+
const prInfo = await deduplicatedLookupPR(targetSha, adapter, prLookupOptions, inflightPR);
|
|
2250
2431
|
if (prInfo) {
|
|
2251
2432
|
nodes.push({
|
|
2252
2433
|
type: "pull_request",
|
|
2253
2434
|
sha: prInfo.mergeCommit,
|
|
2254
|
-
trackingMethod: prInfo.
|
|
2255
|
-
confidence: prInfo.
|
|
2435
|
+
trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
|
|
2436
|
+
confidence: resolvedViaToConfidence(prInfo.resolvedVia),
|
|
2256
2437
|
prNumber: prInfo.number,
|
|
2257
2438
|
prUrl: prInfo.url || void 0,
|
|
2258
2439
|
prTitle: prInfo.title || void 0,
|
|
@@ -2262,7 +2443,8 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
|
|
|
2262
2443
|
}
|
|
2263
2444
|
return nodes;
|
|
2264
2445
|
}
|
|
2265
|
-
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
|
|
2446
|
+
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
|
|
2447
|
+
const inflightPR = /* @__PURE__ */ new Map();
|
|
2266
2448
|
const results = await Promise.allSettled(
|
|
2267
2449
|
map8(
|
|
2268
2450
|
analyzed,
|
|
@@ -2273,7 +2455,9 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
|
|
|
2273
2455
|
options,
|
|
2274
2456
|
execOptions,
|
|
2275
2457
|
repoId,
|
|
2276
|
-
|
|
2458
|
+
inflightPR,
|
|
2459
|
+
skipPatchIdScan,
|
|
2460
|
+
preferredBase
|
|
2277
2461
|
)
|
|
2278
2462
|
)
|
|
2279
2463
|
);
|
|
@@ -2281,13 +2465,16 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
|
|
|
2281
2465
|
}
|
|
2282
2466
|
var legacyCacheCleaned = false;
|
|
2283
2467
|
async function trace(options) {
|
|
2284
|
-
const
|
|
2468
|
+
const mode = resolveTraceMode(options.mode);
|
|
2469
|
+
const { file, cwd } = await resolveFileContext(options.file, options.cwd);
|
|
2470
|
+
const warnings = [];
|
|
2471
|
+
const execOptions = { cwd, warnings };
|
|
2285
2472
|
if (!legacyCacheCleaned) {
|
|
2286
2473
|
legacyCacheCleaned = true;
|
|
2287
2474
|
cleanupLegacyCache().catch(() => {
|
|
2288
2475
|
});
|
|
2289
2476
|
}
|
|
2290
|
-
const platform = await detectPlatform2(options);
|
|
2477
|
+
const platform = await detectPlatform2({ ...options, cwd });
|
|
2291
2478
|
let repoId;
|
|
2292
2479
|
if (platform.remote) {
|
|
2293
2480
|
repoId = {
|
|
@@ -2296,15 +2483,15 @@ async function trace(options) {
|
|
|
2296
2483
|
repo: platform.remote.repo
|
|
2297
2484
|
};
|
|
2298
2485
|
} else {
|
|
2299
|
-
repoId = await resolveRepoIdentity(
|
|
2486
|
+
repoId = await resolveRepoIdentity(cwd);
|
|
2300
2487
|
}
|
|
2301
2488
|
const blameAuth = await runBlameAndAuth(
|
|
2302
2489
|
platform.adapter,
|
|
2303
|
-
options,
|
|
2490
|
+
{ ...options, mode, file, cwd },
|
|
2304
2491
|
execOptions
|
|
2305
2492
|
);
|
|
2306
2493
|
const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
|
|
2307
|
-
|
|
2494
|
+
warnings.push(...platform.warnings, ...blameAuth.warnings);
|
|
2308
2495
|
if (options.cacheOnly && options.noCache) {
|
|
2309
2496
|
warnings.push(
|
|
2310
2497
|
"Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
|
|
@@ -2313,13 +2500,13 @@ async function trace(options) {
|
|
|
2313
2500
|
const featureFlags = computeFeatureFlags(operatingLevel, options);
|
|
2314
2501
|
let cloneStatus = { partialClone: false, shallow: false };
|
|
2315
2502
|
try {
|
|
2316
|
-
const result = await checkCloneStatus({ cwd
|
|
2503
|
+
const result = await checkCloneStatus({ cwd });
|
|
2317
2504
|
if (result) cloneStatus = result;
|
|
2318
2505
|
} catch {
|
|
2319
2506
|
}
|
|
2320
2507
|
if (cloneStatus.partialClone) {
|
|
2321
2508
|
warnings.push(
|
|
2322
|
-
"Partial clone detected. Patch-ID scan (Strategy
|
|
2509
|
+
"Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
|
|
2323
2510
|
);
|
|
2324
2511
|
}
|
|
2325
2512
|
if (cloneStatus.shallow) {
|
|
@@ -2327,14 +2514,27 @@ async function trace(options) {
|
|
|
2327
2514
|
"Shallow repository detected. Ancestry-path results may be incomplete."
|
|
2328
2515
|
);
|
|
2329
2516
|
}
|
|
2517
|
+
let preferredBase;
|
|
2518
|
+
try {
|
|
2519
|
+
const branchResult = await gitExec(
|
|
2520
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
2521
|
+
execOptions
|
|
2522
|
+
);
|
|
2523
|
+
const branch = branchResult.stdout.trim();
|
|
2524
|
+
if (branch && branch !== "HEAD") {
|
|
2525
|
+
preferredBase = branch;
|
|
2526
|
+
}
|
|
2527
|
+
} catch {
|
|
2528
|
+
}
|
|
2330
2529
|
const nodes = await buildTraceNodes(
|
|
2331
2530
|
blameAuth.analyzed,
|
|
2332
2531
|
featureFlags,
|
|
2333
2532
|
platform.adapter,
|
|
2334
|
-
options,
|
|
2533
|
+
{ ...options, mode, file, cwd },
|
|
2335
2534
|
execOptions,
|
|
2336
2535
|
repoId,
|
|
2337
|
-
cloneStatus.partialClone || void 0
|
|
2536
|
+
cloneStatus.partialClone || void 0,
|
|
2537
|
+
preferredBase
|
|
2338
2538
|
);
|
|
2339
2539
|
return { nodes, operatingLevel, featureFlags, warnings };
|
|
2340
2540
|
}
|
|
@@ -14,7 +14,9 @@ export declare class GitHubAdapter implements PlatformAdapter {
|
|
|
14
14
|
cwd?: string;
|
|
15
15
|
});
|
|
16
16
|
checkAuth(): Promise<AuthStatus>;
|
|
17
|
-
getPRForCommit(sha: string
|
|
17
|
+
getPRForCommit(sha: string, options?: {
|
|
18
|
+
preferredBase?: string;
|
|
19
|
+
}): Promise<PRInfo | null>;
|
|
18
20
|
private detectDefaultBranch;
|
|
19
21
|
getPRCommits(prNumber: number): Promise<string[]>;
|
|
20
22
|
getLinkedIssues(prNumber: number): Promise<IssueInfo[]>;
|
|
@@ -14,7 +14,9 @@ export declare class GitLabAdapter implements PlatformAdapter {
|
|
|
14
14
|
cwd?: string;
|
|
15
15
|
});
|
|
16
16
|
checkAuth(): Promise<AuthStatus>;
|
|
17
|
-
getPRForCommit(sha: string
|
|
17
|
+
getPRForCommit(sha: string, options?: {
|
|
18
|
+
preferredBase?: string;
|
|
19
|
+
}): Promise<PRInfo | null>;
|
|
18
20
|
private detectDefaultBranch;
|
|
19
21
|
getPRCommits(prNumber: number): Promise<string[]>;
|
|
20
22
|
getLinkedIssues(prNumber: number): Promise<IssueInfo[]>;
|
package/dist/types/blame.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface BlameResult {
|
|
|
12
12
|
date: string;
|
|
13
13
|
/** The actual content of the blamed line */
|
|
14
14
|
lineContent: string;
|
|
15
|
+
/** Final line number in the current file */
|
|
16
|
+
finalLine: number;
|
|
15
17
|
/** Original filename if the line was moved/renamed */
|
|
16
18
|
originalFile?: string;
|
|
17
19
|
/** Original line number before any moves/renames */
|
package/dist/types/cache.d.ts
CHANGED