@lumy-pack/line-lore 0.0.6 → 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/dist/cli.mjs +202 -47
- package/dist/core/ancestry/ancestry.d.ts +14 -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 +18 -2
- package/dist/index.cjs +201 -46
- package/dist/index.mjs +201 -46
- 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 +2 -0
- package/dist/types/git.d.ts +2 -0
- package/dist/types/platform.d.ts +3 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -386,6 +386,35 @@ async function findMergeCommit(commitSha, options) {
|
|
|
386
386
|
timeout: remaining
|
|
387
387
|
});
|
|
388
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
|
+
}
|
|
417
|
+
}
|
|
389
418
|
async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
|
|
390
419
|
try {
|
|
391
420
|
const result = await gitExec(
|
|
@@ -403,7 +432,25 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
|
|
|
403
432
|
);
|
|
404
433
|
const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
|
|
405
434
|
if (lines.length === 0) return null;
|
|
406
|
-
|
|
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;
|
|
407
454
|
} catch {
|
|
408
455
|
return null;
|
|
409
456
|
}
|
|
@@ -425,23 +472,38 @@ function parseMergeLogLine(line) {
|
|
|
425
472
|
const subject = parts.slice(subjectStart).join(" ");
|
|
426
473
|
return { mergeCommitSha, parentShas, subject };
|
|
427
474
|
}
|
|
428
|
-
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) {
|
|
429
488
|
const ghMatch = /Merge pull request #(\d+)/.exec(subject);
|
|
430
489
|
if (ghMatch) return parseInt(ghMatch[1], 10);
|
|
431
490
|
const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
|
|
432
491
|
if (squashMatch) return parseInt(squashMatch[1], 10);
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
}
|
|
435
496
|
const adoMatch = /Merged PR (\d+):/.exec(subject);
|
|
436
497
|
if (adoMatch) return parseInt(adoMatch[1], 10);
|
|
437
498
|
return null;
|
|
438
499
|
}
|
|
439
|
-
var DEFAULT_ANCESTRY_TIMEOUT;
|
|
500
|
+
var DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
|
|
440
501
|
var init_ancestry = __esm({
|
|
441
502
|
"src/core/ancestry/ancestry.ts"() {
|
|
442
503
|
"use strict";
|
|
443
504
|
init_executor();
|
|
444
505
|
DEFAULT_ANCESTRY_TIMEOUT = 3e4;
|
|
506
|
+
MAX_CANDIDATES = 10;
|
|
445
507
|
}
|
|
446
508
|
});
|
|
447
509
|
|
|
@@ -569,7 +631,8 @@ function toCachedPR(pr) {
|
|
|
569
631
|
url: pr.url,
|
|
570
632
|
mergeCommit: pr.mergeCommit,
|
|
571
633
|
baseBranch: pr.baseBranch,
|
|
572
|
-
mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0
|
|
634
|
+
mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
|
|
635
|
+
resolvedVia: pr.resolvedVia
|
|
573
636
|
};
|
|
574
637
|
}
|
|
575
638
|
function fromCachedPR(cached) {
|
|
@@ -584,7 +647,9 @@ function fromCachedPR(cached) {
|
|
|
584
647
|
url: cached.url,
|
|
585
648
|
mergeCommit: cached.mergeCommit,
|
|
586
649
|
baseBranch: cached.baseBranch,
|
|
587
|
-
mergedAt
|
|
650
|
+
mergedAt,
|
|
651
|
+
// Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
|
|
652
|
+
resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
|
|
588
653
|
};
|
|
589
654
|
}
|
|
590
655
|
async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
@@ -595,15 +660,30 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
|
595
660
|
const cached = await cache.get(commitSha);
|
|
596
661
|
if (cached) return fromCachedPR(cached);
|
|
597
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
|
+
}
|
|
598
672
|
let mergeBasedPR = null;
|
|
599
673
|
const mergeResult = await findMergeCommit(commitSha, options);
|
|
600
674
|
if (mergeResult) {
|
|
601
|
-
const prNumber = extractPRFromMergeMessage(
|
|
675
|
+
const prNumber = extractPRFromMergeMessage(
|
|
676
|
+
mergeResult.subject,
|
|
677
|
+
options?.platform
|
|
678
|
+
);
|
|
602
679
|
if (prNumber) {
|
|
603
680
|
if (adapter) {
|
|
604
|
-
const prInfo = await adapter.getPRForCommit(
|
|
681
|
+
const prInfo = await adapter.getPRForCommit(
|
|
682
|
+
mergeResult.mergeCommitSha,
|
|
683
|
+
prSelectOptions
|
|
684
|
+
);
|
|
605
685
|
if (prInfo?.mergedAt) {
|
|
606
|
-
mergeBasedPR = prInfo;
|
|
686
|
+
mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
|
|
607
687
|
}
|
|
608
688
|
}
|
|
609
689
|
if (!mergeBasedPR) {
|
|
@@ -613,7 +693,8 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
|
613
693
|
author: "",
|
|
614
694
|
url: "",
|
|
615
695
|
mergeCommit: mergeResult.mergeCommitSha,
|
|
616
|
-
baseBranch: ""
|
|
696
|
+
baseBranch: "",
|
|
697
|
+
resolvedVia: "ancestry"
|
|
617
698
|
};
|
|
618
699
|
}
|
|
619
700
|
if (!options?.deep || mergeBasedPR.mergedAt) {
|
|
@@ -626,11 +707,24 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
|
|
|
626
707
|
await cache.set(commitSha, toCachedPR(mergeBasedPR));
|
|
627
708
|
return mergeBasedPR;
|
|
628
709
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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;
|
|
634
728
|
}
|
|
635
729
|
}
|
|
636
730
|
if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
|
|
@@ -687,7 +781,7 @@ var VERSION;
|
|
|
687
781
|
var init_version = __esm({
|
|
688
782
|
"src/version.ts"() {
|
|
689
783
|
"use strict";
|
|
690
|
-
VERSION = "0.0.
|
|
784
|
+
VERSION = "0.0.7";
|
|
691
785
|
}
|
|
692
786
|
});
|
|
693
787
|
|
|
@@ -784,6 +878,7 @@ import { Command } from "commander";
|
|
|
784
878
|
|
|
785
879
|
// src/core/core.ts
|
|
786
880
|
import { createHash as createHash2 } from "crypto";
|
|
881
|
+
import { dirname, isAbsolute, relative } from "path";
|
|
787
882
|
import { map as map8 } from "@winglet/common-utils";
|
|
788
883
|
|
|
789
884
|
// src/ast/parser.ts
|
|
@@ -1062,7 +1157,7 @@ async function checkGitHealth(options) {
|
|
|
1062
1157
|
const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
|
|
1063
1158
|
if (cloneStatus.partialClone) {
|
|
1064
1159
|
hints.push(
|
|
1065
|
-
"Partial clone detected. Patch-ID scan (Strategy
|
|
1160
|
+
"Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
|
|
1066
1161
|
);
|
|
1067
1162
|
}
|
|
1068
1163
|
if (cloneStatus.shallow) {
|
|
@@ -1163,7 +1258,7 @@ var GitHubAdapter = class {
|
|
|
1163
1258
|
return { authenticated: false, hostname: this.hostname };
|
|
1164
1259
|
}
|
|
1165
1260
|
}
|
|
1166
|
-
async getPRForCommit(sha) {
|
|
1261
|
+
async getPRForCommit(sha, options) {
|
|
1167
1262
|
if (this.scheduler.isRateLimited()) return null;
|
|
1168
1263
|
try {
|
|
1169
1264
|
const result = await shellExec(
|
|
@@ -1180,18 +1275,20 @@ var GitHubAdapter = class {
|
|
|
1180
1275
|
);
|
|
1181
1276
|
const prs = JSON.parse(result.stdout);
|
|
1182
1277
|
if (!isArray(prs) || prs.length === 0) return null;
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
+
}
|
|
1188
1285
|
return {
|
|
1189
1286
|
number: data.number,
|
|
1190
1287
|
title: data.title ?? "",
|
|
1191
1288
|
author: data.user ?? "",
|
|
1192
1289
|
url: data.html_url ?? "",
|
|
1193
1290
|
mergeCommit: data.merge_commit_sha ?? sha,
|
|
1194
|
-
baseBranch: data.base ??
|
|
1291
|
+
baseBranch: data.base ?? "",
|
|
1195
1292
|
mergedAt: data.merged_at
|
|
1196
1293
|
};
|
|
1197
1294
|
} catch {
|
|
@@ -1368,7 +1465,7 @@ var GitLabAdapter = class {
|
|
|
1368
1465
|
return { authenticated: false, hostname: this.hostname };
|
|
1369
1466
|
}
|
|
1370
1467
|
}
|
|
1371
|
-
async getPRForCommit(sha) {
|
|
1468
|
+
async getPRForCommit(sha, options) {
|
|
1372
1469
|
if (this.scheduler.isRateLimited()) return null;
|
|
1373
1470
|
try {
|
|
1374
1471
|
const result = await shellExec(
|
|
@@ -1392,18 +1489,20 @@ var GitLabAdapter = class {
|
|
|
1392
1489
|
return aTime - bTime;
|
|
1393
1490
|
});
|
|
1394
1491
|
if (mergedMRs.length === 0) return null;
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
+
}
|
|
1400
1499
|
return {
|
|
1401
1500
|
number: mr.iid,
|
|
1402
1501
|
title: mr.title ?? "",
|
|
1403
1502
|
author: mr.author?.username ?? "",
|
|
1404
1503
|
url: mr.web_url ?? "",
|
|
1405
1504
|
mergeCommit: mr.merge_commit_sha ?? sha,
|
|
1406
|
-
baseBranch: mr.target_branch ??
|
|
1505
|
+
baseBranch: mr.target_branch ?? "",
|
|
1407
1506
|
mergedAt: mr.merged_at
|
|
1408
1507
|
};
|
|
1409
1508
|
} catch {
|
|
@@ -2180,6 +2279,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
|
|
|
2180
2279
|
|
|
2181
2280
|
// src/core/core.ts
|
|
2182
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
|
+
}
|
|
2183
2304
|
function computeFeatureFlags(operatingLevel, options) {
|
|
2184
2305
|
return {
|
|
2185
2306
|
astDiff: isAstAvailable() && !options.noAst,
|
|
@@ -2197,6 +2318,22 @@ async function resolveRepoIdentity(cwd) {
|
|
|
2197
2318
|
return { host: "_local", owner: "_", repo: "_unknown" };
|
|
2198
2319
|
}
|
|
2199
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
|
+
}
|
|
2200
2337
|
async function detectPlatform2(options) {
|
|
2201
2338
|
const warnings = [];
|
|
2202
2339
|
let adapter = null;
|
|
@@ -2243,7 +2380,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
|
|
|
2243
2380
|
}
|
|
2244
2381
|
return { analyzed: blameResult.value, operatingLevel, warnings };
|
|
2245
2382
|
}
|
|
2246
|
-
async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
|
|
2383
|
+
async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
|
|
2247
2384
|
const nodes = [];
|
|
2248
2385
|
const commitNode = {
|
|
2249
2386
|
type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
|
|
@@ -2277,14 +2414,16 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
|
|
|
2277
2414
|
cacheOnly: options.cacheOnly,
|
|
2278
2415
|
deep: featureFlags.deepTrace,
|
|
2279
2416
|
repoId,
|
|
2280
|
-
skipPatchIdScan
|
|
2417
|
+
skipPatchIdScan,
|
|
2418
|
+
preferredBase,
|
|
2419
|
+
platform: adapter?.platform
|
|
2281
2420
|
});
|
|
2282
2421
|
if (prInfo) {
|
|
2283
2422
|
nodes.push({
|
|
2284
2423
|
type: "pull_request",
|
|
2285
2424
|
sha: prInfo.mergeCommit,
|
|
2286
|
-
trackingMethod: prInfo.
|
|
2287
|
-
confidence: prInfo.
|
|
2425
|
+
trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
|
|
2426
|
+
confidence: resolvedViaToConfidence(prInfo.resolvedVia),
|
|
2288
2427
|
prNumber: prInfo.number,
|
|
2289
2428
|
prUrl: prInfo.url || void 0,
|
|
2290
2429
|
prTitle: prInfo.title || void 0,
|
|
@@ -2294,7 +2433,7 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
|
|
|
2294
2433
|
}
|
|
2295
2434
|
return nodes;
|
|
2296
2435
|
}
|
|
2297
|
-
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
|
|
2436
|
+
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
|
|
2298
2437
|
const results = await Promise.allSettled(
|
|
2299
2438
|
map8(
|
|
2300
2439
|
analyzed,
|
|
@@ -2305,7 +2444,8 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
|
|
|
2305
2444
|
options,
|
|
2306
2445
|
execOptions,
|
|
2307
2446
|
repoId,
|
|
2308
|
-
skipPatchIdScan
|
|
2447
|
+
skipPatchIdScan,
|
|
2448
|
+
preferredBase
|
|
2309
2449
|
)
|
|
2310
2450
|
)
|
|
2311
2451
|
);
|
|
@@ -2313,13 +2453,15 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
|
|
|
2313
2453
|
}
|
|
2314
2454
|
var legacyCacheCleaned = false;
|
|
2315
2455
|
async function trace(options) {
|
|
2316
|
-
const
|
|
2456
|
+
const { file, cwd } = await resolveFileContext(options.file, options.cwd);
|
|
2457
|
+
const warnings = [];
|
|
2458
|
+
const execOptions = { cwd, warnings };
|
|
2317
2459
|
if (!legacyCacheCleaned) {
|
|
2318
2460
|
legacyCacheCleaned = true;
|
|
2319
2461
|
cleanupLegacyCache().catch(() => {
|
|
2320
2462
|
});
|
|
2321
2463
|
}
|
|
2322
|
-
const platform = await detectPlatform2(options);
|
|
2464
|
+
const platform = await detectPlatform2({ ...options, cwd });
|
|
2323
2465
|
let repoId;
|
|
2324
2466
|
if (platform.remote) {
|
|
2325
2467
|
repoId = {
|
|
@@ -2328,15 +2470,15 @@ async function trace(options) {
|
|
|
2328
2470
|
repo: platform.remote.repo
|
|
2329
2471
|
};
|
|
2330
2472
|
} else {
|
|
2331
|
-
repoId = await resolveRepoIdentity(
|
|
2473
|
+
repoId = await resolveRepoIdentity(cwd);
|
|
2332
2474
|
}
|
|
2333
2475
|
const blameAuth = await runBlameAndAuth(
|
|
2334
2476
|
platform.adapter,
|
|
2335
|
-
options,
|
|
2477
|
+
{ ...options, file, cwd },
|
|
2336
2478
|
execOptions
|
|
2337
2479
|
);
|
|
2338
2480
|
const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
|
|
2339
|
-
|
|
2481
|
+
warnings.push(...platform.warnings, ...blameAuth.warnings);
|
|
2340
2482
|
if (options.cacheOnly && options.noCache) {
|
|
2341
2483
|
warnings.push(
|
|
2342
2484
|
"Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
|
|
@@ -2345,13 +2487,13 @@ async function trace(options) {
|
|
|
2345
2487
|
const featureFlags = computeFeatureFlags(operatingLevel, options);
|
|
2346
2488
|
let cloneStatus = { partialClone: false, shallow: false };
|
|
2347
2489
|
try {
|
|
2348
|
-
const result = await checkCloneStatus({ cwd
|
|
2490
|
+
const result = await checkCloneStatus({ cwd });
|
|
2349
2491
|
if (result) cloneStatus = result;
|
|
2350
2492
|
} catch {
|
|
2351
2493
|
}
|
|
2352
2494
|
if (cloneStatus.partialClone) {
|
|
2353
2495
|
warnings.push(
|
|
2354
|
-
"Partial clone detected. Patch-ID scan (Strategy
|
|
2496
|
+
"Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
|
|
2355
2497
|
);
|
|
2356
2498
|
}
|
|
2357
2499
|
if (cloneStatus.shallow) {
|
|
@@ -2359,14 +2501,27 @@ async function trace(options) {
|
|
|
2359
2501
|
"Shallow repository detected. Ancestry-path results may be incomplete."
|
|
2360
2502
|
);
|
|
2361
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
|
+
}
|
|
2362
2516
|
const nodes = await buildTraceNodes(
|
|
2363
2517
|
blameAuth.analyzed,
|
|
2364
2518
|
featureFlags,
|
|
2365
2519
|
platform.adapter,
|
|
2366
|
-
options,
|
|
2520
|
+
{ ...options, file, cwd },
|
|
2367
2521
|
execOptions,
|
|
2368
2522
|
repoId,
|
|
2369
|
-
cloneStatus.partialClone || void 0
|
|
2523
|
+
cloneStatus.partialClone || void 0,
|
|
2524
|
+
preferredBase
|
|
2370
2525
|
);
|
|
2371
2526
|
return { nodes, operatingLevel, featureFlags, warnings };
|
|
2372
2527
|
}
|
|
@@ -8,4 +8,17 @@ export declare const DEFAULT_ANCESTRY_TIMEOUT = 30000;
|
|
|
8
8
|
export declare function findMergeCommit(commitSha: string, options?: GitExecOptions & {
|
|
9
9
|
ref?: string;
|
|
10
10
|
}): Promise<AncestryResult | null>;
|
|
11
|
-
|
|
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,15 +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;
|
|
5
9
|
/** Return cached results only — skip all fallback strategies */
|
|
6
10
|
cacheOnly?: boolean;
|
|
7
11
|
deep?: boolean;
|
|
8
12
|
repoId?: RepoIdentity;
|
|
9
|
-
/** Skip Strategy
|
|
13
|
+
/** Skip Strategy 5 (patch-id scan) — set automatically for partial clone environments */
|
|
10
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;
|
|
11
19
|
}
|
|
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
|
+
*/
|
|
12
28
|
export declare function lookupPR(commitSha: string, adapter: PlatformAdapter | null, options?: PRLookupOptions,
|
|
13
29
|
/** @internal recursion depth tracker — do not set from external callers */
|
|
14
|
-
_recursionDepth?: number): Promise<
|
|
30
|
+
_recursionDepth?: number): Promise<PRLookupResult | null>;
|
|
15
31
|
export declare function resetPRCache(): void;
|