@applitools/core 4.61.0 → 4.61.1-debug.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 +94 -0
- package/dist/classic/utils/extract-default-environments.js +2 -2
- package/dist/cli/cli.js +160 -0
- package/dist/open-eyes.js +16 -11
- package/dist/ufg/check.js +1 -0
- package/dist/ufg/create-render-target-from-snapshot.js +1 -0
- package/dist/universal/core-server-process.js +31 -0
- package/dist/universal/core-server.js +216 -0
- package/dist/universal/history.js +90 -0
- package/dist/universal/refer.js +67 -0
- package/dist/universal/spec-driver.js +189 -0
- package/dist/universal/types.js +2 -0
- package/dist/universal/ws-server.js +66 -0
- package/dist/utils/extract-git-info.js +738 -140
- package/package.json +15 -12
- package/types/automation/types.d.ts +1 -0
- package/types/cli/cli.d.ts +2 -0
- package/types/universal/core-server-process.d.ts +9 -0
- package/types/universal/core-server.d.ts +18 -0
- package/types/universal/history.d.ts +2 -0
- package/types/universal/refer.d.ts +8 -0
- package/types/universal/spec-driver.d.ts +15 -0
- package/types/universal/types.d.ts +205 -0
- package/types/universal/ws-server.d.ts +15 -0
- package/types/utils/extract-git-info.d.ts +146 -6
|
@@ -26,12 +26,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.extractBranchLookupFallbackList = exports.isISODate = exports.extractBranchingTimestamp = exports.extractBuildIdFromCI = exports.extractCIBranchName = exports.extractGitRepo = exports.extractGitBranch = exports.extractLatestCommitInfo = exports.getPrimaryRemoteName = exports.cacheKey = void 0;
|
|
29
|
+
exports.extractBranchLookupFallbackList = exports.getBranchAncestryByLocalBranches = exports.getBranchAncestryByCommits = exports.isISODate = exports.extractBranchingTimestamp = exports.extractBuildIdFromCI = exports.extractCIBranchName = exports.extractGitRepo = exports.extractGitBranch = exports.extractLatestCommitInfo = exports.extractDefaultBranch = exports.checkGitEnvironment = 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
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Gate for verbose, deep-dive diagnostic logging in this file.
|
|
36
|
+
*
|
|
37
|
+
* Toggled by GitHub Actions' standard `RUNNER_DEBUG=1` env var (also settable
|
|
38
|
+
* locally for reproduction). When OFF, only high-signal log lines emit:
|
|
39
|
+
* - which fallback path was taken
|
|
40
|
+
* - errors and unusual states
|
|
41
|
+
* - the final extracted value
|
|
42
|
+
* When ON, every per-branch / per-commit decision, every retry, and every
|
|
43
|
+
* `[PERF]` timing is also logged. Use this guard for anything noisy enough
|
|
44
|
+
* to bury the signal in CI logs.
|
|
45
|
+
*/
|
|
35
46
|
const isDebugMode = () => process.env.RUNNER_DEBUG === '1';
|
|
36
47
|
exports.cacheKey = 'default';
|
|
37
48
|
/**
|
|
@@ -54,7 +65,266 @@ exports.getPrimaryRemoteName = utils.general.cachify(async function ({ execOptio
|
|
|
54
65
|
cwd: (_a = args[0].execOptions) === null || _a === void 0 ? void 0 : _a.cwd,
|
|
55
66
|
});
|
|
56
67
|
});
|
|
68
|
+
/**
|
|
69
|
+
* One-shot, cached probe of the full git environment, staged from cheapest
|
|
70
|
+
* to most expensive:
|
|
71
|
+
* 1. is `git` installed and on PATH? (`git --version`)
|
|
72
|
+
* 2. are we inside a git work tree? (`git rev-parse --is-inside-work-tree`)
|
|
73
|
+
* 3. is a primary remote configured? (`git remote`)
|
|
74
|
+
* 4. is that remote reachable? (`git ls-remote <r> HEAD`)
|
|
75
|
+
*
|
|
76
|
+
* Use this as the outermost guard for any code path that shells out to git.
|
|
77
|
+
* Each step short-circuits later ones: missing binary skips repo/remote
|
|
78
|
+
* checks; non-repo skips remote checks; no remote configured skips the
|
|
79
|
+
* reachability probe. Callers should consult `ok` for local-only git
|
|
80
|
+
* features and `remoteAccessible` for anything that talks to the network.
|
|
81
|
+
*
|
|
82
|
+
* Cached per `cwd`. The first invocation emits a clearly-visible banner so
|
|
83
|
+
* support can grep one distinctive line — `APPLITOOLS GIT ENVIRONMENT CHECK`
|
|
84
|
+
* — and immediately tell which sub-check failed. Subsequent invocations
|
|
85
|
+
* return the cached verdict silently.
|
|
86
|
+
*/
|
|
87
|
+
exports.checkGitEnvironment = utils.general.cachify(async function ({ execOptions, logger = (0, logger_1.makeLogger)(), }) {
|
|
88
|
+
// INVARIANT: this function MUST NOT reject. It is cachified, so a rejected
|
|
89
|
+
// promise would be memoized and every one of the 11 downstream callers
|
|
90
|
+
// would re-throw on every subsequent call. Every path below either
|
|
91
|
+
// returns via `finalize()` (which itself can't throw — see below) or is
|
|
92
|
+
// caught by the outermost safety net at the bottom.
|
|
93
|
+
var _a, _b, _c, _d, _e, _f;
|
|
94
|
+
let gitInstalled = false;
|
|
95
|
+
let insideRepo = false;
|
|
96
|
+
let remoteAccessible = false;
|
|
97
|
+
let remoteName;
|
|
98
|
+
const reasons = [];
|
|
99
|
+
/**
|
|
100
|
+
* Build the result, emit the banner, and return. Defensive: even if the
|
|
101
|
+
* underlying logger throws (e.g. a wrapped transport with a bug), the
|
|
102
|
+
* banner call is swallowed so this function still returns a usable
|
|
103
|
+
* `GitEnvironmentResult`.
|
|
104
|
+
*/
|
|
105
|
+
const finalize = (extraReason) => {
|
|
106
|
+
if (extraReason)
|
|
107
|
+
reasons.push(extraReason);
|
|
108
|
+
const result = {
|
|
109
|
+
gitInstalled,
|
|
110
|
+
insideRepo,
|
|
111
|
+
remoteAccessible,
|
|
112
|
+
ok: gitInstalled && insideRepo,
|
|
113
|
+
reason: reasons.length ? reasons.join('; ') : 'ok',
|
|
114
|
+
remoteName,
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
logGitEnvironmentBanner(logger, result);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Logger failures must NEVER make the env check itself fail.
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
try {
|
|
126
|
+
// 1. Is git itself installed? `git --version` is the cheapest sanity check.
|
|
127
|
+
// Non-fatal: any failure here returns via `finalize` with a clean result.
|
|
128
|
+
const versionResult = await executeWithLog('git --version', { execOptions, logger });
|
|
129
|
+
if (versionResult.code !== 0) {
|
|
130
|
+
return finalize(`git binary: ${((_a = versionResult.stderr) === null || _a === void 0 ? void 0 : _a.trim()) || `exit ${versionResult.code} (command not found?)`}`);
|
|
131
|
+
}
|
|
132
|
+
gitInstalled = true;
|
|
133
|
+
// 2. Are we inside a git work tree? Non-fatal — falls through to finalize.
|
|
134
|
+
const repoResult = await executeWithLog('git rev-parse --is-inside-work-tree', { execOptions, logger });
|
|
135
|
+
if (repoResult.code !== 0 || repoResult.stdout.trim() !== 'true') {
|
|
136
|
+
return finalize(`repo: ${((_b = repoResult.stderr) === null || _b === void 0 ? void 0 : _b.trim()) || 'not inside a git work tree'}`);
|
|
137
|
+
}
|
|
138
|
+
insideRepo = true;
|
|
139
|
+
// 3. Is any remote configured? Non-fatal — falls through to finalize.
|
|
140
|
+
const remotesResult = await executeWithLog('git remote', { execOptions, logger });
|
|
141
|
+
const remotesText = remotesResult.stdout.trim();
|
|
142
|
+
if (remotesResult.code !== 0 || !remotesText) {
|
|
143
|
+
return finalize(`remote: ${((_c = remotesResult.stderr) === null || _c === void 0 ? void 0 : _c.trim()) || 'no remote configured'}`);
|
|
144
|
+
}
|
|
145
|
+
const remotes = remotesText.split(/\s+/).filter(Boolean);
|
|
146
|
+
// `remotes` always has ≥1 element here because `remotesText` is non-empty
|
|
147
|
+
// and split+filter on a non-empty string yields at least one element.
|
|
148
|
+
remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
|
|
149
|
+
// 4. Can we reach the remote? Non-fatal — falls through to finalize.
|
|
150
|
+
const probe = await executeWithLog(`git ls-remote ${remoteName} HEAD`, { execOptions, logger });
|
|
151
|
+
if (probe.code !== 0) {
|
|
152
|
+
return finalize(`remote: ${((_d = probe.stderr) === null || _d === void 0 ? void 0 : _d.trim()) || `ls-remote exit ${probe.code}`}`);
|
|
153
|
+
}
|
|
154
|
+
remoteAccessible = true;
|
|
155
|
+
return finalize();
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
// Catches `executeWithLog` rejections (e.g. spawn ENOENT when the git
|
|
159
|
+
// binary is missing entirely). Falls through to a clean finalize.
|
|
160
|
+
return finalize(`probe threw: ${(_e = err === null || err === void 0 ? void 0 : err.message) !== null && _e !== void 0 ? _e : err}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (catastrophic) {
|
|
164
|
+
// Absolute last-resort safety net. Should never fire — `finalize` is
|
|
165
|
+
// designed not to throw — but if anything truly unexpected happens
|
|
166
|
+
// (out-of-memory, logger-throw-past-our-guard, etc.), return a
|
|
167
|
+
// synthetic "all-failed" result rather than rejecting the promise.
|
|
168
|
+
return {
|
|
169
|
+
gitInstalled: false,
|
|
170
|
+
insideRepo: false,
|
|
171
|
+
remoteAccessible: false,
|
|
172
|
+
ok: false,
|
|
173
|
+
reason: `checkGitEnvironment catastrophic failure: ${(_f = catastrophic === null || catastrophic === void 0 ? void 0 : catastrophic.message) !== null && _f !== void 0 ? _f : catastrophic}`,
|
|
174
|
+
remoteName: undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}, args => { var _a, _b; return ({ cwd: (_b = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.execOptions) === null || _b === void 0 ? void 0 : _b.cwd }); });
|
|
178
|
+
/**
|
|
179
|
+
* Unified, grep-friendly banner. The `APPLITOOLS GIT ENVIRONMENT CHECK`
|
|
180
|
+
* prefix is unique to this codepath, so support can find one line in any
|
|
181
|
+
* log and read off the entire environment verdict at a glance.
|
|
182
|
+
*/
|
|
183
|
+
function logGitEnvironmentBanner(logger, r) {
|
|
184
|
+
const sep = '============================================================';
|
|
185
|
+
const allGood = r.gitInstalled && r.insideRepo && r.remoteAccessible;
|
|
186
|
+
// Header — names the first failed sub-check (or "all ok").
|
|
187
|
+
logger.log(sep);
|
|
188
|
+
if (allGood) {
|
|
189
|
+
logger.log('APPLITOOLS GIT ENVIRONMENT CHECK ✓ all ok');
|
|
190
|
+
}
|
|
191
|
+
else if (!r.gitInstalled) {
|
|
192
|
+
logger.log('APPLITOOLS GIT ENVIRONMENT CHECK ✗ GIT NOT INSTALLED');
|
|
193
|
+
}
|
|
194
|
+
else if (!r.insideRepo) {
|
|
195
|
+
logger.log('APPLITOOLS GIT ENVIRONMENT CHECK ✗ NOT IN A GIT REPOSITORY');
|
|
196
|
+
}
|
|
197
|
+
else if (!r.remoteAccessible) {
|
|
198
|
+
logger.log('APPLITOOLS GIT ENVIRONMENT CHECK ✗ REMOTE NOT ACCESSIBLE');
|
|
199
|
+
}
|
|
200
|
+
// Per-condition status lines — show every sub-check that was reached so
|
|
201
|
+
// a reader can see at a glance which stage went wrong.
|
|
202
|
+
logger.log(` git binary: ${r.gitInstalled ? '✓ installed' : '✗ NOT FOUND'}`);
|
|
203
|
+
if (r.gitInstalled) {
|
|
204
|
+
logger.log(` repo: ${r.insideRepo ? '✓ inside work tree' : '✗ NOT INSIDE A WORK TREE'}`);
|
|
205
|
+
}
|
|
206
|
+
if (r.gitInstalled && r.insideRepo) {
|
|
207
|
+
const remoteLine = r.remoteAccessible
|
|
208
|
+
? `✓ accessible (${r.remoteName})`
|
|
209
|
+
: r.remoteName
|
|
210
|
+
? `✗ NOT ACCESSIBLE (${r.remoteName})`
|
|
211
|
+
: '✗ NONE CONFIGURED';
|
|
212
|
+
logger.log(` remote: ${remoteLine}`);
|
|
213
|
+
}
|
|
214
|
+
// Reason + remediation hints — only for the failure mode that triggered.
|
|
215
|
+
if (!allGood) {
|
|
216
|
+
logger.log(` reason: ${r.reason}`);
|
|
217
|
+
if (!r.gitInstalled) {
|
|
218
|
+
logger.log(' → All git-based SCM features will be SKIPPED.');
|
|
219
|
+
logger.log(' → Install git or add it to PATH to enable branch detection and baseline inheritance.');
|
|
220
|
+
}
|
|
221
|
+
else if (!r.insideRepo) {
|
|
222
|
+
logger.log(' → All git-based SCM features will be SKIPPED.');
|
|
223
|
+
logger.log(' → Run this from inside a git working tree to enable branch detection.');
|
|
224
|
+
}
|
|
225
|
+
else if (!r.remoteAccessible) {
|
|
226
|
+
logger.log(' → All remote-dependent fallbacks will be SKIPPED.');
|
|
227
|
+
logger.log(' → Branch lookup may be incomplete. Verify network / credentials / remote config.');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
logger.log(sep);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Programmatically detect the default branch of the current repository.
|
|
234
|
+
*
|
|
235
|
+
* Fallback chain (each step short-circuits on success; never throws):
|
|
236
|
+
* 1. `APPLITOOLS_DEFAULT_BRANCH` env var — explicit user override.
|
|
237
|
+
* 2. `git symbolic-ref --short refs/remotes/<remote>/HEAD` — authoritative
|
|
238
|
+
* source on any clone that has fetched at least once. Returns e.g.
|
|
239
|
+
* `origin/master`; the `<remote>/` prefix is stripped.
|
|
240
|
+
* 3. `git ls-remote --symref <remote> HEAD` — works even when the local
|
|
241
|
+
* `refs/remotes/<remote>/HEAD` symref was never set up (single-branch
|
|
242
|
+
* clones, older git on CI).
|
|
243
|
+
* 4. `git config --get init.defaultBranch` — honors a user-global preference
|
|
244
|
+
* if everything else fails.
|
|
245
|
+
* 5. Literal `'main'` — last resort.
|
|
246
|
+
*
|
|
247
|
+
* Steps 2 and 3 catch non-standard defaults (e.g. JFrog's
|
|
248
|
+
* `JFROG/artifactory-mfe/preRelease/jfmfe-1.995.x-rc`). Cached per `cwd`
|
|
249
|
+
* to match `getPrimaryRemoteName`.
|
|
250
|
+
*/
|
|
251
|
+
exports.extractDefaultBranch = utils.general.cachify(async function ({ execOptions, logger = (0, logger_1.makeLogger)() }) {
|
|
252
|
+
var _a;
|
|
253
|
+
// 1. Explicit env override — works even without git installed / outside a repo.
|
|
254
|
+
const envOverride = (_a = process.env.APPLITOOLS_DEFAULT_BRANCH) === null || _a === void 0 ? void 0 : _a.trim();
|
|
255
|
+
if (envOverride) {
|
|
256
|
+
logger.log(`extractDefaultBranch: using APPLITOOLS_DEFAULT_BRANCH="${envOverride}"`);
|
|
257
|
+
return envOverride;
|
|
258
|
+
}
|
|
259
|
+
// Function-entry env gate. All remaining steps (2–4) shell out to git;
|
|
260
|
+
// if git isn't installed or we're not in a repo, skip straight to 'main'.
|
|
261
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
262
|
+
if (!env.ok) {
|
|
263
|
+
logger.log("extractDefaultBranch: git environment not ok, falling back to 'main'");
|
|
264
|
+
return 'main';
|
|
265
|
+
}
|
|
266
|
+
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
267
|
+
// 2. git symbolic-ref --short refs/remotes/<remote>/HEAD
|
|
268
|
+
try {
|
|
269
|
+
const r = await executeWithLog(`git symbolic-ref --short refs/remotes/${remoteName}/HEAD`, {
|
|
270
|
+
execOptions,
|
|
271
|
+
logger,
|
|
272
|
+
});
|
|
273
|
+
if (!r.stderr && r.stdout.trim()) {
|
|
274
|
+
const out = r.stdout.trim();
|
|
275
|
+
const normalized = out.replace(new RegExp(`^${remoteName}/`), '');
|
|
276
|
+
if (normalized) {
|
|
277
|
+
logger.log(`extractDefaultBranch: resolved via symbolic-ref -> "${normalized}"`);
|
|
278
|
+
return normalized;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
// Expected when the symref isn't set up (e.g. fresh single-branch clone). Verbose-only.
|
|
284
|
+
if (isDebugMode())
|
|
285
|
+
logger.log('extractDefaultBranch: symbolic-ref step failed, continuing', err);
|
|
286
|
+
}
|
|
287
|
+
// 3. git ls-remote --symref <remote> HEAD
|
|
288
|
+
try {
|
|
289
|
+
const r = await executeWithLog(`git ls-remote --symref ${remoteName} HEAD`, { execOptions, logger });
|
|
290
|
+
if (!r.stderr && r.stdout) {
|
|
291
|
+
// Output starts with a line like: "ref: refs/heads/<name>\tHEAD"
|
|
292
|
+
const match = r.stdout.match(/^ref:\s+refs\/heads\/([^\s]+)\s+HEAD/m);
|
|
293
|
+
if (match && match[1]) {
|
|
294
|
+
logger.log(`extractDefaultBranch: resolved via ls-remote --symref -> "${match[1]}"`);
|
|
295
|
+
return match[1];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
// Expected on networks without remote access. Verbose-only.
|
|
301
|
+
if (isDebugMode())
|
|
302
|
+
logger.log('extractDefaultBranch: ls-remote --symref step failed, continuing', err);
|
|
303
|
+
}
|
|
304
|
+
// 4. git config --get init.defaultBranch (user-global preference)
|
|
305
|
+
try {
|
|
306
|
+
const r = await executeWithLog('git config --get init.defaultBranch', { execOptions, logger });
|
|
307
|
+
if (!r.stderr && r.stdout.trim()) {
|
|
308
|
+
logger.log(`extractDefaultBranch: resolved via init.defaultBranch -> "${r.stdout.trim()}"`);
|
|
309
|
+
return r.stdout.trim();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
// No user-global preference set — totally normal. Verbose-only.
|
|
314
|
+
if (isDebugMode())
|
|
315
|
+
logger.log('extractDefaultBranch: init.defaultBranch step failed, continuing', err);
|
|
316
|
+
}
|
|
317
|
+
// 5. Last resort
|
|
318
|
+
logger.log('extractDefaultBranch: falling back to literal "main"');
|
|
319
|
+
return 'main';
|
|
320
|
+
}, args => { var _a, _b; return ({ cwd: (_b = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.execOptions) === null || _b === void 0 ? void 0 : _b.cwd }); });
|
|
57
321
|
exports.extractLatestCommitInfo = utils.general.cachify(async function ({ execOptions, logger = (0, logger_1.makeLogger)(), }) {
|
|
322
|
+
// Function-entry env gate. Without git or a repo, there is no commit to extract.
|
|
323
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
324
|
+
if (!env.ok) {
|
|
325
|
+
logger.log('extractLatestCommitInfo: git environment not ok, skipping');
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
58
328
|
let result;
|
|
59
329
|
try {
|
|
60
330
|
const githubPullRequestLastCommitSha = await extractGithubPullRequestLastCommitSha();
|
|
@@ -90,11 +360,18 @@ exports.extractLatestCommitInfo = utils.general.cachify(async function ({ execOp
|
|
|
90
360
|
}, () => exports.cacheKey);
|
|
91
361
|
exports.extractGitBranch = utils.general.cachify(async function ({ execOptions, logger = (0, logger_1.makeLogger)() }) {
|
|
92
362
|
// 1. Try CI environment variables first (fast, no subprocess, handles detached HEAD in CI)
|
|
363
|
+
// This step works even without git installed / outside a repo.
|
|
93
364
|
const ciBranch = extractCIBranchName();
|
|
94
365
|
if (ciBranch) {
|
|
95
366
|
logger.log(`Extracted branch name from CI environment: "${ciBranch}"`);
|
|
96
367
|
return ciBranch;
|
|
97
368
|
}
|
|
369
|
+
// Function-entry env gate for the git-using fallback path below.
|
|
370
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
371
|
+
if (!env.ok) {
|
|
372
|
+
logger.log('extractGitBranch: git environment not ok, no branch name available');
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
98
375
|
// 2. Fall back to git command (works when on an actual branch locally)
|
|
99
376
|
const result = await executeWithLog('git branch --show-current', { execOptions, logger });
|
|
100
377
|
if (result.stderr) {
|
|
@@ -110,6 +387,12 @@ exports.extractGitBranch = utils.general.cachify(async function ({ execOptions,
|
|
|
110
387
|
}
|
|
111
388
|
}, 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 }); });
|
|
112
389
|
exports.extractGitRepo = utils.general.cachify(async function ({ execOptions, logger = (0, logger_1.makeLogger)() }) {
|
|
390
|
+
// Function-entry env gate. Without git or a repo, we can't extract a remote URL.
|
|
391
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
392
|
+
if (!env.ok) {
|
|
393
|
+
logger.log('extractGitRepo: git environment not ok, returning empty repo info');
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
113
396
|
const remotes = await extractRemotes();
|
|
114
397
|
logger.log(`Extracted remotes from git: ${remotes}`);
|
|
115
398
|
const remote = remotes.includes('origin') ? 'origin' : remotes[0];
|
|
@@ -160,11 +443,43 @@ async function extractBuildIdFromCI() {
|
|
|
160
443
|
);
|
|
161
444
|
}
|
|
162
445
|
exports.extractBuildIdFromCI = extractBuildIdFromCI;
|
|
446
|
+
/**
|
|
447
|
+
* Extract the ISO-8601 timestamp at which `branchName` branched off from
|
|
448
|
+
* `parentBranchName`.
|
|
449
|
+
*
|
|
450
|
+
* Algorithm (each step short-circuits on success; returns `undefined` on
|
|
451
|
+
* total failure — never throws):
|
|
452
|
+
* 1. If the repo is a shallow clone, unshallow (or treeless-fetch) so the
|
|
453
|
+
* branch topology is visible to `git merge-base`.
|
|
454
|
+
* 2. `git merge-base <remote>/<branch> <remote>/<parent>` — CI typically
|
|
455
|
+
* operates on remote refs only, so try those first.
|
|
456
|
+
* 3. Fall back to local refs (`git merge-base <branch> <parent>`).
|
|
457
|
+
* 4. If still no merge-base, see if the parent exists on the remote and,
|
|
458
|
+
* if so, fetch it by name (`--filter=tree:0`) and retry. If the parent
|
|
459
|
+
* isn't on the remote at all, give up — we can't compute a branching
|
|
460
|
+
* point for a branch that doesn't exist anywhere.
|
|
461
|
+
* 5. Direct-ancestor refinement: if the merge-base equals the parent
|
|
462
|
+
* branch's tip, the parent hasn't diverged from `branchName` yet
|
|
463
|
+
* (parent is a strict ancestor). The "branching point" is then the
|
|
464
|
+
* parent commit of the first child-only commit — not the merge-base
|
|
465
|
+
* itself, which would be the parent's tip and produce a too-recent
|
|
466
|
+
* timestamp.
|
|
467
|
+
* 6. Resolve the final hash's committer-date via `git show -s --format=%aI`.
|
|
468
|
+
*
|
|
469
|
+
* Cached per (branchName, parentBranchName, cwd).
|
|
470
|
+
*/
|
|
163
471
|
exports.extractBranchingTimestamp = utils.general.cachify(async function ({ branchName, parentBranchName, execOptions, logger = (0, logger_1.makeLogger)(), }) {
|
|
164
|
-
var _a;
|
|
165
472
|
logger = logger.extend({ tags: [`extract-branching-timestamp-${utils.general.shortid()}`] });
|
|
166
|
-
//
|
|
473
|
+
// Function-entry env gate. Without git or a repo, merge-base is impossible.
|
|
474
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
475
|
+
if (!env.ok) {
|
|
476
|
+
logger.log('extractBranchingTimestamp: git environment not ok, skipping');
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
// Cached lookup — resolves to `origin` on a typical clone.
|
|
167
480
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
481
|
+
// Step 1: ensure topology is visible. Shallow clones don't have enough
|
|
482
|
+
// history for merge-base to find a common ancestor.
|
|
168
483
|
const shallowCheckResult = await executeWithLog('git rev-parse --is-shallow-repository', {
|
|
169
484
|
execOptions,
|
|
170
485
|
logger,
|
|
@@ -174,53 +489,94 @@ exports.extractBranchingTimestamp = utils.general.cachify(async function ({ bran
|
|
|
174
489
|
logger.log('extractBranchingTimestamp - Repository is a shallow clone, attempting to unshallow');
|
|
175
490
|
await executeFetchStrategy(isShallow, execOptions, logger);
|
|
176
491
|
}
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
492
|
+
// Helper: best common ancestor SHA between two refs, or null on git failure.
|
|
493
|
+
// `null` is expected during the fallback chain (e.g. first attempt fails
|
|
494
|
+
// because the remote ref isn't fetched), so the log line is verbose-only.
|
|
495
|
+
async function getMergeBase(ref1, ref2) {
|
|
496
|
+
const { stdout, stderr } = await executeWithLog(`git merge-base ${ref1} ${ref2}`, { execOptions, logger });
|
|
497
|
+
if (stderr) {
|
|
498
|
+
if (isDebugMode())
|
|
499
|
+
logger.log(`getMergeBase failed for ${ref1} and ${ref2}: ${stderr}`);
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
return stdout.trim();
|
|
184
503
|
}
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
504
|
+
// Helper: resolve a commit hash to its committer-date in ISO-8601, or
|
|
505
|
+
// undefined on failure. `%aI` is the author-date in strict ISO format.
|
|
506
|
+
async function getTimestampForHash(hash) {
|
|
507
|
+
if (!hash)
|
|
508
|
+
return undefined;
|
|
509
|
+
const { stdout, stderr } = await executeWithLog(`git show -s --format=%aI ${hash}`, { execOptions, logger });
|
|
510
|
+
const timestamp = stdout.trim();
|
|
511
|
+
if (stderr || !isISODate(timestamp)) {
|
|
512
|
+
logger.log(`Error extracting timestamp for hash ${hash}: stderr: ${stderr}, stdout: ${stdout}`);
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
return timestamp;
|
|
516
|
+
}
|
|
517
|
+
// Steps 2–4: resolve the merge-base, escalating from remote → local → fetch.
|
|
518
|
+
let mergeBaseHash = null;
|
|
519
|
+
// Step 2: remote refs (preferred — CI typically has these populated).
|
|
520
|
+
mergeBaseHash = await getMergeBase(`${remoteName}/${branchName}`, `${remoteName}/${parentBranchName}`);
|
|
521
|
+
// Step 3: local refs fallback.
|
|
522
|
+
if (!mergeBaseHash) {
|
|
523
|
+
mergeBaseHash = await getMergeBase(branchName, parentBranchName);
|
|
524
|
+
}
|
|
525
|
+
// Step 4: still no merge-base — fetch the parent by name and retry, but
|
|
526
|
+
// only if it actually exists on the remote (no point fetching a branch
|
|
527
|
+
// that was never pushed). `getAllRemoteBranches` is the natural guard
|
|
528
|
+
// here: when the remote is unreachable it returns an empty set, so the
|
|
529
|
+
// `.has(parentBranchName)` check falls through to "not on remote".
|
|
530
|
+
if (!mergeBaseHash) {
|
|
188
531
|
const remoteBranches = await getAllRemoteBranches({ execOptions, logger });
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// Branch exists on remote, proceed with fetch
|
|
201
|
-
logger.log(`Fetching missing branch ${missingBranch} from remote`);
|
|
202
|
-
const command = `HASH=$(git merge-base ${branchName} ${parentBranchName}) && git show -q --format=%aI $HASH`;
|
|
203
|
-
/*
|
|
204
|
-
// --filter=tree:0 creates a treeless clone.
|
|
205
|
-
// These clones download all reachable commits while fetching trees and blobs on-demand.
|
|
206
|
-
// These clones are best for build environments where the repository will be deleted
|
|
207
|
-
// after a single build, but you still need access to commit history.
|
|
208
|
-
*/
|
|
209
|
-
result = await executeWithLog(`git fetch ${remoteName} ${normalizedBranchName}:${normalizedBranchName} --filter=tree:0 && ${command}`, {
|
|
210
|
-
execOptions,
|
|
211
|
-
logger,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
532
|
+
if (remoteBranches.has(parentBranchName)) {
|
|
533
|
+
logger.log(`Fetching missing branch ${parentBranchName} from remote and retrying merge-base`);
|
|
534
|
+
await executeWithLog(`git fetch ${remoteName} ${parentBranchName}:${parentBranchName} --filter=tree:0`, {
|
|
535
|
+
execOptions,
|
|
536
|
+
logger,
|
|
537
|
+
});
|
|
538
|
+
mergeBaseHash = await getMergeBase(branchName, parentBranchName);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
logger.log(`Parent branch ${parentBranchName} not found on remote, cannot determine branching point.`);
|
|
542
|
+
return undefined;
|
|
215
543
|
}
|
|
216
544
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return timestamp;
|
|
545
|
+
if (!mergeBaseHash) {
|
|
546
|
+
logger.log(`Could not find a merge-base for ${branchName} and ${parentBranchName}.`);
|
|
547
|
+
return undefined;
|
|
221
548
|
}
|
|
222
|
-
|
|
223
|
-
|
|
549
|
+
// Step 5: direct-ancestor refinement.
|
|
550
|
+
// Resolve both branch tips so we can compare against the merge-base.
|
|
551
|
+
const branchNameSha = (await executeWithLog(`git rev-parse ${branchName}`, { execOptions, logger })).stdout.trim();
|
|
552
|
+
const parentBranchNameSha = (await executeWithLog(`git rev-parse ${parentBranchName}`, { execOptions, logger })).stdout.trim();
|
|
553
|
+
let finalHash = mergeBaseHash;
|
|
554
|
+
// If merge-base == parent's tip, parent is a strict ancestor of branchName
|
|
555
|
+
// (no divergence yet). Using merge-base directly would return the parent's
|
|
556
|
+
// tip timestamp; we want the moment of divergence instead.
|
|
557
|
+
if (mergeBaseHash === parentBranchNameSha && mergeBaseHash !== branchNameSha) {
|
|
558
|
+
logger.log(`'${parentBranchName}' is a direct ancestor of '${branchName}'. Finding the true branching point.`);
|
|
559
|
+
// The first child-only commit (oldest commit on branchName not on parent)
|
|
560
|
+
// — its parent commit IS the divergence point.
|
|
561
|
+
const firstCommitOnChild = (await executeWithLog(`git rev-list ^${parentBranchName} ${branchName} | tail -n 1`, {
|
|
562
|
+
execOptions,
|
|
563
|
+
logger,
|
|
564
|
+
})).stdout.trim();
|
|
565
|
+
if (firstCommitOnChild) {
|
|
566
|
+
const parentOfFirstCommit = (await executeWithLog(`git rev-parse ${firstCommitOnChild}^`, { execOptions, logger })).stdout.trim();
|
|
567
|
+
if (isDebugMode())
|
|
568
|
+
logger.log(`Branching point identified as: ${parentOfFirstCommit}`);
|
|
569
|
+
finalHash = parentOfFirstCommit;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
logger.log('Could not determine the first commit on the child branch. Using merge-base as fallback.');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Step 6: hash -> ISO timestamp.
|
|
576
|
+
const timestamp = await getTimestampForHash(finalHash);
|
|
577
|
+
if (timestamp) {
|
|
578
|
+
logger.log(`git branching timestamp for parent '${parentBranchName}' successfully extracted: ${timestamp}`);
|
|
579
|
+
return timestamp;
|
|
224
580
|
}
|
|
225
581
|
}, args => {
|
|
226
582
|
var _a;
|
|
@@ -261,6 +617,243 @@ async function parallelWithLimit(items, concurrency, fn) {
|
|
|
261
617
|
}
|
|
262
618
|
return results;
|
|
263
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Enumerate ancestor branches in the window MERGE_BASE..gitBranchName by
|
|
622
|
+
* walking the commits in that window and asking which branches contain each.
|
|
623
|
+
*
|
|
624
|
+
* Mirrors the reference shell algorithm in
|
|
625
|
+
* `.team-lead/scripts-byCommits.sh`. Returns a sorted, deduplicated list
|
|
626
|
+
* of branch names (no `refs/` prefix). Never throws — returns `[]` on git
|
|
627
|
+
* failure. Always includes `defaultBranch` and `gitBranchName`.
|
|
628
|
+
*/
|
|
629
|
+
async function getBranchAncestryByCommits({ gitBranchName, defaultBranch, execOptions, logger = (0, logger_1.makeLogger)(), }) {
|
|
630
|
+
logger = logger.extend({ tags: [`get-branch-ancestry-by-commits-${utils.general.shortid()}`] });
|
|
631
|
+
if (isDebugMode()) {
|
|
632
|
+
logger.log(`[byCommits] Start: gitBranchName=${gitBranchName}, defaultBranch=${defaultBranch}`);
|
|
633
|
+
}
|
|
634
|
+
// Function-entry env gate.
|
|
635
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
636
|
+
if (!env.ok) {
|
|
637
|
+
logger.log('[byCommits] git environment not ok, returning []');
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
// Step 1: find MERGE_BASE(HEAD, defaultBranch). This anchors the
|
|
642
|
+
// "ancestry window" we care about — commits made on the current branch
|
|
643
|
+
// after this point are uniquely ours.
|
|
644
|
+
const mergeBaseResult = await executeWithLog(`git merge-base ${gitBranchName} ${defaultBranch}`, {
|
|
645
|
+
execOptions,
|
|
646
|
+
logger,
|
|
647
|
+
});
|
|
648
|
+
if (mergeBaseResult.stderr || !mergeBaseResult.stdout.trim()) {
|
|
649
|
+
// Default-on: an empty result without explanation is the worst kind of debugging.
|
|
650
|
+
logger.log(`[byCommits] ✗ merge-base ${gitBranchName} ${defaultBranch} failed (stderr="${mergeBaseResult.stderr}"), returning []`);
|
|
651
|
+
return [];
|
|
652
|
+
}
|
|
653
|
+
const mergeBase = mergeBaseResult.stdout.trim();
|
|
654
|
+
if (isDebugMode())
|
|
655
|
+
logger.log(`[byCommits] Merge-base resolved: ${mergeBase}`);
|
|
656
|
+
// Step 2: enumerate every commit in the window MERGE_BASE..HEAD.
|
|
657
|
+
const logResult = await executeWithLog(`git log --format=%H ${mergeBase}..${gitBranchName}`, { execOptions, logger });
|
|
658
|
+
if (logResult.stderr) {
|
|
659
|
+
logger.log(`[byCommits] ✗ git log ${mergeBase}..${gitBranchName} failed (stderr="${logResult.stderr}"), returning []`);
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
const commits = logResult.stdout.trim().split('\n').filter(Boolean);
|
|
663
|
+
if (isDebugMode())
|
|
664
|
+
logger.log(`[byCommits] Walking ${commits.length} commit(s) in window ${mergeBase}..${gitBranchName}`);
|
|
665
|
+
// Step 3: for each commit, ask which branches contain it. The union of
|
|
666
|
+
// those answers gives us every branch that could be a parent.
|
|
667
|
+
const candidates = new Set([defaultBranch, gitBranchName]);
|
|
668
|
+
const containResults = await parallelWithLimit(commits, 10, async (hash) => {
|
|
669
|
+
const r = await executeWithLog(`git branch -a --contains ${hash} --format='%(refname:short)'`, {
|
|
670
|
+
execOptions,
|
|
671
|
+
logger,
|
|
672
|
+
});
|
|
673
|
+
if (r.stderr)
|
|
674
|
+
return [];
|
|
675
|
+
return r.stdout
|
|
676
|
+
.split('\n')
|
|
677
|
+
.map(line => line.trim())
|
|
678
|
+
.filter(b => b && b !== 'HEAD');
|
|
679
|
+
});
|
|
680
|
+
// Verbose: full per-commit dump (can be hundreds of entries on long branches).
|
|
681
|
+
if (isDebugMode()) {
|
|
682
|
+
logger.log(`[byCommits] Branches containing commits in ${mergeBase}..${gitBranchName}:`, containResults);
|
|
683
|
+
}
|
|
684
|
+
for (const list of containResults)
|
|
685
|
+
for (const b of list)
|
|
686
|
+
candidates.add(b);
|
|
687
|
+
if (isDebugMode()) {
|
|
688
|
+
logger.log(`[byCommits] Unioned candidates before descendant filter (${candidates.size}): ${Array.from(candidates).join(', ')}`);
|
|
689
|
+
}
|
|
690
|
+
// Step 4: filter out descendant branches. A candidate that has
|
|
691
|
+
// gitBranchName as its ancestor is downstream of us (a feature branch
|
|
692
|
+
// built ON TOP of this one), not an upstream parent — exclude it.
|
|
693
|
+
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
694
|
+
let droppedSelf = 0;
|
|
695
|
+
let droppedDescendants = 0;
|
|
696
|
+
const finalCandidates = await parallelWithLimit(Array.from(candidates), 10, async (branch) => {
|
|
697
|
+
const normalizedBranch = branch.replace(new RegExp(`^${remoteName}/`), '');
|
|
698
|
+
if (normalizedBranch === gitBranchName) {
|
|
699
|
+
droppedSelf++;
|
|
700
|
+
return null; // Exclude the current branch itself
|
|
701
|
+
}
|
|
702
|
+
// `merge-base --is-ancestor` exits 0 when arg1 is an ancestor of arg2.
|
|
703
|
+
// So exit 0 here means "current branch IS an ancestor of candidate"
|
|
704
|
+
// → candidate is a descendant → drop it.
|
|
705
|
+
const isDescendantResult = await executeWithLog(`git merge-base --is-ancestor ${gitBranchName} ${normalizedBranch}`, {
|
|
706
|
+
execOptions,
|
|
707
|
+
logger,
|
|
708
|
+
});
|
|
709
|
+
if (isDescendantResult.code === 0) {
|
|
710
|
+
droppedDescendants++;
|
|
711
|
+
// Verbose: fires once per descendant candidate.
|
|
712
|
+
if (isDebugMode())
|
|
713
|
+
logger.log(`[byCommits] DROP descendant: ${normalizedBranch}`);
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
if (isDebugMode())
|
|
717
|
+
logger.log(`[byCommits] KEEP ancestor: ${normalizedBranch}`);
|
|
718
|
+
return normalizedBranch;
|
|
719
|
+
});
|
|
720
|
+
const finalCleanBranches = finalCandidates.filter((b) => b !== null);
|
|
721
|
+
const result = Array.from(new Set(finalCleanBranches)).sort();
|
|
722
|
+
if (isDebugMode()) {
|
|
723
|
+
logger.log(`[byCommits] Done. dropped(self=${droppedSelf}, descendants=${droppedDescendants}) → ${result.length} ancestor(s): ${result.join(', ')}`);
|
|
724
|
+
}
|
|
725
|
+
return result;
|
|
726
|
+
}
|
|
727
|
+
catch (err) {
|
|
728
|
+
logger.log('[byCommits] ✗ ERROR', err);
|
|
729
|
+
return [];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
exports.getBranchAncestryByCommits = getBranchAncestryByCommits;
|
|
733
|
+
/**
|
|
734
|
+
* Enumerate ancestor branches by iterating every branch ref and applying
|
|
735
|
+
* Rule 1 (the candidate's merge-base with HEAD must not lie before the
|
|
736
|
+
* primary MERGE_BASE) and Rule 2 (HEAD must not be an ancestor of the
|
|
737
|
+
* candidate).
|
|
738
|
+
*
|
|
739
|
+
* Mirrors the reference shell algorithm in
|
|
740
|
+
* `.team-lead/scripts-byLocalBranches.sh`, with one deliberate extension:
|
|
741
|
+
* the shell script reads `refs/heads/` only, but in CI we typically have
|
|
742
|
+
* exactly one local branch checked out and dozens of remote refs. To make
|
|
743
|
+
* this method useful in CI we ALSO walk `refs/remotes/<remote>/` and strip
|
|
744
|
+
* the remote prefix. The function name is preserved for fidelity to the
|
|
745
|
+
* reference algorithm.
|
|
746
|
+
*
|
|
747
|
+
* Returns a sorted, deduplicated list of branch names. Never throws —
|
|
748
|
+
* returns `[]` on git failure. Always includes `defaultBranch` and
|
|
749
|
+
* `gitBranchName`.
|
|
750
|
+
*/
|
|
751
|
+
async function getBranchAncestryByLocalBranches({ gitBranchName, defaultBranch, execOptions, logger = (0, logger_1.makeLogger)(), }) {
|
|
752
|
+
logger = logger.extend({ tags: [`get-branch-ancestry-by-local-branches-${utils.general.shortid()}`] });
|
|
753
|
+
if (isDebugMode()) {
|
|
754
|
+
logger.log(`[byLocalBranches] Start: gitBranchName=${gitBranchName}, defaultBranch=${defaultBranch}`);
|
|
755
|
+
}
|
|
756
|
+
// Function-entry env gate.
|
|
757
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
758
|
+
if (!env.ok) {
|
|
759
|
+
logger.log('[byLocalBranches] git environment not ok, returning []');
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
// Per-rule counters for the final summary — invaluable when debugging
|
|
763
|
+
// "why did so many branches get filtered out?"
|
|
764
|
+
let droppedNoMb = 0;
|
|
765
|
+
let droppedRule1 = 0;
|
|
766
|
+
let droppedRule2 = 0;
|
|
767
|
+
let pinned = 0;
|
|
768
|
+
try {
|
|
769
|
+
// 1. Merge-base of CURRENT and DEFAULT. This is the "primary MERGE_BASE"
|
|
770
|
+
// used by Rule 1 below.
|
|
771
|
+
const mergeBaseResult = await executeWithLog(`git merge-base ${gitBranchName} ${defaultBranch}`, {
|
|
772
|
+
execOptions,
|
|
773
|
+
logger,
|
|
774
|
+
});
|
|
775
|
+
if (mergeBaseResult.stderr || !mergeBaseResult.stdout.trim()) {
|
|
776
|
+
// Default-on: an empty result without explanation is the worst kind of debugging.
|
|
777
|
+
logger.log(`[byLocalBranches] ✗ merge-base ${gitBranchName} ${defaultBranch} failed (stderr="${mergeBaseResult.stderr}"), returning []`);
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
const mergeBase = mergeBaseResult.stdout.trim();
|
|
781
|
+
if (isDebugMode())
|
|
782
|
+
logger.log(`[byLocalBranches] Primary merge-base: ${mergeBase}`);
|
|
783
|
+
// 2. Enumerate candidate branches from BOTH local AND remote refs.
|
|
784
|
+
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
785
|
+
const refsResult = await executeWithLog(`git for-each-ref --format='%(refname:short)' refs/heads/ refs/remotes/${remoteName}/`, { execOptions, logger });
|
|
786
|
+
if (refsResult.stderr) {
|
|
787
|
+
logger.log(`[byLocalBranches] ✗ for-each-ref failed (stderr="${refsResult.stderr}"), returning []`);
|
|
788
|
+
return [];
|
|
789
|
+
}
|
|
790
|
+
const candidateBranches = new Set([defaultBranch, gitBranchName]);
|
|
791
|
+
for (const raw of refsResult.stdout.split('\n')) {
|
|
792
|
+
const trimmed = raw.trim();
|
|
793
|
+
if (!trimmed)
|
|
794
|
+
continue;
|
|
795
|
+
const normalized = trimmed.replace(new RegExp(`^${remoteName}/`), '');
|
|
796
|
+
if (normalized && normalized !== 'HEAD')
|
|
797
|
+
candidateBranches.add(normalized);
|
|
798
|
+
}
|
|
799
|
+
if (isDebugMode()) {
|
|
800
|
+
logger.log(`[byLocalBranches] Enumerated ${candidateBranches.size} candidate(s): ${Array.from(candidateBranches).join(', ')}`);
|
|
801
|
+
}
|
|
802
|
+
// 3. Apply Rule 1 and Rule 2 for every non-pinned candidate.
|
|
803
|
+
// Pinned = defaultBranch and gitBranchName, which always pass through.
|
|
804
|
+
const results = await parallelWithLimit(Array.from(candidateBranches), 10, async (branch) => {
|
|
805
|
+
if (branch === defaultBranch || branch === gitBranchName) {
|
|
806
|
+
pinned++;
|
|
807
|
+
if (isDebugMode())
|
|
808
|
+
logger.log(`[byLocalBranches] KEEP pinned: ${branch}`);
|
|
809
|
+
return branch;
|
|
810
|
+
}
|
|
811
|
+
// Rule 1: skip branches whose merge-base with CURRENT is at or before
|
|
812
|
+
// MERGE_BASE (they diverged before our window — they're not in
|
|
813
|
+
// the current branch's ancestry, they're parallel history).
|
|
814
|
+
const branchMbR = await executeWithLog(`git merge-base ${branch} ${gitBranchName}`, { execOptions, logger });
|
|
815
|
+
if (branchMbR.stderr || !branchMbR.stdout.trim()) {
|
|
816
|
+
droppedNoMb++;
|
|
817
|
+
if (isDebugMode()) {
|
|
818
|
+
logger.log(`[byLocalBranches] DROP no-merge-base: ${branch} (stderr="${branchMbR.stderr}")`);
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
const branchMb = branchMbR.stdout.trim().split('\n')[0];
|
|
823
|
+
const r1 = await executeWithLog(`git merge-base --is-ancestor ${branchMb} ${mergeBase}`, { execOptions, logger });
|
|
824
|
+
if (r1.code === 0) {
|
|
825
|
+
droppedRule1++;
|
|
826
|
+
if (isDebugMode()) {
|
|
827
|
+
logger.log(`[byLocalBranches] DROP Rule 1 (diverged before window): ${branch} — its mb=${branchMb} is ancestor of primary ${mergeBase}`);
|
|
828
|
+
}
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
// Rule 2: skip branches strictly ahead of CURRENT (HEAD is an ancestor
|
|
832
|
+
// of them — they're descendants, not parents).
|
|
833
|
+
const r2 = await executeWithLog(`git merge-base --is-ancestor ${gitBranchName} ${branch}`, { execOptions, logger });
|
|
834
|
+
if (r2.code === 0) {
|
|
835
|
+
droppedRule2++;
|
|
836
|
+
if (isDebugMode()) {
|
|
837
|
+
logger.log(`[byLocalBranches] DROP Rule 2 (descendant): ${branch} — ${gitBranchName} is its ancestor`);
|
|
838
|
+
}
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
if (isDebugMode())
|
|
842
|
+
logger.log(`[byLocalBranches] KEEP ancestor: ${branch} (mb=${branchMb})`);
|
|
843
|
+
return branch;
|
|
844
|
+
});
|
|
845
|
+
const result = Array.from(new Set(results.filter((b) => b !== null))).sort();
|
|
846
|
+
if (isDebugMode()) {
|
|
847
|
+
logger.log(`[byLocalBranches] Done. pinned=${pinned}, dropped(no-mb=${droppedNoMb}, rule1=${droppedRule1}, rule2=${droppedRule2}) → ${result.length} ancestor(s): ${result.join(', ')}`);
|
|
848
|
+
}
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
logger.log('[byLocalBranches] ✗ ERROR', err);
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
exports.getBranchAncestryByLocalBranches = getBranchAncestryByLocalBranches;
|
|
264
857
|
/**
|
|
265
858
|
* Get all remote branches from the primary remote (cached for performance)
|
|
266
859
|
* Uses git ls-remote which is fast and doesn't require fetching data
|
|
@@ -272,6 +865,13 @@ const getAllRemoteBranches = utils.general.cachify(async function ({ execOptions
|
|
|
272
865
|
logger.log('[getAllRemoteBranches] Starting git ls-remote to fetch all remote branches...');
|
|
273
866
|
logger.log('[getAllRemoteBranches] execOptions.cwd:', (execOptions === null || execOptions === void 0 ? void 0 : execOptions.cwd) || 'undefined');
|
|
274
867
|
}
|
|
868
|
+
// Short-circuit when the remote isn't reachable — no point retrying
|
|
869
|
+
// ls-remote when we know it will fail.
|
|
870
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
871
|
+
if (!env.remoteAccessible) {
|
|
872
|
+
logger.log('[getAllRemoteBranches] Skipping ls-remote (remote not accessible), returning empty set');
|
|
873
|
+
return new Set();
|
|
874
|
+
}
|
|
275
875
|
try {
|
|
276
876
|
// Get the primary remote name (cached)
|
|
277
877
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
@@ -332,9 +932,15 @@ const getAllRemoteBranches = utils.general.cachify(async function ({ execOptions
|
|
|
332
932
|
// Custom cache key generator - only cache by cwd, not by logger instance
|
|
333
933
|
args => { var _a; return ({ cwd: (_a = args[0].execOptions) === null || _a === void 0 ? void 0 : _a.cwd }); }, 5 * 60 * 1000);
|
|
334
934
|
/**
|
|
335
|
-
* Determine fetch strategy and execute appropriate fetch operation
|
|
935
|
+
* Determine fetch strategy and execute appropriate fetch operation.
|
|
936
|
+
* No-op when the remote isn't reachable.
|
|
336
937
|
*/
|
|
337
938
|
async function executeFetchStrategy(isShallow, execOptions, logger) {
|
|
939
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
940
|
+
if (!env.remoteAccessible) {
|
|
941
|
+
logger.log('executeFetchStrategy: skipping fetch (remote not accessible)');
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
338
944
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
339
945
|
if (isShallow) {
|
|
340
946
|
// Shallow clone needs full unshallow for initial topology discovery
|
|
@@ -366,6 +972,13 @@ async function executeFetchStrategy(isShallow, execOptions, logger) {
|
|
|
366
972
|
* @param logger - Logger instance
|
|
367
973
|
*/
|
|
368
974
|
async function ensureRemoteBranchesAvailable(branchName, isShallow, execOptions, logger) {
|
|
975
|
+
// Skip entirely when the remote is unreachable — neither the config rewrite
|
|
976
|
+
// nor the downstream fetches will do anything useful.
|
|
977
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
978
|
+
if (!env.remoteAccessible) {
|
|
979
|
+
logger.log('ensureRemoteBranchesAvailable: skipping (remote not accessible)');
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
369
982
|
try {
|
|
370
983
|
// Get the primary remote name (cached)
|
|
371
984
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
@@ -396,12 +1009,17 @@ async function ensureRemoteBranchesAvailable(branchName, isShallow, execOptions,
|
|
|
396
1009
|
* Extracts a list of ancestor branches for the given branch, ordered by most recent commit timestamp.
|
|
397
1010
|
*
|
|
398
1011
|
* Algorithm:
|
|
399
|
-
* 1. Check if shallow clone and should skip (via APPLITOOLS_SKIP_BRANCH_LOOKUP_IN_SHALLOW_CLONE)
|
|
400
|
-
* 2. Ensure remote branches are available (fetch if needed for single-branch/shallow clones)
|
|
401
|
-
* 3.
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
1012
|
+
* 1. Check if shallow clone and should skip (via APPLITOOLS_SKIP_BRANCH_LOOKUP_IN_SHALLOW_CLONE).
|
|
1013
|
+
* 2. Ensure remote branches are available (fetch if needed for single-branch/shallow clones).
|
|
1014
|
+
* 3. Detect the repo's actual default branch via `extractDefaultBranch()` (handles non-standard
|
|
1015
|
+
* defaults like JFrog's `JFROG/.../...-rc`).
|
|
1016
|
+
* 4. Discover ancestor branches using the strategy selected by
|
|
1017
|
+
* `APPLITOOLS_BRANCH_ANCESTRY_STRATEGY` (`commits` (default) or `local`):
|
|
1018
|
+
* - `getBranchAncestryByCommits` walks the MERGE_BASE..HEAD commit window;
|
|
1019
|
+
* - `getBranchAncestryByLocalBranches` iterates `refs/heads/` + `refs/remotes/<remote>/`.
|
|
1020
|
+
* 5. For each discovered branch, extract the branching timestamp.
|
|
1021
|
+
* 6. Sort results by timestamp descending (most recent first), tie-break by branch name.
|
|
1022
|
+
* 7. Root-branch safety net: if nothing survived, fall back to the detected default branch.
|
|
405
1023
|
*
|
|
406
1024
|
* @param gitBranchName - The branch to analyze
|
|
407
1025
|
* @param execOptions - Git execution options (e.g., cwd)
|
|
@@ -411,59 +1029,69 @@ async function ensureRemoteBranchesAvailable(branchName, isShallow, execOptions,
|
|
|
411
1029
|
exports.extractBranchLookupFallbackList = utils.general.cachify(async function ({ gitBranchName, execOptions, logger = (0, logger_1.makeLogger)(), enableShallowClone = true, }) {
|
|
412
1030
|
const functionStartTime = Date.now();
|
|
413
1031
|
logger = logger.extend({ tags: [`extract-branch-fallback-list-${utils.general.shortid()}`] });
|
|
414
|
-
|
|
1032
|
+
if (isDebugMode())
|
|
1033
|
+
logger.log(`[PERF] extractBranchLookupFallbackList started for branch: ${gitBranchName}`);
|
|
1034
|
+
// Function-entry env gate. Everything downstream shells out to git.
|
|
1035
|
+
const env = await (0, exports.checkGitEnvironment)({ execOptions, logger });
|
|
1036
|
+
if (!env.ok) {
|
|
1037
|
+
logger.log('extractBranchLookupFallbackList: git environment not ok, skipping');
|
|
1038
|
+
return undefined;
|
|
1039
|
+
}
|
|
415
1040
|
try {
|
|
416
|
-
// 1.
|
|
417
|
-
//
|
|
1041
|
+
// 1. Shallow-clone gate: APPLITOOLS_SKIP_BRANCH_LOOKUP_IN_SHALLOW_CLONE
|
|
1042
|
+
// lets users opt out of expensive unshallow-fetches in CI.
|
|
418
1043
|
const shallowCheckStartTime = Date.now();
|
|
419
1044
|
const shallowCheckResult = await executeWithLog('git rev-parse --is-shallow-repository', {
|
|
420
1045
|
execOptions,
|
|
421
1046
|
logger,
|
|
422
1047
|
});
|
|
423
|
-
|
|
1048
|
+
if (isDebugMode())
|
|
1049
|
+
logger.log(`[PERF] Shallow check took ${Date.now() - shallowCheckStartTime}ms`);
|
|
424
1050
|
const isShallow = shallowCheckResult.stdout.trim() === 'true';
|
|
425
1051
|
if (!enableShallowClone && isShallow) {
|
|
426
1052
|
logger.log('Shallow clone detected and APPLITOOLS_SKIP_BRANCH_LOOKUP_IN_SHALLOW_CLONE is enabled, skipping branch lookup');
|
|
427
1053
|
return undefined;
|
|
428
1054
|
}
|
|
429
|
-
// 2. Ensure
|
|
430
|
-
//
|
|
1055
|
+
// 2. Ensure topology is fetchable: single-branch / shallow clones don't
|
|
1056
|
+
// have the other branches' refs locally — fetch them so subsequent
|
|
1057
|
+
// merge-base calls have something to work with.
|
|
431
1058
|
const ensureRemoteStartTime = Date.now();
|
|
432
1059
|
await ensureRemoteBranchesAvailable(gitBranchName, isShallow, execOptions, logger);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
//
|
|
436
|
-
// only along the current branch's history using --first-parent and --simplify-by-decoration
|
|
437
|
-
const topologyStartTime = Date.now();
|
|
438
|
-
logger.log(`Discovering ancestor branches using git topology for ${gitBranchName}...`);
|
|
439
|
-
// Get the primary remote name (cached)
|
|
1060
|
+
if (isDebugMode())
|
|
1061
|
+
logger.log(`[PERF] ensureRemoteBranchesAvailable took ${Date.now() - ensureRemoteStartTime}ms`);
|
|
1062
|
+
// Cached lookup of the primary remote (typically `origin`).
|
|
440
1063
|
const remoteName = await (0, exports.getPrimaryRemoteName)({ execOptions, logger });
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
1064
|
+
// 3. Detect the repo's actual default branch (handles JFrog-style non-standard names).
|
|
1065
|
+
const defaultBranch = await (0, exports.extractDefaultBranch)({ execOptions, logger });
|
|
1066
|
+
logger.log(`Default branch detected: ${defaultBranch}`);
|
|
1067
|
+
// 4. Discover ancestor branches using the chosen strategy.
|
|
1068
|
+
// APPLITOOLS_BRANCH_ANCESTRY_STRATEGY=commits (default) | local
|
|
1069
|
+
const ancestryStrategy = (process.env.APPLITOOLS_BRANCH_ANCESTRY_STRATEGY || 'commits').trim().toLowerCase();
|
|
1070
|
+
const ancestryDriver = ancestryStrategy === 'local' ? getBranchAncestryByLocalBranches : getBranchAncestryByCommits;
|
|
1071
|
+
const ancestryStartTime = Date.now();
|
|
1072
|
+
logger.log(`Discovering ancestor branches via "${ancestryStrategy}" strategy for ${gitBranchName} (default=${defaultBranch})...`);
|
|
1073
|
+
const ancestry = await ancestryDriver({ gitBranchName, defaultBranch, execOptions, logger });
|
|
1074
|
+
if (isDebugMode())
|
|
1075
|
+
logger.log(`[PERF] Ancestry discovery took ${Date.now() - ancestryStartTime}ms`);
|
|
1076
|
+
if (isDebugMode()) {
|
|
1077
|
+
logger.log(`Ancestry discovery found ${ancestry.length} candidate branches: ${ancestry.join(', ')}`);
|
|
456
1078
|
}
|
|
457
|
-
|
|
458
|
-
|
|
1079
|
+
else {
|
|
1080
|
+
logger.log(`Ancestry discovery found ${ancestry.length} candidate branches`);
|
|
1081
|
+
}
|
|
1082
|
+
// The helpers return defaultBranch + gitBranchName + ancestors; strip current branch
|
|
1083
|
+
// for parity with the rest of the function (caller adds it back via timestamp logic).
|
|
1084
|
+
let allBranches = ancestry.filter(b => b !== gitBranchName);
|
|
459
1085
|
if (isDebugMode()) {
|
|
460
|
-
logger.log(`
|
|
1086
|
+
logger.log(`Ancestry discovered ${allBranches.length} potential ancestor branches: ${allBranches.join(', ')}`);
|
|
461
1087
|
}
|
|
462
1088
|
else {
|
|
463
|
-
logger.log(`
|
|
1089
|
+
logger.log(`Ancestry discovered ${allBranches.length} potential ancestor branches`);
|
|
464
1090
|
}
|
|
465
|
-
//
|
|
466
|
-
//
|
|
1091
|
+
// 4b. Drop branches that the ancestry helper found locally but that
|
|
1092
|
+
// don't exist on the remote — fetching/merge-base'ing against them
|
|
1093
|
+
// would just waste time and surface confusing errors. Cheap because
|
|
1094
|
+
// `getAllRemoteBranches` is cached.
|
|
467
1095
|
const remoteBranchesStartTime = Date.now();
|
|
468
1096
|
if (isDebugMode()) {
|
|
469
1097
|
logger.log('[Remote Filtering] ========================================');
|
|
@@ -516,51 +1144,15 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
516
1144
|
logger.log('[Remote Filtering] Continuing with all discovered branches');
|
|
517
1145
|
}
|
|
518
1146
|
}
|
|
519
|
-
logger.log(`[Remote Filtering] [PERF] Remote branch filtering took ${Date.now() - remoteBranchesStartTime}ms`);
|
|
520
1147
|
if (isDebugMode()) {
|
|
1148
|
+
logger.log(`[Remote Filtering] [PERF] Remote branch filtering took ${Date.now() - remoteBranchesStartTime}ms`);
|
|
521
1149
|
logger.log('[Remote Filtering] ========================================');
|
|
522
1150
|
}
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
const filteringStartTime = Date.now();
|
|
527
|
-
logger.log(`Filtering out sibling branches to keep only true ancestors...`);
|
|
528
|
-
// PERFORMANCE: Check ancestors in parallel with concurrency limit to avoid overwhelming git
|
|
529
|
-
const ANCESTOR_CHECK_CONCURRENCY = 10;
|
|
530
|
-
const ancestorChecks = await parallelWithLimit(allBranches, ANCESTOR_CHECK_CONCURRENCY, async (candidateBranch) => {
|
|
531
|
-
try {
|
|
532
|
-
// Check if candidate branch is an ancestor of current branch
|
|
533
|
-
// git merge-base --is-ancestor <commit> <commit> exits with status 0 if true, 1 if false
|
|
534
|
-
const isAncestorResult = await executeWithLog(`git merge-base --is-ancestor ${candidateBranch} ${gitBranchName}`, {
|
|
535
|
-
execOptions,
|
|
536
|
-
logger,
|
|
537
|
-
});
|
|
538
|
-
// Exit code 0 means it's an ancestor, exit code 1 means it's not
|
|
539
|
-
if (isAncestorResult.code === 0) {
|
|
540
|
-
logger.log(`✓ ${candidateBranch} is a true ancestor`);
|
|
541
|
-
return { branch: candidateBranch, isAncestor: true };
|
|
542
|
-
}
|
|
543
|
-
else if (isAncestorResult.code === 1) {
|
|
544
|
-
logger.log(`✗ ${candidateBranch} is a sibling, not an ancestor`);
|
|
545
|
-
return { branch: candidateBranch, isAncestor: false };
|
|
546
|
-
}
|
|
547
|
-
else {
|
|
548
|
-
logger.log(`⚠ Could not determine if ${candidateBranch} is an ancestor (exit code: ${isAncestorResult.code}), including it to be safe`);
|
|
549
|
-
return { branch: candidateBranch, isAncestor: true };
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
catch (err) {
|
|
553
|
-
logger.log(`Error checking if ${candidateBranch} is a true ancestor:`, err);
|
|
554
|
-
// If we can't determine, include it to be safe (better to have false positives than miss ancestors)
|
|
555
|
-
return { branch: candidateBranch, isAncestor: true };
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
const trueAncestors = ancestorChecks.filter(result => result.isAncestor).map(result => result.branch);
|
|
559
|
-
logger.log(`[PERF] Sibling filtering took ${Date.now() - filteringStartTime}ms`);
|
|
560
|
-
logger.log(`Filtered to ${trueAncestors.length} true ancestors: ${trueAncestors.join(', ')}`);
|
|
561
|
-
// 5. For each true ancestor branch, calculate the branching timestamp
|
|
1151
|
+
// 5. For each surviving ancestor, compute the branching timestamp in
|
|
1152
|
+
// parallel. The ancestry helper has already pruned siblings and
|
|
1153
|
+
// descendants, so every entry here should be a real ancestor.
|
|
562
1154
|
const timestampStartTime = Date.now();
|
|
563
|
-
const branchesWithTimestamps = await Promise.all(
|
|
1155
|
+
const branchesWithTimestamps = await Promise.all(allBranches.map(async (ancestorBranch) => {
|
|
564
1156
|
const timestamp = await (0, exports.extractBranchingTimestamp)({
|
|
565
1157
|
branchName: gitBranchName,
|
|
566
1158
|
parentBranchName: ancestorBranch,
|
|
@@ -569,7 +1161,8 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
569
1161
|
});
|
|
570
1162
|
return timestamp ? { branchName: ancestorBranch, latestViableTimestamp: timestamp } : null;
|
|
571
1163
|
}));
|
|
572
|
-
|
|
1164
|
+
if (isDebugMode())
|
|
1165
|
+
logger.log(`[PERF] Timestamp extraction took ${Date.now() - timestampStartTime}ms`);
|
|
573
1166
|
// 6. Filter out null results and sort by timestamp (most recent first), then by branch name
|
|
574
1167
|
const validBranches = branchesWithTimestamps
|
|
575
1168
|
.filter((item) => item !== null)
|
|
@@ -613,17 +1206,15 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
613
1206
|
.map(line => line.trim())
|
|
614
1207
|
.filter(line => line && !line.includes('->') && !line.includes('HEAD'))
|
|
615
1208
|
.map(line => line.replace(new RegExp(`^${remoteName}/`), '').replace(/^\* /, '')); // Remove remote prefix and '* ' prefix
|
|
616
|
-
//
|
|
617
|
-
|
|
1209
|
+
// Prefer the programmatically-detected default branch (handles non-standard names
|
|
1210
|
+
// like JFrog's `JFROG/artifactory-mfe/preRelease/jfmfe-1.995.x-rc`) over a hardcoded list.
|
|
618
1211
|
let rootBranch = null;
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
logger.log(`Found root branch using common name: ${rootBranch}`);
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
1212
|
+
if (branchesContainingRoot.includes(defaultBranch)) {
|
|
1213
|
+
rootBranch = defaultBranch;
|
|
1214
|
+
logger.log(`Found root branch via detected defaultBranch: ${rootBranch}`);
|
|
625
1215
|
}
|
|
626
|
-
// If
|
|
1216
|
+
// If the detected default isn't among the root-containing branches,
|
|
1217
|
+
// fall back to the oldest branch (most likely the historical root).
|
|
627
1218
|
if (!rootBranch && branchesContainingRoot.length > 0) {
|
|
628
1219
|
// Find the branch with the oldest creation timestamp among those containing root commit
|
|
629
1220
|
const branchTimestamps = await Promise.all(branchesContainingRoot.slice(0, 10).map(async (branch) => {
|
|
@@ -696,8 +1287,15 @@ exports.extractBranchLookupFallbackList = utils.general.cachify(async function (
|
|
|
696
1287
|
catch (err) {
|
|
697
1288
|
logger.log('Failed to detect and add root branch, continuing without it', err);
|
|
698
1289
|
}
|
|
699
|
-
|
|
700
|
-
|
|
1290
|
+
if (isDebugMode()) {
|
|
1291
|
+
logger.log(`[PERF] extractBranchLookupFallbackList completed in ${Date.now() - functionStartTime}ms total`);
|
|
1292
|
+
}
|
|
1293
|
+
if (isDebugMode()) {
|
|
1294
|
+
logger.log(`Successfully extracted branch lookup fallback list (${validBranches.length} entries): ${JSON.stringify(validBranches)}`);
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
logger.log(`Successfully extracted branch lookup fallback list (${validBranches.length} entries)`);
|
|
1298
|
+
}
|
|
701
1299
|
return validBranches.length > 0 ? validBranches : undefined;
|
|
702
1300
|
}
|
|
703
1301
|
catch (err) {
|