buddy-workbench 0.2.8 → 0.2.10
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/package.json +1 -1
- package/server/routes/jira-filters.js +44 -21
- package/server/routes/package-upgrade.js +17 -6
- package/server/routes/pr-conflicts.js +32 -27
- package/ui/dist/assets/index-BdK43fGD.css +1 -0
- package/ui/dist/assets/{index-CR2sU8VH.js → index-RvymXcJp.js} +143 -143
- package/ui/dist/assets/{xmindAdapter-DP0UYxfN.js → xmindAdapter-BgI7SsNo.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-K8FGtNlV.css +0 -1
package/package.json
CHANGED
|
@@ -354,24 +354,49 @@ async function searchJiraWithFallbacksPaginated(jiraHost, headers, jqlCandidates
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
async function fetchCommentsForIssue(jiraHost, headers, issueKey, maxResults = 50) {
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const latestResponse = await httpClient.get(latestUrl, { headers });
|
|
367
|
-
if (latestResponse.status === 200 && Array.isArray(latestResponse.data?.comments)) {
|
|
368
|
-
return latestResponse.data.comments;
|
|
357
|
+
const requestComments = async (url) => {
|
|
358
|
+
const maxRetries = 4;
|
|
359
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
360
|
+
const response = await httpClient.get(url, { headers });
|
|
361
|
+
if (response.status === 200 && Array.isArray(response.data?.comments)) return response;
|
|
362
|
+
if (response.status !== 429 || attempt >= maxRetries) {
|
|
363
|
+
const error = new Error(`Jira comments request for ${issueKey} returned HTTP ${response.status}`);
|
|
364
|
+
error.status = response.status;
|
|
365
|
+
throw error;
|
|
369
366
|
}
|
|
367
|
+
|
|
368
|
+
const retryAfter = response.headers?.['retry-after'] ?? response.headers?.['Retry-After'];
|
|
369
|
+
const retryAfterSeconds = Number(retryAfter);
|
|
370
|
+
const retryAfterDate = Date.parse(retryAfter);
|
|
371
|
+
const delayMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0
|
|
372
|
+
? retryAfterSeconds * 1000
|
|
373
|
+
: Number.isFinite(retryAfterDate)
|
|
374
|
+
? Math.max(0, retryAfterDate - Date.now())
|
|
375
|
+
: Math.min(1000 * (2 ** attempt), 10000);
|
|
376
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
370
377
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const url = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?maxResults=${maxResults}`;
|
|
381
|
+
const response = await requestComments(url);
|
|
382
|
+
const total = response.data?.total || 0;
|
|
383
|
+
const comments = response.data.comments;
|
|
384
|
+
if (total > comments.length && total > maxResults) {
|
|
385
|
+
const startAt = Math.max(0, total - maxResults);
|
|
386
|
+
const latestUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?startAt=${startAt}&maxResults=${maxResults}`;
|
|
387
|
+
const latestResponse = await requestComments(latestUrl);
|
|
388
|
+
return latestResponse.data.comments;
|
|
389
|
+
}
|
|
390
|
+
return comments;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function mapInBatches(items, batchSize, mapper) {
|
|
394
|
+
const results = [];
|
|
395
|
+
for (let index = 0; index < items.length; index += batchSize) {
|
|
396
|
+
const batchResults = await Promise.all(items.slice(index, index + batchSize).map(mapper));
|
|
397
|
+
results.push(...batchResults);
|
|
374
398
|
}
|
|
399
|
+
return results;
|
|
375
400
|
}
|
|
376
401
|
|
|
377
402
|
router.get('/recently-created', (_req, res) => {
|
|
@@ -409,7 +434,7 @@ router.get('/recently-commented', async (_req, res) => {
|
|
|
409
434
|
const issues = await searchJiraWithFallbacksPaginated(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter');
|
|
410
435
|
const results = [];
|
|
411
436
|
|
|
412
|
-
const
|
|
437
|
+
const issueComments = await mapInBatches(issues, 5, async (issue) => {
|
|
413
438
|
const key = issue.key;
|
|
414
439
|
const fields = issue.fields || {};
|
|
415
440
|
const comments = await fetchCommentsForIssue(jiraHost, headers, key, 50);
|
|
@@ -436,8 +461,7 @@ router.get('/recently-commented', async (_req, res) => {
|
|
|
436
461
|
return null;
|
|
437
462
|
});
|
|
438
463
|
|
|
439
|
-
const
|
|
440
|
-
for (const item of settled) {
|
|
464
|
+
for (const item of issueComments) {
|
|
441
465
|
if (item) results.push(item);
|
|
442
466
|
}
|
|
443
467
|
|
|
@@ -478,7 +502,7 @@ router.get('/recently-mentioned', async (_req, res) => {
|
|
|
478
502
|
const issues = await searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter', 40);
|
|
479
503
|
const results = [];
|
|
480
504
|
|
|
481
|
-
const
|
|
505
|
+
const issueMentions = await mapInBatches(issues.slice(0, 30), 5, async (issue) => {
|
|
482
506
|
const key = issue.key;
|
|
483
507
|
const fields = issue.fields || {};
|
|
484
508
|
const comments = await fetchCommentsForIssue(jiraHost, headers, key, 50);
|
|
@@ -511,8 +535,7 @@ router.get('/recently-mentioned', async (_req, res) => {
|
|
|
511
535
|
return null;
|
|
512
536
|
});
|
|
513
537
|
|
|
514
|
-
const
|
|
515
|
-
for (const item of settled) {
|
|
538
|
+
for (const item of issueMentions) {
|
|
516
539
|
if (item) results.push(item);
|
|
517
540
|
}
|
|
518
541
|
|
|
@@ -12,6 +12,7 @@ import { recordApiError } from '../lib/api-errors.js';
|
|
|
12
12
|
const router = Router();
|
|
13
13
|
const exec = promisify(execFile);
|
|
14
14
|
const running = new Set();
|
|
15
|
+
const BITBUCKET_REQUEST_BATCH_SIZE = 5;
|
|
15
16
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
16
17
|
const httpClient = axios.create({
|
|
17
18
|
httpsAgent,
|
|
@@ -239,14 +240,24 @@ router.post('/repos/branches', async (req, res) => {
|
|
|
239
240
|
const repos = repoIds.map((repoId) => data.repos.find((repo) => repo.id === repoId)).filter(Boolean);
|
|
240
241
|
if (!repos.length) return res.json([]);
|
|
241
242
|
try {
|
|
242
|
-
const remote =
|
|
243
|
+
const remote = [];
|
|
244
|
+
for (let index = 0; index < repos.length; index += BITBUCKET_REQUEST_BATCH_SIZE) {
|
|
245
|
+
remote.push(...await Promise.all(repos.slice(index, index + BITBUCKET_REQUEST_BATCH_SIZE).map(fetchRemoteBranches)));
|
|
246
|
+
}
|
|
243
247
|
const branchSets = remote.map(({ branches }) => new Set(branches.map((branch) => branch.name)));
|
|
244
248
|
const commonNames = remote[0].branches.map((branch) => branch.name).filter((name) => branchSets.every((branches) => branches.has(name)));
|
|
245
|
-
const commonBranches =
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
const commonBranches = [];
|
|
250
|
+
for (let index = 0; index < commonNames.length; index += BITBUCKET_REQUEST_BATCH_SIZE) {
|
|
251
|
+
const batch = await Promise.all(commonNames.slice(index, index + BITBUCKET_REQUEST_BATCH_SIZE).map(async (name) => {
|
|
252
|
+
const branches = remote.map(({ base, branches: items }) => ({ base, branch: items.find((item) => item.name === name) })).filter(({ branch }) => branch);
|
|
253
|
+
const updated = [];
|
|
254
|
+
for (let branchIndex = 0; branchIndex < branches.length; branchIndex += BITBUCKET_REQUEST_BATCH_SIZE) {
|
|
255
|
+
updated.push(...await Promise.all(branches.slice(branchIndex, branchIndex + BITBUCKET_REQUEST_BATCH_SIZE).map(({ base, branch }) => fetchBranchUpdatedAt(base, branch))));
|
|
256
|
+
}
|
|
257
|
+
return { name, updatedAt: Math.max(...updated.map((item) => item.updatedAt)), rank: Math.min(...updated.map((item) => item.rank)) };
|
|
258
|
+
}));
|
|
259
|
+
commonBranches.push(...batch);
|
|
260
|
+
}
|
|
250
261
|
commonBranches.sort((a, b) => b.updatedAt - a.updatedAt || a.rank - b.rank || a.name.localeCompare(b.name));
|
|
251
262
|
res.json(commonBranches.map(({ name, updatedAt }) => ({ name, updatedAt: updatedAt ? new Date(updatedAt).toISOString() : null })));
|
|
252
263
|
} catch (error) {
|
|
@@ -10,6 +10,7 @@ import { readPackageUpgradeData } from '../repositories/package-upgrade.js';
|
|
|
10
10
|
import { getGitRemoteUrl } from '../services/git.js';
|
|
11
11
|
|
|
12
12
|
const router = Router();
|
|
13
|
+
const CONFLICT_CHECK_BATCH_SIZE = 5;
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
14
15
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
15
16
|
const client = axios.create({ httpsAgent, validateStatus: () => true });
|
|
@@ -335,35 +336,39 @@ router.get('/my-conflicts', async (_req, res) => {
|
|
|
335
336
|
}
|
|
336
337
|
});
|
|
337
338
|
|
|
338
|
-
const conflicts =
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
...baseConflict,
|
|
357
|
-
resolutionPrUrl: webPullRequestUrl(host, resolutionPr),
|
|
358
|
-
resolutionPrId: resolutionPr.id
|
|
339
|
+
const conflicts = [];
|
|
340
|
+
for (let index = 0; index < values.length; index += CONFLICT_CHECK_BATCH_SIZE) {
|
|
341
|
+
const batch = await Promise.all(values.slice(index, index + CONFLICT_CHECK_BATCH_SIZE).map(async (pr) => {
|
|
342
|
+
const project = pr.toRef?.repository?.project?.key;
|
|
343
|
+
const repository = pr.toRef?.repository?.slug;
|
|
344
|
+
if (!project || !repository || !pr.id) return null;
|
|
345
|
+
if (String(pr.description || '').match(/This branch resolves conflicts for Pull Request #\d+/i)) return null;
|
|
346
|
+
|
|
347
|
+
const resolutionPr = resolutionPrs.get(pullRequestKey(project, repository, pr.id));
|
|
348
|
+
const baseConflict = {
|
|
349
|
+
id: pr.id,
|
|
350
|
+
title: pr.title || `Pull Request #${pr.id}`,
|
|
351
|
+
repositoryName: pr.toRef?.repository?.name || repository,
|
|
352
|
+
sourceBranch: pr.fromRef?.displayId || pr.fromRef?.id,
|
|
353
|
+
targetBranch: pr.toRef?.displayId || pr.toRef?.id,
|
|
354
|
+
updatedDate: pr.updatedDate,
|
|
355
|
+
url: webPullRequestUrl(host, pr)
|
|
359
356
|
};
|
|
360
|
-
|
|
357
|
+
if (resolutionPr) {
|
|
358
|
+
return {
|
|
359
|
+
...baseConflict,
|
|
360
|
+
resolutionPrUrl: webPullRequestUrl(host, resolutionPr),
|
|
361
|
+
resolutionPrId: resolutionPr.id
|
|
362
|
+
};
|
|
363
|
+
}
|
|
361
364
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
365
|
+
const mergeUrl = `https://${host}/rest/api/latest/projects/${encodeURIComponent(project)}/repos/${encodeURIComponent(repository)}/pull-requests/${pr.id}/merge`;
|
|
366
|
+
const mergeResponse = await client.get(mergeUrl, { headers: authHeaders() });
|
|
367
|
+
if (mergeResponse.status < 200 || mergeResponse.status >= 300 || !mergeResponse.data?.conflicted) return null;
|
|
368
|
+
return baseConflict;
|
|
369
|
+
}));
|
|
370
|
+
conflicts.push(...batch);
|
|
371
|
+
}
|
|
367
372
|
const sorted = conflicts.filter(Boolean).sort((a, b) => (b.updatedDate || 0) - (a.updatedDate || 0));
|
|
368
373
|
const unresolved = sorted.filter((pr) => !pr.resolutionPrUrl);
|
|
369
374
|
const resolved = sorted.filter((pr) => pr.resolutionPrUrl);
|