@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
|
-
|
|
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('/');
|
|
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
|
-
}, ()
|
|
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
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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.
|
|
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.
|
|
83
|
+
"@applitools/core-base": "1.31.0",
|
|
83
84
|
"@applitools/dom-capture": "11.6.7",
|
|
84
|
-
"@applitools/dom-snapshot": "4.15.
|
|
85
|
+
"@applitools/dom-snapshot": "4.15.3",
|
|
85
86
|
"@applitools/driver": "1.24.3",
|
|
86
|
-
"@applitools/ec-client": "1.12.
|
|
87
|
+
"@applitools/ec-client": "1.12.15",
|
|
87
88
|
"@applitools/logger": "2.2.7",
|
|
88
|
-
"@applitools/nml-client": "1.11.
|
|
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 {};
|