@lumy-pack/line-lore 0.0.5 → 0.0.7
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 +71 -0
- package/dist/cli.mjs +331 -61
- package/dist/core/ancestry/ancestry.d.ts +15 -1
- package/dist/core/ancestry/index.d.ts +1 -1
- package/dist/core/pr-lookup/index.d.ts +1 -0
- package/dist/core/pr-lookup/pr-lookup.d.ts +23 -1
- package/dist/git/health.d.ts +4 -1
- package/dist/git/index.d.ts +1 -1
- package/dist/index.cjs +327 -59
- package/dist/index.mjs +327 -58
- package/dist/platform/github/github-adapter.d.ts +3 -1
- package/dist/platform/gitlab/gitlab-adapter.d.ts +3 -1
- package/dist/types/cache.d.ts +13 -0
- package/dist/types/git.d.ts +8 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform.d.ts +3 -1
- package/dist/types/trace.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -369,14 +369,51 @@ var init_executor = __esm({
|
|
|
369
369
|
import { filter as filter4, isTruthy as isTruthy4 } from "@winglet/common-utils";
|
|
370
370
|
async function findMergeCommit(commitSha, options) {
|
|
371
371
|
const ref = options?.ref ?? "HEAD";
|
|
372
|
+
const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
|
|
373
|
+
const startTime = Date.now();
|
|
372
374
|
const firstParentResult = await findMergeCommitWithArgs(
|
|
373
375
|
commitSha,
|
|
374
376
|
ref,
|
|
375
377
|
["--first-parent"],
|
|
376
|
-
options
|
|
378
|
+
{ ...options, timeout: budget }
|
|
377
379
|
);
|
|
378
380
|
if (firstParentResult) return firstParentResult;
|
|
379
|
-
|
|
381
|
+
const elapsed = Date.now() - startTime;
|
|
382
|
+
const remaining = budget - elapsed;
|
|
383
|
+
if (remaining <= 0) return null;
|
|
384
|
+
return findMergeCommitWithArgs(commitSha, ref, [], {
|
|
385
|
+
...options,
|
|
386
|
+
timeout: remaining
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
|
|
390
|
+
if (mergeResult.parentShas.length < 2) return true;
|
|
391
|
+
const firstParent = mergeResult.parentShas[0];
|
|
392
|
+
const branchParents = mergeResult.parentShas.slice(1);
|
|
393
|
+
const onMainline = await isAncestor(targetSha, firstParent, options);
|
|
394
|
+
if (onMainline === null) return false;
|
|
395
|
+
if (onMainline) return false;
|
|
396
|
+
for (const branchParent of branchParents) {
|
|
397
|
+
const onBranch = await isAncestor(targetSha, branchParent, options);
|
|
398
|
+
if (onBranch === null) return false;
|
|
399
|
+
if (onBranch) return true;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
async function isAncestor(commitA, commitB, options) {
|
|
404
|
+
try {
|
|
405
|
+
const result = await gitExec(
|
|
406
|
+
["merge-base", "--is-ancestor", commitA, commitB],
|
|
407
|
+
{
|
|
408
|
+
cwd: options?.cwd,
|
|
409
|
+
timeout: options?.timeout ?? 5e3,
|
|
410
|
+
allowExitCodes: [1]
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
return result.exitCode === 0;
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
380
417
|
}
|
|
381
418
|
async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
|
|
382
419
|
try {
|
|
@@ -395,7 +432,25 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
|
|
|
395
432
|
);
|
|
396
433
|
const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
|
|
397
434
|
if (lines.length === 0) return null;
|
|
398
|
-
|
|
435
|
+
const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
|
|
436
|
+
let verifiedCount = 0;
|
|
437
|
+
for (let i = 0; i < candidateCount; i++) {
|
|
438
|
+
const candidate = parseMergeLogLine(lines[i]);
|
|
439
|
+
if (!candidate) continue;
|
|
440
|
+
verifiedCount++;
|
|
441
|
+
const verified = await verifyMergeIntroducesCommit(
|
|
442
|
+
commitSha,
|
|
443
|
+
candidate,
|
|
444
|
+
options
|
|
445
|
+
);
|
|
446
|
+
if (verified) return candidate;
|
|
447
|
+
}
|
|
448
|
+
if (verifiedCount > 0 && options?.warnings) {
|
|
449
|
+
options.warnings.push(
|
|
450
|
+
`ancestry: all ${verifiedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
399
454
|
} catch {
|
|
400
455
|
return null;
|
|
401
456
|
}
|
|
@@ -417,19 +472,38 @@ function parseMergeLogLine(line) {
|
|
|
417
472
|
const subject = parts.slice(subjectStart).join(" ");
|
|
418
473
|
return { mergeCommitSha, parentShas, subject };
|
|
419
474
|
}
|
|
420
|
-
function
|
|
475
|
+
async function getCommitSubject(sha, options) {
|
|
476
|
+
try {
|
|
477
|
+
const result = await gitExec(["log", "-1", "--format=%s", sha], {
|
|
478
|
+
cwd: options?.cwd,
|
|
479
|
+
timeout: options?.timeout ?? 5e3
|
|
480
|
+
});
|
|
481
|
+
const subject = result.stdout.trim();
|
|
482
|
+
return subject || null;
|
|
483
|
+
} catch {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function extractPRFromMergeMessage(subject, platform) {
|
|
421
488
|
const ghMatch = /Merge pull request #(\d+)/.exec(subject);
|
|
422
489
|
if (ghMatch) return parseInt(ghMatch[1], 10);
|
|
423
490
|
const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
|
|
424
491
|
if (squashMatch) return parseInt(squashMatch[1], 10);
|
|
425
|
-
|
|
426
|
-
|
|
492
|
+
if (!platform || platform === "gitlab" || platform === "gitlab-self-hosted") {
|
|
493
|
+
const glMatch = /See merge request\s+\S*!(\d+)\s*$/.exec(subject);
|
|
494
|
+
if (glMatch) return parseInt(glMatch[1], 10);
|
|
495
|
+
}
|
|
496
|
+
const adoMatch = /Merged PR (\d+):/.exec(subject);
|
|
497
|
+
if (adoMatch) return parseInt(adoMatch[1], 10);
|
|
427
498
|
return null;
|
|
428
499
|
}
|
|
500
|
+
var DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
|
|
429
501
|
var init_ancestry = __esm({
|
|
430
502
|
"src/core/ancestry/ancestry.ts"() {
|
|
431
503
|
"use strict";
|
|
432
504
|
init_executor();
|
|
505
|
+
DEFAULT_ANCESTRY_TIMEOUT = 3e4;
|
|
506
|
+
MAX_CANDIDATES = 10;
|
|
433
507
|
}
|
|
434
508
|
});
|
|
435
509
|
|
|
@@ -549,19 +623,67 @@ function getCache2(repoId, noCache) {
|
|
|
549
623
|
}
|
|
550
624
|
return cache;
|
|
551
625
|
}
|
|
552
|
-
|
|
553
|
-
|
|
626
|
+
function toCachedPR(pr) {
|
|
627
|
+
return {
|
|
628
|
+
number: pr.number,
|
|
629
|
+
title: pr.title,
|
|
630
|
+
author: pr.author,
|
|
631
|
+
url: pr.url,
|
|
632
|
+
mergeCommit: pr.mergeCommit,
|
|
633
|
+
baseBranch: pr.baseBranch,
|
|
634
|
+
mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
|
|
635
|
+
resolvedVia: pr.resolvedVia
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function fromCachedPR(cached) {
|
|
639
|
+
let mergedAt;
|
|
640
|
+
if (cached.mergedAt != null) {
|
|
641
|
+
mergedAt = typeof cached.mergedAt === "number" ? new Date(cached.mergedAt).toISOString() : String(cached.mergedAt);
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
number: cached.number,
|
|
645
|
+
title: cached.title,
|
|
646
|
+
author: cached.author,
|
|
647
|
+
url: cached.url,
|
|
648
|
+
mergeCommit: cached.mergeCommit,
|
|
649
|
+
baseBranch: cached.baseBranch,
|
|
650
|
+
mergedAt,
|
|
651
|
+
// Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
|
|
652
|
+
resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
656
|
+
const cache = getCache2(
|
|
657
|
+
options?.repoId,
|
|
658
|
+
options?.cacheOnly ? false : options?.noCache
|
|
659
|
+
);
|
|
554
660
|
const cached = await cache.get(commitSha);
|
|
555
|
-
if (cached) return cached;
|
|
661
|
+
if (cached) return fromCachedPR(cached);
|
|
662
|
+
if (options?.cacheOnly) return null;
|
|
663
|
+
const prSelectOptions = options?.preferredBase ? { preferredBase: options.preferredBase } : void 0;
|
|
664
|
+
if (adapter) {
|
|
665
|
+
const directPR = await adapter.getPRForCommit(commitSha, prSelectOptions);
|
|
666
|
+
if (directPR?.mergedAt) {
|
|
667
|
+
const result = { ...directPR, resolvedVia: "api" };
|
|
668
|
+
await cache.set(commitSha, toCachedPR(result));
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
556
672
|
let mergeBasedPR = null;
|
|
557
673
|
const mergeResult = await findMergeCommit(commitSha, options);
|
|
558
674
|
if (mergeResult) {
|
|
559
|
-
const prNumber = extractPRFromMergeMessage(
|
|
675
|
+
const prNumber = extractPRFromMergeMessage(
|
|
676
|
+
mergeResult.subject,
|
|
677
|
+
options?.platform
|
|
678
|
+
);
|
|
560
679
|
if (prNumber) {
|
|
561
680
|
if (adapter) {
|
|
562
|
-
const prInfo = await adapter.getPRForCommit(
|
|
681
|
+
const prInfo = await adapter.getPRForCommit(
|
|
682
|
+
mergeResult.mergeCommitSha,
|
|
683
|
+
prSelectOptions
|
|
684
|
+
);
|
|
563
685
|
if (prInfo?.mergedAt) {
|
|
564
|
-
mergeBasedPR = prInfo;
|
|
686
|
+
mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
|
|
565
687
|
}
|
|
566
688
|
}
|
|
567
689
|
if (!mergeBasedPR) {
|
|
@@ -571,35 +693,56 @@ async function lookupPR(commitSha, adapter, options) {
|
|
|
571
693
|
author: "",
|
|
572
694
|
url: "",
|
|
573
695
|
mergeCommit: mergeResult.mergeCommitSha,
|
|
574
|
-
baseBranch: ""
|
|
696
|
+
baseBranch: "",
|
|
697
|
+
resolvedVia: "ancestry"
|
|
575
698
|
};
|
|
576
699
|
}
|
|
577
700
|
if (!options?.deep || mergeBasedPR.mergedAt) {
|
|
578
|
-
await cache.set(commitSha, mergeBasedPR);
|
|
701
|
+
await cache.set(commitSha, toCachedPR(mergeBasedPR));
|
|
579
702
|
return mergeBasedPR;
|
|
580
703
|
}
|
|
581
704
|
}
|
|
582
705
|
}
|
|
583
706
|
if (mergeBasedPR) {
|
|
584
|
-
await cache.set(commitSha, mergeBasedPR);
|
|
707
|
+
await cache.set(commitSha, toCachedPR(mergeBasedPR));
|
|
585
708
|
return mergeBasedPR;
|
|
586
709
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
710
|
+
const commitSubject = await getCommitSubject(commitSha, options);
|
|
711
|
+
if (commitSubject) {
|
|
712
|
+
const directPrNumber = extractPRFromMergeMessage(
|
|
713
|
+
commitSubject,
|
|
714
|
+
options?.platform
|
|
715
|
+
);
|
|
716
|
+
if (directPrNumber) {
|
|
717
|
+
const subjectPR = {
|
|
718
|
+
number: directPrNumber,
|
|
719
|
+
title: commitSubject,
|
|
720
|
+
author: "",
|
|
721
|
+
url: "",
|
|
722
|
+
mergeCommit: commitSha,
|
|
723
|
+
baseBranch: "",
|
|
724
|
+
resolvedVia: "message"
|
|
725
|
+
};
|
|
726
|
+
await cache.set(commitSha, toCachedPR(subjectPR));
|
|
727
|
+
return subjectPR;
|
|
592
728
|
}
|
|
593
729
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
730
|
+
if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
|
|
731
|
+
const patchIdMatch = await findPatchIdMatch(commitSha, {
|
|
732
|
+
...options,
|
|
733
|
+
scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
|
|
734
|
+
});
|
|
735
|
+
if (patchIdMatch) {
|
|
736
|
+
const result = await lookupPR(
|
|
737
|
+
patchIdMatch.matchedSha,
|
|
738
|
+
adapter,
|
|
739
|
+
options,
|
|
740
|
+
_recursionDepth + 1
|
|
741
|
+
);
|
|
742
|
+
if (result) {
|
|
743
|
+
await cache.set(commitSha, toCachedPR(result));
|
|
744
|
+
return result;
|
|
745
|
+
}
|
|
603
746
|
}
|
|
604
747
|
}
|
|
605
748
|
return null;
|
|
@@ -607,7 +750,7 @@ async function lookupPR(commitSha, adapter, options) {
|
|
|
607
750
|
function resetPRCache() {
|
|
608
751
|
cacheRegistry2.clear();
|
|
609
752
|
}
|
|
610
|
-
var cacheRegistry2, DEEP_SCAN_DEPTH;
|
|
753
|
+
var cacheRegistry2, DEEP_SCAN_DEPTH, MAX_RECURSION_DEPTH;
|
|
611
754
|
var init_pr_lookup = __esm({
|
|
612
755
|
"src/core/pr-lookup/pr-lookup.ts"() {
|
|
613
756
|
"use strict";
|
|
@@ -616,6 +759,7 @@ var init_pr_lookup = __esm({
|
|
|
616
759
|
init_patch_id2();
|
|
617
760
|
cacheRegistry2 = /* @__PURE__ */ new Map();
|
|
618
761
|
DEEP_SCAN_DEPTH = 2e3;
|
|
762
|
+
MAX_RECURSION_DEPTH = 2;
|
|
619
763
|
}
|
|
620
764
|
});
|
|
621
765
|
|
|
@@ -637,7 +781,7 @@ var VERSION;
|
|
|
637
781
|
var init_version = __esm({
|
|
638
782
|
"src/version.ts"() {
|
|
639
783
|
"use strict";
|
|
640
|
-
VERSION = "0.0.
|
|
784
|
+
VERSION = "0.0.7";
|
|
641
785
|
}
|
|
642
786
|
});
|
|
643
787
|
|
|
@@ -734,6 +878,7 @@ import { Command } from "commander";
|
|
|
734
878
|
|
|
735
879
|
// src/core/core.ts
|
|
736
880
|
import { createHash as createHash2 } from "crypto";
|
|
881
|
+
import { dirname, isAbsolute, relative } from "path";
|
|
737
882
|
import { map as map8 } from "@winglet/common-utils";
|
|
738
883
|
|
|
739
884
|
// src/ast/parser.ts
|
|
@@ -962,6 +1107,27 @@ function isVersionAtLeast(version2, minVersion) {
|
|
|
962
1107
|
}
|
|
963
1108
|
return true;
|
|
964
1109
|
}
|
|
1110
|
+
async function checkCloneStatus(options) {
|
|
1111
|
+
let partialClone = false;
|
|
1112
|
+
let shallow = false;
|
|
1113
|
+
try {
|
|
1114
|
+
const shallowResult = await gitExec(
|
|
1115
|
+
["rev-parse", "--is-shallow-repository"],
|
|
1116
|
+
{ cwd: options?.cwd }
|
|
1117
|
+
);
|
|
1118
|
+
shallow = shallowResult.stdout.trim() === "true";
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const partialResult = await gitExec(
|
|
1123
|
+
["config", "--get", "extensions.partialclone"],
|
|
1124
|
+
{ cwd: options?.cwd }
|
|
1125
|
+
);
|
|
1126
|
+
partialClone = partialResult.stdout.trim().length > 0;
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
return { partialClone, shallow };
|
|
1130
|
+
}
|
|
965
1131
|
async function checkGitHealth(options) {
|
|
966
1132
|
const hints = [];
|
|
967
1133
|
let gitVersion = "0.0.0";
|
|
@@ -988,7 +1154,18 @@ async function checkGitHealth(options) {
|
|
|
988
1154
|
`Upgrade git to ${BLOOM_FILTER_MIN_VERSION.join(".")}+ for bloom filter support (current: ${gitVersion}).`
|
|
989
1155
|
);
|
|
990
1156
|
}
|
|
991
|
-
|
|
1157
|
+
const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
|
|
1158
|
+
if (cloneStatus.partialClone) {
|
|
1159
|
+
hints.push(
|
|
1160
|
+
"Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
if (cloneStatus.shallow) {
|
|
1164
|
+
hints.push(
|
|
1165
|
+
"Shallow repository detected. Ancestry-path results may be incomplete."
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
return { commitGraph, bloomFilter, gitVersion, hints, ...cloneStatus };
|
|
992
1169
|
}
|
|
993
1170
|
|
|
994
1171
|
// src/git/remote.ts
|
|
@@ -1081,7 +1258,7 @@ var GitHubAdapter = class {
|
|
|
1081
1258
|
return { authenticated: false, hostname: this.hostname };
|
|
1082
1259
|
}
|
|
1083
1260
|
}
|
|
1084
|
-
async getPRForCommit(sha) {
|
|
1261
|
+
async getPRForCommit(sha, options) {
|
|
1085
1262
|
if (this.scheduler.isRateLimited()) return null;
|
|
1086
1263
|
try {
|
|
1087
1264
|
const result = await shellExec(
|
|
@@ -1098,18 +1275,20 @@ var GitHubAdapter = class {
|
|
|
1098
1275
|
);
|
|
1099
1276
|
const prs = JSON.parse(result.stdout);
|
|
1100
1277
|
if (!isArray(prs) || prs.length === 0) return null;
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1278
|
+
let data = prs[0];
|
|
1279
|
+
if (options?.preferredBase) {
|
|
1280
|
+
const preferred = prs.find(
|
|
1281
|
+
(pr) => pr.base === options.preferredBase
|
|
1282
|
+
);
|
|
1283
|
+
if (preferred) data = preferred;
|
|
1284
|
+
}
|
|
1106
1285
|
return {
|
|
1107
1286
|
number: data.number,
|
|
1108
1287
|
title: data.title ?? "",
|
|
1109
1288
|
author: data.user ?? "",
|
|
1110
1289
|
url: data.html_url ?? "",
|
|
1111
1290
|
mergeCommit: data.merge_commit_sha ?? sha,
|
|
1112
|
-
baseBranch: data.base ??
|
|
1291
|
+
baseBranch: data.base ?? "",
|
|
1113
1292
|
mergedAt: data.merged_at
|
|
1114
1293
|
};
|
|
1115
1294
|
} catch {
|
|
@@ -1286,7 +1465,7 @@ var GitLabAdapter = class {
|
|
|
1286
1465
|
return { authenticated: false, hostname: this.hostname };
|
|
1287
1466
|
}
|
|
1288
1467
|
}
|
|
1289
|
-
async getPRForCommit(sha) {
|
|
1468
|
+
async getPRForCommit(sha, options) {
|
|
1290
1469
|
if (this.scheduler.isRateLimited()) return null;
|
|
1291
1470
|
try {
|
|
1292
1471
|
const result = await shellExec(
|
|
@@ -1310,18 +1489,20 @@ var GitLabAdapter = class {
|
|
|
1310
1489
|
return aTime - bTime;
|
|
1311
1490
|
});
|
|
1312
1491
|
if (mergedMRs.length === 0) return null;
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1492
|
+
let mr = mergedMRs[0];
|
|
1493
|
+
if (options?.preferredBase) {
|
|
1494
|
+
const preferred = mergedMRs.find(
|
|
1495
|
+
(m) => m.target_branch === options.preferredBase
|
|
1496
|
+
);
|
|
1497
|
+
if (preferred) mr = preferred;
|
|
1498
|
+
}
|
|
1318
1499
|
return {
|
|
1319
1500
|
number: mr.iid,
|
|
1320
1501
|
title: mr.title ?? "",
|
|
1321
1502
|
author: mr.author?.username ?? "",
|
|
1322
1503
|
url: mr.web_url ?? "",
|
|
1323
1504
|
mergeCommit: mr.merge_commit_sha ?? sha,
|
|
1324
|
-
baseBranch: mr.target_branch ??
|
|
1505
|
+
baseBranch: mr.target_branch ?? "",
|
|
1325
1506
|
mergedAt: mr.merged_at
|
|
1326
1507
|
};
|
|
1327
1508
|
} catch {
|
|
@@ -2098,6 +2279,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
|
|
|
2098
2279
|
|
|
2099
2280
|
// src/core/core.ts
|
|
2100
2281
|
init_pr_lookup2();
|
|
2282
|
+
function resolvedViaToTrackingMethod(resolvedVia) {
|
|
2283
|
+
switch (resolvedVia) {
|
|
2284
|
+
case "api":
|
|
2285
|
+
return "api";
|
|
2286
|
+
case "ancestry":
|
|
2287
|
+
return "ancestry-path";
|
|
2288
|
+
case "message":
|
|
2289
|
+
return "message-parse";
|
|
2290
|
+
case "patch-id":
|
|
2291
|
+
return "patch-id";
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function resolvedViaToConfidence(resolvedVia) {
|
|
2295
|
+
switch (resolvedVia) {
|
|
2296
|
+
case "api":
|
|
2297
|
+
case "ancestry":
|
|
2298
|
+
return "exact";
|
|
2299
|
+
case "message":
|
|
2300
|
+
case "patch-id":
|
|
2301
|
+
return "heuristic";
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2101
2304
|
function computeFeatureFlags(operatingLevel, options) {
|
|
2102
2305
|
return {
|
|
2103
2306
|
astDiff: isAstAvailable() && !options.noAst,
|
|
@@ -2115,6 +2318,22 @@ async function resolveRepoIdentity(cwd) {
|
|
|
2115
2318
|
return { host: "_local", owner: "_", repo: "_unknown" };
|
|
2116
2319
|
}
|
|
2117
2320
|
}
|
|
2321
|
+
async function resolveFileContext(file, cwd) {
|
|
2322
|
+
if (cwd || !isAbsolute(file)) return { file, cwd };
|
|
2323
|
+
const fileDir = dirname(file);
|
|
2324
|
+
try {
|
|
2325
|
+
const result = await gitExec(["rev-parse", "--show-toplevel"], {
|
|
2326
|
+
cwd: fileDir
|
|
2327
|
+
});
|
|
2328
|
+
const repoRoot = result.stdout.trim();
|
|
2329
|
+
return {
|
|
2330
|
+
file: relative(repoRoot, file),
|
|
2331
|
+
cwd: repoRoot
|
|
2332
|
+
};
|
|
2333
|
+
} catch {
|
|
2334
|
+
return { file, cwd };
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2118
2337
|
async function detectPlatform2(options) {
|
|
2119
2338
|
const warnings = [];
|
|
2120
2339
|
let adapter = null;
|
|
@@ -2161,7 +2380,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
|
|
|
2161
2380
|
}
|
|
2162
2381
|
return { analyzed: blameResult.value, operatingLevel, warnings };
|
|
2163
2382
|
}
|
|
2164
|
-
async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId) {
|
|
2383
|
+
async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
|
|
2165
2384
|
const nodes = [];
|
|
2166
2385
|
const commitNode = {
|
|
2167
2386
|
type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
|
|
@@ -2192,15 +2411,19 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
|
|
|
2192
2411
|
const prInfo = await lookupPR(targetSha, adapter, {
|
|
2193
2412
|
...execOptions,
|
|
2194
2413
|
noCache: options.noCache,
|
|
2414
|
+
cacheOnly: options.cacheOnly,
|
|
2195
2415
|
deep: featureFlags.deepTrace,
|
|
2196
|
-
repoId
|
|
2416
|
+
repoId,
|
|
2417
|
+
skipPatchIdScan,
|
|
2418
|
+
preferredBase,
|
|
2419
|
+
platform: adapter?.platform
|
|
2197
2420
|
});
|
|
2198
2421
|
if (prInfo) {
|
|
2199
2422
|
nodes.push({
|
|
2200
2423
|
type: "pull_request",
|
|
2201
2424
|
sha: prInfo.mergeCommit,
|
|
2202
|
-
trackingMethod: prInfo.
|
|
2203
|
-
confidence: prInfo.
|
|
2425
|
+
trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
|
|
2426
|
+
confidence: resolvedViaToConfidence(prInfo.resolvedVia),
|
|
2204
2427
|
prNumber: prInfo.number,
|
|
2205
2428
|
prUrl: prInfo.url || void 0,
|
|
2206
2429
|
prTitle: prInfo.title || void 0,
|
|
@@ -2210,24 +2433,35 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
|
|
|
2210
2433
|
}
|
|
2211
2434
|
return nodes;
|
|
2212
2435
|
}
|
|
2213
|
-
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId) {
|
|
2436
|
+
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
|
|
2214
2437
|
const results = await Promise.allSettled(
|
|
2215
2438
|
map8(
|
|
2216
2439
|
analyzed,
|
|
2217
|
-
(entry) => processEntry(
|
|
2440
|
+
(entry) => processEntry(
|
|
2441
|
+
entry,
|
|
2442
|
+
featureFlags,
|
|
2443
|
+
adapter,
|
|
2444
|
+
options,
|
|
2445
|
+
execOptions,
|
|
2446
|
+
repoId,
|
|
2447
|
+
skipPatchIdScan,
|
|
2448
|
+
preferredBase
|
|
2449
|
+
)
|
|
2218
2450
|
)
|
|
2219
2451
|
);
|
|
2220
2452
|
return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
|
|
2221
2453
|
}
|
|
2222
2454
|
var legacyCacheCleaned = false;
|
|
2223
2455
|
async function trace(options) {
|
|
2224
|
-
const
|
|
2456
|
+
const { file, cwd } = await resolveFileContext(options.file, options.cwd);
|
|
2457
|
+
const warnings = [];
|
|
2458
|
+
const execOptions = { cwd, warnings };
|
|
2225
2459
|
if (!legacyCacheCleaned) {
|
|
2226
2460
|
legacyCacheCleaned = true;
|
|
2227
2461
|
cleanupLegacyCache().catch(() => {
|
|
2228
2462
|
});
|
|
2229
2463
|
}
|
|
2230
|
-
const platform = await detectPlatform2(options);
|
|
2464
|
+
const platform = await detectPlatform2({ ...options, cwd });
|
|
2231
2465
|
let repoId;
|
|
2232
2466
|
if (platform.remote) {
|
|
2233
2467
|
repoId = {
|
|
@@ -2236,23 +2470,58 @@ async function trace(options) {
|
|
|
2236
2470
|
repo: platform.remote.repo
|
|
2237
2471
|
};
|
|
2238
2472
|
} else {
|
|
2239
|
-
repoId = await resolveRepoIdentity(
|
|
2473
|
+
repoId = await resolveRepoIdentity(cwd);
|
|
2240
2474
|
}
|
|
2241
2475
|
const blameAuth = await runBlameAndAuth(
|
|
2242
2476
|
platform.adapter,
|
|
2243
|
-
options,
|
|
2477
|
+
{ ...options, file, cwd },
|
|
2244
2478
|
execOptions
|
|
2245
2479
|
);
|
|
2246
2480
|
const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
|
|
2247
|
-
|
|
2481
|
+
warnings.push(...platform.warnings, ...blameAuth.warnings);
|
|
2482
|
+
if (options.cacheOnly && options.noCache) {
|
|
2483
|
+
warnings.push(
|
|
2484
|
+
"Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2248
2487
|
const featureFlags = computeFeatureFlags(operatingLevel, options);
|
|
2488
|
+
let cloneStatus = { partialClone: false, shallow: false };
|
|
2489
|
+
try {
|
|
2490
|
+
const result = await checkCloneStatus({ cwd });
|
|
2491
|
+
if (result) cloneStatus = result;
|
|
2492
|
+
} catch {
|
|
2493
|
+
}
|
|
2494
|
+
if (cloneStatus.partialClone) {
|
|
2495
|
+
warnings.push(
|
|
2496
|
+
"Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
if (cloneStatus.shallow) {
|
|
2500
|
+
warnings.push(
|
|
2501
|
+
"Shallow repository detected. Ancestry-path results may be incomplete."
|
|
2502
|
+
);
|
|
2503
|
+
}
|
|
2504
|
+
let preferredBase;
|
|
2505
|
+
try {
|
|
2506
|
+
const branchResult = await gitExec(
|
|
2507
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
2508
|
+
execOptions
|
|
2509
|
+
);
|
|
2510
|
+
const branch = branchResult.stdout.trim();
|
|
2511
|
+
if (branch && branch !== "HEAD") {
|
|
2512
|
+
preferredBase = branch;
|
|
2513
|
+
}
|
|
2514
|
+
} catch {
|
|
2515
|
+
}
|
|
2249
2516
|
const nodes = await buildTraceNodes(
|
|
2250
2517
|
blameAuth.analyzed,
|
|
2251
2518
|
featureFlags,
|
|
2252
2519
|
platform.adapter,
|
|
2253
|
-
options,
|
|
2520
|
+
{ ...options, file, cwd },
|
|
2254
2521
|
execOptions,
|
|
2255
|
-
repoId
|
|
2522
|
+
repoId,
|
|
2523
|
+
cloneStatus.partialClone || void 0,
|
|
2524
|
+
preferredBase
|
|
2256
2525
|
);
|
|
2257
2526
|
return { nodes, operatingLevel, featureFlags, warnings };
|
|
2258
2527
|
}
|
|
@@ -2459,7 +2728,7 @@ function formatNodeHuman(node) {
|
|
|
2459
2728
|
init_normalizer();
|
|
2460
2729
|
init_errors();
|
|
2461
2730
|
function registerTraceCommand(program2) {
|
|
2462
|
-
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("--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) => {
|
|
2731
|
+
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) => {
|
|
2463
2732
|
const lineStr = opts.line;
|
|
2464
2733
|
const parts = lineStr.split(",");
|
|
2465
2734
|
const line = parseInt(parts[0], 10);
|
|
@@ -2470,7 +2739,8 @@ function registerTraceCommand(program2) {
|
|
|
2470
2739
|
endLine,
|
|
2471
2740
|
deep: opts.deep,
|
|
2472
2741
|
noAst: opts.ast === false,
|
|
2473
|
-
noCache: opts.cache === false
|
|
2742
|
+
noCache: opts.cache === false,
|
|
2743
|
+
cacheOnly: opts.cacheOnly
|
|
2474
2744
|
};
|
|
2475
2745
|
const cliOptions = {
|
|
2476
2746
|
json: opts.json,
|
|
@@ -4,7 +4,21 @@ export interface AncestryResult {
|
|
|
4
4
|
parentShas: string[];
|
|
5
5
|
subject: string;
|
|
6
6
|
}
|
|
7
|
+
export declare const DEFAULT_ANCESTRY_TIMEOUT = 30000;
|
|
7
8
|
export declare function findMergeCommit(commitSha: string, options?: GitExecOptions & {
|
|
8
9
|
ref?: string;
|
|
9
10
|
}): Promise<AncestryResult | null>;
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Verify that a merge commit actually introduced the target commit
|
|
13
|
+
* through its branch side (non-first parent), not from the mainline.
|
|
14
|
+
*
|
|
15
|
+
* Dual condition:
|
|
16
|
+
* 1. Target IS an ancestor of at least one non-first parent (branch side)
|
|
17
|
+
* 2. Target is NOT an ancestor of the first parent (mainline side)
|
|
18
|
+
*
|
|
19
|
+
* Returns false on git command failure (fail-skip policy).
|
|
20
|
+
*/
|
|
21
|
+
export declare function verifyMergeIntroducesCommit(targetSha: string, mergeResult: AncestryResult, options?: GitExecOptions): Promise<boolean>;
|
|
22
|
+
/** Retrieve the subject line of a single commit. Returns null on git failure. */
|
|
23
|
+
export declare function getCommitSubject(sha: string, options?: GitExecOptions): Promise<string | null>;
|
|
24
|
+
export declare function extractPRFromMergeMessage(subject: string, platform?: string): number | null;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { extractPRFromMergeMessage, findMergeCommit } from './ancestry.js';
|
|
1
|
+
export { extractPRFromMergeMessage, findMergeCommit, getCommitSubject, verifyMergeIntroducesCommit, } from './ancestry.js';
|
|
2
2
|
export type { AncestryResult } from './ancestry.js';
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import type { RepoIdentity } from '../../cache/index.js';
|
|
2
2
|
import type { GitExecOptions, PRInfo, PlatformAdapter } from '../../types/index.js';
|
|
3
|
+
export type ResolvedVia = 'api' | 'ancestry' | 'message' | 'patch-id';
|
|
4
|
+
export interface PRLookupResult extends PRInfo {
|
|
5
|
+
resolvedVia: ResolvedVia;
|
|
6
|
+
}
|
|
3
7
|
export interface PRLookupOptions extends GitExecOptions {
|
|
4
8
|
noCache?: boolean;
|
|
9
|
+
/** Return cached results only — skip all fallback strategies */
|
|
10
|
+
cacheOnly?: boolean;
|
|
5
11
|
deep?: boolean;
|
|
6
12
|
repoId?: RepoIdentity;
|
|
13
|
+
/** Skip Strategy 5 (patch-id scan) — set automatically for partial clone environments */
|
|
14
|
+
skipPatchIdScan?: boolean;
|
|
15
|
+
/** Preferred base branch for PR selection — when multiple PRs match, prefer the one targeting this branch */
|
|
16
|
+
preferredBase?: string;
|
|
17
|
+
/** Platform type for platform-aware merge message parsing */
|
|
18
|
+
platform?: string;
|
|
7
19
|
}
|
|
8
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Multi-strategy PR lookup pipeline:
|
|
22
|
+
* Strategy 1: Cache
|
|
23
|
+
* Strategy 2: API direct (ground truth — Level 2)
|
|
24
|
+
* Strategy 3: Ancestry-path + merge commit verification (structural proof)
|
|
25
|
+
* Strategy 4: Blame commit message parsing (heuristic — squash merge detection)
|
|
26
|
+
* Strategy 5: Patch-ID matching + recursion (last resort)
|
|
27
|
+
*/
|
|
28
|
+
export declare function lookupPR(commitSha: string, adapter: PlatformAdapter | null, options?: PRLookupOptions,
|
|
29
|
+
/** @internal recursion depth tracker — do not set from external callers */
|
|
30
|
+
_recursionDepth?: number): Promise<PRLookupResult | null>;
|
|
9
31
|
export declare function resetPRCache(): void;
|