@remogram/provider-gitea-api 0.1.0-beta.4 → 0.1.0-beta.6
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 +319 -6
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -10,8 +10,21 @@ import {
|
|
|
10
10
|
gitAheadBehind,
|
|
11
11
|
refsInventory,
|
|
12
12
|
crInventory,
|
|
13
|
-
|
|
13
|
+
buildMergePlanFromProviderFacts,
|
|
14
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,
|
|
15
28
|
ERROR_CODES,
|
|
16
29
|
forgeError,
|
|
17
30
|
forgeIngestCapabilityFacts,
|
|
@@ -43,6 +56,7 @@ import {
|
|
|
43
56
|
buildOpenPullListMeta,
|
|
44
57
|
giteaOpenPullSortQuery,
|
|
45
58
|
appendSortQuery,
|
|
59
|
+
assertWriteCommandConfigured,
|
|
46
60
|
} from '@remogram/core';
|
|
47
61
|
const PUBLIC_GITEA_HOST = 'gitea.com';
|
|
48
62
|
const PUBLIC_GITEA_API = 'https://gitea.com/api/v1';
|
|
@@ -56,9 +70,22 @@ const AUTH_CAPABILITIES = [
|
|
|
56
70
|
'merge_plan',
|
|
57
71
|
'sync_plan',
|
|
58
72
|
'cr_open',
|
|
73
|
+
'status_set',
|
|
74
|
+
'whoami',
|
|
75
|
+
'branch_protection',
|
|
76
|
+
'cr_files',
|
|
77
|
+
'cr_comments',
|
|
78
|
+
'forge_changes',
|
|
59
79
|
];
|
|
60
80
|
|
|
61
|
-
const STRUCTURED_COMMANDS = apiProviderCommands({
|
|
81
|
+
const STRUCTURED_COMMANDS = apiProviderCommands({
|
|
82
|
+
writeCommandsImplemented: true,
|
|
83
|
+
statusSetImplemented: true,
|
|
84
|
+
branchProtectionImplemented: true,
|
|
85
|
+
crFilesImplemented: true,
|
|
86
|
+
crCommentsImplemented: true,
|
|
87
|
+
forgeChangesImplemented: true,
|
|
88
|
+
});
|
|
62
89
|
|
|
63
90
|
export function giteaToken() {
|
|
64
91
|
return process.env.GITEA_TOKEN || null;
|
|
@@ -259,6 +286,195 @@ export async function repoStatus(ctx) {
|
|
|
259
286
|
};
|
|
260
287
|
}
|
|
261
288
|
|
|
289
|
+
export async function whoami(ctx) {
|
|
290
|
+
requireToken();
|
|
291
|
+
const user = await giteaFetch(ctx.config, ctx.parsed, '/user');
|
|
292
|
+
return buildProviderIdentityFromGiteaUser(user);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function branchProtection(ctx, { branchRef }) {
|
|
296
|
+
requireToken();
|
|
297
|
+
const protection = await giteaFetch(
|
|
298
|
+
ctx.config,
|
|
299
|
+
ctx.parsed,
|
|
300
|
+
repoApiPath(ctx.config, 'branch_protections', branchRef),
|
|
301
|
+
);
|
|
302
|
+
return buildBranchProtectionFromGiteaProtection(branchRef, protection);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function crFiles(ctx, { number }) {
|
|
306
|
+
requireToken();
|
|
307
|
+
if (number == null) {
|
|
308
|
+
throw Object.assign(new Error('--number required'), {
|
|
309
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR changed paths'),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const path = repoApiPath(ctx.config, 'pulls', number, 'files');
|
|
313
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
314
|
+
let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
|
|
315
|
+
const allFiles = [];
|
|
316
|
+
let listTruncated = false;
|
|
317
|
+
let entryCount = 0;
|
|
318
|
+
|
|
319
|
+
for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
|
|
320
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
321
|
+
async ({ page: pageNum, limit }) => {
|
|
322
|
+
const body = await giteaFetch(
|
|
323
|
+
ctx.config,
|
|
324
|
+
ctx.parsed,
|
|
325
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
326
|
+
);
|
|
327
|
+
if (!Array.isArray(body)) {
|
|
328
|
+
throw Object.assign(new Error('Provider returned non-array pull files list'), {
|
|
329
|
+
forgeError: forgeError(
|
|
330
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
331
|
+
'Provider returned non-array pull files list',
|
|
332
|
+
),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return body;
|
|
336
|
+
},
|
|
337
|
+
page,
|
|
338
|
+
activeLimit,
|
|
339
|
+
);
|
|
340
|
+
activeLimit = usedLimit;
|
|
341
|
+
entryCount += items.length;
|
|
342
|
+
allFiles.push(...items);
|
|
343
|
+
if (items.length < usedLimit) break;
|
|
344
|
+
if (page === MAX_CHECK_PAGES) {
|
|
345
|
+
listTruncated = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const body = buildCrFilesFromGiteaFiles(number, allFiles);
|
|
350
|
+
if (listTruncated) {
|
|
351
|
+
return buildCrFilesBody({
|
|
352
|
+
pr_number: body.pr_number,
|
|
353
|
+
changed_paths: body.changed_paths,
|
|
354
|
+
paths_truncated: true,
|
|
355
|
+
path_count: entryCount,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return body;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function crComments(ctx, { number }) {
|
|
362
|
+
requireToken();
|
|
363
|
+
if (number == null) {
|
|
364
|
+
throw Object.assign(new Error('--number required'), {
|
|
365
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR review comments'),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const path = repoApiPath(ctx.config, 'pulls', number, 'comments');
|
|
369
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
370
|
+
let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
|
|
371
|
+
const allComments = [];
|
|
372
|
+
let listTruncated = false;
|
|
373
|
+
let entryCount = 0;
|
|
374
|
+
|
|
375
|
+
for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
|
|
376
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
377
|
+
async ({ page: pageNum, limit }) => {
|
|
378
|
+
const body = await giteaFetch(
|
|
379
|
+
ctx.config,
|
|
380
|
+
ctx.parsed,
|
|
381
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
382
|
+
);
|
|
383
|
+
if (!Array.isArray(body)) {
|
|
384
|
+
throw Object.assign(new Error('Provider returned non-array pull comments list'), {
|
|
385
|
+
forgeError: forgeError(
|
|
386
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
387
|
+
'Provider returned non-array pull comments list',
|
|
388
|
+
),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return body;
|
|
392
|
+
},
|
|
393
|
+
page,
|
|
394
|
+
activeLimit,
|
|
395
|
+
);
|
|
396
|
+
activeLimit = usedLimit;
|
|
397
|
+
entryCount += items.length;
|
|
398
|
+
allComments.push(...items);
|
|
399
|
+
if (items.length < usedLimit) break;
|
|
400
|
+
if (page === MAX_CHECK_PAGES) {
|
|
401
|
+
listTruncated = true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const body = buildCrCommentsFromGiteaComments(number, allComments);
|
|
406
|
+
if (listTruncated) {
|
|
407
|
+
return buildCrCommentsBody({
|
|
408
|
+
pr_number: body.pr_number,
|
|
409
|
+
comments: body.comments,
|
|
410
|
+
comments_truncated: true,
|
|
411
|
+
comment_count: entryCount,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return body;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function forgeChanges(ctx, { since }) {
|
|
418
|
+
requireToken();
|
|
419
|
+
const sinceIso = parseSinceObservedAt(since);
|
|
420
|
+
const path = `${repoApiPath(ctx.config, 'pulls')}?state=all&sort=recentupdate`;
|
|
421
|
+
const pageSep = '&';
|
|
422
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
423
|
+
const allPulls = [];
|
|
424
|
+
let listTruncated = false;
|
|
425
|
+
let entryCount = 0;
|
|
426
|
+
|
|
427
|
+
for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
|
|
428
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
429
|
+
async ({ page: pageNum, limit }) => {
|
|
430
|
+
const body = await giteaFetch(
|
|
431
|
+
ctx.config,
|
|
432
|
+
ctx.parsed,
|
|
433
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
434
|
+
);
|
|
435
|
+
if (!Array.isArray(body)) {
|
|
436
|
+
throw Object.assign(new Error('Provider returned non-array pull list'), {
|
|
437
|
+
forgeError: forgeError(
|
|
438
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
439
|
+
'Provider returned non-array pull list',
|
|
440
|
+
),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return body;
|
|
444
|
+
},
|
|
445
|
+
page,
|
|
446
|
+
activeLimit,
|
|
447
|
+
);
|
|
448
|
+
activeLimit = usedLimit;
|
|
449
|
+
entryCount += items.length;
|
|
450
|
+
allPulls.push(...items);
|
|
451
|
+
if (items.length < usedLimit) break;
|
|
452
|
+
if (page === MAX_CHECK_PAGES) {
|
|
453
|
+
listTruncated = true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let body = buildForgeChangesFromGiteaPulls(sinceIso, allPulls, { listTruncated });
|
|
458
|
+
const checkNumbers = new Set();
|
|
459
|
+
for (const event of body.events) {
|
|
460
|
+
if (event.kind === 'pr_opened' || event.kind === 'head_sha_moved') {
|
|
461
|
+
checkNumbers.add(event.pr_number);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const checkEvents = [];
|
|
466
|
+
for (const number of checkNumbers) {
|
|
467
|
+
const checks = await prChecks(ctx, { number });
|
|
468
|
+
checkEvents.push(buildChecksConclusionObservedEvent(number, checks));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (checkEvents.length > 0) {
|
|
472
|
+
body = appendForgeChangeEvents(body, checkEvents, { listTruncated });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return body;
|
|
476
|
+
}
|
|
477
|
+
|
|
262
478
|
export function providerCapabilities() {
|
|
263
479
|
const check_sources = ['commit_statuses'];
|
|
264
480
|
return {
|
|
@@ -269,7 +485,7 @@ export function providerCapabilities() {
|
|
|
269
485
|
host_binding: 'verified_remote_host',
|
|
270
486
|
pagination: 'supported',
|
|
271
487
|
write_support: true,
|
|
272
|
-
write_commands: ['cr_open'],
|
|
488
|
+
write_commands: ['cr_open', 'status_set'],
|
|
273
489
|
...forgeIngestCapabilityFacts(),
|
|
274
490
|
...checkPaginationCapabilityFacts({
|
|
275
491
|
strategy: 'offset_limit',
|
|
@@ -369,7 +585,100 @@ export async function findOpenPullByHeadBase(ctx, head, base) {
|
|
|
369
585
|
return null;
|
|
370
586
|
}
|
|
371
587
|
|
|
588
|
+
function statusSetIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
|
|
589
|
+
return forgeError(
|
|
590
|
+
ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
|
|
591
|
+
'Cannot prove no commit status exists for sha+context within scan limit; retry or set manually',
|
|
592
|
+
null,
|
|
593
|
+
{
|
|
594
|
+
idempotency_scan: {
|
|
595
|
+
pages: pagesScanned,
|
|
596
|
+
max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
597
|
+
page_size: pageSizeUsed,
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Paginated commit-status scan for idempotent status set; fail-closed when scan cap prevents proof of absence. */
|
|
604
|
+
export async function findCommitStatusByContext(ctx, sha, context) {
|
|
605
|
+
requireToken();
|
|
606
|
+
const path = repoApiPath(ctx.config, 'commits', sha, 'statuses');
|
|
607
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
608
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
609
|
+
let bestMatch = null;
|
|
610
|
+
|
|
611
|
+
for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
|
|
612
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
613
|
+
async ({ page: pageNum, limit }) => {
|
|
614
|
+
const body = await giteaFetch(
|
|
615
|
+
ctx.config,
|
|
616
|
+
ctx.parsed,
|
|
617
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
618
|
+
);
|
|
619
|
+
if (!Array.isArray(body)) {
|
|
620
|
+
throw Object.assign(new Error('Provider returned non-array commit status list'), {
|
|
621
|
+
forgeError: forgeError(
|
|
622
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
623
|
+
'Provider returned non-array commit status list',
|
|
624
|
+
),
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return body;
|
|
628
|
+
},
|
|
629
|
+
page,
|
|
630
|
+
activeLimit,
|
|
631
|
+
);
|
|
632
|
+
activeLimit = usedLimit;
|
|
633
|
+
|
|
634
|
+
for (const record of items) {
|
|
635
|
+
if (record?.context !== context) continue;
|
|
636
|
+
if (!bestMatch || giteaStatusRecordOrder(record, bestMatch) > 0) {
|
|
637
|
+
bestMatch = record;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (items.length < usedLimit) return bestMatch;
|
|
642
|
+
if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
|
|
643
|
+
throw Object.assign(new Error('Commit status idempotency scan incomplete'), {
|
|
644
|
+
forgeError: statusSetIdempotencyScanIncompleteError(page, usedLimit),
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return bestMatch;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export async function statusSet(ctx, args) {
|
|
652
|
+
assertWriteCommandConfigured(ctx.config, 'status_set');
|
|
653
|
+
const parsed = parseStatusSetArgs(args);
|
|
654
|
+
const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
|
|
655
|
+
if (existing) {
|
|
656
|
+
const existingState = normalizeStatusSetState(existing.status ?? existing.state);
|
|
657
|
+
if (existingState === parsed.state) {
|
|
658
|
+
return buildCommitStatusSetBody(existing, parsed, { reusedExisting: true });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const payload = {
|
|
662
|
+
state: parsed.state,
|
|
663
|
+
context: parsed.context,
|
|
664
|
+
};
|
|
665
|
+
if (parsed.description != null) payload.description = parsed.description;
|
|
666
|
+
if (parsed.target_url != null) payload.target_url = parsed.target_url;
|
|
667
|
+
const response = await giteaFetch(
|
|
668
|
+
ctx.config,
|
|
669
|
+
ctx.parsed,
|
|
670
|
+
repoApiPath(ctx.config, 'statuses', parsed.sha),
|
|
671
|
+
{
|
|
672
|
+
method: 'POST',
|
|
673
|
+
headers: { 'Content-Type': 'application/json' },
|
|
674
|
+
body: JSON.stringify(payload),
|
|
675
|
+
},
|
|
676
|
+
);
|
|
677
|
+
return buildCommitStatusSetBody(response, parsed);
|
|
678
|
+
}
|
|
679
|
+
|
|
372
680
|
export async function crOpen(ctx, { head, base, title, body: prBody }) {
|
|
681
|
+
assertWriteCommandConfigured(ctx.config, 'cr_open');
|
|
373
682
|
assertGitRef(head, 'head');
|
|
374
683
|
assertGitRef(base, 'base');
|
|
375
684
|
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
@@ -664,9 +973,7 @@ export function summarizeChecks(statuses) {
|
|
|
664
973
|
}
|
|
665
974
|
|
|
666
975
|
export async function mergePlan(ctx, opts) {
|
|
667
|
-
|
|
668
|
-
const checks = await prChecks(ctx, { number: view.pr_number });
|
|
669
|
-
return buildMergePlanBodyFromFacts(ctx, view, checks, opts);
|
|
976
|
+
return buildMergePlanFromProviderFacts(ctx, opts, { prView, prChecks, crFiles });
|
|
670
977
|
}
|
|
671
978
|
|
|
672
979
|
export async function syncPlan(ctx, remoteName = 'origin') {
|
|
@@ -709,4 +1016,10 @@ export const provider = {
|
|
|
709
1016
|
mergePlan,
|
|
710
1017
|
syncPlan,
|
|
711
1018
|
crOpen,
|
|
1019
|
+
statusSet,
|
|
1020
|
+
whoami,
|
|
1021
|
+
branchProtection,
|
|
1022
|
+
crFiles,
|
|
1023
|
+
crComments,
|
|
1024
|
+
forgeChanges,
|
|
712
1025
|
};
|
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.6",
|
|
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.6"
|
|
26
26
|
}
|
|
27
27
|
}
|