@applitools/core 4.53.1 → 4.54.0

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 CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.54.0](https://github.com/Applitools-Dev/sdk/compare/js/core@4.53.2...js/core@4.54.0) (2025-12-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * Baseline branch fallback list | FLD-3837 ([#3373](https://github.com/Applitools-Dev/sdk/issues/3373)) ([e94bb10](https://github.com/Applitools-Dev/sdk/commit/e94bb10ad6b49322a56e4ce6dfde560b237e9ac0))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * @applitools/nml-client bumped to 1.11.13
14
+
15
+ * @applitools/core-base bumped to 1.31.0
16
+ #### Features
17
+
18
+ * Baseline branch fallback list | FLD-3837 ([#3373](https://github.com/Applitools-Dev/sdk/issues/3373)) ([e94bb10](https://github.com/Applitools-Dev/sdk/commit/e94bb10ad6b49322a56e4ce6dfde560b237e9ac0))
19
+ * @applitools/ec-client bumped to 1.12.15
20
+
21
+
22
+ ## [4.53.2](https://github.com/Applitools-Dev/sdk/compare/js/core@4.53.1...js/core@4.53.2) (2025-12-07)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * Upgrade core version ([#3398](https://github.com/Applitools-Dev/sdk/issues/3398)) ([68858c7](https://github.com/Applitools-Dev/sdk/commit/68858c7024e0413c1cc6af68752b1c3a9a04bb0b))
28
+
29
+
30
+ ### Dependencies
31
+
32
+ * @applitools/dom-snapshot bumped to 4.15.3
33
+ #### Bug Fixes
34
+
35
+ * capture JavaScript-modified CSS selectors in nested [@layer](https://github.com/layer) rules ([#3391](https://github.com/Applitools-Dev/sdk/issues/3391)) ([b3bceb5](https://github.com/Applitools-Dev/sdk/commit/b3bceb5bfe894f3548173d23942e09d0e04b7e04))
36
+
3
37
  ## [4.53.1](https://github.com/Applitools-Dev/sdk/compare/js/core@4.53.0...js/core@4.53.1) (2025-12-01)
4
38
 
5
39
 
@@ -326,6 +326,7 @@ async function runOfflineSnapshots(options) {
326
326
  const checkLogger = logger.extend({ tags: ['open-check-and-close'] });
327
327
  const { mergedCheckSettings, baseTarget } = await throttledRender(target, checkLogger);
328
328
  const settings = (0, merge_configs_1.mergeConfigs)(target.settings, mergedCheckSettings);
329
+ const processId = utils.general.guid();
329
330
  return core.base.openCheckAndCloseEyes({
330
331
  target: { ...baseTarget, isTransformed: true },
331
332
  settings: {
@@ -338,9 +339,10 @@ async function runOfflineSnapshots(options) {
338
339
  },
339
340
  logger: checkLogger,
340
341
  heartbeat: {
341
- processId: '',
342
+ processId: processId,
342
343
  acquire(settings) {
343
344
  logger.log('acquire | heartbeat is not used (offline)', settings);
345
+ return processId;
344
346
  },
345
347
  release() {
346
348
  logger.log('release | heartbeat is not used (offline)');
package/dist/open-eyes.js CHANGED
@@ -44,6 +44,7 @@ function makeOpenEyes({ type: defaultType = 'classic', clients, batch, removeDup
44
44
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
45
45
  var _1, _2, _3, _4, _5;
46
46
  logger = logger.extend(mainLogger, { tags: [`eyes-${type}-${utils.general.shortid()}`] });
47
+ logger.log(`Opening Eyes in directory "${cwd}"`);
47
48
  const settings = { environments: config === null || config === void 0 ? void 0 : config.check.environments, ...config === null || config === void 0 ? void 0 : config.open, ...openSettings };
48
49
  const eyesServerSettings = (0, populate_eyes_server_settings_1.populateEyesServerSettings)(settings);
49
50
  logger.mask(eyesServerSettings.apiKey);
@@ -104,6 +105,7 @@ function makeOpenEyes({ type: defaultType = 'classic', clients, batch, removeDup
104
105
  }
105
106
  }
106
107
  const account = await core.getAccountInfo({ settings: eyesServerSettings, logger });
108
+ await handleBranchFallbackList(account, settings, logger);
107
109
  const componentConcurrency = account.serverConcurrency.componentConcurrency;
108
110
  settings.sessionConcurrency = handleConcurrency({
109
111
  userConcurrency: concurrency,
@@ -192,6 +194,35 @@ function makeOpenEyes({ type: defaultType = 'classic', clients, batch, removeDup
192
194
  }
193
195
  }
194
196
  };
197
+ async function handleBranchFallbackList(account, settings, logger) {
198
+ var _a, _b;
199
+ const scm = getScmSettings(account.scmSettings);
200
+ // Extract gitBranchName (the actual git branch name)
201
+ if (!settings.ignoreGitBranching && scm.lookupFallbackList) {
202
+ logger.log(`Extracting git branch name, ${cwd}`);
203
+ settings.gitBranchName = await (0, extract_git_info_1.extractGitBranch)({
204
+ execOptions: { cwd },
205
+ logger,
206
+ ignoreGitBranching: settings.ignoreGitBranching,
207
+ });
208
+ logger.log(`Extracted git branch name: ${settings.gitBranchName}`);
209
+ (_a = settings.branchName) !== null && _a !== void 0 ? _a : (settings.branchName = settings.gitBranchName);
210
+ // logger.log(`Using branch name: ${settings.branchName}`)
211
+ }
212
+ else {
213
+ logger.log(`Skipping extraction of git branch name as ignoreGitBranching is set to true`);
214
+ }
215
+ if (settings.gitBranchName && !settings.ignoreGitBranching && scm.lookupFallbackList) {
216
+ logger.log(`Extracting branch lookup fallback list for branch "${settings.gitBranchName}, ${cwd}"`);
217
+ // add write to coralogix before and after
218
+ settings.branchLookupFallbackList = await (0, extract_git_info_1.extractBranchLookupFallbackList)({
219
+ gitBranchName: `${settings.gitBranchName}`,
220
+ enableShallowClone: (_b = scm.enableShallowClone) !== null && _b !== void 0 ? _b : false,
221
+ execOptions: { cwd },
222
+ logger,
223
+ });
224
+ }
225
+ }
195
226
  }
196
227
  exports.makeOpenEyes = makeOpenEyes;
197
228
  function refineMissingApiKeyException(error, sdk) {
@@ -213,7 +244,6 @@ function refineMissingApiKeyException(error, sdk) {
213
244
  eyes_selenium: undefined,
214
245
  eyes_universal: undefined,
215
246
  eyes_capybara: undefined,
216
- eyes_calabash: undefined,
217
247
  },
218
248
  java: {
219
249
  java_appium: undefined,
@@ -269,6 +299,24 @@ exports.getLatestCommitInfoFromEnvVars = getLatestCommitInfoFromEnvVars;
269
299
  function isRealBatchId(batchId) {
270
300
  return !!batchId && !batchId.startsWith('generated');
271
301
  }
302
+ function getScmSettings(scmSettings) {
303
+ var _a, _b;
304
+ // shallow clone is enabled by default and is currently not sent from the server
305
+ let enableShallowClone = (_a = scmSettings.isShallowCloneEnabled) !== null && _a !== void 0 ? _a : true;
306
+ let lookupFallbackList = (_b = scmSettings.isFallbackBranchListEnabled) !== null && _b !== void 0 ? _b : false;
307
+ const envLookupFallbackList = utils.general.getEnvValue('ENABLE_BRANCH_LOOKUP_FALLBACK_LIST', 'boolean');
308
+ const envShallowClone = utils.general.getEnvValue('SKIP_BRANCH_LOOKUP_IN_SHALLOW_CLONE', 'boolean');
309
+ if (envLookupFallbackList !== undefined) {
310
+ lookupFallbackList = envLookupFallbackList;
311
+ }
312
+ if (envShallowClone !== undefined) {
313
+ enableShallowClone = !envShallowClone;
314
+ }
315
+ return {
316
+ lookupFallbackList,
317
+ enableShallowClone,
318
+ };
319
+ }
272
320
  function handleConcurrency({ userConcurrency, componentConcurrency, useServerConcurrency, shouldPrintConcurrencyWarning, logger, }) {
273
321
  let sessionConcurrency;
274
322
  // if user concurrency is 0 or negative, we ignore it and use server concurrency or default
@@ -26,12 +26,34 @@ 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.isISODate = exports.extractBranchingTimestamp = exports.extractBuildIdFromCI = exports.extractGitRepo = exports.extractGitBranch = exports.extractLatestCommitInfo = exports.cacheKey = void 0;
29
+ exports.extractBranchLookupFallbackList = exports.isISODate = exports.extractBranchingTimestamp = exports.extractBuildIdFromCI = exports.extractGitRepo = exports.extractGitBranch = exports.extractLatestCommitInfo = 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"));
33
33
  const logger_1 = require("@applitools/logger");
34
+ // Check if debug mode is enabled for verbose git info logging
35
+ const isDebugMode = () => process.env.RUNNER_DEBUG === '1';
34
36
  exports.cacheKey = 'default';
37
+ /**
38
+ * Get the primary remote name (cached for performance)
39
+ * Prefers 'origin' if it exists, otherwise uses the first available remote
40
+ */
41
+ exports.getPrimaryRemoteName = utils.general.cachify(async function ({ execOptions, logger = (0, logger_1.makeLogger)() }) {
42
+ const result = await executeWithLog('git remote show', { execOptions, logger });
43
+ if (result.stderr) {
44
+ logger.log(`Error during extracting remotes from git`, result.stderr);
45
+ return 'origin'; // Fallback to 'origin' if we can't determine remotes
46
+ }
47
+ const remotes = result.stdout.trim().split(/\s+/);
48
+ const remote = remotes.includes('origin') ? 'origin' : remotes[0] || 'origin';
49
+ logger.log(`Primary remote name: ${remote}`);
50
+ return remote;
51
+ }, args => {
52
+ var _a;
53
+ return ({
54
+ cwd: (_a = args[0].execOptions) === null || _a === void 0 ? void 0 : _a.cwd,
55
+ });
56
+ });
35
57
  exports.extractLatestCommitInfo = utils.general.cachify(async function ({ execOptions, logger = (0, logger_1.makeLogger)(), }) {
36
58
  let result;
37
59
  try {
@@ -59,7 +81,8 @@ exports.extractLatestCommitInfo = utils.general.cachify(async function ({ execOp
59
81
  async function extractGithubPullRequestLastCommitSha() {
60
82
  var _a, _b, _c;
61
83
  if (((_a = process.env.GITHUB_EVENT_NAME) === null || _a === void 0 ? void 0 : _a.startsWith('pull_request')) && process.env.GITHUB_EVENT_PATH) {
62
- await executeWithLog(`git fetch origin --depth=2`, { execOptions, logger });
84
+ const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
85
+ await executeWithLog(`git fetch ${remoteName} --depth=2`, { execOptions, logger });
63
86
  const event = await fs_1.default.promises.readFile(process.env.GITHUB_EVENT_PATH, 'utf-8').then(JSON.parse);
64
87
  return (_c = (_b = event === null || event === void 0 ? void 0 : event.pull_request) === null || _b === void 0 ? void 0 : _b.head) === null || _c === void 0 ? void 0 : _c.sha;
65
88
  }
@@ -70,7 +93,7 @@ exports.extractGitBranch = utils.general.cachify(async function ({ execOptions,
70
93
  return process.env.GITHUB_HEAD_REF;
71
94
  }
72
95
  if (process.env.GITHUB_REF) {
73
- return process.env.GITHUB_REF.split('/').splice(2).join('/'); // refs/heads/<branch_name>
96
+ return process.env.GITHUB_REF.split('/').splice(2).join('/');
74
97
  }
75
98
  const result = await executeWithLog('git branch --show-current', { execOptions, logger });
76
99
  if (result.stderr) {
@@ -81,7 +104,7 @@ exports.extractGitBranch = utils.general.cachify(async function ({ execOptions,
81
104
  logger.log(`Extracted current git branch: "${branch}"`);
82
105
  return branch;
83
106
  }
84
- }, () => exports.cacheKey);
107
+ }, args => { var _a, _b, _c; return ({ cwd: (_b = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.execOptions) === null || _b === void 0 ? void 0 : _b.cwd, ignoreGitBranching: (_c = args[0]) === null || _c === void 0 ? void 0 : _c.ignoreGitBranching }); });
85
108
  exports.extractGitRepo = utils.general.cachify(async function ({ execOptions, logger = (0, logger_1.makeLogger)() }) {
86
109
  const remotes = await extractRemotes();
87
110
  logger.log(`Extracted remotes from git: ${remotes}`);
@@ -125,22 +148,55 @@ exports.extractBuildIdFromCI = extractBuildIdFromCI;
125
148
  exports.extractBranchingTimestamp = utils.general.cachify(async function ({ branchName, parentBranchName, execOptions, logger = (0, logger_1.makeLogger)(), }) {
126
149
  var _a;
127
150
  logger = logger.extend({ tags: [`extract-branching-timestamp-${utils.general.shortid()}`] });
151
+ // Get the primary remote name (cached)
152
+ const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
153
+ // Step 1: Try with remote refs first (fast path - uses already-fetched remote data)
128
154
  const command = `HASH=$(git merge-base ${branchName} ${parentBranchName}) && git show -q --format=%aI $HASH`;
129
155
  let result = await executeWithLog(command, { execOptions, logger });
130
- // both parent and current branches could be missing, that's why we have to iterate the missing branches check check twice
131
- for (let i = 0; i < 2; i++) {
132
- if (result.stderr) {
133
- const [, missingBranch] = (_a = result.stderr.match(/Not a valid object name ([^\s]+)/)) !== null && _a !== void 0 ? _a : [];
134
- if (missingBranch) {
135
- result = await executeWithLog(`git fetch origin ${missingBranch}:${missingBranch} && ${command}`, {
136
- execOptions,
137
- logger,
138
- });
156
+ // Step 2: If remote refs failed, try local refs
157
+ if (result.stderr) {
158
+ const commandWithRemoteRefs = `HASH=$(git merge-base ${remoteName}/${branchName} ${remoteName}/${parentBranchName}) && git show -q --format=%aI $HASH`;
159
+ result = await executeWithLog(commandWithRemoteRefs, { execOptions, logger });
160
+ }
161
+ // Step 3: Handle missing branches with smart fetch (check remote existence first)
162
+ // Fetch remote branches list once if there's an error (cached call - virtually free)
163
+ if (result.stderr) {
164
+ const remoteBranches = await getAllRemoteBranches({ execOptions, logger });
165
+ // Both parent and current branches could be missing, iterate up to twice
166
+ for (let i = 0; i < 2; i++) {
167
+ if (result.stderr) {
168
+ const [, missingBranch] = (_a = result.stderr.match(/Not a valid object name ([^\s]+)/)) !== null && _a !== void 0 ? _a : [];
169
+ if (missingBranch) {
170
+ // Normalize branch name by removing remote prefix
171
+ const normalizedBranchName = missingBranch.replace(new RegExp(`^${remoteName}/`), '');
172
+ if (!remoteBranches.has(normalizedBranchName)) {
173
+ logger.log(`Branch ${missingBranch} not found on remote, skipping fetch`);
174
+ return undefined; // Exit early - no point in fetching non-existent branch
175
+ }
176
+ // Branch exists on remote, proceed with fetch
177
+ logger.log(`Fetching missing branch ${missingBranch} from remote`);
178
+ const command = `HASH=$(git merge-base ${branchName} ${parentBranchName}) && git show -q --format=%aI $HASH`;
179
+ /*
180
+ // --filter=tree:0 creates a treeless clone.
181
+ // These clones download all reachable commits while fetching trees and blobs on-demand.
182
+ // These clones are best for build environments where the repository will be deleted
183
+ // after a single build, but you still need access to commit history.
184
+ */
185
+ result = await executeWithLog(`git fetch ${remoteName} ${normalizedBranchName}:${normalizedBranchName} --filter=tree:0 && ${command}`, {
186
+ execOptions,
187
+ logger,
188
+ });
189
+ }
139
190
  }
140
191
  }
141
192
  }
193
+ // Step 4: Fallback to unshallow if still no result
142
194
  if (!result.stdout) {
143
- result = await executeWithLog(`git fetch origin --unshallow && ${command}`, { execOptions, logger });
195
+ const command = `HASH=$(git merge-base ${branchName} ${parentBranchName}) && git show -q --format=%aI $HASH`;
196
+ result = await executeWithLog(`git fetch ${remoteName} --unshallow --filter=tree:0 && ${command}`, {
197
+ execOptions,
198
+ logger,
199
+ });
144
200
  }
145
201
  const timestamp = result.stdout.replace(/\s/g, '');
146
202
  if (isISODate(timestamp)) {
@@ -150,7 +206,14 @@ exports.extractBranchingTimestamp = utils.general.cachify(async function ({ bran
150
206
  else {
151
207
  logger.log(`Error during extracting merge timestamp: git branching timestamp is an invalid ISO date string: ${timestamp}. stderr: ${result.stderr}, stdout: ${result.stdout}`);
152
208
  }
153
- }, () => exports.cacheKey);
209
+ }, args => {
210
+ var _a;
211
+ return ({
212
+ branchName: args[0].branchName,
213
+ parentBranchName: args[0].parentBranchName,
214
+ cwd: (_a = args[0].execOptions) === null || _a === void 0 ? void 0 : _a.cwd,
215
+ });
216
+ });
154
217
  async function executeWithLog(command, { execOptions, logger = (0, logger_1.makeLogger)() } = {
155
218
  execOptions: {},
156
219
  logger: (0, logger_1.makeLogger)(),
@@ -166,3 +229,469 @@ function isISODate(str) {
166
229
  return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\+\d{2}:\d{2})?/.test(str);
167
230
  }
168
231
  exports.isISODate = isISODate;
232
+ /**
233
+ * Execute operations in parallel with a concurrency limit to avoid overwhelming system resources
234
+ * @param items - Array of items to process
235
+ * @param concurrency - Maximum number of concurrent operations
236
+ * @param fn - Async function to execute for each item
237
+ * @returns Array of results in the same order as input items
238
+ */
239
+ async function parallelWithLimit(items, concurrency, fn) {
240
+ const results = [];
241
+ for (let i = 0; i < items.length; i += concurrency) {
242
+ const batch = items.slice(i, i + concurrency);
243
+ const batchResults = await Promise.all(batch.map(fn));
244
+ results.push(...batchResults);
245
+ }
246
+ return results;
247
+ }
248
+ /**
249
+ * Get all remote branches from the primary remote (cached for performance)
250
+ * Uses git ls-remote which is fast and doesn't require fetching data
251
+ * Cache TTL: 5 minutes (remote branches don't change frequently during a test run)
252
+ */
253
+ const getAllRemoteBranches = utils.general.cachify(async function ({ execOptions, logger }) {
254
+ var _a;
255
+ if (isDebugMode()) {
256
+ logger.log('[getAllRemoteBranches] Starting git ls-remote to fetch all remote branches...');
257
+ logger.log('[getAllRemoteBranches] execOptions.cwd:', (execOptions === null || execOptions === void 0 ? void 0 : execOptions.cwd) || 'undefined');
258
+ }
259
+ try {
260
+ // Get the primary remote name (cached)
261
+ const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
262
+ const startTime = Date.now();
263
+ const result = await executeWithLog(`git ls-remote --heads ${remoteName}`, {
264
+ execOptions,
265
+ logger,
266
+ });
267
+ if (isDebugMode()) {
268
+ logger.log(`[getAllRemoteBranches] git ls-remote completed in ${Date.now() - startTime}ms`);
269
+ logger.log('[getAllRemoteBranches] stdout length:', ((_a = result.stdout) === null || _a === void 0 ? void 0 : _a.length) || 0);
270
+ logger.log('[getAllRemoteBranches] stderr:', result.stderr || '(empty)');
271
+ logger.log('[getAllRemoteBranches] exit code:', result.code);
272
+ }
273
+ const branches = new Set();
274
+ if (result.stdout.trim()) {
275
+ const lines = result.stdout.split('\n');
276
+ if (isDebugMode()) {
277
+ logger.log(`[getAllRemoteBranches] Processing ${lines.length} lines from git ls-remote output`);
278
+ }
279
+ lines.forEach((line, index) => {
280
+ // Line format: "commit_sha\trefs/heads/branch-name"
281
+ const match = line.match(/refs\/heads\/(.+)$/);
282
+ if (match) {
283
+ const branchName = match[1];
284
+ branches.add(branchName);
285
+ if (isDebugMode() && index < 5) {
286
+ // Log first 5 branches for debugging
287
+ logger.log(`[getAllRemoteBranches] Found branch: ${branchName}`);
288
+ }
289
+ }
290
+ });
291
+ if (isDebugMode() && lines.length > 5) {
292
+ logger.log(`[getAllRemoteBranches] ... and ${lines.length - 5} more branches`);
293
+ }
294
+ }
295
+ else {
296
+ logger.log('[getAllRemoteBranches] WARNING: git ls-remote returned empty output');
297
+ }
298
+ logger.log(`[getAllRemoteBranches] ✓ Found ${branches.size} remote branches via git ls-remote`);
299
+ if (isDebugMode()) {
300
+ const branchList = Array.from(branches).slice(0, 20);
301
+ logger.log('[getAllRemoteBranches] Branch list:', branchList.join(', ') + (branches.size > 20 ? ` ... and ${branches.size - 20} more` : ''));
302
+ }
303
+ return branches;
304
+ }
305
+ catch (err) {
306
+ logger.log('[getAllRemoteBranches] ✗ ERROR: Could not fetch remote branches list');
307
+ logger.log('[getAllRemoteBranches] Error details:', err);
308
+ logger.log('[getAllRemoteBranches] Error type:', typeof err);
309
+ if (err instanceof Error) {
310
+ logger.log('[getAllRemoteBranches] Error message:', err.message);
311
+ logger.log('[getAllRemoteBranches] Error stack:', err.stack);
312
+ }
313
+ return new Set();
314
+ }
315
+ },
316
+ // Custom cache key generator - only cache by cwd, not by logger instance
317
+ args => { var _a; return ({ cwd: (_a = args[0].execOptions) === null || _a === void 0 ? void 0 : _a.cwd }); }, 5 * 60 * 1000);
318
+ /**
319
+ * Determine fetch strategy and execute appropriate fetch operation
320
+ */
321
+ async function executeFetchStrategy(isShallow, execOptions, logger) {
322
+ const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
323
+ if (isShallow) {
324
+ // Shallow clone needs full unshallow for initial topology discovery
325
+ logger.log(`Shallow repository detected, unshallowing to enable topology discovery...`);
326
+ await executeWithLog(`git fetch ${remoteName} --unshallow --filter=tree:0`, {
327
+ execOptions,
328
+ logger,
329
+ });
330
+ logger.log(`Repository unshallowed successfully`);
331
+ }
332
+ else {
333
+ // Non-shallow clone (e.g., single-branch clone) needs to fetch all remote branches for topology discovery
334
+ logger.log(`Non-shallow clone detected, fetching all remote branches for topology discovery...`);
335
+ await executeWithLog(`git fetch ${remoteName} --filter=tree:0`, {
336
+ execOptions,
337
+ logger,
338
+ });
339
+ logger.log(`All remote branches fetched successfully`);
340
+ }
341
+ }
342
+ /**
343
+ * Helper function to check if we're dealing with a remote branch scenario
344
+ * and fetch specific remote branches if needed for complete ancestor checking
345
+ *
346
+ * @param branchName - The current branch being analyzed
347
+ * @param branchesToFetch - Array of ancestor branch names that need to be fetched (empty = fetch all)
348
+ * @param isShallow - Whether the repository is a shallow clone
349
+ * @param execOptions - Git execution options
350
+ * @param logger - Logger instance
351
+ */
352
+ async function ensureRemoteBranchesAvailable(branchName, isShallow, execOptions, logger) {
353
+ try {
354
+ // Get the primary remote name (cached)
355
+ const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
356
+ // Check if current branch tracks a remote branch
357
+ const symbolicResult = await executeWithLog(`git rev-parse --abbrev-ref ${branchName}@{upstream} `, {
358
+ execOptions,
359
+ logger,
360
+ });
361
+ const isRemoteBranch = symbolicResult.stdout.trim().startsWith(`${remoteName}/`);
362
+ if (!isRemoteBranch) {
363
+ return;
364
+ }
365
+ logger.log(`Detected remote branch scenario, fetching ancestor branches...`);
366
+ // Configure fetch to get all remote branches
367
+ await executeWithLog(`git config remote.${remoteName}.fetch "+refs/heads/*:refs/remotes/${remoteName}/*"`, {
368
+ execOptions,
369
+ logger,
370
+ });
371
+ // Execute appropriate fetch strategy
372
+ await executeFetchStrategy(isShallow, execOptions, logger);
373
+ logger.log(`Remote branches fetched successfully for complete ancestor check`);
374
+ }
375
+ catch (err) {
376
+ logger.log('Note: Could not determine if branch is remote, continuing with topology discovery', err);
377
+ }
378
+ }
379
+ /**
380
+ * Extracts a list of ancestor branches for the given branch, ordered by most recent commit timestamp.
381
+ *
382
+ * Algorithm:
383
+ * 1. Check if shallow clone and should skip (via APPLITOOLS_SKIP_BRANCH_LOOKUP_IN_SHALLOW_CLONE)
384
+ * 2. Ensure remote branches are available (fetch if needed for single-branch/shallow clones)
385
+ * 3. Use git topology to discover ancestor branches efficiently (O(log N) via --first-parent --simplify-by-decoration)
386
+ * 4. For each discovered branch, extract the branching timestamp (latest viable commit where branch existed)
387
+ * 5. Sort results by timestamp descending (most recent first)
388
+ * 6. Return cached results (5-minute TTL) to avoid redundant git operations
389
+ *
390
+ * @param gitBranchName - The branch to analyze
391
+ * @param execOptions - Git execution options (e.g., cwd)
392
+ * @param logger - Logger instance for debugging
393
+ * @returns Array of ancestor branches with timestamps, or undefined if skipped
394
+ */
395
+ exports.extractBranchLookupFallbackList = utils.general.cachify(async function ({ gitBranchName, execOptions, logger = (0, logger_1.makeLogger)(), enableShallowClone = true, }) {
396
+ const functionStartTime = Date.now();
397
+ logger = logger.extend({ tags: [`extract-branch-fallback-list-${utils.general.shortid()}`] });
398
+ logger.log(`[PERF] extractBranchLookupFallbackList started for branch: ${gitBranchName}`);
399
+ try {
400
+ // 1. Check if this is a shallow clone and if we should skip based on env var
401
+ // add this to control of the user
402
+ const shallowCheckStartTime = Date.now();
403
+ const shallowCheckResult = await executeWithLog('git rev-parse --is-shallow-repository', {
404
+ execOptions,
405
+ logger,
406
+ });
407
+ logger.log(`[PERF] Shallow check took ${Date.now() - shallowCheckStartTime}ms`);
408
+ const isShallow = shallowCheckResult.stdout.trim() === 'true';
409
+ if (!enableShallowClone && isShallow) {
410
+ logger.log('Shallow clone detected and APPLITOOLS_SKIP_BRANCH_LOOKUP_IN_SHALLOW_CLONE is enabled, skipping branch lookup');
411
+ return undefined;
412
+ }
413
+ // 2. Ensure remote branches are available BEFORE topology discovery
414
+ // This is necessary for single-branch clones and shallow clones where branches don't exist locally
415
+ const ensureRemoteStartTime = Date.now();
416
+ await ensureRemoteBranchesAvailable(gitBranchName, isShallow, execOptions, logger);
417
+ logger.log(`[PERF] ensureRemoteBranchesAvailable took ${Date.now() - ensureRemoteStartTime}ms`);
418
+ // 3. OPTIMIZATION: Use git topology to discover only ancestor branches (O(log N) instead of O(N))
419
+ // Instead of listing all remote branches and checking each one, we walk the git graph
420
+ // only along the current branch's history using --first-parent and --simplify-by-decoration
421
+ const topologyStartTime = Date.now();
422
+ logger.log(`Discovering ancestor branches using git topology for ${gitBranchName}...`);
423
+ // Get the primary remote name (cached)
424
+ const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
425
+ const logResult = await executeWithLog(`git log --first-parent --simplify-by-decoration --format="%D" ${gitBranchName}`, { execOptions, logger });
426
+ const foundBranches = new Set();
427
+ if (!logResult.stderr && logResult.stdout.trim()) {
428
+ const rawLines = logResult.stdout.split('\n');
429
+ for (const line of rawLines) {
430
+ // Parse refs like: "HEAD -> feat, origin/feat, tag: v1, origin/main"
431
+ const refs = line.split(',').map(r => r.trim());
432
+ for (const ref of refs) {
433
+ if (ref.includes('tag:'))
434
+ continue; // Ignore tags
435
+ const cleanRef = ref.replace('HEAD -> ', '').replace(new RegExp(`^${remoteName}/`), ''); // Normalize
436
+ if (cleanRef && cleanRef !== gitBranchName && cleanRef !== 'HEAD')
437
+ foundBranches.add(cleanRef);
438
+ }
439
+ }
440
+ }
441
+ let allBranches = Array.from(foundBranches);
442
+ logger.log(`[PERF] Topology discovery took ${Date.now() - topologyStartTime}ms`);
443
+ if (isDebugMode()) {
444
+ logger.log(`Topology discovered ${allBranches.length} potential ancestor branches: ${allBranches.join(', ')}`);
445
+ }
446
+ else {
447
+ logger.log(`Topology discovered ${allBranches.length} potential ancestor branches`);
448
+ }
449
+ // 3. Filter out branches that don't exist on remote (if we can determine remote branches)
450
+ // This prevents unnecessary ancestor checks and fetch attempts for missing branches
451
+ const remoteBranchesStartTime = Date.now();
452
+ if (isDebugMode()) {
453
+ logger.log('[Remote Filtering] ========================================');
454
+ logger.log('[Remote Filtering] Starting remote branch filtering...');
455
+ logger.log('[Remote Filtering] Branches to check:', allBranches.join(', '));
456
+ logger.log('[Remote Filtering] Number of branches before filtering:', allBranches.length);
457
+ }
458
+ try {
459
+ if (isDebugMode()) {
460
+ logger.log('[Remote Filtering] Calling getAllRemoteBranches...');
461
+ }
462
+ const remoteBranches = await getAllRemoteBranches({ execOptions, logger });
463
+ if (isDebugMode()) {
464
+ logger.log('[Remote Filtering] getAllRemoteBranches returned');
465
+ logger.log('[Remote Filtering] Remote branches size:', remoteBranches.size);
466
+ const remoteBranchList = Array.from(remoteBranches).slice(0, 20);
467
+ logger.log('[Remote Filtering] Remote branches:', remoteBranchList.join(', ') + (remoteBranches.size > 20 ? ` ... and ${remoteBranches.size - 20} more` : ''));
468
+ }
469
+ if (remoteBranches.size > 0) {
470
+ const beforeFilter = allBranches.length;
471
+ if (isDebugMode()) {
472
+ logger.log('[Remote Filtering] Filtering branches against remote...');
473
+ // Log each branch check only in debug mode
474
+ allBranches.forEach(branch => {
475
+ const exists = remoteBranches.has(branch);
476
+ logger.log(`[Remote Filtering] Branch "${branch}": ${exists ? '✓ EXISTS' : '✗ MISSING'} on remote`);
477
+ });
478
+ }
479
+ allBranches = allBranches.filter(branch => remoteBranches.has(branch));
480
+ const filtered = beforeFilter - allBranches.length;
481
+ if (isDebugMode()) {
482
+ logger.log('[Remote Filtering] Branches after filtering:', allBranches.join(', '));
483
+ logger.log('[Remote Filtering] Number of branches after filtering:', allBranches.length);
484
+ }
485
+ if (filtered > 0) {
486
+ logger.log(`[Remote Filtering] ✓ Filtered out ${filtered} branches not found on remote`);
487
+ }
488
+ else if (isDebugMode()) {
489
+ logger.log('[Remote Filtering] No branches were filtered out (all exist on remote)');
490
+ }
491
+ }
492
+ else {
493
+ logger.log('[Remote Filtering] WARNING: No remote branches found, skipping filter');
494
+ }
495
+ }
496
+ catch (err) {
497
+ logger.log('[Remote Filtering] ✗ ERROR: Could not filter by remote branches');
498
+ if (isDebugMode()) {
499
+ logger.log('[Remote Filtering] Error details:', err);
500
+ logger.log('[Remote Filtering] Continuing with all discovered branches');
501
+ }
502
+ }
503
+ logger.log(`[Remote Filtering] [PERF] Remote branch filtering took ${Date.now() - remoteBranchesStartTime}ms`);
504
+ if (isDebugMode()) {
505
+ logger.log('[Remote Filtering] ========================================');
506
+ }
507
+ // 4. Filter out sibling branches - keep only TRUE ancestors
508
+ // A branch is a true ancestor if it's in the ancestry path of the current branch
509
+ // Use git merge-base --is-ancestor to check if candidate is an ancestor of current branch
510
+ const filteringStartTime = Date.now();
511
+ logger.log(`Filtering out sibling branches to keep only true ancestors...`);
512
+ // PERFORMANCE: Check ancestors in parallel with concurrency limit to avoid overwhelming git
513
+ const ANCESTOR_CHECK_CONCURRENCY = 10;
514
+ const ancestorChecks = await parallelWithLimit(allBranches, ANCESTOR_CHECK_CONCURRENCY, async (candidateBranch) => {
515
+ try {
516
+ // Check if candidate branch is an ancestor of current branch
517
+ // git merge-base --is-ancestor <commit> <commit> exits with status 0 if true, 1 if false
518
+ const isAncestorResult = await executeWithLog(`git merge-base --is-ancestor ${candidateBranch} ${gitBranchName}`, {
519
+ execOptions,
520
+ logger,
521
+ });
522
+ // Exit code 0 means it's an ancestor, exit code 1 means it's not
523
+ if (isAncestorResult.code === 0) {
524
+ logger.log(`✓ ${candidateBranch} is a true ancestor`);
525
+ return { branch: candidateBranch, isAncestor: true };
526
+ }
527
+ else if (isAncestorResult.code === 1) {
528
+ logger.log(`✗ ${candidateBranch} is a sibling, not an ancestor`);
529
+ return { branch: candidateBranch, isAncestor: false };
530
+ }
531
+ else {
532
+ logger.log(`⚠ Could not determine if ${candidateBranch} is an ancestor (exit code: ${isAncestorResult.code}), including it to be safe`);
533
+ return { branch: candidateBranch, isAncestor: true };
534
+ }
535
+ }
536
+ catch (err) {
537
+ logger.log(`Error checking if ${candidateBranch} is a true ancestor:`, err);
538
+ // If we can't determine, include it to be safe (better to have false positives than miss ancestors)
539
+ return { branch: candidateBranch, isAncestor: true };
540
+ }
541
+ });
542
+ const trueAncestors = ancestorChecks.filter(result => result.isAncestor).map(result => result.branch);
543
+ logger.log(`[PERF] Sibling filtering took ${Date.now() - filteringStartTime}ms`);
544
+ logger.log(`Filtered to ${trueAncestors.length} true ancestors: ${trueAncestors.join(', ')}`);
545
+ // 5. For each true ancestor branch, calculate the branching timestamp
546
+ const timestampStartTime = Date.now();
547
+ const branchesWithTimestamps = await Promise.all(trueAncestors.map(async (ancestorBranch) => {
548
+ const timestamp = await (0, exports.extractBranchingTimestamp)({
549
+ branchName: gitBranchName,
550
+ parentBranchName: ancestorBranch,
551
+ execOptions,
552
+ logger,
553
+ });
554
+ return timestamp ? { branchName: ancestorBranch, latestViableTimestamp: timestamp } : null;
555
+ }));
556
+ logger.log(`[PERF] Timestamp extraction took ${Date.now() - timestampStartTime}ms`);
557
+ // 6. Filter out null results and sort by timestamp (most recent first), then by branch name
558
+ const validBranches = branchesWithTimestamps
559
+ .filter((item) => item !== null)
560
+ .sort((a, b) => {
561
+ // Sort descending by timestamp (newest first)
562
+ const timeDiff = new Date(b.latestViableTimestamp).getTime() - new Date(a.latestViableTimestamp).getTime();
563
+ // If timestamps are equal, sort alphabetically by branch name
564
+ if (timeDiff === 0) {
565
+ return a.branchName.localeCompare(b.branchName);
566
+ }
567
+ return timeDiff;
568
+ });
569
+ // 7. Root branch fallback (safety net if no branches were found)
570
+ // The root branch is the branch that contains the first commit in the repository
571
+ try {
572
+ // Find the first commit (root commit)
573
+ const firstCommitResult = await executeWithLog('git rev-list --max-parents=0 HEAD', {
574
+ execOptions,
575
+ logger,
576
+ });
577
+ if (firstCommitResult.stdout.trim()) {
578
+ const firstCommitHash = firstCommitResult.stdout.trim().split('\n')[0];
579
+ logger.log(`Found root commit: ${firstCommitHash}`);
580
+ // Find which branches contain this root commit
581
+ // Try remote branches first, then fall back to local branches
582
+ let branchesContainingRootResult = await executeWithLog(`git branch -r --contains ${firstCommitHash}`, {
583
+ execOptions,
584
+ logger,
585
+ });
586
+ // If no remote branches found, try local branches
587
+ if (!branchesContainingRootResult.stdout.trim()) {
588
+ logger.log('No remote branches contain root commit, trying local branches');
589
+ branchesContainingRootResult = await executeWithLog(`git branch --contains ${firstCommitHash}`, {
590
+ execOptions,
591
+ logger,
592
+ });
593
+ }
594
+ if (branchesContainingRootResult.stdout.trim()) {
595
+ const branchesContainingRoot = branchesContainingRootResult.stdout
596
+ .split('\n')
597
+ .map(line => line.trim())
598
+ .filter(line => line && !line.includes('->') && !line.includes('HEAD'))
599
+ .map(line => line.replace(new RegExp(`^${remoteName}/`), '').replace(/^\* /, '')); // Remove remote prefix and '* ' prefix
600
+ // Try common root branch names first: main, master, develop
601
+ const commonRootNames = ['main', 'master', 'develop', 'trunk'];
602
+ let rootBranch = null;
603
+ for (const commonName of commonRootNames) {
604
+ if (branchesContainingRoot.includes(commonName)) {
605
+ rootBranch = commonName;
606
+ logger.log(`Found root branch using common name: ${rootBranch}`);
607
+ break;
608
+ }
609
+ }
610
+ // If no common name found, use the oldest branch (most likely the root)
611
+ if (!rootBranch && branchesContainingRoot.length > 0) {
612
+ // Find the branch with the oldest creation timestamp among those containing root commit
613
+ const branchTimestamps = await Promise.all(branchesContainingRoot.slice(0, 10).map(async (branch) => {
614
+ try {
615
+ const branchTimestampResult = await executeWithLog(`git log -1 --format=%aI ${branch}`, {
616
+ execOptions,
617
+ logger,
618
+ });
619
+ return {
620
+ branch,
621
+ timestamp: branchTimestampResult.stdout.trim(),
622
+ };
623
+ }
624
+ catch {
625
+ return null;
626
+ }
627
+ }));
628
+ const validTimestamps = branchTimestamps
629
+ .filter((item) => item !== null && isISODate(item.timestamp))
630
+ .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
631
+ if (validTimestamps.length > 0) {
632
+ rootBranch = validTimestamps[0].branch;
633
+ logger.log(`Found root branch by oldest timestamp: ${rootBranch}`);
634
+ }
635
+ }
636
+ // Add root branch if found and not already in the list
637
+ if (rootBranch && rootBranch !== gitBranchName) {
638
+ const alreadyIncluded = validBranches.some(b => b.branchName === rootBranch);
639
+ if (!alreadyIncluded) {
640
+ // Verify that root branch is actually an ancestor before adding it
641
+ // This prevents adding sibling branches that happen to contain the root commit
642
+ try {
643
+ const isAncestorResult = await executeWithLog(`git merge-base --is-ancestor ${rootBranch} ${gitBranchName}`, {
644
+ execOptions,
645
+ logger,
646
+ });
647
+ if (isAncestorResult.code === 0) {
648
+ logger.log(`Root branch ${rootBranch} is a true ancestor, adding it as final fallback`);
649
+ // Get the timestamp of the root commit (merge-base between current branch and root branch)
650
+ const rootTimestamp = await (0, exports.extractBranchingTimestamp)({
651
+ branchName: gitBranchName,
652
+ parentBranchName: rootBranch,
653
+ execOptions,
654
+ logger,
655
+ });
656
+ if (rootTimestamp) {
657
+ validBranches.push({
658
+ branchName: rootBranch,
659
+ latestViableTimestamp: rootTimestamp,
660
+ });
661
+ logger.log(`Added root branch ${rootBranch} with timestamp ${rootTimestamp}`);
662
+ }
663
+ }
664
+ else {
665
+ logger.log(`Root branch ${rootBranch} is not an ancestor of ${gitBranchName}, skipping`);
666
+ }
667
+ }
668
+ catch (err) {
669
+ logger.log(`Error checking if root branch ${rootBranch} is an ancestor:`, err);
670
+ // If we can't determine, don't add it (better to be conservative)
671
+ }
672
+ }
673
+ else {
674
+ logger.log(`Root branch ${rootBranch} already included in the list`);
675
+ }
676
+ }
677
+ }
678
+ }
679
+ }
680
+ catch (err) {
681
+ logger.log('Failed to detect and add root branch, continuing without it', err);
682
+ }
683
+ logger.log(`[PERF] extractBranchLookupFallbackList completed in ${Date.now() - functionStartTime}ms total`);
684
+ logger.log('Successfully extracted branch lookup fallback list', JSON.stringify(validBranches));
685
+ return validBranches.length > 0 ? validBranches : undefined;
686
+ }
687
+ catch (err) {
688
+ logger.log('Error during extracting branch lookup fallback list', err);
689
+ return undefined;
690
+ }
691
+ }, args => {
692
+ var _a;
693
+ return ({
694
+ gitBranchName: args[0].gitBranchName,
695
+ cwd: (_a = args[0].execOptions) === null || _a === void 0 ? void 0 : _a.cwd,
696
+ });
697
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/core",
3
- "version": "4.53.1",
3
+ "version": "4.54.0",
4
4
  "homepage": "https://applitools.com",
5
5
  "bugs": {
6
6
  "url": "https://github.com/applitools/eyes.sdk.javascript1/issues"
@@ -51,12 +51,13 @@
51
51
  "build:bin": "yarn build && sea",
52
52
  "build:bin:zip": "zip -j ./bin/core.zip $(find ./bin -type f -not -name '*.zip' -not -name '*.tar.gz' | xargs)",
53
53
  "build:bin:tgz": "tar -czf ./bin/core.tar.gz $(find ./bin -type f -not -name '*.zip' -not -name '*.tar.gz' | xargs)",
54
- "test": "run --top-level mocha './test/**/*.spec.ts' --exclude './test/bin/**' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
54
+ "test": "run --top-level mocha './test/**/*.spec.ts' --exclude './test/bin/**' --exclude './test/e2e/mocha-sync/**' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
55
55
  "test:local": "MOCHA_OMIT_TAGS=sauce,browserstack run test",
56
56
  "test:sauce": "MOCHA_ONLY_TAGS=sauce,browserstack run test",
57
57
  "test:bin": "MOCHA_GROUP=bin run --top-level mocha './test/bin/**/*.spec.ts' --parallel --jobs ${MOCHA_JOBS:-15} --require ./test/mocha-global-setup.js",
58
- "test:e2e": "MOCHA_GROUP=e2e run --top-level mocha './test/e2e/**/*.spec.ts' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
58
+ "test:e2e": "MOCHA_GROUP=e2e run --top-level mocha './test/e2e/**/*.spec.ts' --exclude './test/e2e/mocha-sync/**' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
59
59
  "test:it": "MOCHA_GROUP=it run --top-level mocha './test/it/**/*.spec.ts' --require ./test/mocha-global-setup.js",
60
+ "test:e2e:sync": "MOCHA_GROUP=e2e SYNC=true run --top-level mocha './test/e2e/mocha-sync/**/*.spec.ts' --exit --require ./test/mocha-global-setup.js",
60
61
  "test:unit": "MOCHA_GROUP=unit run --top-level mocha './test/unit/**/*.spec.ts' --require ./test/mocha-global-setup.js",
61
62
  "setup": "run --top-level browsers:setup && run --top-level xvfb:setup",
62
63
  "setup:standalone": "sh -c 'yarn chromedriver --port=4444 --verbose &'"
@@ -79,13 +80,13 @@
79
80
  }
80
81
  },
81
82
  "dependencies": {
82
- "@applitools/core-base": "1.30.1",
83
+ "@applitools/core-base": "1.31.0",
83
84
  "@applitools/dom-capture": "11.6.7",
84
- "@applitools/dom-snapshot": "4.15.2",
85
+ "@applitools/dom-snapshot": "4.15.3",
85
86
  "@applitools/driver": "1.24.3",
86
- "@applitools/ec-client": "1.12.14",
87
+ "@applitools/ec-client": "1.12.15",
87
88
  "@applitools/logger": "2.2.7",
88
- "@applitools/nml-client": "1.11.12",
89
+ "@applitools/nml-client": "1.11.13",
89
90
  "@applitools/req": "1.8.7",
90
91
  "@applitools/screenshoter": "3.12.10",
91
92
  "@applitools/snippets": "2.7.0",
@@ -4,6 +4,7 @@ import { type Logger } from '@applitools/logger';
4
4
  type Options = {
5
5
  execOptions?: ExecOptions;
6
6
  logger: Logger;
7
+ ignoreGitBranching?: boolean;
7
8
  };
8
9
  type ExtractGitBranchingTimestampOptions = {
9
10
  branchName: string;
@@ -16,6 +17,19 @@ type ExtractCurrentCommitTimestampOptions = {
16
17
  logger?: Logger;
17
18
  };
18
19
  export declare const cacheKey = "default";
20
+ /**
21
+ * Get the primary remote name (cached for performance)
22
+ * Prefers 'origin' if it exists, otherwise uses the first available remote
23
+ */
24
+ export declare const getPrimaryRemoteName: (({ execOptions, logger }: {
25
+ execOptions?: ExecOptions | undefined;
26
+ logger?: Logger | undefined;
27
+ }) => Promise<string>) & {
28
+ getCachedValues(): Promise<string>[];
29
+ setCachedValue(key: any, value: Promise<string>): void;
30
+ clearCache(): void;
31
+ TTL?: number | undefined;
32
+ };
19
33
  export declare const extractLatestCommitInfo: (({ execOptions, logger, }: ExtractCurrentCommitTimestampOptions) => Promise<{
20
34
  timestamp: string;
21
35
  sha: string;
@@ -60,4 +74,40 @@ export declare const extractBranchingTimestamp: (({ branchName, parentBranchName
60
74
  TTL?: number | undefined;
61
75
  };
62
76
  export declare function isISODate(str: string): boolean;
77
+ /**
78
+ * Extracts a list of ancestor branches for the given branch, ordered by most recent commit timestamp.
79
+ *
80
+ * Algorithm:
81
+ * 1. Check if shallow clone and should skip (via APPLITOOLS_SKIP_BRANCH_LOOKUP_IN_SHALLOW_CLONE)
82
+ * 2. Ensure remote branches are available (fetch if needed for single-branch/shallow clones)
83
+ * 3. Use git topology to discover ancestor branches efficiently (O(log N) via --first-parent --simplify-by-decoration)
84
+ * 4. For each discovered branch, extract the branching timestamp (latest viable commit where branch existed)
85
+ * 5. Sort results by timestamp descending (most recent first)
86
+ * 6. Return cached results (5-minute TTL) to avoid redundant git operations
87
+ *
88
+ * @param gitBranchName - The branch to analyze
89
+ * @param execOptions - Git execution options (e.g., cwd)
90
+ * @param logger - Logger instance for debugging
91
+ * @returns Array of ancestor branches with timestamps, or undefined if skipped
92
+ */
93
+ export declare const extractBranchLookupFallbackList: (({ gitBranchName, execOptions, logger, enableShallowClone, }: {
94
+ gitBranchName: string;
95
+ execOptions?: ExecOptions | undefined;
96
+ logger?: Logger | undefined;
97
+ enableShallowClone: boolean;
98
+ }) => Promise<Array<{
99
+ branchName: string;
100
+ latestViableTimestamp: string;
101
+ }> | undefined>) & {
102
+ getCachedValues(): Promise<{
103
+ branchName: string;
104
+ latestViableTimestamp: string;
105
+ }[] | undefined>[];
106
+ setCachedValue(key: any, value: Promise<{
107
+ branchName: string;
108
+ latestViableTimestamp: string;
109
+ }[] | undefined>): void;
110
+ clearCache(): void;
111
+ TTL?: number | undefined;
112
+ };
63
113
  export {};