@remogram/provider-gitea-api 0.1.0-beta.3 → 0.1.0-beta.5
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/index.js +641 -29
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fetchJson,
|
|
3
|
+
fetchJsonWithMeta,
|
|
3
4
|
sanitizeField,
|
|
4
5
|
sanitizeUrl,
|
|
5
6
|
assertGitRef,
|
|
@@ -9,19 +10,52 @@ import {
|
|
|
9
10
|
gitAheadBehind,
|
|
10
11
|
refsInventory,
|
|
11
12
|
crInventory,
|
|
12
|
-
|
|
13
|
+
buildMergePlanBodyFromFacts,
|
|
14
|
+
buildChangeRequestOpenedBody,
|
|
15
|
+
buildCommitStatusSetBody,
|
|
16
|
+
parseStatusSetArgs,
|
|
17
|
+
normalizeStatusSetState,
|
|
18
|
+
buildProviderIdentityFromGiteaUser,
|
|
19
|
+
buildBranchProtectionFromGiteaProtection,
|
|
20
|
+
buildCrFilesBody,
|
|
21
|
+
buildCrFilesFromGiteaFiles,
|
|
22
|
+
buildCrCommentsBody,
|
|
23
|
+
buildCrCommentsFromGiteaComments,
|
|
24
|
+
buildForgeChangesFromGiteaPulls,
|
|
25
|
+
buildChecksConclusionObservedEvent,
|
|
26
|
+
appendForgeChangeEvents,
|
|
27
|
+
parseSinceObservedAt,
|
|
13
28
|
ERROR_CODES,
|
|
14
29
|
forgeError,
|
|
15
30
|
forgeIngestCapabilityFacts,
|
|
16
31
|
checkPaginationCapabilityFacts,
|
|
32
|
+
idempotencyScanCapabilityFacts,
|
|
33
|
+
openPullListCapabilityFacts,
|
|
17
34
|
DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
18
35
|
MAX_CHECK_STATUS_PAGES,
|
|
36
|
+
DEFAULT_OPEN_PULL_LIST_PAGE_SIZE,
|
|
37
|
+
MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
19
38
|
paginateCheckStatusPages,
|
|
20
39
|
paginateOffsetListPages,
|
|
21
40
|
fetchWithIngestPageBackoff,
|
|
22
41
|
fetchPageWithIngestBackoff,
|
|
23
42
|
withPerPageParam,
|
|
24
43
|
apiProviderCommands,
|
|
44
|
+
normalizeCrInventorySort,
|
|
45
|
+
DEFAULT_CR_INVENTORY_SLICE_SORT,
|
|
46
|
+
parseTotalCountHeader,
|
|
47
|
+
isCrInventoryFastPathEligible,
|
|
48
|
+
validateFastPathPageLength,
|
|
49
|
+
isNumberSortFastPathEligible,
|
|
50
|
+
isRecentCreatedFastPathEligible,
|
|
51
|
+
giteaRecentCreatedTailPage,
|
|
52
|
+
isNumberSortFullCollectRequired,
|
|
53
|
+
resolveListTruncatedWithTrustedTotal,
|
|
54
|
+
prepareGiteaOpenPullPageItems,
|
|
55
|
+
orderOpenPullNumbers,
|
|
56
|
+
buildOpenPullListMeta,
|
|
57
|
+
giteaOpenPullSortQuery,
|
|
58
|
+
appendSortQuery,
|
|
25
59
|
} from '@remogram/core';
|
|
26
60
|
const PUBLIC_GITEA_HOST = 'gitea.com';
|
|
27
61
|
const PUBLIC_GITEA_API = 'https://gitea.com/api/v1';
|
|
@@ -34,9 +68,23 @@ const AUTH_CAPABILITIES = [
|
|
|
34
68
|
'pr_checks',
|
|
35
69
|
'merge_plan',
|
|
36
70
|
'sync_plan',
|
|
71
|
+
'cr_open',
|
|
72
|
+
'status_set',
|
|
73
|
+
'whoami',
|
|
74
|
+
'branch_protection',
|
|
75
|
+
'cr_files',
|
|
76
|
+
'cr_comments',
|
|
77
|
+
'forge_changes',
|
|
37
78
|
];
|
|
38
79
|
|
|
39
|
-
const STRUCTURED_COMMANDS = apiProviderCommands(
|
|
80
|
+
const STRUCTURED_COMMANDS = apiProviderCommands({
|
|
81
|
+
writeCommandsImplemented: true,
|
|
82
|
+
statusSetImplemented: true,
|
|
83
|
+
branchProtectionImplemented: true,
|
|
84
|
+
crFilesImplemented: true,
|
|
85
|
+
crCommentsImplemented: true,
|
|
86
|
+
forgeChangesImplemented: true,
|
|
87
|
+
});
|
|
40
88
|
|
|
41
89
|
export function giteaToken() {
|
|
42
90
|
return process.env.GITEA_TOKEN || null;
|
|
@@ -132,8 +180,32 @@ export async function giteaFetch(config, parsed, path, options = {}) {
|
|
|
132
180
|
});
|
|
133
181
|
}
|
|
134
182
|
|
|
183
|
+
export async function giteaFetchWithMeta(config, parsed, path, options = {}) {
|
|
184
|
+
const token = requireToken();
|
|
185
|
+
const url = `${apiBase(config, parsed)}${path}`;
|
|
186
|
+
return fetchJsonWithMeta(url, {
|
|
187
|
+
...options,
|
|
188
|
+
headers: { ...authHeaders(token), ...(options.headers || {}) },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
135
192
|
const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
|
|
136
|
-
const GITEA_PAGE_SIZE =
|
|
193
|
+
const GITEA_PAGE_SIZE = DEFAULT_OPEN_PULL_LIST_PAGE_SIZE;
|
|
194
|
+
|
|
195
|
+
function idempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
|
|
196
|
+
return forgeError(
|
|
197
|
+
ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
|
|
198
|
+
'Cannot prove no open pull exists for head+base within scan limit; use cr inventory or open manually',
|
|
199
|
+
null,
|
|
200
|
+
{
|
|
201
|
+
idempotency_scan: {
|
|
202
|
+
pages: pagesScanned,
|
|
203
|
+
max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
204
|
+
page_size: pageSizeUsed,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
}
|
|
137
209
|
|
|
138
210
|
export async function giteaFetchPaginated(config, parsed, path) {
|
|
139
211
|
return paginateCheckStatusPages({
|
|
@@ -213,6 +285,195 @@ export async function repoStatus(ctx) {
|
|
|
213
285
|
};
|
|
214
286
|
}
|
|
215
287
|
|
|
288
|
+
export async function whoami(ctx) {
|
|
289
|
+
requireToken();
|
|
290
|
+
const user = await giteaFetch(ctx.config, ctx.parsed, '/user');
|
|
291
|
+
return buildProviderIdentityFromGiteaUser(user);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function branchProtection(ctx, { branchRef }) {
|
|
295
|
+
requireToken();
|
|
296
|
+
const protection = await giteaFetch(
|
|
297
|
+
ctx.config,
|
|
298
|
+
ctx.parsed,
|
|
299
|
+
repoApiPath(ctx.config, 'branch_protections', branchRef),
|
|
300
|
+
);
|
|
301
|
+
return buildBranchProtectionFromGiteaProtection(branchRef, protection);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function crFiles(ctx, { number }) {
|
|
305
|
+
requireToken();
|
|
306
|
+
if (number == null) {
|
|
307
|
+
throw Object.assign(new Error('--number required'), {
|
|
308
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR changed paths'),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const path = repoApiPath(ctx.config, 'pulls', number, 'files');
|
|
312
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
313
|
+
let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
|
|
314
|
+
const allFiles = [];
|
|
315
|
+
let listTruncated = false;
|
|
316
|
+
let entryCount = 0;
|
|
317
|
+
|
|
318
|
+
for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
|
|
319
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
320
|
+
async ({ page: pageNum, limit }) => {
|
|
321
|
+
const body = await giteaFetch(
|
|
322
|
+
ctx.config,
|
|
323
|
+
ctx.parsed,
|
|
324
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
325
|
+
);
|
|
326
|
+
if (!Array.isArray(body)) {
|
|
327
|
+
throw Object.assign(new Error('Provider returned non-array pull files list'), {
|
|
328
|
+
forgeError: forgeError(
|
|
329
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
330
|
+
'Provider returned non-array pull files list',
|
|
331
|
+
),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return body;
|
|
335
|
+
},
|
|
336
|
+
page,
|
|
337
|
+
activeLimit,
|
|
338
|
+
);
|
|
339
|
+
activeLimit = usedLimit;
|
|
340
|
+
entryCount += items.length;
|
|
341
|
+
allFiles.push(...items);
|
|
342
|
+
if (items.length < usedLimit) break;
|
|
343
|
+
if (page === MAX_CHECK_PAGES) {
|
|
344
|
+
listTruncated = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const body = buildCrFilesFromGiteaFiles(number, allFiles);
|
|
349
|
+
if (listTruncated) {
|
|
350
|
+
return buildCrFilesBody({
|
|
351
|
+
pr_number: body.pr_number,
|
|
352
|
+
changed_paths: body.changed_paths,
|
|
353
|
+
paths_truncated: true,
|
|
354
|
+
path_count: entryCount,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return body;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function crComments(ctx, { number }) {
|
|
361
|
+
requireToken();
|
|
362
|
+
if (number == null) {
|
|
363
|
+
throw Object.assign(new Error('--number required'), {
|
|
364
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR review comments'),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const path = repoApiPath(ctx.config, 'pulls', number, 'comments');
|
|
368
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
369
|
+
let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
|
|
370
|
+
const allComments = [];
|
|
371
|
+
let listTruncated = false;
|
|
372
|
+
let entryCount = 0;
|
|
373
|
+
|
|
374
|
+
for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
|
|
375
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
376
|
+
async ({ page: pageNum, limit }) => {
|
|
377
|
+
const body = await giteaFetch(
|
|
378
|
+
ctx.config,
|
|
379
|
+
ctx.parsed,
|
|
380
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
381
|
+
);
|
|
382
|
+
if (!Array.isArray(body)) {
|
|
383
|
+
throw Object.assign(new Error('Provider returned non-array pull comments list'), {
|
|
384
|
+
forgeError: forgeError(
|
|
385
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
386
|
+
'Provider returned non-array pull comments list',
|
|
387
|
+
),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return body;
|
|
391
|
+
},
|
|
392
|
+
page,
|
|
393
|
+
activeLimit,
|
|
394
|
+
);
|
|
395
|
+
activeLimit = usedLimit;
|
|
396
|
+
entryCount += items.length;
|
|
397
|
+
allComments.push(...items);
|
|
398
|
+
if (items.length < usedLimit) break;
|
|
399
|
+
if (page === MAX_CHECK_PAGES) {
|
|
400
|
+
listTruncated = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const body = buildCrCommentsFromGiteaComments(number, allComments);
|
|
405
|
+
if (listTruncated) {
|
|
406
|
+
return buildCrCommentsBody({
|
|
407
|
+
pr_number: body.pr_number,
|
|
408
|
+
comments: body.comments,
|
|
409
|
+
comments_truncated: true,
|
|
410
|
+
comment_count: entryCount,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return body;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function forgeChanges(ctx, { since }) {
|
|
417
|
+
requireToken();
|
|
418
|
+
const sinceIso = parseSinceObservedAt(since);
|
|
419
|
+
const path = `${repoApiPath(ctx.config, 'pulls')}?state=all&sort=recentupdate`;
|
|
420
|
+
const pageSep = '&';
|
|
421
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
422
|
+
const allPulls = [];
|
|
423
|
+
let listTruncated = false;
|
|
424
|
+
let entryCount = 0;
|
|
425
|
+
|
|
426
|
+
for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
|
|
427
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
428
|
+
async ({ page: pageNum, limit }) => {
|
|
429
|
+
const body = await giteaFetch(
|
|
430
|
+
ctx.config,
|
|
431
|
+
ctx.parsed,
|
|
432
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
433
|
+
);
|
|
434
|
+
if (!Array.isArray(body)) {
|
|
435
|
+
throw Object.assign(new Error('Provider returned non-array pull list'), {
|
|
436
|
+
forgeError: forgeError(
|
|
437
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
438
|
+
'Provider returned non-array pull list',
|
|
439
|
+
),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return body;
|
|
443
|
+
},
|
|
444
|
+
page,
|
|
445
|
+
activeLimit,
|
|
446
|
+
);
|
|
447
|
+
activeLimit = usedLimit;
|
|
448
|
+
entryCount += items.length;
|
|
449
|
+
allPulls.push(...items);
|
|
450
|
+
if (items.length < usedLimit) break;
|
|
451
|
+
if (page === MAX_CHECK_PAGES) {
|
|
452
|
+
listTruncated = true;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let body = buildForgeChangesFromGiteaPulls(sinceIso, allPulls, { listTruncated });
|
|
457
|
+
const checkNumbers = new Set();
|
|
458
|
+
for (const event of body.events) {
|
|
459
|
+
if (event.kind === 'pr_opened' || event.kind === 'head_sha_moved') {
|
|
460
|
+
checkNumbers.add(event.pr_number);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const checkEvents = [];
|
|
465
|
+
for (const number of checkNumbers) {
|
|
466
|
+
const checks = await prChecks(ctx, { number });
|
|
467
|
+
checkEvents.push(buildChecksConclusionObservedEvent(number, checks));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (checkEvents.length > 0) {
|
|
471
|
+
body = appendForgeChangeEvents(body, checkEvents, { listTruncated });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return body;
|
|
475
|
+
}
|
|
476
|
+
|
|
216
477
|
export function providerCapabilities() {
|
|
217
478
|
const check_sources = ['commit_statuses'];
|
|
218
479
|
return {
|
|
@@ -222,13 +483,27 @@ export function providerCapabilities() {
|
|
|
222
483
|
mergeability_confidence: 'direct',
|
|
223
484
|
host_binding: 'verified_remote_host',
|
|
224
485
|
pagination: 'supported',
|
|
225
|
-
write_support:
|
|
486
|
+
write_support: true,
|
|
487
|
+
write_commands: ['cr_open', 'status_set'],
|
|
226
488
|
...forgeIngestCapabilityFacts(),
|
|
227
489
|
...checkPaginationCapabilityFacts({
|
|
228
490
|
strategy: 'offset_limit',
|
|
229
491
|
pageSizeParam: 'limit',
|
|
230
492
|
sourceCount: check_sources.length,
|
|
231
493
|
}),
|
|
494
|
+
...idempotencyScanCapabilityFacts(),
|
|
495
|
+
...openPullListCapabilityFacts({
|
|
496
|
+
totalCountSource: 'response_header',
|
|
497
|
+
totalCountHeader: 'X-Total-Count',
|
|
498
|
+
sliceSortNotes: {
|
|
499
|
+
recent_created:
|
|
500
|
+
'sort=oldest; fetches tail page when total exceeds limit; page reversed for newest-first',
|
|
501
|
+
number_asc:
|
|
502
|
+
'full-list collect within compliant_max when total exceeds limit, then client sort',
|
|
503
|
+
number_desc:
|
|
504
|
+
'full-list collect within compliant_max when total exceeds limit, then client sort',
|
|
505
|
+
},
|
|
506
|
+
}),
|
|
232
507
|
};
|
|
233
508
|
}
|
|
234
509
|
|
|
@@ -261,37 +536,364 @@ export async function getPull(ctx, { number }) {
|
|
|
261
536
|
return giteaFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'pulls', number));
|
|
262
537
|
}
|
|
263
538
|
|
|
264
|
-
|
|
539
|
+
/** Paginated open-pull scan for idempotent cr open; fail-closed when scan cap prevents proof of absence. */
|
|
540
|
+
export async function findOpenPullByHeadBase(ctx, head, base) {
|
|
541
|
+
requireToken();
|
|
542
|
+
const path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
543
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
544
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
545
|
+
|
|
546
|
+
for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
|
|
547
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
548
|
+
async ({ page: pageNum, limit }) => {
|
|
549
|
+
const body = await giteaFetch(
|
|
550
|
+
ctx.config,
|
|
551
|
+
ctx.parsed,
|
|
552
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
553
|
+
);
|
|
554
|
+
if (!Array.isArray(body)) {
|
|
555
|
+
throw Object.assign(new Error('Provider returned non-array open pull list'), {
|
|
556
|
+
forgeError: forgeError(
|
|
557
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
558
|
+
'Provider returned non-array open pull list',
|
|
559
|
+
),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return body;
|
|
563
|
+
},
|
|
564
|
+
page,
|
|
565
|
+
activeLimit,
|
|
566
|
+
);
|
|
567
|
+
activeLimit = usedLimit;
|
|
568
|
+
|
|
569
|
+
const match =
|
|
570
|
+
items.find(
|
|
571
|
+
(pr) =>
|
|
572
|
+
String(pr?.state ?? '').toLowerCase() === 'open' &&
|
|
573
|
+
pr?.head?.ref === head &&
|
|
574
|
+
pr?.base?.ref === base,
|
|
575
|
+
) ?? null;
|
|
576
|
+
if (match) return match;
|
|
577
|
+
if (items.length < usedLimit) return null;
|
|
578
|
+
if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
|
|
579
|
+
throw Object.assign(new Error('Open pull idempotency scan incomplete'), {
|
|
580
|
+
forgeError: idempotencyScanIncompleteError(page, usedLimit),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function statusSetIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
|
|
588
|
+
return forgeError(
|
|
589
|
+
ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
|
|
590
|
+
'Cannot prove no commit status exists for sha+context within scan limit; retry or set manually',
|
|
591
|
+
null,
|
|
592
|
+
{
|
|
593
|
+
idempotency_scan: {
|
|
594
|
+
pages: pagesScanned,
|
|
595
|
+
max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
596
|
+
page_size: pageSizeUsed,
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Paginated commit-status scan for idempotent status set; fail-closed when scan cap prevents proof of absence. */
|
|
603
|
+
export async function findCommitStatusByContext(ctx, sha, context) {
|
|
265
604
|
requireToken();
|
|
605
|
+
const path = repoApiPath(ctx.config, 'commits', sha, 'statuses');
|
|
606
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
607
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
608
|
+
let bestMatch = null;
|
|
609
|
+
|
|
610
|
+
for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
|
|
611
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
612
|
+
async ({ page: pageNum, limit }) => {
|
|
613
|
+
const body = await giteaFetch(
|
|
614
|
+
ctx.config,
|
|
615
|
+
ctx.parsed,
|
|
616
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
617
|
+
);
|
|
618
|
+
if (!Array.isArray(body)) {
|
|
619
|
+
throw Object.assign(new Error('Provider returned non-array commit status list'), {
|
|
620
|
+
forgeError: forgeError(
|
|
621
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
622
|
+
'Provider returned non-array commit status list',
|
|
623
|
+
),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
return body;
|
|
627
|
+
},
|
|
628
|
+
page,
|
|
629
|
+
activeLimit,
|
|
630
|
+
);
|
|
631
|
+
activeLimit = usedLimit;
|
|
632
|
+
|
|
633
|
+
for (const record of items) {
|
|
634
|
+
if (record?.context !== context) continue;
|
|
635
|
+
if (!bestMatch || giteaStatusRecordOrder(record, bestMatch) > 0) {
|
|
636
|
+
bestMatch = record;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (items.length < usedLimit) return bestMatch;
|
|
641
|
+
if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
|
|
642
|
+
throw Object.assign(new Error('Commit status idempotency scan incomplete'), {
|
|
643
|
+
forgeError: statusSetIdempotencyScanIncompleteError(page, usedLimit),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return bestMatch;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export async function statusSet(ctx, args) {
|
|
651
|
+
const parsed = parseStatusSetArgs(args);
|
|
652
|
+
const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
|
|
653
|
+
if (existing) {
|
|
654
|
+
const existingState = normalizeStatusSetState(existing.status ?? existing.state);
|
|
655
|
+
if (existingState === parsed.state) {
|
|
656
|
+
return buildCommitStatusSetBody(existing, parsed, { reusedExisting: true });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const payload = {
|
|
660
|
+
state: parsed.state,
|
|
661
|
+
context: parsed.context,
|
|
662
|
+
};
|
|
663
|
+
if (parsed.description != null) payload.description = parsed.description;
|
|
664
|
+
if (parsed.target_url != null) payload.target_url = parsed.target_url;
|
|
665
|
+
const response = await giteaFetch(
|
|
666
|
+
ctx.config,
|
|
667
|
+
ctx.parsed,
|
|
668
|
+
repoApiPath(ctx.config, 'statuses', parsed.sha),
|
|
669
|
+
{
|
|
670
|
+
method: 'POST',
|
|
671
|
+
headers: { 'Content-Type': 'application/json' },
|
|
672
|
+
body: JSON.stringify(payload),
|
|
673
|
+
},
|
|
674
|
+
);
|
|
675
|
+
return buildCommitStatusSetBody(response, parsed);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function crOpen(ctx, { head, base, title, body: prBody }) {
|
|
679
|
+
assertGitRef(head, 'head');
|
|
680
|
+
assertGitRef(base, 'base');
|
|
681
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
682
|
+
throw Object.assign(new Error('--title required'), {
|
|
683
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--title required for cr open'),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
const payload = {
|
|
687
|
+
title: sanitizeField(title),
|
|
688
|
+
head: sanitizeField(head),
|
|
689
|
+
base: sanitizeField(base),
|
|
690
|
+
};
|
|
691
|
+
if (prBody != null && String(prBody).trim() !== '') {
|
|
692
|
+
payload.body = sanitizeField(String(prBody));
|
|
693
|
+
}
|
|
694
|
+
const existing = await findOpenPullByHeadBase(ctx, payload.head, payload.base);
|
|
695
|
+
if (existing) {
|
|
696
|
+
return buildChangeRequestOpenedBody(existing, { head, base, title }, { reusedExisting: true });
|
|
697
|
+
}
|
|
698
|
+
const pull = await giteaFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'pulls'), {
|
|
699
|
+
method: 'POST',
|
|
700
|
+
headers: { 'Content-Type': 'application/json' },
|
|
701
|
+
body: JSON.stringify(payload),
|
|
702
|
+
});
|
|
703
|
+
return buildChangeRequestOpenedBody(pull, { head, base, title });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const GITEA_OPEN_PULL_COMPLIANT_MAX =
|
|
707
|
+
DEFAULT_OPEN_PULL_LIST_PAGE_SIZE * MAX_CHECK_STATUS_PAGES;
|
|
708
|
+
|
|
709
|
+
async function probeGiteaOpenPullPageOne(ctx, retainMax, sliceSort) {
|
|
710
|
+
const maxTrusted = GITEA_OPEN_PULL_COMPLIANT_MAX * 2;
|
|
711
|
+
let path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
712
|
+
path = appendSortQuery(path, giteaOpenPullSortQuery(sliceSort));
|
|
713
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
714
|
+
const requestLimit = Math.min(retainMax, GITEA_PAGE_SIZE);
|
|
715
|
+
try {
|
|
716
|
+
const { body, headers } = await giteaFetchWithMeta(
|
|
717
|
+
ctx.config,
|
|
718
|
+
ctx.parsed,
|
|
719
|
+
`${path}${pageSep}limit=${requestLimit}&page=1`,
|
|
720
|
+
);
|
|
721
|
+
if (!Array.isArray(body)) return null;
|
|
722
|
+
const totalCount = parseTotalCountHeader(headers, 'X-Total-Count', { maxTrusted });
|
|
723
|
+
if (totalCount == null) return null;
|
|
724
|
+
const listTruncated = totalCount > GITEA_OPEN_PULL_COMPLIANT_MAX;
|
|
725
|
+
return { body, totalCount, listTruncated, requestLimit };
|
|
726
|
+
} catch {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function buildGiteaOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, listTruncated) {
|
|
732
|
+
const pageItems = prepareGiteaOpenPullPageItems(body, sliceSort);
|
|
733
|
+
let numbers = orderOpenPullNumbers(pageItems, (pr) => pr?.number, sliceSort);
|
|
734
|
+
if (numbers.length > retainMax) numbers = numbers.slice(0, retainMax);
|
|
735
|
+
return buildOpenPullListMeta({
|
|
736
|
+
totalCount,
|
|
737
|
+
numbers,
|
|
738
|
+
listTruncated,
|
|
739
|
+
sliceSort,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function fetchGiteaRecentCreatedTailSlice(ctx, retainMax, sliceSort, totalCount) {
|
|
744
|
+
const tailPage = giteaRecentCreatedTailPage(totalCount, GITEA_PAGE_SIZE);
|
|
745
|
+
let path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
746
|
+
path = appendSortQuery(path, giteaOpenPullSortQuery(sliceSort));
|
|
747
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
748
|
+
let body;
|
|
749
|
+
try {
|
|
750
|
+
body = await giteaFetch(
|
|
751
|
+
ctx.config,
|
|
752
|
+
ctx.parsed,
|
|
753
|
+
`${path}${pageSep}limit=${GITEA_PAGE_SIZE}&page=${tailPage}`,
|
|
754
|
+
);
|
|
755
|
+
} catch {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
if (!Array.isArray(body) || body.length === 0) return null;
|
|
759
|
+
return buildGiteaOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, false);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function giteaProbePaginationOpts(probe, extra = {}) {
|
|
763
|
+
const { body, totalCount, requestLimit } = probe;
|
|
764
|
+
return {
|
|
765
|
+
trustedTotalCount: totalCount,
|
|
766
|
+
seededFirstPage: { items: body, usedLimit: requestLimit },
|
|
767
|
+
...extra,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function paginateGiteaOpenPullList(ctx, opts, sliceSort, paginationOpts = {}) {
|
|
772
|
+
const {
|
|
773
|
+
trustedTotalCount = null,
|
|
774
|
+
numberSortFullCollect = false,
|
|
775
|
+
seededFirstPage = null,
|
|
776
|
+
startPage = 1,
|
|
777
|
+
maxPages = MAX_CHECK_STATUS_PAGES,
|
|
778
|
+
suppressFinalPageProbe = false,
|
|
779
|
+
} = paginationOpts;
|
|
266
780
|
const listLimit =
|
|
267
781
|
opts.limit != null && Number.isInteger(Number(opts.limit)) && Number(opts.limit) > 0
|
|
268
782
|
? Number(opts.limit)
|
|
269
783
|
: null;
|
|
784
|
+
const retainMax =
|
|
785
|
+
listLimit == null &&
|
|
786
|
+
opts.retain_max != null &&
|
|
787
|
+
Number.isInteger(Number(opts.retain_max)) &&
|
|
788
|
+
Number(opts.retain_max) > 0
|
|
789
|
+
? Number(opts.retain_max)
|
|
790
|
+
: null;
|
|
270
791
|
const pageSize =
|
|
271
792
|
listLimit != null ? Math.min(listLimit, GITEA_PAGE_SIZE) : GITEA_PAGE_SIZE;
|
|
272
|
-
|
|
793
|
+
let path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
794
|
+
path = appendSortQuery(path, giteaOpenPullSortQuery(sliceSort));
|
|
273
795
|
const pageSep = path.includes('?') ? '&' : '?';
|
|
274
|
-
const
|
|
796
|
+
const effectiveRetainMax = numberSortFullCollect ? null : retainMax;
|
|
797
|
+
const {
|
|
798
|
+
items: all,
|
|
799
|
+
list_truncated: listTruncated,
|
|
800
|
+
entry_count: entryCount,
|
|
801
|
+
walked_count: walkedCount,
|
|
802
|
+
} = await paginateOffsetListPages({
|
|
275
803
|
pageSize,
|
|
276
804
|
listLimit,
|
|
805
|
+
retainMax: effectiveRetainMax,
|
|
806
|
+
trustedEntryCount: trustedTotalCount,
|
|
807
|
+
seededFirstPage,
|
|
808
|
+
startPage,
|
|
809
|
+
maxPages,
|
|
810
|
+
suppressFinalPageProbe,
|
|
277
811
|
...(listLimit != null && pageSize < listLimit ? { maxPagesTruncatesWithLimit: true } : {}),
|
|
278
812
|
fetchPage: async ({ page, limit }) => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
let numbers =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
813
|
+
const body = await giteaFetch(
|
|
814
|
+
ctx.config,
|
|
815
|
+
ctx.parsed,
|
|
816
|
+
`${path}${pageSep}limit=${limit}&page=${page}`,
|
|
817
|
+
);
|
|
818
|
+
return Array.isArray(body) ? body : [];
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
let numbers = orderOpenPullNumbers(
|
|
822
|
+
prepareGiteaOpenPullPageItems(all, sliceSort),
|
|
823
|
+
(pr) => pr?.number,
|
|
824
|
+
sliceSort,
|
|
825
|
+
);
|
|
826
|
+
const outputCap = listLimit ?? retainMax;
|
|
827
|
+
if (outputCap != null && numbers.length > outputCap) {
|
|
828
|
+
numbers = numbers.slice(0, outputCap);
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
numbers,
|
|
832
|
+
list_truncated: resolveListTruncatedWithTrustedTotal({
|
|
833
|
+
listTruncated,
|
|
834
|
+
trustedTotalCount,
|
|
835
|
+
walkedCount,
|
|
836
|
+
fullCollect: numberSortFullCollect,
|
|
837
|
+
}),
|
|
838
|
+
...(entryCount != null ? { entry_count: entryCount } : {}),
|
|
839
|
+
slice_sort: sliceSort,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export async function listOpenPullsWithMeta(ctx, opts = {}) {
|
|
844
|
+
requireToken();
|
|
845
|
+
const sliceSort = normalizeCrInventorySort(opts.sort ?? DEFAULT_CR_INVENTORY_SLICE_SORT);
|
|
846
|
+
if (!isCrInventoryFastPathEligible(opts)) {
|
|
847
|
+
return paginateGiteaOpenPullList(ctx, opts, sliceSort);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const retainMax = Number(opts.retain_max);
|
|
851
|
+
const probe = await probeGiteaOpenPullPageOne(ctx, retainMax, sliceSort);
|
|
852
|
+
if (!probe) {
|
|
853
|
+
return paginateGiteaOpenPullList(ctx, opts, sliceSort);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const { body, totalCount, listTruncated, requestLimit } = probe;
|
|
857
|
+
|
|
858
|
+
if (listTruncated) {
|
|
859
|
+
if (body.length === 0) {
|
|
860
|
+
return paginateGiteaOpenPullList(ctx, opts, sliceSort, giteaProbePaginationOpts(probe));
|
|
861
|
+
}
|
|
862
|
+
return buildGiteaOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, true);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (
|
|
866
|
+
sliceSort === 'recent_created' &&
|
|
867
|
+
!isRecentCreatedFastPathEligible(totalCount, retainMax, sliceSort, 'gitea-api')
|
|
868
|
+
) {
|
|
869
|
+
const tail = await fetchGiteaRecentCreatedTailSlice(ctx, retainMax, sliceSort, totalCount);
|
|
870
|
+
if (tail) return tail;
|
|
871
|
+
const tailRetry = await fetchGiteaRecentCreatedTailSlice(ctx, retainMax, sliceSort, totalCount);
|
|
872
|
+
if (tailRetry) return tailRetry;
|
|
873
|
+
const tailPage = giteaRecentCreatedTailPage(totalCount, GITEA_PAGE_SIZE);
|
|
874
|
+
return paginateGiteaOpenPullList(ctx, opts, sliceSort, {
|
|
875
|
+
trustedTotalCount: totalCount,
|
|
876
|
+
startPage: tailPage,
|
|
877
|
+
maxPages: tailPage,
|
|
878
|
+
suppressFinalPageProbe: true,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (
|
|
883
|
+
isRecentCreatedFastPathEligible(totalCount, retainMax, sliceSort, 'gitea-api') &&
|
|
884
|
+
isNumberSortFastPathEligible(totalCount, retainMax, sliceSort) &&
|
|
885
|
+
validateFastPathPageLength(totalCount, requestLimit, body.length)
|
|
886
|
+
) {
|
|
887
|
+
return buildGiteaOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, false);
|
|
293
888
|
}
|
|
294
|
-
|
|
889
|
+
|
|
890
|
+
const numberSortFullCollect = isNumberSortFullCollectRequired(totalCount, retainMax, sliceSort);
|
|
891
|
+
return paginateGiteaOpenPullList(
|
|
892
|
+
ctx,
|
|
893
|
+
opts,
|
|
894
|
+
sliceSort,
|
|
895
|
+
giteaProbePaginationOpts(probe, { numberSortFullCollect }),
|
|
896
|
+
);
|
|
295
897
|
}
|
|
296
898
|
|
|
297
899
|
export async function listOpenPulls(ctx, opts = {}) {
|
|
@@ -370,13 +972,16 @@ export function summarizeChecks(statuses) {
|
|
|
370
972
|
export async function mergePlan(ctx, opts) {
|
|
371
973
|
const view = await prView(ctx, opts);
|
|
372
974
|
const checks = await prChecks(ctx, { number: view.pr_number });
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
975
|
+
let mergeOpts = opts;
|
|
976
|
+
if (opts.allowed_paths) {
|
|
977
|
+
try {
|
|
978
|
+
const crFilesBody = await crFiles(ctx, { number: view.pr_number });
|
|
979
|
+
mergeOpts = { ...opts, changed_paths: crFilesBody.changed_paths };
|
|
980
|
+
} catch {
|
|
981
|
+
// Fall back to local git diff in resolveMergePlanPathScope.
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return buildMergePlanBodyFromFacts(ctx, view, checks, mergeOpts);
|
|
380
985
|
}
|
|
381
986
|
|
|
382
987
|
export async function syncPlan(ctx, remoteName = 'origin') {
|
|
@@ -418,4 +1023,11 @@ export const provider = {
|
|
|
418
1023
|
prChecks,
|
|
419
1024
|
mergePlan,
|
|
420
1025
|
syncPlan,
|
|
1026
|
+
crOpen,
|
|
1027
|
+
statusSet,
|
|
1028
|
+
whoami,
|
|
1029
|
+
branchProtection,
|
|
1030
|
+
crFiles,
|
|
1031
|
+
crComments,
|
|
1032
|
+
forgeChanges,
|
|
421
1033
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remogram/provider-gitea-api",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.5",
|
|
4
4
|
"description": "Gitea REST API forge provider for remogram",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@remogram/core": "0.1.0-beta.
|
|
25
|
+
"@remogram/core": "0.1.0-beta.5"
|
|
26
26
|
}
|
|
27
27
|
}
|