@applitools/core 4.67.0 → 4.67.2
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/CHANGELOG.md +50 -0
- package/dist/open-eyes.js +70 -5
- package/dist/utils/extract-git-info.js +298 -116
- package/package.json +11 -11
- package/types/open-eyes.d.ts +7 -0
- package/types/utils/extract-git-info.d.ts +47 -42
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.67.2](https://github.com/Applitools-Dev/sdk/compare/js/core@4.67.1...js/core@4.67.2) (2026-07-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Dependencies
|
|
7
|
+
|
|
8
|
+
* @applitools/dom-snapshot bumped to 4.17.8
|
|
9
|
+
#### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* strip basic auth from stylesheet sheet.href | FLD-4738 ([#4036](https://github.com/Applitools-Dev/sdk/issues/4036)) ([2ff5259](https://github.com/Applitools-Dev/sdk/commit/2ff525919157598df8e22650a69f59efdb448311))
|
|
12
|
+
* use sheet.href for stylesheet links | FLD-4738 ([#4033](https://github.com/Applitools-Dev/sdk/issues/4033)) ([20d2e65](https://github.com/Applitools-Dev/sdk/commit/20d2e65fb83b9680729a0565e5f4301d2ea79e04))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
* @applitools/snippets bumped to 2.9.5
|
|
17
|
+
#### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* honor maxAmountToScroll in lazyLoad when scrollTop overshoots ([#4032](https://github.com/Applitools-Dev/sdk/issues/4032)) ([b4393d7](https://github.com/Applitools-Dev/sdk/commit/b4393d76cc9457c2a36896dc1bb6cbaa0c6ed2b4))
|
|
20
|
+
* @applitools/driver bumped to 1.26.7
|
|
21
|
+
|
|
22
|
+
* @applitools/spec-driver-webdriver bumped to 1.6.7
|
|
23
|
+
|
|
24
|
+
* @applitools/spec-driver-selenium bumped to 1.8.7
|
|
25
|
+
|
|
26
|
+
* @applitools/spec-driver-playwright bumped to 1.9.7
|
|
27
|
+
|
|
28
|
+
* @applitools/spec-driver-puppeteer bumped to 1.8.7
|
|
29
|
+
|
|
30
|
+
* @applitools/screenshoter bumped to 3.12.26
|
|
31
|
+
|
|
32
|
+
* @applitools/nml-client bumped to 1.11.38
|
|
33
|
+
|
|
34
|
+
* @applitools/ec-client bumped to 1.12.40
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## [4.67.1](https://github.com/Applitools-Dev/sdk/compare/js/core@4.67.0...js/core@4.67.1) (2026-07-29)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Bug Fixes
|
|
41
|
+
|
|
42
|
+
* capture CSS Font Loading API fonts in DOM snapshot | FLD-4754 ([#4024](https://github.com/Applitools-Dev/sdk/issues/4024)) ([3df71ed](https://github.com/Applitools-Dev/sdk/commit/3df71ede021072c7ad249d548465b72776672282))
|
|
43
|
+
* include related branches in git branch-lookup fallback list | AD-15279 ([#4015](https://github.com/Applitools-Dev/sdk/issues/4015)) ([bceb7e1](https://github.com/Applitools-Dev/sdk/commit/bceb7e1bbb98a1138105629eec446f94e328ae30))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Dependencies
|
|
47
|
+
|
|
48
|
+
* @applitools/dom-snapshot bumped to 4.17.7
|
|
49
|
+
#### Bug Fixes
|
|
50
|
+
|
|
51
|
+
* capture CSS Font Loading API fonts in DOM snapshot | FLD-4754 ([#4024](https://github.com/Applitools-Dev/sdk/issues/4024)) ([3df71ed](https://github.com/Applitools-Dev/sdk/commit/3df71ede021072c7ad249d548465b72776672282))
|
|
52
|
+
|
|
3
53
|
## [4.67.0](https://github.com/Applitools-Dev/sdk/compare/js/core@4.66.2...js/core@4.67.0) (2026-07-23)
|
|
4
54
|
|
|
5
55
|
|
package/dist/open-eyes.js
CHANGED
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.handleConcurrency = exports.getLatestCommitInfoFromEnvVars = exports.makeOpenEyes = void 0;
|
|
26
|
+
exports.handleConcurrency = exports.getLatestCommitInfoFromEnvVars = exports.makeOpenEyes = exports.sanitizeFinding = void 0;
|
|
27
27
|
const driver_1 = require("@applitools/driver");
|
|
28
28
|
const core_1 = require("./classic/core");
|
|
29
29
|
const core_2 = require("./ufg/core");
|
|
@@ -38,6 +38,24 @@ const utils = __importStar(require("@applitools/utils"));
|
|
|
38
38
|
const core_base_1 = require("@applitools/core-base");
|
|
39
39
|
const extract_git_info_1 = require("./utils/extract-git-info");
|
|
40
40
|
const types_1 = require("./types");
|
|
41
|
+
const reportedScmPreconditionFindings = new Map();
|
|
42
|
+
const scmPreconditionKey = (cwd, gitBranchName) => `${cwd} ${gitBranchName !== null && gitBranchName !== void 0 ? gitBranchName : ''}`;
|
|
43
|
+
const findingsHash = (findings) => findings
|
|
44
|
+
.map(finding => finding.code)
|
|
45
|
+
.sort()
|
|
46
|
+
.join(',');
|
|
47
|
+
const CREDENTIALS_IN_URL = /([a-z][a-z0-9+.-]*:\/\/)[^/@\s]*@/gi;
|
|
48
|
+
const MAX_FINDING_DETAIL_LENGTH = 512;
|
|
49
|
+
// SECURITY: `detail` is `env.reason` / raw git stderr, so it can embed a remote URL with `user:token@` userinfo.
|
|
50
|
+
function sanitizeFinding(finding) {
|
|
51
|
+
// Pre-slice: CREDENTIALS_IN_URL backtracks quadratically on remote-controlled git stderr (≤1 MB via exec maxBuffer).
|
|
52
|
+
let detail = finding.detail.slice(0, 4 * MAX_FINDING_DETAIL_LENGTH).replace(CREDENTIALS_IN_URL, '$1***@');
|
|
53
|
+
if (detail.length > MAX_FINDING_DETAIL_LENGTH) {
|
|
54
|
+
detail = `${detail.slice(0, MAX_FINDING_DETAIL_LENGTH - 1)}…`;
|
|
55
|
+
}
|
|
56
|
+
return { code: finding.code, detail };
|
|
57
|
+
}
|
|
58
|
+
exports.sanitizeFinding = sanitizeFinding;
|
|
41
59
|
function makeOpenEyes({ type: defaultType = 'classic', clients, batch, removeDuplicateTests, core, cores, spec, environment, logger: mainLogger, asyncCache, concurrency, useServerConcurrency = false, cwd = process.cwd(), }) {
|
|
42
60
|
let shouldPrintConcurrencyWarning = true;
|
|
43
61
|
return async function openEyes({ type = defaultType, settings: openSettings, config, target, logger = mainLogger, }) {
|
|
@@ -204,8 +222,9 @@ function makeOpenEyes({ type: defaultType = 'classic', clients, batch, removeDup
|
|
|
204
222
|
async function handleBranchFallbackList(account, settings, logger) {
|
|
205
223
|
var _a, _b;
|
|
206
224
|
const scm = getScmSettings(account.scmSettings);
|
|
225
|
+
const integrationActive = !settings.ignoreGitBranching && scm.lookupFallbackList;
|
|
207
226
|
// Extract gitBranchName (the actual git branch name)
|
|
208
|
-
if (
|
|
227
|
+
if (integrationActive) {
|
|
209
228
|
logger.log(`Extracting git branch name, ${cwd}`);
|
|
210
229
|
settings.gitBranchName = await (0, extract_git_info_1.extractGitBranch)({
|
|
211
230
|
execOptions: { cwd },
|
|
@@ -219,17 +238,63 @@ function makeOpenEyes({ type: defaultType = 'classic', clients, batch, removeDup
|
|
|
219
238
|
else {
|
|
220
239
|
logger.log(`Skipping extraction of git branch name as ignoreGitBranching is set to true`);
|
|
221
240
|
}
|
|
222
|
-
|
|
241
|
+
// No branch name means the extractor never runs, so probe the env here instead.
|
|
242
|
+
if (integrationActive && !settings.gitBranchName) {
|
|
243
|
+
const env = await (0, extract_git_info_1.checkGitEnvironment)({ execOptions: { cwd }, logger });
|
|
244
|
+
reportScmPreconditionFailure({
|
|
245
|
+
key: scmPreconditionKey(cwd),
|
|
246
|
+
account,
|
|
247
|
+
settings,
|
|
248
|
+
findings: (0, extract_git_info_1.gitEnvironmentFindings)(env),
|
|
249
|
+
logger,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (settings.gitBranchName && integrationActive) {
|
|
223
253
|
logger.log(`Extracting branch lookup fallback list for branch "${settings.gitBranchName}, ${cwd}"`);
|
|
224
|
-
|
|
225
|
-
settings.branchLookupFallbackList = await (0, extract_git_info_1.extractBranchLookupFallbackList)({
|
|
254
|
+
const outcome = await extract_git_info_1.extractBranchLookupFallbackList.withDiagnostics({
|
|
226
255
|
gitBranchName: `${settings.gitBranchName}`,
|
|
227
256
|
enableShallowClone: (_b = scm.enableShallowClone) !== null && _b !== void 0 ? _b : false,
|
|
228
257
|
execOptions: { cwd },
|
|
229
258
|
logger,
|
|
230
259
|
});
|
|
260
|
+
settings.branchLookupFallbackList = outcome.list;
|
|
261
|
+
if (outcome.status === 'precondition-failure') {
|
|
262
|
+
reportScmPreconditionFailure({
|
|
263
|
+
key: scmPreconditionKey(cwd, settings.gitBranchName),
|
|
264
|
+
account,
|
|
265
|
+
settings,
|
|
266
|
+
findings: outcome.diagnostics.findings,
|
|
267
|
+
defaultBranch: outcome.diagnostics.defaultBranch,
|
|
268
|
+
logger,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
231
271
|
}
|
|
232
272
|
}
|
|
273
|
+
function reportScmPreconditionFailure({ key, account, settings, findings, defaultBranch, logger, }) {
|
|
274
|
+
const hash = findingsHash(findings);
|
|
275
|
+
if (reportedScmPreconditionFindings.get(key) === hash)
|
|
276
|
+
return;
|
|
277
|
+
reportedScmPreconditionFindings.set(key, hash);
|
|
278
|
+
emitScmPreconditionFailureEvent({ account, settings, findings, defaultBranch, logger });
|
|
279
|
+
}
|
|
280
|
+
function emitScmPreconditionFailureEvent({ account, settings, findings, defaultBranch, logger, }) {
|
|
281
|
+
var _a;
|
|
282
|
+
void core.logEvent({
|
|
283
|
+
settings: {
|
|
284
|
+
...account.eyesServer,
|
|
285
|
+
level: 'Warn',
|
|
286
|
+
event: {
|
|
287
|
+
type: 'scmBranchLookupPreconditionFailure',
|
|
288
|
+
gitBranchName: settings.gitBranchName,
|
|
289
|
+
defaultBranch,
|
|
290
|
+
findings: findings.map(sanitizeFinding),
|
|
291
|
+
batchId: (_a = settings.batch) === null || _a === void 0 ? void 0 : _a.id,
|
|
292
|
+
userTestId: settings.userTestId,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
logger,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
233
298
|
}
|
|
234
299
|
exports.makeOpenEyes = makeOpenEyes;
|
|
235
300
|
function refineMissingApiKeyException(error, sdk) {
|
|
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.extractBranchLookupFallbackList = exports.getBranchAncestryByLocalBranches = exports.getBranchAncestryByCommits = exports.isISODate = exports.extractBranchingTimestamp = exports.extractBuildIdFromCI = exports.extractCIBranchName = exports.extractGitRepo = exports.extractGitBranch = exports.extractLatestCommitInfo = exports.extractDefaultBranch = exports.checkGitEnvironment = exports.resolveBranchToLocalRef = exports.getPrimaryRemoteName = exports.cacheKey = void 0;
|
|
29
|
+
exports.extractBranchLookupFallbackList = exports.getBranchAncestryByLocalBranches = exports.getBranchAncestryByCommits = exports.isISODate = exports.extractBranchingTimestamp = exports.extractBuildIdFromCI = exports.extractCIBranchName = exports.extractGitRepo = exports.extractGitBranch = exports.extractLatestCommitInfo = exports.extractDefaultBranch = exports.gitEnvironmentFindings = exports.checkGitEnvironment = exports.resolveBranchToLocalRef = exports.getPrimaryRemoteName = exports.cacheKey = void 0;
|
|
30
30
|
const utils = __importStar(require("@applitools/utils"));
|
|
31
31
|
const fs_1 = __importDefault(require("fs"));
|
|
32
32
|
const path_1 = __importDefault(require("path"));
|
|
@@ -211,6 +211,17 @@ exports.checkGitEnvironment = utils.general.cachify(async function ({ execOption
|
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
213
|
}, args => { var _a, _b; return ({ cwd: (_b = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.execOptions) === null || _b === void 0 ? void 0 : _b.cwd }); });
|
|
214
|
+
/** The finding `code`s are a telemetry wire contract — Coralogix dashboards match on them; do not rename. */
|
|
215
|
+
function gitEnvironmentFindings(env) {
|
|
216
|
+
if (!env.gitInstalled)
|
|
217
|
+
return [{ code: 'git-not-installed', detail: env.reason }];
|
|
218
|
+
if (!env.insideRepo)
|
|
219
|
+
return [{ code: 'not-a-work-tree', detail: env.reason }];
|
|
220
|
+
if (!env.remoteAccessible)
|
|
221
|
+
return [{ code: 'remote-unreachable', detail: env.reason }];
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
exports.gitEnvironmentFindings = gitEnvironmentFindings;
|
|
214
225
|
/**
|
|
215
226
|
* Grep-friendly env banner. The `APPLITOOLS GIT ENVIRONMENT CHECK` prefix is
|
|
216
227
|
* unique to this codepath, so support can find the full verdict in one line.
|
|
@@ -679,26 +690,28 @@ async function parallelWithLimit(items, concurrency, fn) {
|
|
|
679
690
|
}
|
|
680
691
|
return results;
|
|
681
692
|
}
|
|
693
|
+
// Ranks must stay distinct: sharing one drops the tie through to the alphabetical compare.
|
|
694
|
+
const RELATIONSHIP_RANK = {
|
|
695
|
+
lineage: 0,
|
|
696
|
+
'fork-point': 1,
|
|
697
|
+
'merge-absorbed': 2,
|
|
698
|
+
'equal-tip': 3,
|
|
699
|
+
base: 4,
|
|
700
|
+
unknown: 5,
|
|
701
|
+
};
|
|
702
|
+
function relationshipRank(relationship) {
|
|
703
|
+
var _a;
|
|
704
|
+
return (_a = RELATIONSHIP_RANK[relationship]) !== null && _a !== void 0 ? _a : RELATIONSHIP_RANK.unknown;
|
|
705
|
+
}
|
|
706
|
+
// NaN would make the comparator inconsistent and the whole sort undefined, so sink the entry instead.
|
|
707
|
+
function epochOrOldest(timestamp) {
|
|
708
|
+
const epoch = new Date(timestamp).getTime();
|
|
709
|
+
return Number.isNaN(epoch) ? -Number.MAX_VALUE : epoch;
|
|
710
|
+
}
|
|
682
711
|
/**
|
|
683
|
-
* Enumerate
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
* Inclusion contract (fork-depth — shares semantics with
|
|
688
|
-
* {@link getBranchAncestryByLocalBranches}):
|
|
689
|
-
* - `gitBranchName` is **always excluded** by the self check.
|
|
690
|
-
* - `defaultBranch` is **always kept** as the base: it defines the floor
|
|
691
|
-
* (`merge-base(HEAD, defaultBranch)`), so its own merge-base with HEAD equals
|
|
692
|
-
* the floor and would fail the strict fork-depth test by construction.
|
|
693
|
-
* - Descendants of HEAD are dropped (built on top of us; cannot be baselines).
|
|
694
|
-
* - A candidate `C` is kept iff it forked from HEAD's history **strictly later**
|
|
695
|
-
* than the default branch did — i.e. some `merge-base(HEAD, C)` is a strict
|
|
696
|
-
* descendant of the floor. Cousins that share only the floor (diverged no
|
|
697
|
-
* later than the default branch) are dropped. This prunes the over-inclusive
|
|
698
|
-
* candidate seed (the per-commit `--contains` walk pulls in cousins via
|
|
699
|
-
* criss-cross/ghost merges).
|
|
700
|
-
*
|
|
701
|
-
* Use {@link getBranchAncestryByLocalBranches} if you need both refs pinned.
|
|
712
|
+
* Enumerate RELATED (parent-shaped) branches in MERGE_BASE..gitBranchName,
|
|
713
|
+
* classified by keep-class. Returns a sorted, deduped list (no `refs/` prefix).
|
|
714
|
+
* Never throws — `[]` on git failure.
|
|
702
715
|
*/
|
|
703
716
|
async function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOptions, logger = (0, logger_1.makeLogger)(), }) {
|
|
704
717
|
logger = logger.extend({ tags: [`get-branch-ancestry-by-commits-${utils.general.shortid()}`] });
|
|
@@ -736,6 +749,24 @@ async function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOp
|
|
|
736
749
|
const mergeBase = mergeBaseResult.stdout.trim();
|
|
737
750
|
if (isDebugMode())
|
|
738
751
|
logger.log(`[byCommits] Merge-base resolved: ${mergeBase}`);
|
|
752
|
+
// Step 1b: HEAD's first-parent chain (floor, HEAD], plus every criss-cross floor base.
|
|
753
|
+
// Stays null if listing fails, which makes step 4's strict-lineage filter skip (KEEP).
|
|
754
|
+
let fpChain = null;
|
|
755
|
+
const fpChainResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'rev-list', '--first-parent', gitBranchName, `^${mergeBase}`]), { execOptions, logger });
|
|
756
|
+
if (fpChainResult.code === 0) {
|
|
757
|
+
fpChain = new Set(fpChainResult.stdout
|
|
758
|
+
.split('\n')
|
|
759
|
+
.map(line => line.trim())
|
|
760
|
+
.filter(Boolean));
|
|
761
|
+
const floorBases = await runGitContainsWithRetry((0, shell_quote_1.quote)(['git', 'merge-base', '--all', gitBranchName, defaultBranch]), { execOptions, logger });
|
|
762
|
+
for (const base of floorBases
|
|
763
|
+
.split('\n')
|
|
764
|
+
.map(line => line.trim())
|
|
765
|
+
.filter(Boolean)) {
|
|
766
|
+
fpChain.add(base);
|
|
767
|
+
}
|
|
768
|
+
fpChain.add(mergeBase);
|
|
769
|
+
}
|
|
739
770
|
// Step 2: enumerate every commit in the window MERGE_BASE..HEAD.
|
|
740
771
|
const logResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'log', '--format=%H', `${mergeBase}..${gitBranchName}`]), {
|
|
741
772
|
execOptions,
|
|
@@ -780,30 +811,14 @@ async function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOp
|
|
|
780
811
|
if (isDebugMode()) {
|
|
781
812
|
logger.log(`[byCommits] Unioned candidates before descendant filter (${candidates.size}): ${Array.from(candidates).join(', ')}`);
|
|
782
813
|
}
|
|
783
|
-
// Step 4:
|
|
784
|
-
//
|
|
785
|
-
// iff it forked from HEAD's history STRICTLY LATER than the default branch
|
|
786
|
-
// did. Concretely, per candidate C:
|
|
787
|
-
// (a) self — drop C if it normalizes to HEAD.
|
|
788
|
-
// default base — always keep the default branch: it defines the floor, so
|
|
789
|
-
// its own merge-base with HEAD equals the floor and fails
|
|
790
|
-
// the strict-descendant test (c) by construction; it is the
|
|
791
|
-
// base and is always kept.
|
|
792
|
-
// (b) descendant — drop C if HEAD is its ancestor (built on top of us).
|
|
793
|
-
// (c) fork-depth — keep C iff some merge-base(HEAD, C) is a STRICT
|
|
794
|
-
// descendant of the floor. Cousins that share only the floor
|
|
795
|
-
// (diverged no later than the default branch) are dropped.
|
|
796
|
-
// Criss-cross robustness: `git merge-base` without `--all` returns one
|
|
797
|
-
// unspecified base, which would make (c) nondeterministic in criss-cross
|
|
798
|
-
// topologies (e.g. ghost merges). Use `--all` and keep if ANY base is a
|
|
799
|
-
// strict descendant of the floor.
|
|
800
|
-
// Error policy throughout: an operational/transient git error → KEEP. A
|
|
801
|
-
// false-drop loses a real baseline forever; a false-keep is recoverable
|
|
802
|
-
// downstream.
|
|
814
|
+
// Step 4: per-candidate keep/drop classifier (`mergeBase` is the floor).
|
|
815
|
+
// Error policy throughout: transient git error → KEEP. A false-drop loses a baseline forever.
|
|
803
816
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
804
817
|
let droppedSelf = 0;
|
|
805
818
|
let droppedDescendants = 0;
|
|
806
819
|
let droppedCousins = 0;
|
|
820
|
+
let droppedAbsorbedSiblings = 0;
|
|
821
|
+
let droppedDiverged = 0;
|
|
807
822
|
const finalCandidates = await parallelWithLimit(Array.from(candidates), 10, async (branch) => {
|
|
808
823
|
// `branch` may be local (`staging`) or remote-tracking (`origin/staging`).
|
|
809
824
|
// Use the as-is form for git ops (git resolves both); strip only for the
|
|
@@ -812,23 +827,53 @@ async function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOp
|
|
|
812
827
|
const normalizedBranch = branch.replace(new RegExp(`^${remoteName}/`), '');
|
|
813
828
|
if (normalizedBranch === gitBranchName) {
|
|
814
829
|
droppedSelf++;
|
|
815
|
-
|
|
830
|
+
if (isDebugMode()) {
|
|
831
|
+
logger.log(`[byCommits] DROP self: ${normalizedBranch} — it is the branch under evaluation, not an ancestor`);
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
816
834
|
}
|
|
817
835
|
// Default branch always kept (see (c)-by-construction note above). Name
|
|
818
836
|
// compare rather than a first-parent check: the default branch is the base.
|
|
819
837
|
if (normalizedBranch === defaultBranch) {
|
|
820
838
|
if (isDebugMode())
|
|
821
839
|
logger.log(`[byCommits] KEEP base (default branch): ${normalizedBranch}`);
|
|
822
|
-
return normalizedBranch;
|
|
840
|
+
return { name: normalizedBranch, relationship: 'base' };
|
|
823
841
|
}
|
|
824
|
-
// (b)
|
|
825
|
-
// ancestor of the candidate?" → the candidate is a DESCENDANT of HEAD.
|
|
826
|
-
// Clean-true → drop. Clean-false / error → fall through to (c).
|
|
842
|
+
// (b) is HEAD an ancestor of the candidate? Clean-false/error → (c).
|
|
827
843
|
const isDescendant = await runGitPredicateWithRetry((0, shell_quote_1.quote)(['git', 'merge-base', '--is-ancestor', gitBranchName, branch]), { execOptions, logger });
|
|
828
844
|
if (isDescendant === true) {
|
|
845
|
+
const headShaResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'rev-parse', gitBranchName]), { execOptions, logger });
|
|
846
|
+
const candShaResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'rev-parse', branch]), { execOptions, logger });
|
|
847
|
+
const headSha = headShaResult.code === 0 ? headShaResult.stdout.trim() : '';
|
|
848
|
+
const candSha = candShaResult.code === 0 ? candShaResult.stdout.trim() : '';
|
|
849
|
+
if (headSha && headSha === candSha) {
|
|
850
|
+
if (isDebugMode()) {
|
|
851
|
+
logger.log(`[byCommits] KEEP equal-tip parent: ${normalizedBranch} — tips are the same commit ${headSha.slice(0, 9)} (fast-forward merge or fork with no own commits); previously dropped as a descendant`);
|
|
852
|
+
}
|
|
853
|
+
return { name: normalizedBranch, relationship: 'equal-tip' };
|
|
854
|
+
}
|
|
855
|
+
if (headSha) {
|
|
856
|
+
// Bound at HEAD~1, not HEAD: `^<sha>` excludes it, and the range must still contain the tip we look for.
|
|
857
|
+
let candFpResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'rev-list', '--first-parent', branch, `^${headSha}~1`]), { execOptions, logger });
|
|
858
|
+
if (candFpResult.code !== 0) {
|
|
859
|
+
// Root-commit edge (no HEAD~1) — fall back to a capped walk.
|
|
860
|
+
candFpResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'rev-list', '--first-parent', '-n', '5000', branch]), {
|
|
861
|
+
execOptions,
|
|
862
|
+
logger,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const onFirstParentChain = candFpResult.code === 0 && candFpResult.stdout.split('\n').some(line => line.trim() === headSha);
|
|
866
|
+
if (!onFirstParentChain) {
|
|
867
|
+
if (isDebugMode()) {
|
|
868
|
+
logger.log(`[byCommits] KEEP merge-absorbed parent: ${normalizedBranch} — ${gitBranchName} (${headSha.slice(0, 9)}) is reachable from it only via a merge commit's 2nd parent; the candidate merged this branch in, it was not built on top of it`);
|
|
869
|
+
}
|
|
870
|
+
return { name: normalizedBranch, relationship: 'merge-absorbed' };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
829
873
|
droppedDescendants++;
|
|
830
|
-
if (isDebugMode())
|
|
831
|
-
logger.log(`[byCommits] DROP descendant: ${normalizedBranch}`);
|
|
874
|
+
if (isDebugMode()) {
|
|
875
|
+
logger.log(`[byCommits] DROP descendant: ${normalizedBranch} — ${gitBranchName} is an ancestor of it (this branch was merged into / built on top of the candidate, so the candidate is downstream, not a parent)`);
|
|
876
|
+
}
|
|
832
877
|
return null;
|
|
833
878
|
}
|
|
834
879
|
// (c) fork-depth. Compute ALL merge bases of HEAD and the candidate.
|
|
@@ -844,12 +889,16 @@ async function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOp
|
|
|
844
889
|
if (mbCs.length === 0) {
|
|
845
890
|
if (isDebugMode())
|
|
846
891
|
logger.log(`[byCommits] KEEP (no merge-base resolved, conservative): ${normalizedBranch}`);
|
|
847
|
-
return normalizedBranch;
|
|
892
|
+
return { name: normalizedBranch, relationship: 'lineage' };
|
|
893
|
+
}
|
|
894
|
+
const candTipResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'rev-parse', branch]), { execOptions, logger });
|
|
895
|
+
const candTip = candTipResult.code === 0 ? candTipResult.stdout.trim() : '';
|
|
896
|
+
if (candTip && candTip === mergeBase) {
|
|
897
|
+
if (isDebugMode()) {
|
|
898
|
+
logger.log(`[byCommits] KEEP fork-point parent: ${normalizedBranch} — its tip IS the fork point ${mergeBase.slice(0, 9)} (the merge-base with ${gitBranchName}); this branch was checked out from it and the parent has not advanced; previously dropped as a cousin`);
|
|
899
|
+
}
|
|
900
|
+
return { name: normalizedBranch, relationship: 'fork-point' };
|
|
848
901
|
}
|
|
849
|
-
// Keep iff ANY base is a STRICT descendant of the floor: base != floor AND
|
|
850
|
-
// floor is an ancestor of base. A base == floor (or below it) means C
|
|
851
|
-
// diverged no later than the default branch → cousin for that base. On a
|
|
852
|
-
// transient error probing a base → treat as strict (conservative keep).
|
|
853
902
|
let anyStrictlyAboveFloor = false;
|
|
854
903
|
for (const mbC of mbCs) {
|
|
855
904
|
if (mbC === mergeBase)
|
|
@@ -859,23 +908,47 @@ async function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOp
|
|
|
859
908
|
anyStrictlyAboveFloor = true;
|
|
860
909
|
break;
|
|
861
910
|
}
|
|
862
|
-
// clean-false → mbC is strictly BELOW the floor (older) → not a fork point
|
|
863
|
-
// via this base; keep checking the remaining bases.
|
|
864
911
|
}
|
|
865
912
|
if (!anyStrictlyAboveFloor) {
|
|
866
913
|
droppedCousins++;
|
|
867
|
-
if (isDebugMode())
|
|
868
|
-
logger.log(`[byCommits] DROP cousin (shares only the floor): ${normalizedBranch}`);
|
|
914
|
+
if (isDebugMode()) {
|
|
915
|
+
logger.log(`[byCommits] DROP cousin (shares only the floor): ${normalizedBranch} — merge-base(${gitBranchName}, ${normalizedBranch}) = [${mbCs.join(', ')}], none is a strict descendant of floor ${mergeBase} (= merge-base with ${defaultBranch}); the branches only meet on the ${defaultBranch} trunk`);
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
if (fpChain && !mbCs.some(mbC => fpChain.has(mbC))) {
|
|
920
|
+
droppedAbsorbedSiblings++;
|
|
921
|
+
if (isDebugMode()) {
|
|
922
|
+
logger.log(`[byCommits] DROP absorbed-sibling: ${normalizedBranch} — merge-base(s) [${mbCs
|
|
923
|
+
.map(mbC => mbC.slice(0, 9))
|
|
924
|
+
.join(', ')}] reach ${gitBranchName} only via a merge's 2nd parent (content merged into this branch's ancestry), not via its own first-parent line`);
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
if (candTip && !mbCs.includes(candTip)) {
|
|
929
|
+
droppedDiverged++;
|
|
930
|
+
if (isDebugMode()) {
|
|
931
|
+
logger.log(`[byCommits] DROP diverged sibling: ${normalizedBranch} — has own commits beyond the shared point ${mbCs[0].slice(0, 9)}; a sibling's work, not this branch's ancestry`);
|
|
932
|
+
}
|
|
869
933
|
return null;
|
|
870
934
|
}
|
|
871
935
|
if (isDebugMode())
|
|
872
936
|
logger.log(`[byCommits] KEEP lineage point: ${normalizedBranch}`);
|
|
873
|
-
return normalizedBranch;
|
|
937
|
+
return { name: normalizedBranch, relationship: 'lineage' };
|
|
874
938
|
});
|
|
875
|
-
|
|
876
|
-
const
|
|
939
|
+
// Dedup by name — the same branch can surface both bare and `origin/`-prefixed.
|
|
940
|
+
const byName = new Map();
|
|
941
|
+
for (const entry of finalCandidates) {
|
|
942
|
+
if (!entry)
|
|
943
|
+
continue;
|
|
944
|
+
const existing = byName.get(entry.name);
|
|
945
|
+
if (!existing || relationshipRank(entry.relationship) < relationshipRank(existing.relationship)) {
|
|
946
|
+
byName.set(entry.name, entry);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const result = Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
877
950
|
if (isDebugMode()) {
|
|
878
|
-
logger.log(`[byCommits] Done. dropped(self=${droppedSelf}, descendants=${droppedDescendants}, cousins=${droppedCousins}) → ${result.length}
|
|
951
|
+
logger.log(`[byCommits] Done. dropped(self=${droppedSelf}, descendants=${droppedDescendants}, cousins=${droppedCousins}, absorbed-siblings=${droppedAbsorbedSiblings}, diverged=${droppedDiverged}) → ${result.length} related branch(es): ${result.map(entry => `${entry.name}(${entry.relationship})`).join(', ')}`);
|
|
879
952
|
}
|
|
880
953
|
return result;
|
|
881
954
|
}
|
|
@@ -890,10 +963,7 @@ exports.getBranchAncestryByCommits = getBranchAncestryByCommits;
|
|
|
890
963
|
* Rule 1 (fork-depth: candidate must have forked from HEAD strictly later than
|
|
891
964
|
* the default branch did — some merge-base with HEAD is a strict descendant of
|
|
892
965
|
* the primary MERGE_BASE) and Rule 2 (HEAD must not be an ancestor of the
|
|
893
|
-
* candidate).
|
|
894
|
-
* {@link getBranchAncestryByCommits}: cousins sharing only the floor are dropped
|
|
895
|
-
* in BOTH strategies, while the default branch and genuine lineage points are
|
|
896
|
-
* kept in both.
|
|
966
|
+
* candidate).
|
|
897
967
|
*
|
|
898
968
|
* Walks `refs/heads/` AND `refs/remotes/<remote>/` (stripping the prefix), since
|
|
899
969
|
* CI typically has one local branch and dozens of remote refs.
|
|
@@ -1139,33 +1209,49 @@ const getAllRemoteBranches = utils.general.cachify(async function ({ execOptions
|
|
|
1139
1209
|
// Cache by cwd only, not by logger instance.
|
|
1140
1210
|
args => { var _a; return ({ cwd: (_a = args[0].execOptions) === null || _a === void 0 ? void 0 : _a.cwd }); }, 5 * 60 * 1000);
|
|
1141
1211
|
/**
|
|
1142
|
-
* Determine fetch strategy and execute appropriate fetch
|
|
1143
|
-
*
|
|
1212
|
+
* Determine fetch strategy and execute the appropriate fetch. No-op when the remote isn't reachable.
|
|
1213
|
+
* `stillShallow` is re-probed after a failed unshallow, so callers can report an incomplete topology.
|
|
1144
1214
|
*/
|
|
1145
1215
|
async function executeFetchStrategy(isShallow, execOptions, logger) {
|
|
1146
1216
|
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
1147
1217
|
if (!env.remoteAccessible) {
|
|
1148
1218
|
logger.log('executeFetchStrategy: skipping fetch (remote not accessible)');
|
|
1149
|
-
return;
|
|
1219
|
+
return {};
|
|
1150
1220
|
}
|
|
1151
1221
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
1152
1222
|
if (isShallow) {
|
|
1153
1223
|
// Shallow clone → unshallow for topology discovery.
|
|
1154
1224
|
logger.log(`Shallow repository detected, unshallowing to enable topology discovery...`);
|
|
1155
|
-
await executeWithLog((0, shell_quote_1.quote)(['git', 'fetch', remoteName, '--unshallow', '--filter=blob:none']), {
|
|
1225
|
+
const unshallowResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'fetch', remoteName, '--unshallow', '--filter=blob:none']), {
|
|
1156
1226
|
execOptions,
|
|
1157
1227
|
logger,
|
|
1158
1228
|
});
|
|
1159
|
-
|
|
1229
|
+
if (unshallowResult.code === 0) {
|
|
1230
|
+
logger.log(`Repository unshallowed successfully`);
|
|
1231
|
+
return {};
|
|
1232
|
+
}
|
|
1233
|
+
logger.log(`Unshallow fetch failed (exit code ${unshallowResult.code})`);
|
|
1234
|
+
const verify = await executeWithLog('git rev-parse --is-shallow-repository', { execOptions, logger });
|
|
1235
|
+
const stillShallow = verify.stdout.trim() === 'true';
|
|
1236
|
+
if (stillShallow) {
|
|
1237
|
+
logger.log(`✗ repository is STILL SHALLOW after the unshallow attempt (exit ${unshallowResult.code}) — topology discovery will be incomplete`);
|
|
1238
|
+
}
|
|
1239
|
+
return { stillShallow };
|
|
1160
1240
|
}
|
|
1161
1241
|
else {
|
|
1162
1242
|
// Single-branch clone → fetch all remote branches for topology discovery.
|
|
1163
1243
|
logger.log(`Non-shallow clone detected, fetching all remote branches for topology discovery...`);
|
|
1164
|
-
await executeWithLog((0, shell_quote_1.quote)(['git', 'fetch', remoteName, '--filter=tree:0']), {
|
|
1244
|
+
const fetchResult = await executeWithLog((0, shell_quote_1.quote)(['git', 'fetch', remoteName, '--filter=tree:0']), {
|
|
1165
1245
|
execOptions,
|
|
1166
1246
|
logger,
|
|
1167
1247
|
});
|
|
1168
|
-
|
|
1248
|
+
if (fetchResult.code === 0) {
|
|
1249
|
+
logger.log(`All remote branches fetched successfully`);
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
logger.log(`All-branches fetch failed (exit code ${fetchResult.code})`);
|
|
1253
|
+
}
|
|
1254
|
+
return {};
|
|
1169
1255
|
}
|
|
1170
1256
|
}
|
|
1171
1257
|
/**
|
|
@@ -1177,7 +1263,7 @@ async function ensureRemoteBranchesAvailable(branchName, isShallow, execOptions,
|
|
|
1177
1263
|
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
1178
1264
|
if (!env.remoteAccessible) {
|
|
1179
1265
|
logger.log('ensureRemoteBranchesAvailable: skipping (remote not accessible)');
|
|
1180
|
-
return;
|
|
1266
|
+
return {};
|
|
1181
1267
|
}
|
|
1182
1268
|
try {
|
|
1183
1269
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
@@ -1188,13 +1274,12 @@ async function ensureRemoteBranchesAvailable(branchName, isShallow, execOptions,
|
|
|
1188
1274
|
});
|
|
1189
1275
|
const isRemoteBranch = symbolicResult.stdout.trim().startsWith(`${remoteName}/`);
|
|
1190
1276
|
if (!isRemoteBranch) {
|
|
1191
|
-
return;
|
|
1277
|
+
return {};
|
|
1192
1278
|
}
|
|
1193
1279
|
logger.log(`Detected remote branch scenario, fetching ancestor branches...`);
|
|
1194
1280
|
// Widen the refspec to fetch all remote branches, then fetch.
|
|
1195
1281
|
await executeWithLog((0, shell_quote_1.quote)(['git', 'config', `remote.${remoteName}.fetch`, `+refs/heads/*:refs/remotes/${remoteName}/*`]), { execOptions, logger });
|
|
1196
|
-
await executeFetchStrategy(isShallow, execOptions, logger);
|
|
1197
|
-
logger.log(`Remote branches fetched successfully for complete ancestor check`);
|
|
1282
|
+
const fetchOutcome = await executeFetchStrategy(isShallow, execOptions, logger);
|
|
1198
1283
|
// Single-branch clones / actions/checkout@v4 never write
|
|
1199
1284
|
// refs/remotes/<remote>/HEAD. Populate it so extractDefaultBranch step 2
|
|
1200
1285
|
// (symbolic-ref) succeeds. Non-fatal — steps 3-5 still cover us.
|
|
@@ -1204,35 +1289,25 @@ async function ensureRemoteBranchesAvailable(branchName, isShallow, execOptions,
|
|
|
1204
1289
|
catch (err) {
|
|
1205
1290
|
logger.log('ensureRemoteBranchesAvailable: remote set-head -a failed (continuing)', err);
|
|
1206
1291
|
}
|
|
1292
|
+
return fetchOutcome;
|
|
1207
1293
|
}
|
|
1208
1294
|
catch (err) {
|
|
1209
1295
|
logger.log('Note: Could not determine if branch is remote, continuing with topology discovery', err);
|
|
1296
|
+
return {};
|
|
1210
1297
|
}
|
|
1211
1298
|
}
|
|
1212
|
-
|
|
1213
|
-
* List ancestor branches for `gitBranchName`, ordered by most-recent timestamp.
|
|
1214
|
-
*
|
|
1215
|
-
* 1. Shallow-clone gate (skip via `enableShallowClone`, from server account-info).
|
|
1216
|
-
* 2. Fetch remote branches if needed (single-branch/shallow clones).
|
|
1217
|
-
* 3. Detect the actual default branch (`extractDefaultBranch` — non-standard names).
|
|
1218
|
-
* 4. Discover ancestors via `APPLITOOLS_BRANCH_ANCESTRY_STRATEGY`:
|
|
1219
|
-
* `commits` (default, walks MERGE_BASE..HEAD) | `local` (iterates refs).
|
|
1220
|
-
* 5. Extract each branch's branching timestamp.
|
|
1221
|
-
* 6. Sort by timestamp desc, tie-break by name.
|
|
1222
|
-
* 7. Root-branch safety net: fall back to the detected default branch.
|
|
1223
|
-
*
|
|
1224
|
-
* @returns ancestor branches with timestamps, or undefined if skipped.
|
|
1225
|
-
*/
|
|
1226
|
-
exports.extractBranchLookupFallbackList = utils.general.cachify(async function ({ gitBranchName, execOptions, logger = (0, logger_1.makeLogger)(), enableShallowClone = true, }) {
|
|
1299
|
+
const computeBranchLookupFallbackList = utils.general.cachify(async function ({ gitBranchName, execOptions, logger = (0, logger_1.makeLogger)(), enableShallowClone = true, }) {
|
|
1227
1300
|
const functionStartTime = Date.now();
|
|
1228
1301
|
logger = logger.extend({ tags: [`extract-branch-fallback-list-${utils.general.shortid()}`] });
|
|
1229
1302
|
if (isDebugMode())
|
|
1230
1303
|
logger.log(`[PERF] extractBranchLookupFallbackList started for branch: ${gitBranchName}`);
|
|
1304
|
+
// Findings accumulated en route, attached to whichever terminal failure fires.
|
|
1305
|
+
const findings = [];
|
|
1231
1306
|
// Env gate. Everything downstream shells out to git.
|
|
1232
1307
|
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
1233
1308
|
if (!env.ok) {
|
|
1234
1309
|
logger.log('extractBranchLookupFallbackList: git environment not ok, skipping');
|
|
1235
|
-
return
|
|
1310
|
+
return { status: 'precondition-failure', diagnostics: { findings: gitEnvironmentFindings(env) } };
|
|
1236
1311
|
}
|
|
1237
1312
|
try {
|
|
1238
1313
|
// 1. Shallow-clone gate: when the caller opted out AND the clone is
|
|
@@ -1247,12 +1322,27 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1247
1322
|
const isShallow = shallowCheckResult.stdout.trim() === 'true';
|
|
1248
1323
|
if (!enableShallowClone && isShallow) {
|
|
1249
1324
|
logger.log('Shallow clone detected and enableShallowClone=false, skipping branch lookup');
|
|
1250
|
-
return
|
|
1325
|
+
return {
|
|
1326
|
+
status: 'precondition-failure',
|
|
1327
|
+
diagnostics: {
|
|
1328
|
+
findings: [
|
|
1329
|
+
{ code: 'shallow-skip', detail: 'shallow clone and enableShallowClone=false — branch lookup skipped' },
|
|
1330
|
+
...findings,
|
|
1331
|
+
...gitEnvironmentFindings(env),
|
|
1332
|
+
],
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1251
1335
|
}
|
|
1252
1336
|
// 2. Fetch the other branches' refs so subsequent merge-base calls work
|
|
1253
1337
|
// (single-branch/shallow clones don't have them locally).
|
|
1254
1338
|
const ensureRemoteStartTime = Date.now();
|
|
1255
|
-
await ensureRemoteBranchesAvailable(gitBranchName, isShallow, execOptions, logger);
|
|
1339
|
+
const remoteAvailability = await ensureRemoteBranchesAvailable(gitBranchName, isShallow, execOptions, logger);
|
|
1340
|
+
if (remoteAvailability.stillShallow) {
|
|
1341
|
+
findings.push({
|
|
1342
|
+
code: 'still-shallow-after-unshallow',
|
|
1343
|
+
detail: 'repository remained shallow after the unshallow attempt — topology discovery incomplete',
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1256
1346
|
if (isDebugMode())
|
|
1257
1347
|
logger.log(`[PERF] ensureRemoteBranchesAvailable took ${Date.now() - ensureRemoteStartTime}ms`);
|
|
1258
1348
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
@@ -1270,26 +1360,50 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1270
1360
|
});
|
|
1271
1361
|
if (!resolvedDefaultBranch) {
|
|
1272
1362
|
logger.log(`extractBranchLookupFallbackList: default branch "${defaultBranch}" not resolvable locally or on remote, skipping ancestry`);
|
|
1273
|
-
return
|
|
1363
|
+
return {
|
|
1364
|
+
status: 'precondition-failure',
|
|
1365
|
+
diagnostics: {
|
|
1366
|
+
findings: [
|
|
1367
|
+
{
|
|
1368
|
+
code: 'default-branch-unresolvable',
|
|
1369
|
+
detail: `default branch "${defaultBranch}" not resolvable locally or on the remote`,
|
|
1370
|
+
},
|
|
1371
|
+
...findings,
|
|
1372
|
+
...gitEnvironmentFindings(env),
|
|
1373
|
+
],
|
|
1374
|
+
defaultBranch,
|
|
1375
|
+
},
|
|
1376
|
+
};
|
|
1274
1377
|
}
|
|
1275
1378
|
// 4. Discover ancestors: APPLITOOLS_BRANCH_ANCESTRY_STRATEGY=commits|local.
|
|
1276
1379
|
const ancestryStrategy = (process.env.APPLITOOLS_BRANCH_ANCESTRY_STRATEGY || 'commits').trim().toLowerCase();
|
|
1277
|
-
const ancestryDriver = ancestryStrategy === 'local' ? getBranchAncestryByLocalBranches : getBranchAncestryByCommits;
|
|
1278
1380
|
const ancestryStartTime = Date.now();
|
|
1279
1381
|
logger.log(`Discovering ancestor branches via "${ancestryStrategy}" strategy for ${gitBranchName} (default=${defaultBranch})...`);
|
|
1280
|
-
const ancestry =
|
|
1382
|
+
const ancestry = ancestryStrategy === 'local'
|
|
1383
|
+
? (await getBranchAncestryByLocalBranches({ gitBranchName, defaultBranch, execOptions, logger })).map(name => ({
|
|
1384
|
+
name,
|
|
1385
|
+
relationship: 'unknown',
|
|
1386
|
+
}))
|
|
1387
|
+
: await getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOptions, logger });
|
|
1281
1388
|
if (isDebugMode())
|
|
1282
1389
|
logger.log(`[PERF] Ancestry discovery took ${Date.now() - ancestryStartTime}ms`);
|
|
1283
1390
|
if (isDebugMode()) {
|
|
1284
|
-
logger.log(`Ancestry discovery found ${ancestry.length} candidate branches: ${ancestry
|
|
1391
|
+
logger.log(`Ancestry discovery found ${ancestry.length} candidate branches: ${ancestry
|
|
1392
|
+
.map(entry => entry.name)
|
|
1393
|
+
.join(', ')}`);
|
|
1285
1394
|
}
|
|
1286
1395
|
else {
|
|
1287
1396
|
logger.log(`Ancestry discovery found ${ancestry.length} candidate branches`);
|
|
1288
1397
|
}
|
|
1289
1398
|
// Strip the current branch (caller adds it back via timestamp logic).
|
|
1290
|
-
let allBranches = ancestry.filter(
|
|
1399
|
+
let allBranches = ancestry.filter(entry => entry.name !== gitBranchName);
|
|
1400
|
+
if (isDebugMode() && allBranches.length !== ancestry.length) {
|
|
1401
|
+
logger.log(`[FallbackList] DROP self: ${gitBranchName} — stripped from the ancestor list (it is the branch under evaluation)`);
|
|
1402
|
+
}
|
|
1291
1403
|
if (isDebugMode()) {
|
|
1292
|
-
logger.log(`Ancestry discovered ${allBranches.length} potential ancestor branches: ${allBranches
|
|
1404
|
+
logger.log(`Ancestry discovered ${allBranches.length} potential ancestor branches: ${allBranches
|
|
1405
|
+
.map(entry => entry.name)
|
|
1406
|
+
.join(', ')}`);
|
|
1293
1407
|
}
|
|
1294
1408
|
else {
|
|
1295
1409
|
logger.log(`Ancestry discovered ${allBranches.length} potential ancestor branches`);
|
|
@@ -1301,7 +1415,7 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1301
1415
|
if (isDebugMode()) {
|
|
1302
1416
|
logger.log('[Remote Filtering] ========================================');
|
|
1303
1417
|
logger.log('[Remote Filtering] Starting remote branch filtering...');
|
|
1304
|
-
logger.log('[Remote Filtering] Branches to check:', allBranches.join(', '));
|
|
1418
|
+
logger.log('[Remote Filtering] Branches to check:', allBranches.map(entry => entry.name).join(', '));
|
|
1305
1419
|
logger.log('[Remote Filtering] Number of branches before filtering:', allBranches.length);
|
|
1306
1420
|
}
|
|
1307
1421
|
try {
|
|
@@ -1319,15 +1433,20 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1319
1433
|
const beforeFilter = allBranches.length;
|
|
1320
1434
|
if (isDebugMode()) {
|
|
1321
1435
|
logger.log('[Remote Filtering] Filtering branches against remote...');
|
|
1322
|
-
allBranches.forEach(
|
|
1323
|
-
const exists = remoteBranches.has(
|
|
1324
|
-
logger.log(`[Remote Filtering] Branch "${
|
|
1436
|
+
allBranches.forEach(entry => {
|
|
1437
|
+
const exists = remoteBranches.has(entry.name);
|
|
1438
|
+
logger.log(`[Remote Filtering] Branch "${entry.name}": ${exists ? '✓ EXISTS' : '✗ MISSING'} on remote`);
|
|
1325
1439
|
});
|
|
1326
1440
|
}
|
|
1327
|
-
|
|
1441
|
+
if (isDebugMode()) {
|
|
1442
|
+
for (const entry of allBranches.filter(candidate => !remoteBranches.has(candidate.name))) {
|
|
1443
|
+
logger.log(`[Remote Filtering] DROP not-on-remote: ${entry.name} — exists locally but has no refs/heads/${entry.name} on the remote (per git ls-remote), so the server could never match a baseline for it`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
allBranches = allBranches.filter(entry => remoteBranches.has(entry.name));
|
|
1328
1447
|
const filtered = beforeFilter - allBranches.length;
|
|
1329
1448
|
if (isDebugMode()) {
|
|
1330
|
-
logger.log('[Remote Filtering] Branches after filtering:', allBranches.join(', '));
|
|
1449
|
+
logger.log('[Remote Filtering] Branches after filtering:', allBranches.map(entry => entry.name).join(', '));
|
|
1331
1450
|
logger.log('[Remote Filtering] Number of branches after filtering:', allBranches.length);
|
|
1332
1451
|
}
|
|
1333
1452
|
if (filtered > 0) {
|
|
@@ -1354,7 +1473,8 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1354
1473
|
}
|
|
1355
1474
|
// 5. Compute each surviving ancestor's branching timestamp in parallel.
|
|
1356
1475
|
const timestampStartTime = Date.now();
|
|
1357
|
-
const branchesWithTimestamps = await Promise.all(allBranches.map(async (
|
|
1476
|
+
const branchesWithTimestamps = await Promise.all(allBranches.map(async (ancestorEntry) => {
|
|
1477
|
+
const ancestorBranch = ancestorEntry.name;
|
|
1358
1478
|
let timestamp = await (0, exports.extractBranchingTimestamp)({
|
|
1359
1479
|
branchName: gitBranchName,
|
|
1360
1480
|
parentBranchName: ancestorBranch,
|
|
@@ -1386,19 +1506,25 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1386
1506
|
timestamp = tipTimestamp;
|
|
1387
1507
|
}
|
|
1388
1508
|
}
|
|
1389
|
-
|
|
1509
|
+
if (!timestamp && isDebugMode()) {
|
|
1510
|
+
logger.log(`[Timestamps] DROP no-timestamp: ${ancestorBranch} — no merge-base timestamp after retry AND the tip-commit-date fallback failed (branch unresolvable or non-ISO date); confirmed ancestor is lost from the final list`);
|
|
1511
|
+
}
|
|
1512
|
+
return timestamp
|
|
1513
|
+
? { branchName: ancestorBranch, latestViableTimestamp: timestamp, relationship: ancestorEntry.relationship }
|
|
1514
|
+
: null;
|
|
1390
1515
|
}));
|
|
1391
1516
|
if (isDebugMode())
|
|
1392
1517
|
logger.log(`[PERF] Timestamp extraction took ${Date.now() - timestampStartTime}ms`);
|
|
1393
|
-
// 6. Drop nulls; sort by timestamp desc, tie-break by branch name.
|
|
1394
1518
|
const validBranches = branchesWithTimestamps
|
|
1395
1519
|
.filter((item) => item !== null)
|
|
1396
1520
|
.sort((a, b) => {
|
|
1397
|
-
const timeDiff =
|
|
1398
|
-
if (timeDiff
|
|
1399
|
-
return
|
|
1400
|
-
|
|
1401
|
-
|
|
1521
|
+
const timeDiff = epochOrOldest(b.latestViableTimestamp) - epochOrOldest(a.latestViableTimestamp);
|
|
1522
|
+
if (timeDiff !== 0)
|
|
1523
|
+
return timeDiff;
|
|
1524
|
+
const rankDiff = relationshipRank(a.relationship) - relationshipRank(b.relationship);
|
|
1525
|
+
if (rankDiff !== 0)
|
|
1526
|
+
return rankDiff;
|
|
1527
|
+
return a.branchName.localeCompare(b.branchName);
|
|
1402
1528
|
});
|
|
1403
1529
|
// 7. Root-branch safety net (branch containing the repo's first commit).
|
|
1404
1530
|
try {
|
|
@@ -1481,6 +1607,7 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1481
1607
|
validBranches.push({
|
|
1482
1608
|
branchName: rootBranch,
|
|
1483
1609
|
latestViableTimestamp: rootTimestamp,
|
|
1610
|
+
relationship: 'base',
|
|
1484
1611
|
});
|
|
1485
1612
|
logger.log(`Added root branch ${rootBranch} with timestamp ${rootTimestamp}`);
|
|
1486
1613
|
}
|
|
@@ -1507,12 +1634,44 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1507
1634
|
if (isDebugMode()) {
|
|
1508
1635
|
logger.log(`[PERF] extractBranchLookupFallbackList completed in ${Date.now() - functionStartTime}ms total`);
|
|
1509
1636
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1637
|
+
// Deliberately not debug-gated: backend-side analysis consumes this line. `relationship` is log-only.
|
|
1638
|
+
logger.log(`Branch lookup fallback branches with relationship: ${validBranches
|
|
1639
|
+
.map(branch => `${branch.branchName}(${branch.relationship})`)
|
|
1640
|
+
.join(', ')}`);
|
|
1641
|
+
const wireList = validBranches.map(({ branchName, latestViableTimestamp }) => ({
|
|
1642
|
+
branchName,
|
|
1643
|
+
latestViableTimestamp,
|
|
1644
|
+
}));
|
|
1645
|
+
logger.log('Successfully extracted branch lookup fallback list', JSON.stringify(wireList));
|
|
1646
|
+
if (wireList.length > 0)
|
|
1647
|
+
return { status: 'ok', list: wireList, diagnostics: { findings } };
|
|
1648
|
+
// The default branch legitimately has no ancestors, so an empty list there is not a failure.
|
|
1649
|
+
if (gitBranchName === defaultBranch)
|
|
1650
|
+
return { status: 'ok', diagnostics: { findings, defaultBranch } };
|
|
1651
|
+
return {
|
|
1652
|
+
status: 'precondition-failure',
|
|
1653
|
+
diagnostics: {
|
|
1654
|
+
findings: [
|
|
1655
|
+
{ code: 'empty-list', detail: `no fallback branches found for "${gitBranchName}"` },
|
|
1656
|
+
...findings,
|
|
1657
|
+
...gitEnvironmentFindings(env),
|
|
1658
|
+
],
|
|
1659
|
+
defaultBranch,
|
|
1660
|
+
},
|
|
1661
|
+
};
|
|
1512
1662
|
}
|
|
1513
1663
|
catch (err) {
|
|
1514
1664
|
logger.log('Error during extracting branch lookup fallback list', err);
|
|
1515
|
-
return
|
|
1665
|
+
return {
|
|
1666
|
+
status: 'precondition-failure',
|
|
1667
|
+
diagnostics: {
|
|
1668
|
+
findings: [
|
|
1669
|
+
{ code: 'extraction-error', detail: err instanceof Error ? err.message : String(err) },
|
|
1670
|
+
...findings,
|
|
1671
|
+
...gitEnvironmentFindings(env),
|
|
1672
|
+
],
|
|
1673
|
+
},
|
|
1674
|
+
};
|
|
1516
1675
|
}
|
|
1517
1676
|
}, args => {
|
|
1518
1677
|
var _a;
|
|
@@ -1521,3 +1680,26 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
1521
1680
|
cwd: (_a = args[0].execOptions) === null || _a === void 0 ? void 0 : _a.cwd,
|
|
1522
1681
|
});
|
|
1523
1682
|
});
|
|
1683
|
+
/**
|
|
1684
|
+
* List ancestor branches for `gitBranchName`, ordered by most-recent timestamp.
|
|
1685
|
+
*
|
|
1686
|
+
* 1. Shallow-clone gate (skip via `enableShallowClone`, from server account-info).
|
|
1687
|
+
* 2. Fetch remote branches if needed (single-branch/shallow clones).
|
|
1688
|
+
* 3. Detect the actual default branch (`extractDefaultBranch` — non-standard names).
|
|
1689
|
+
* 4. Discover ancestors via `APPLITOOLS_BRANCH_ANCESTRY_STRATEGY`:
|
|
1690
|
+
* `commits` (default, walks MERGE_BASE..HEAD, classified) | `local` (iterates refs, unclassified).
|
|
1691
|
+
* 5. Extract each branch's branching timestamp.
|
|
1692
|
+
* 6. Sort by timestamp desc, tie-break by keep-class rank then name.
|
|
1693
|
+
* 7. Root-branch safety net: fall back to the detected default branch.
|
|
1694
|
+
*
|
|
1695
|
+
* For failure diagnostics call {@link extractBranchLookupFallbackList.withDiagnostics} —
|
|
1696
|
+
* it shares this function's cache entry, so it costs no extra git work.
|
|
1697
|
+
*
|
|
1698
|
+
* @returns ancestor branches with timestamps, or undefined if skipped.
|
|
1699
|
+
*/
|
|
1700
|
+
exports.extractBranchLookupFallbackList = Object.assign(async function (options) {
|
|
1701
|
+
return (await computeBranchLookupFallbackList(options)).list;
|
|
1702
|
+
}, {
|
|
1703
|
+
withDiagnostics: (options) => computeBranchLookupFallbackList(options),
|
|
1704
|
+
clearCache: computeBranchLookupFallbackList.clearCache,
|
|
1705
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applitools/core",
|
|
3
|
-
"version": "4.67.
|
|
3
|
+
"version": "4.67.2",
|
|
4
4
|
"homepage": "https://applitools.com",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/applitools/eyes.sdk.javascript1/issues"
|
|
@@ -63,14 +63,14 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@applitools/core-base": "1.37.0",
|
|
65
65
|
"@applitools/dom-capture": "11.8.4",
|
|
66
|
-
"@applitools/dom-snapshot": "4.17.
|
|
67
|
-
"@applitools/driver": "1.26.
|
|
68
|
-
"@applitools/ec-client": "1.12.
|
|
66
|
+
"@applitools/dom-snapshot": "4.17.8",
|
|
67
|
+
"@applitools/driver": "1.26.7",
|
|
68
|
+
"@applitools/ec-client": "1.12.40",
|
|
69
69
|
"@applitools/logger": "2.3.0",
|
|
70
|
-
"@applitools/nml-client": "1.11.
|
|
70
|
+
"@applitools/nml-client": "1.11.38",
|
|
71
71
|
"@applitools/req": "1.11.2",
|
|
72
|
-
"@applitools/screenshoter": "3.12.
|
|
73
|
-
"@applitools/snippets": "2.9.
|
|
72
|
+
"@applitools/screenshoter": "3.12.26",
|
|
73
|
+
"@applitools/snippets": "2.9.5",
|
|
74
74
|
"@applitools/socket": "1.3.15",
|
|
75
75
|
"@applitools/ufg-client": "1.22.5",
|
|
76
76
|
"@applitools/utils": "1.15.1",
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@applitools/bongo": "^5.10.0",
|
|
86
|
-
"@applitools/spec-driver-playwright": "^1.9.
|
|
87
|
-
"@applitools/spec-driver-puppeteer": "^1.8.
|
|
88
|
-
"@applitools/spec-driver-selenium": "^1.8.
|
|
89
|
-
"@applitools/spec-driver-webdriver": "^1.6.
|
|
86
|
+
"@applitools/spec-driver-playwright": "^1.9.7",
|
|
87
|
+
"@applitools/spec-driver-puppeteer": "^1.8.7",
|
|
88
|
+
"@applitools/spec-driver-selenium": "^1.8.7",
|
|
89
|
+
"@applitools/spec-driver-webdriver": "^1.6.7",
|
|
90
90
|
"@applitools/test-server": "^1.4.6",
|
|
91
91
|
"@applitools/test-utils": "^1.5.17",
|
|
92
92
|
"@applitools/tunnel-client": "^1.12.2",
|
package/types/open-eyes.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ type Options<TSpec extends SpecType, TType extends 'classic' | 'ufg'> = {
|
|
|
23
23
|
concurrency?: number;
|
|
24
24
|
useServerConcurrency?: boolean;
|
|
25
25
|
};
|
|
26
|
+
export declare function sanitizeFinding(finding: {
|
|
27
|
+
code: string;
|
|
28
|
+
detail: string;
|
|
29
|
+
}): {
|
|
30
|
+
code: string;
|
|
31
|
+
detail: string;
|
|
32
|
+
};
|
|
26
33
|
export declare function makeOpenEyes<TSpec extends SpecType, TDefaultType extends 'classic' | 'ufg' = 'classic'>({ type: defaultType, clients, batch, removeDuplicateTests, core, cores, spec, environment, logger: mainLogger, asyncCache, concurrency, useServerConcurrency, cwd, }: Options<TSpec, TDefaultType>): <TType extends "classic" | "ufg" = TDefaultType>({ type, settings: openSettings, config, target, logger, }: {
|
|
27
34
|
type?: TType | undefined;
|
|
28
35
|
settings?: Partial<OpenSettings<TDefaultType> & OpenSettings<TType>> | undefined;
|
|
@@ -88,6 +88,8 @@ export declare const checkGitEnvironment: (({ execOptions, logger, }: {
|
|
|
88
88
|
clearCache(): void;
|
|
89
89
|
TTL?: number | undefined;
|
|
90
90
|
};
|
|
91
|
+
/** The finding `code`s are a telemetry wire contract — Coralogix dashboards match on them; do not rename. */
|
|
92
|
+
export declare function gitEnvironmentFindings(env: GitEnvironmentResult): BranchLookupFinding[];
|
|
91
93
|
/**
|
|
92
94
|
* Detect the repo's default branch. Fallback chain (short-circuits on success;
|
|
93
95
|
* never throws):
|
|
@@ -176,37 +178,23 @@ type BranchAncestryOptions = {
|
|
|
176
178
|
execOptions?: ExecOptions;
|
|
177
179
|
logger?: Logger;
|
|
178
180
|
};
|
|
181
|
+
export type BranchRelationship = 'lineage' | 'fork-point' | 'merge-absorbed' | 'equal-tip' | 'base';
|
|
182
|
+
export type BranchAncestryEntry = {
|
|
183
|
+
name: string;
|
|
184
|
+
relationship: BranchRelationship;
|
|
185
|
+
};
|
|
179
186
|
/**
|
|
180
|
-
* Enumerate
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* Inclusion contract (fork-depth — shares semantics with
|
|
185
|
-
* {@link getBranchAncestryByLocalBranches}):
|
|
186
|
-
* - `gitBranchName` is **always excluded** by the self check.
|
|
187
|
-
* - `defaultBranch` is **always kept** as the base: it defines the floor
|
|
188
|
-
* (`merge-base(HEAD, defaultBranch)`), so its own merge-base with HEAD equals
|
|
189
|
-
* the floor and would fail the strict fork-depth test by construction.
|
|
190
|
-
* - Descendants of HEAD are dropped (built on top of us; cannot be baselines).
|
|
191
|
-
* - A candidate `C` is kept iff it forked from HEAD's history **strictly later**
|
|
192
|
-
* than the default branch did — i.e. some `merge-base(HEAD, C)` is a strict
|
|
193
|
-
* descendant of the floor. Cousins that share only the floor (diverged no
|
|
194
|
-
* later than the default branch) are dropped. This prunes the over-inclusive
|
|
195
|
-
* candidate seed (the per-commit `--contains` walk pulls in cousins via
|
|
196
|
-
* criss-cross/ghost merges).
|
|
197
|
-
*
|
|
198
|
-
* Use {@link getBranchAncestryByLocalBranches} if you need both refs pinned.
|
|
187
|
+
* Enumerate RELATED (parent-shaped) branches in MERGE_BASE..gitBranchName,
|
|
188
|
+
* classified by keep-class. Returns a sorted, deduped list (no `refs/` prefix).
|
|
189
|
+
* Never throws — `[]` on git failure.
|
|
199
190
|
*/
|
|
200
|
-
export declare function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOptions, logger, }: BranchAncestryOptions): Promise<
|
|
191
|
+
export declare function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOptions, logger, }: BranchAncestryOptions): Promise<BranchAncestryEntry[]>;
|
|
201
192
|
/**
|
|
202
193
|
* Enumerate ancestor branches by iterating every branch ref and applying
|
|
203
194
|
* Rule 1 (fork-depth: candidate must have forked from HEAD strictly later than
|
|
204
195
|
* the default branch did — some merge-base with HEAD is a strict descendant of
|
|
205
196
|
* the primary MERGE_BASE) and Rule 2 (HEAD must not be an ancestor of the
|
|
206
|
-
* candidate).
|
|
207
|
-
* {@link getBranchAncestryByCommits}: cousins sharing only the floor are dropped
|
|
208
|
-
* in BOTH strategies, while the default branch and genuine lineage points are
|
|
209
|
-
* kept in both.
|
|
197
|
+
* candidate).
|
|
210
198
|
*
|
|
211
199
|
* Walks `refs/heads/` AND `refs/remotes/<remote>/` (stripping the prefix), since
|
|
212
200
|
* CI typically has one local branch and dozens of remote refs.
|
|
@@ -215,6 +203,33 @@ export declare function getBranchAncestryByCommits({ gitBranchName, defaultBranc
|
|
|
215
203
|
* includes `defaultBranch` and `gitBranchName`.
|
|
216
204
|
*/
|
|
217
205
|
export declare function getBranchAncestryByLocalBranches({ gitBranchName, defaultBranch, execOptions, logger, }: BranchAncestryOptions): Promise<string[]>;
|
|
206
|
+
export type BranchLookupFinding = {
|
|
207
|
+
code: string;
|
|
208
|
+
detail: string;
|
|
209
|
+
};
|
|
210
|
+
export type BranchLookupDiagnostics = {
|
|
211
|
+
findings: BranchLookupFinding[];
|
|
212
|
+
defaultBranch?: string;
|
|
213
|
+
};
|
|
214
|
+
export type BranchLookupOutcome = {
|
|
215
|
+
status: 'ok';
|
|
216
|
+
/** Absent when there are legitimately no ancestors. */
|
|
217
|
+
list?: Array<{
|
|
218
|
+
branchName: string;
|
|
219
|
+
latestViableTimestamp: string;
|
|
220
|
+
}>;
|
|
221
|
+
diagnostics: BranchLookupDiagnostics;
|
|
222
|
+
} | {
|
|
223
|
+
status: 'precondition-failure';
|
|
224
|
+
list?: undefined;
|
|
225
|
+
diagnostics: BranchLookupDiagnostics;
|
|
226
|
+
};
|
|
227
|
+
export type BranchLookupFallbackListOptions = {
|
|
228
|
+
gitBranchName: string;
|
|
229
|
+
execOptions?: ExecOptions;
|
|
230
|
+
logger?: Logger;
|
|
231
|
+
enableShallowClone: boolean;
|
|
232
|
+
};
|
|
218
233
|
/**
|
|
219
234
|
* List ancestor branches for `gitBranchName`, ordered by most-recent timestamp.
|
|
220
235
|
*
|
|
@@ -222,31 +237,21 @@ export declare function getBranchAncestryByLocalBranches({ gitBranchName, defaul
|
|
|
222
237
|
* 2. Fetch remote branches if needed (single-branch/shallow clones).
|
|
223
238
|
* 3. Detect the actual default branch (`extractDefaultBranch` — non-standard names).
|
|
224
239
|
* 4. Discover ancestors via `APPLITOOLS_BRANCH_ANCESTRY_STRATEGY`:
|
|
225
|
-
* `commits` (default, walks MERGE_BASE..HEAD) | `local` (iterates refs).
|
|
240
|
+
* `commits` (default, walks MERGE_BASE..HEAD, classified) | `local` (iterates refs, unclassified).
|
|
226
241
|
* 5. Extract each branch's branching timestamp.
|
|
227
|
-
* 6. Sort by timestamp desc, tie-break by name.
|
|
242
|
+
* 6. Sort by timestamp desc, tie-break by keep-class rank then name.
|
|
228
243
|
* 7. Root-branch safety net: fall back to the detected default branch.
|
|
229
244
|
*
|
|
245
|
+
* For failure diagnostics call {@link extractBranchLookupFallbackList.withDiagnostics} —
|
|
246
|
+
* it shares this function's cache entry, so it costs no extra git work.
|
|
247
|
+
*
|
|
230
248
|
* @returns ancestor branches with timestamps, or undefined if skipped.
|
|
231
249
|
*/
|
|
232
|
-
export declare const extractBranchLookupFallbackList: ((
|
|
233
|
-
gitBranchName: string;
|
|
234
|
-
execOptions?: ExecOptions | undefined;
|
|
235
|
-
logger?: Logger | undefined;
|
|
236
|
-
enableShallowClone: boolean;
|
|
237
|
-
}) => Promise<Array<{
|
|
250
|
+
export declare const extractBranchLookupFallbackList: ((options: BranchLookupFallbackListOptions) => Promise<Array<{
|
|
238
251
|
branchName: string;
|
|
239
252
|
latestViableTimestamp: string;
|
|
240
253
|
}> | undefined>) & {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
latestViableTimestamp: string;
|
|
244
|
-
}[] | undefined>[];
|
|
245
|
-
setCachedValue(key: any, value: Promise<{
|
|
246
|
-
branchName: string;
|
|
247
|
-
latestViableTimestamp: string;
|
|
248
|
-
}[] | undefined>): void;
|
|
249
|
-
clearCache(): void;
|
|
250
|
-
TTL?: number | undefined;
|
|
254
|
+
withDiagnostics: (options: BranchLookupFallbackListOptions) => Promise<BranchLookupOutcome>;
|
|
255
|
+
clearCache: () => void;
|
|
251
256
|
};
|
|
252
257
|
export {};
|