@remogram/provider-gitea-api 0.1.0-beta.0 → 0.1.0-beta.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/branch-protection-internal.js +13 -0
- package/index.js +1185 -43
- package/package.json +4 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @type {((ctx: object, opts: { branchRef: string }) => Promise<object>) | null} */
|
|
2
|
+
let branchProtectionImpl = null;
|
|
3
|
+
|
|
4
|
+
export function setBranchProtectionImpl(fn) {
|
|
5
|
+
branchProtectionImpl = fn;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function resolveBranchProtection(ctx, opts) {
|
|
9
|
+
if (typeof branchProtectionImpl !== 'function') {
|
|
10
|
+
throw new Error('branch protection impl not registered');
|
|
11
|
+
}
|
|
12
|
+
return branchProtectionImpl(ctx, opts);
|
|
13
|
+
}
|
package/index.js
CHANGED
|
@@ -1,27 +1,109 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fetchJson,
|
|
3
|
+
fetchJsonWithMeta,
|
|
3
4
|
sanitizeField,
|
|
5
|
+
sanitizeWriteBody,
|
|
6
|
+
sanitizeWriteTitle,
|
|
4
7
|
sanitizeUrl,
|
|
5
8
|
assertGitRef,
|
|
6
9
|
assertGitRemote,
|
|
7
10
|
gitRevParse,
|
|
8
11
|
gitCurrentBranch,
|
|
9
12
|
gitAheadBehind,
|
|
13
|
+
refsInventory,
|
|
14
|
+
crInventory,
|
|
15
|
+
buildMergePlanFromProviderFacts,
|
|
16
|
+
buildChangeRequestOpenedBody,
|
|
17
|
+
buildIssueOpenedBody,
|
|
18
|
+
parseIssueOpenArgs,
|
|
19
|
+
buildCommitStatusSetBody,
|
|
20
|
+
idempotencyPacketFields,
|
|
21
|
+
parseStatusSetArgs,
|
|
22
|
+
normalizeStatusSetState,
|
|
23
|
+
buildProviderIdentityFromGiteaUser,
|
|
24
|
+
buildBranchProtectionFromGiteaProtection,
|
|
25
|
+
buildPrChecksBody,
|
|
26
|
+
buildCrFilesBody,
|
|
27
|
+
buildCrFilesFromGiteaFiles,
|
|
28
|
+
buildCrCommentsBody,
|
|
29
|
+
buildCrCommentsFromGiteaComments,
|
|
30
|
+
buildForgeChangesFromGiteaPulls,
|
|
31
|
+
buildChecksConclusionObservedEvent,
|
|
32
|
+
appendForgeChangeEvents,
|
|
33
|
+
parseSinceObservedAt,
|
|
10
34
|
ERROR_CODES,
|
|
11
35
|
forgeError,
|
|
36
|
+
assertExpectedSha,
|
|
37
|
+
LIVE_REACHABILITY_TIMEOUT_MS,
|
|
38
|
+
forgeIngestCapabilityFacts,
|
|
39
|
+
checkPaginationCapabilityFacts,
|
|
40
|
+
idempotencyScanCapabilityFacts,
|
|
41
|
+
openPullListCapabilityFacts,
|
|
42
|
+
DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
43
|
+
MAX_CHECK_STATUS_PAGES,
|
|
44
|
+
DEFAULT_OPEN_PULL_LIST_PAGE_SIZE,
|
|
45
|
+
MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
46
|
+
paginateCheckStatusPages,
|
|
47
|
+
paginateOffsetListPages,
|
|
48
|
+
fetchWithIngestPageBackoff,
|
|
49
|
+
fetchPageWithIngestBackoff,
|
|
50
|
+
withPerPageParam,
|
|
51
|
+
apiProviderCommands,
|
|
52
|
+
normalizeCrInventorySort,
|
|
53
|
+
DEFAULT_CR_INVENTORY_SLICE_SORT,
|
|
54
|
+
parseTotalCountHeader,
|
|
55
|
+
isCrInventoryFastPathEligible,
|
|
56
|
+
validateFastPathPageLength,
|
|
57
|
+
isNumberSortFastPathEligible,
|
|
58
|
+
isRecentCreatedFastPathEligible,
|
|
59
|
+
giteaRecentCreatedTailPage,
|
|
60
|
+
isNumberSortFullCollectRequired,
|
|
61
|
+
resolveListTruncatedWithTrustedTotal,
|
|
62
|
+
prepareGiteaOpenPullPageItems,
|
|
63
|
+
orderOpenPullNumbers,
|
|
64
|
+
buildOpenPullListMeta,
|
|
65
|
+
giteaOpenPullSortQuery,
|
|
66
|
+
appendSortQuery,
|
|
67
|
+
assertWriteCommandConfigured,
|
|
68
|
+
fetchWithTimeout,
|
|
69
|
+
readStreamCapped,
|
|
70
|
+
getEffectiveIngestMaxBytes,
|
|
71
|
+
forgeWriteFieldCapabilityFacts,
|
|
12
72
|
} from '@remogram/core';
|
|
73
|
+
import {
|
|
74
|
+
resolveBranchProtection,
|
|
75
|
+
setBranchProtectionImpl,
|
|
76
|
+
} from './branch-protection-internal.js';
|
|
13
77
|
const PUBLIC_GITEA_HOST = 'gitea.com';
|
|
14
78
|
const PUBLIC_GITEA_API = 'https://gitea.com/api/v1';
|
|
15
79
|
const AUTH_CAPABILITIES = [
|
|
16
80
|
'repo_status',
|
|
17
81
|
'ref_compare',
|
|
82
|
+
'ref_inventory',
|
|
83
|
+
'cr_inventory',
|
|
18
84
|
'pr_status',
|
|
19
85
|
'pr_checks',
|
|
20
86
|
'merge_plan',
|
|
21
87
|
'sync_plan',
|
|
88
|
+
'cr_open',
|
|
89
|
+
'status_set',
|
|
90
|
+
'whoami',
|
|
91
|
+
'branch_protection',
|
|
92
|
+
'cr_files',
|
|
93
|
+
'cr_comments',
|
|
94
|
+
'forge_changes',
|
|
22
95
|
];
|
|
23
96
|
|
|
24
|
-
const STRUCTURED_COMMANDS =
|
|
97
|
+
const STRUCTURED_COMMANDS = apiProviderCommands({
|
|
98
|
+
writeCommandsImplemented: true,
|
|
99
|
+
issueOpenImplemented: true,
|
|
100
|
+
statusSetImplemented: true,
|
|
101
|
+
branchProtectionImplemented: true,
|
|
102
|
+
crFilesImplemented: true,
|
|
103
|
+
crCommentsImplemented: true,
|
|
104
|
+
forgeChangesImplemented: true,
|
|
105
|
+
mergeExecuteImplemented: true,
|
|
106
|
+
});
|
|
25
107
|
|
|
26
108
|
export function giteaToken() {
|
|
27
109
|
return process.env.GITEA_TOKEN || null;
|
|
@@ -101,13 +183,36 @@ export function authHeaders(token) {
|
|
|
101
183
|
}
|
|
102
184
|
|
|
103
185
|
export function repoApiPath(config, ...segments) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
186
|
+
return repoApiPathFor(config.owner, config.repo, ...segments);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function repoApiPathFor(owner, repo, ...segments) {
|
|
190
|
+
const encodedOwner = encodeURIComponent(owner);
|
|
191
|
+
const encodedRepo = encodeURIComponent(repo);
|
|
192
|
+
const base = `/repos/${encodedOwner}/${encodedRepo}`;
|
|
107
193
|
if (!segments.length) return base;
|
|
108
194
|
return `${base}/${segments.map((s) => encodeURIComponent(String(s))).join('/')}`;
|
|
109
195
|
}
|
|
110
196
|
|
|
197
|
+
export function forgeSourceRepoIdFromPull(config, pr) {
|
|
198
|
+
const headOwner = sanitizeField(pr.head?.repo?.owner?.login ?? pr.head?.repo?.owner?.name);
|
|
199
|
+
const headRepo = sanitizeField(pr.head?.repo?.name);
|
|
200
|
+
if (!headOwner || !headRepo) return null;
|
|
201
|
+
const configOwner = String(config.owner ?? '').toLowerCase();
|
|
202
|
+
const configRepo = String(config.repo ?? '').toLowerCase();
|
|
203
|
+
if (headOwner.toLowerCase() === configOwner && headRepo.toLowerCase() === configRepo) return null;
|
|
204
|
+
return `${headOwner}/${headRepo}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isGiteaHeadOutOfDate409(err) {
|
|
208
|
+
const status = err.status ?? err.forgeError?.status ?? null;
|
|
209
|
+
if (status !== 409) return false;
|
|
210
|
+
const message = err.forgeError?.message ?? err.message ?? '';
|
|
211
|
+
if (/head out of date/i.test(message)) return true;
|
|
212
|
+
if (/sha mismatch/i.test(message)) return true;
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
111
216
|
export async function giteaFetch(config, parsed, path, options = {}) {
|
|
112
217
|
const token = requireToken();
|
|
113
218
|
const url = `${apiBase(config, parsed)}${path}`;
|
|
@@ -117,6 +222,115 @@ export async function giteaFetch(config, parsed, path, options = {}) {
|
|
|
117
222
|
});
|
|
118
223
|
}
|
|
119
224
|
|
|
225
|
+
export async function giteaFetchWithMeta(config, parsed, path, options = {}) {
|
|
226
|
+
const token = requireToken();
|
|
227
|
+
const url = `${apiBase(config, parsed)}${path}`;
|
|
228
|
+
return fetchJsonWithMeta(url, {
|
|
229
|
+
...options,
|
|
230
|
+
headers: { ...authHeaders(token), ...(options.headers || {}) },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function apiReachability(ctx) {
|
|
235
|
+
if (!giteaToken()) {
|
|
236
|
+
throw Object.assign(new Error('GITEA_TOKEN not set'), {
|
|
237
|
+
forgeError: forgeError(ERROR_CODES.UNAUTHENTICATED_PROVIDER, 'GITEA_TOKEN not set'),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const token = requireToken();
|
|
241
|
+
const url = `${apiBase(ctx.config, ctx.parsed)}${repoApiPath(ctx.config)}`;
|
|
242
|
+
await fetchJson(
|
|
243
|
+
url,
|
|
244
|
+
{ headers: authHeaders(token) },
|
|
245
|
+
LIVE_REACHABILITY_TIMEOUT_MS,
|
|
246
|
+
);
|
|
247
|
+
return { repo_accessible: true };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
|
|
251
|
+
const GITEA_PAGE_SIZE = DEFAULT_OPEN_PULL_LIST_PAGE_SIZE;
|
|
252
|
+
|
|
253
|
+
function idempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
|
|
254
|
+
return forgeError(
|
|
255
|
+
ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
|
|
256
|
+
'Cannot prove no open pull exists for head+base within scan limit; use cr inventory or open manually',
|
|
257
|
+
null,
|
|
258
|
+
{
|
|
259
|
+
idempotency_scan: {
|
|
260
|
+
pages: pagesScanned,
|
|
261
|
+
max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
262
|
+
page_size: pageSizeUsed,
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function giteaFetchPaginated(config, parsed, path) {
|
|
269
|
+
return paginateCheckStatusPages({
|
|
270
|
+
fetchPage: async ({ page, limit }) => {
|
|
271
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
272
|
+
const body = await giteaFetch(
|
|
273
|
+
config,
|
|
274
|
+
parsed,
|
|
275
|
+
`${path}${separator}limit=${limit}&page=${page}`,
|
|
276
|
+
);
|
|
277
|
+
return Array.isArray(body) ? body : [];
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function normalizeGiteaStatusState(state) {
|
|
283
|
+
const normalized = String(state ?? '').toLowerCase();
|
|
284
|
+
if (normalized === 'success' || normalized === 'pass') return 'success';
|
|
285
|
+
if (normalized === 'pending' || normalized === 'running' || normalized === 'waiting') {
|
|
286
|
+
return 'pending';
|
|
287
|
+
}
|
|
288
|
+
if (normalized === 'failure' || normalized === 'fail' || normalized === 'error') {
|
|
289
|
+
return 'failure';
|
|
290
|
+
}
|
|
291
|
+
return 'unknown';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function giteaStatusRecordOrder(a, b) {
|
|
295
|
+
const aUpdated = Date.parse(a.updated_at ?? '') || 0;
|
|
296
|
+
const bUpdated = Date.parse(b.updated_at ?? '') || 0;
|
|
297
|
+
if (aUpdated !== bUpdated) return aUpdated - bUpdated;
|
|
298
|
+
const aId = Number(a.id) || 0;
|
|
299
|
+
const bId = Number(b.id) || 0;
|
|
300
|
+
return aId - bId;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function dedupeGiteaStatusRecords(records) {
|
|
304
|
+
const latestByContext = new Map();
|
|
305
|
+
for (const record of records) {
|
|
306
|
+
const context = record?.context;
|
|
307
|
+
if (context == null || context === '') continue;
|
|
308
|
+
const existing = latestByContext.get(context);
|
|
309
|
+
if (!existing || giteaStatusRecordOrder(record, existing) > 0) {
|
|
310
|
+
latestByContext.set(context, record);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return Array.from(latestByContext.values());
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function mapGiteaCommitStatuses(records, { headSha } = {}) {
|
|
317
|
+
return dedupeGiteaStatusRecords(records).map((s) => ({
|
|
318
|
+
context: sanitizeField(s.context),
|
|
319
|
+
state: normalizeGiteaStatusState(s.status ?? s.state),
|
|
320
|
+
description: sanitizeField(s.description),
|
|
321
|
+
...(s.target_url ? { target_url: sanitizeField(s.target_url) } : {}),
|
|
322
|
+
...(headSha ? { sha: headSha } : {}),
|
|
323
|
+
source: 'commit_status',
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function normalizeGiteaPrState(state) {
|
|
328
|
+
const normalized = String(state ?? '').toLowerCase();
|
|
329
|
+
if (normalized === 'open') return 'open';
|
|
330
|
+
if (normalized === 'closed') return 'closed';
|
|
331
|
+
return normalized || 'unknown';
|
|
332
|
+
}
|
|
333
|
+
|
|
120
334
|
export async function repoStatus(ctx) {
|
|
121
335
|
const token = giteaToken();
|
|
122
336
|
let defaultBranch = null;
|
|
@@ -132,20 +346,278 @@ export async function repoStatus(ctx) {
|
|
|
132
346
|
};
|
|
133
347
|
}
|
|
134
348
|
|
|
135
|
-
export function
|
|
349
|
+
export async function whoami(ctx) {
|
|
350
|
+
requireToken();
|
|
351
|
+
const user = await giteaFetch(ctx.config, ctx.parsed, '/user');
|
|
352
|
+
return buildProviderIdentityFromGiteaUser(user);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function branchProtection(ctx, { branchRef }) {
|
|
356
|
+
requireToken();
|
|
357
|
+
try {
|
|
358
|
+
const protection = await giteaFetch(
|
|
359
|
+
ctx.config,
|
|
360
|
+
ctx.parsed,
|
|
361
|
+
repoApiPath(ctx.config, 'branch_protections', branchRef),
|
|
362
|
+
);
|
|
363
|
+
return buildBranchProtectionFromGiteaProtection(branchRef, protection);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
if (err?.status === 404) {
|
|
366
|
+
return buildBranchProtectionFromGiteaProtection(branchRef, null);
|
|
367
|
+
}
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function branchHeadSha(ctx, branchRef, { repoId } = {}) {
|
|
373
|
+
requireToken();
|
|
374
|
+
assertGitRef(branchRef, 'head_ref');
|
|
375
|
+
let owner = ctx.config.owner;
|
|
376
|
+
let repo = ctx.config.repo;
|
|
377
|
+
if (repoId) {
|
|
378
|
+
const parts = String(repoId).split('/');
|
|
379
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
380
|
+
throw Object.assign(new Error('Invalid repoId'), {
|
|
381
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'repoId must be owner/repo'),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
owner = parts[0];
|
|
385
|
+
repo = parts[1];
|
|
386
|
+
}
|
|
387
|
+
const branch = await giteaFetch(
|
|
388
|
+
ctx.config,
|
|
389
|
+
ctx.parsed,
|
|
390
|
+
repoApiPathFor(owner, repo, 'branches', branchRef),
|
|
391
|
+
);
|
|
392
|
+
const rawSha = sanitizeField(branch?.commit?.id);
|
|
393
|
+
if (!rawSha) {
|
|
394
|
+
throw Object.assign(new Error('Branch commit id missing'), {
|
|
395
|
+
forgeError: forgeError(
|
|
396
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
397
|
+
'Gitea branch response missing commit id',
|
|
398
|
+
),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
return assertExpectedSha(rawSha, 'branch commit id');
|
|
403
|
+
} catch (err) {
|
|
404
|
+
throw Object.assign(new Error('Branch commit id invalid'), {
|
|
405
|
+
forgeError: forgeError(
|
|
406
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
407
|
+
sanitizeField(err.invalidArgs) || 'Gitea branch response commit id is not a valid SHA',
|
|
408
|
+
),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function crFiles(ctx, { number }) {
|
|
414
|
+
requireToken();
|
|
415
|
+
if (number == null) {
|
|
416
|
+
throw Object.assign(new Error('--number required'), {
|
|
417
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR changed paths'),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const path = repoApiPath(ctx.config, 'pulls', number, 'files');
|
|
421
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
422
|
+
let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
|
|
423
|
+
const allFiles = [];
|
|
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 files list'), {
|
|
437
|
+
forgeError: forgeError(
|
|
438
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
439
|
+
'Provider returned non-array pull files list',
|
|
440
|
+
),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return body;
|
|
444
|
+
},
|
|
445
|
+
page,
|
|
446
|
+
activeLimit,
|
|
447
|
+
);
|
|
448
|
+
activeLimit = usedLimit;
|
|
449
|
+
entryCount += items.length;
|
|
450
|
+
allFiles.push(...items);
|
|
451
|
+
if (items.length < usedLimit) break;
|
|
452
|
+
if (page === MAX_CHECK_PAGES) {
|
|
453
|
+
listTruncated = true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const body = buildCrFilesFromGiteaFiles(number, allFiles);
|
|
458
|
+
if (listTruncated) {
|
|
459
|
+
return buildCrFilesBody({
|
|
460
|
+
pr_number: body.pr_number,
|
|
461
|
+
changed_paths: body.changed_paths,
|
|
462
|
+
paths_truncated: true,
|
|
463
|
+
path_count: entryCount,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return body;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export async function crComments(ctx, { number }) {
|
|
470
|
+
requireToken();
|
|
471
|
+
if (number == null) {
|
|
472
|
+
throw Object.assign(new Error('--number required'), {
|
|
473
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR review comments'),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const path = repoApiPath(ctx.config, 'pulls', number, 'comments');
|
|
477
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
478
|
+
let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
|
|
479
|
+
const allComments = [];
|
|
480
|
+
let listTruncated = false;
|
|
481
|
+
let entryCount = 0;
|
|
482
|
+
|
|
483
|
+
for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
|
|
484
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
485
|
+
async ({ page: pageNum, limit }) => {
|
|
486
|
+
const body = await giteaFetch(
|
|
487
|
+
ctx.config,
|
|
488
|
+
ctx.parsed,
|
|
489
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
490
|
+
);
|
|
491
|
+
if (!Array.isArray(body)) {
|
|
492
|
+
throw Object.assign(new Error('Provider returned non-array pull comments list'), {
|
|
493
|
+
forgeError: forgeError(
|
|
494
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
495
|
+
'Provider returned non-array pull comments list',
|
|
496
|
+
),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return body;
|
|
500
|
+
},
|
|
501
|
+
page,
|
|
502
|
+
activeLimit,
|
|
503
|
+
);
|
|
504
|
+
activeLimit = usedLimit;
|
|
505
|
+
entryCount += items.length;
|
|
506
|
+
allComments.push(...items);
|
|
507
|
+
if (items.length < usedLimit) break;
|
|
508
|
+
if (page === MAX_CHECK_PAGES) {
|
|
509
|
+
listTruncated = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const body = buildCrCommentsFromGiteaComments(number, allComments);
|
|
514
|
+
if (listTruncated) {
|
|
515
|
+
return buildCrCommentsBody({
|
|
516
|
+
pr_number: body.pr_number,
|
|
517
|
+
comments: body.comments,
|
|
518
|
+
comments_truncated: true,
|
|
519
|
+
comment_count: entryCount,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return body;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export async function forgeChanges(ctx, { since }) {
|
|
526
|
+
requireToken();
|
|
527
|
+
const sinceIso = parseSinceObservedAt(since);
|
|
528
|
+
const path = `${repoApiPath(ctx.config, 'pulls')}?state=all&sort=recentupdate`;
|
|
529
|
+
const pageSep = '&';
|
|
530
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
531
|
+
const allPulls = [];
|
|
532
|
+
let listTruncated = false;
|
|
533
|
+
let entryCount = 0;
|
|
534
|
+
|
|
535
|
+
for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
|
|
536
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
537
|
+
async ({ page: pageNum, limit }) => {
|
|
538
|
+
const body = await giteaFetch(
|
|
539
|
+
ctx.config,
|
|
540
|
+
ctx.parsed,
|
|
541
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
542
|
+
);
|
|
543
|
+
if (!Array.isArray(body)) {
|
|
544
|
+
throw Object.assign(new Error('Provider returned non-array pull list'), {
|
|
545
|
+
forgeError: forgeError(
|
|
546
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
547
|
+
'Provider returned non-array pull list',
|
|
548
|
+
),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return body;
|
|
552
|
+
},
|
|
553
|
+
page,
|
|
554
|
+
activeLimit,
|
|
555
|
+
);
|
|
556
|
+
activeLimit = usedLimit;
|
|
557
|
+
entryCount += items.length;
|
|
558
|
+
allPulls.push(...items);
|
|
559
|
+
if (items.length < usedLimit) break;
|
|
560
|
+
if (page === MAX_CHECK_PAGES) {
|
|
561
|
+
listTruncated = true;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let body = buildForgeChangesFromGiteaPulls(sinceIso, allPulls, { listTruncated });
|
|
566
|
+
const checkNumbers = new Set();
|
|
567
|
+
for (const event of body.events) {
|
|
568
|
+
if (event.kind === 'pr_opened' || event.kind === 'head_sha_moved') {
|
|
569
|
+
checkNumbers.add(event.pr_number);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const checkEvents = [];
|
|
574
|
+
for (const number of checkNumbers) {
|
|
575
|
+
const checks = await prChecks(ctx, { number });
|
|
576
|
+
checkEvents.push(buildChecksConclusionObservedEvent(number, checks));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (checkEvents.length > 0) {
|
|
580
|
+
body = appendForgeChangeEvents(body, checkEvents, { listTruncated });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return body;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function providerCapabilities(ctx = {}) {
|
|
587
|
+
const check_sources = ['commit_statuses'];
|
|
136
588
|
return {
|
|
137
589
|
commands: STRUCTURED_COMMANDS,
|
|
138
590
|
auth_envs: ['GITEA_TOKEN'],
|
|
139
|
-
check_sources
|
|
591
|
+
check_sources,
|
|
140
592
|
mergeability_confidence: 'direct',
|
|
141
593
|
host_binding: 'verified_remote_host',
|
|
142
|
-
pagination: '
|
|
143
|
-
write_support:
|
|
594
|
+
pagination: 'supported',
|
|
595
|
+
write_support: true,
|
|
596
|
+
write_commands: ['cr_open', 'status_set', 'merge', 'issue_open'],
|
|
597
|
+
...forgeIngestCapabilityFacts(),
|
|
598
|
+
...forgeWriteFieldCapabilityFacts(ctx.writeFieldPolicy),
|
|
599
|
+
...checkPaginationCapabilityFacts({
|
|
600
|
+
strategy: 'offset_limit',
|
|
601
|
+
pageSizeParam: 'limit',
|
|
602
|
+
sourceCount: check_sources.length,
|
|
603
|
+
}),
|
|
604
|
+
...idempotencyScanCapabilityFacts(),
|
|
605
|
+
...openPullListCapabilityFacts({
|
|
606
|
+
totalCountSource: 'response_header',
|
|
607
|
+
totalCountHeader: 'X-Total-Count',
|
|
608
|
+
sliceSortNotes: {
|
|
609
|
+
recent_created:
|
|
610
|
+
'sort=oldest; fetches tail page when total exceeds limit; page reversed for newest-first',
|
|
611
|
+
number_asc:
|
|
612
|
+
'full-list collect within compliant_max when total exceeds limit, then client sort',
|
|
613
|
+
number_desc:
|
|
614
|
+
'full-list collect within compliant_max when total exceeds limit, then client sort',
|
|
615
|
+
},
|
|
616
|
+
}),
|
|
144
617
|
};
|
|
145
618
|
}
|
|
146
619
|
|
|
147
620
|
export async function refsCompare(ctx, baseRef, headRef) {
|
|
148
|
-
requireToken();
|
|
149
621
|
assertGitRef(baseRef, 'base');
|
|
150
622
|
assertGitRef(headRef, 'head');
|
|
151
623
|
const baseSha = gitRevParse(ctx.cwd, baseRef);
|
|
@@ -157,10 +629,10 @@ export async function refsCompare(ctx, baseRef, headRef) {
|
|
|
157
629
|
}
|
|
158
630
|
const counts = gitAheadBehind(ctx.cwd, baseSha, headSha);
|
|
159
631
|
return {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
632
|
+
compare_base_ref: sanitizeField(baseRef),
|
|
633
|
+
compare_base_sha: baseSha,
|
|
634
|
+
compare_head_ref: sanitizeField(headRef),
|
|
635
|
+
compare_head_sha: headSha,
|
|
164
636
|
...counts,
|
|
165
637
|
};
|
|
166
638
|
}
|
|
@@ -171,7 +643,660 @@ export async function getPull(ctx, { number }) {
|
|
|
171
643
|
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR lookup'),
|
|
172
644
|
});
|
|
173
645
|
}
|
|
174
|
-
return
|
|
646
|
+
return giteaFetchPullForView(ctx.config, ctx.parsed, number);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Raw read bound for pull projected ingest (view, write, list) before stripping bulky fields (#478, #574). */
|
|
650
|
+
const GITEA_PULL_INGEST_RAW_READ_MAX = 256 * 1024;
|
|
651
|
+
/** Alias for read-only view call sites. */
|
|
652
|
+
const GITEA_PULL_VIEW_RAW_READ_MAX = GITEA_PULL_INGEST_RAW_READ_MAX;
|
|
653
|
+
/** Raw read bound for issue write/list responses before stripping bulky fields (#572). */
|
|
654
|
+
const GITEA_ISSUE_WRITE_RAW_READ_MAX = 256 * 1024;
|
|
655
|
+
|
|
656
|
+
/** Best-effort regex shrink of bulky pull JSON string fields before JSON.parse (#478, #574).
|
|
657
|
+
* Fail-closed via readStreamCapped(rawReadMax) and getEffectiveIngestMaxBytes() after strip. */
|
|
658
|
+
function stripGiteaPullBulkJsonFields(raw) {
|
|
659
|
+
return String(raw)
|
|
660
|
+
.replace(/"body"\s*:\s*"(?:\\.|[^"\\])*"/g, '"body":""')
|
|
661
|
+
.replace(/"body_html"\s*:\s*"(?:\\.|[^"\\])*"/g, '"body_html":""')
|
|
662
|
+
.replace(/"diff"\s*:\s*"(?:\\.|[^"\\])*"/g, '"diff":""')
|
|
663
|
+
.replace(/"patch"\s*:\s*"(?:\\.|[^"\\])*"/g, '"patch":""');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function stripGiteaIssueBulkJsonFields(raw) {
|
|
667
|
+
return String(raw)
|
|
668
|
+
.replace(/"body"\s*:\s*"(?:\\.|[^"\\])*"/g, '"body":""')
|
|
669
|
+
.replace(/"body_html"\s*:\s*"(?:\\.|[^"\\])*"/g, '"body_html":""');
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function parseProjectedGiteaResponse(res, { stripFn, rawReadMax }) {
|
|
673
|
+
if (res.status >= 300 && res.status < 400) {
|
|
674
|
+
const message = 'HTTP redirect rejected';
|
|
675
|
+
throw Object.assign(new Error(message), {
|
|
676
|
+
forgeError: forgeError(ERROR_CODES.API_ERROR, message, res.status),
|
|
677
|
+
status: res.status,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
const capped = await readStreamCapped(res.body, rawReadMax);
|
|
681
|
+
if (capped.truncated) {
|
|
682
|
+
throw Object.assign(new Error('Provider output exceeded cap'), {
|
|
683
|
+
forgeError: forgeError(ERROR_CODES.OVERSIZED_RAW_OUTPUT, 'Provider response exceeded byte cap'),
|
|
684
|
+
status: res.status,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const stripped = stripFn(capped.text);
|
|
688
|
+
if (Buffer.byteLength(stripped, 'utf8') > getEffectiveIngestMaxBytes().bytes) {
|
|
689
|
+
throw Object.assign(new Error('Provider output exceeded cap after projection'), {
|
|
690
|
+
forgeError: forgeError(ERROR_CODES.OVERSIZED_RAW_OUTPUT, 'Provider response exceeded byte cap'),
|
|
691
|
+
status: res.status,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
let body;
|
|
695
|
+
try {
|
|
696
|
+
body = stripped ? JSON.parse(stripped) : null;
|
|
697
|
+
} catch {
|
|
698
|
+
throw Object.assign(new Error('Unparseable JSON from provider'), {
|
|
699
|
+
forgeError: forgeError(ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT, 'Provider returned invalid JSON'),
|
|
700
|
+
status: res.status,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (!res.ok) {
|
|
704
|
+
const raw = body?.message || body?.error || res.statusText || 'API error';
|
|
705
|
+
const message = sanitizeField(raw) || 'API error';
|
|
706
|
+
throw Object.assign(new Error(message), {
|
|
707
|
+
forgeError: forgeError(ERROR_CODES.API_ERROR, message, res.status),
|
|
708
|
+
status: res.status,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return body;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function giteaFetchProjected(config, parsed, path, { stripFn, rawReadMax, requestOptions = {} }) {
|
|
715
|
+
const token = requireToken();
|
|
716
|
+
const url = `${apiBase(config, parsed)}${path}`;
|
|
717
|
+
const res = await fetchWithTimeout(url, {
|
|
718
|
+
...requestOptions,
|
|
719
|
+
headers: { ...authHeaders(token), ...(requestOptions.headers || {}) },
|
|
720
|
+
});
|
|
721
|
+
return parseProjectedGiteaResponse(res, { stripFn, rawReadMax });
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function giteaFetchPullForView(config, parsed, number) {
|
|
725
|
+
return giteaFetchProjected(config, parsed, repoApiPath(config, 'pulls', number), {
|
|
726
|
+
stripFn: stripGiteaPullBulkJsonFields,
|
|
727
|
+
rawReadMax: GITEA_PULL_VIEW_RAW_READ_MAX,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function giteaFetchIssueWriteResponse(config, parsed, path, requestOptions = {}) {
|
|
732
|
+
return giteaFetchProjected(config, parsed, path, {
|
|
733
|
+
stripFn: stripGiteaIssueBulkJsonFields,
|
|
734
|
+
rawReadMax: GITEA_ISSUE_WRITE_RAW_READ_MAX,
|
|
735
|
+
requestOptions,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function giteaFetchPullWriteResponse(config, parsed, path, requestOptions = {}) {
|
|
740
|
+
return giteaFetchProjected(config, parsed, path, {
|
|
741
|
+
stripFn: stripGiteaPullBulkJsonFields,
|
|
742
|
+
rawReadMax: GITEA_PULL_INGEST_RAW_READ_MAX,
|
|
743
|
+
requestOptions,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Paginated open-pull scan for idempotent cr open; fail-closed when scan cap prevents proof of absence. */
|
|
748
|
+
export async function findOpenPullByHeadBase(ctx, head, base) {
|
|
749
|
+
requireToken();
|
|
750
|
+
const path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
751
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
752
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
753
|
+
|
|
754
|
+
for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
|
|
755
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
756
|
+
async ({ page: pageNum, limit }) => {
|
|
757
|
+
const body = await giteaFetchProjected(
|
|
758
|
+
ctx.config,
|
|
759
|
+
ctx.parsed,
|
|
760
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
761
|
+
{
|
|
762
|
+
stripFn: stripGiteaPullBulkJsonFields,
|
|
763
|
+
rawReadMax: GITEA_PULL_INGEST_RAW_READ_MAX,
|
|
764
|
+
},
|
|
765
|
+
);
|
|
766
|
+
if (!Array.isArray(body)) {
|
|
767
|
+
throw Object.assign(new Error('Provider returned non-array open pull list'), {
|
|
768
|
+
forgeError: forgeError(
|
|
769
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
770
|
+
'Provider returned non-array open pull list',
|
|
771
|
+
),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return body;
|
|
775
|
+
},
|
|
776
|
+
page,
|
|
777
|
+
activeLimit,
|
|
778
|
+
);
|
|
779
|
+
activeLimit = usedLimit;
|
|
780
|
+
|
|
781
|
+
const match =
|
|
782
|
+
items.find(
|
|
783
|
+
(pr) =>
|
|
784
|
+
String(pr?.state ?? '').toLowerCase() === 'open' &&
|
|
785
|
+
pr?.head?.ref === head &&
|
|
786
|
+
pr?.base?.ref === base,
|
|
787
|
+
) ?? null;
|
|
788
|
+
if (match) return match;
|
|
789
|
+
if (items.length < usedLimit) return null;
|
|
790
|
+
if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
|
|
791
|
+
throw Object.assign(new Error('Open pull idempotency scan incomplete'), {
|
|
792
|
+
forgeError: idempotencyScanIncompleteError(page, usedLimit),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function issueOpenIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
|
|
800
|
+
return forgeError(
|
|
801
|
+
ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
|
|
802
|
+
'Cannot prove no open issue exists for title within scan limit; retry or open manually',
|
|
803
|
+
null,
|
|
804
|
+
{
|
|
805
|
+
idempotency_scan: {
|
|
806
|
+
pages: pagesScanned,
|
|
807
|
+
max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
808
|
+
page_size: pageSizeUsed,
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** Paginated open-issue scan for idempotent issue open; fail-closed when scan cap prevents proof of absence. */
|
|
815
|
+
export async function findOpenIssueByTitle(ctx, title) {
|
|
816
|
+
requireToken();
|
|
817
|
+
const path = `${repoApiPath(ctx.config, 'issues')}?state=open`;
|
|
818
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
819
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
820
|
+
|
|
821
|
+
for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
|
|
822
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
823
|
+
async ({ page: pageNum, limit }) => {
|
|
824
|
+
const body = await giteaFetchProjected(
|
|
825
|
+
ctx.config,
|
|
826
|
+
ctx.parsed,
|
|
827
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
828
|
+
{
|
|
829
|
+
stripFn: stripGiteaIssueBulkJsonFields,
|
|
830
|
+
rawReadMax: GITEA_ISSUE_WRITE_RAW_READ_MAX,
|
|
831
|
+
},
|
|
832
|
+
);
|
|
833
|
+
if (!Array.isArray(body)) {
|
|
834
|
+
throw Object.assign(new Error('Provider returned non-array open issue list'), {
|
|
835
|
+
forgeError: forgeError(
|
|
836
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
837
|
+
'Provider returned non-array open issue list',
|
|
838
|
+
),
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return body;
|
|
842
|
+
},
|
|
843
|
+
page,
|
|
844
|
+
activeLimit,
|
|
845
|
+
);
|
|
846
|
+
activeLimit = usedLimit;
|
|
847
|
+
|
|
848
|
+
const match =
|
|
849
|
+
items.find(
|
|
850
|
+
(issue) =>
|
|
851
|
+
String(issue?.state ?? '').toLowerCase() === 'open' &&
|
|
852
|
+
sanitizeField(issue?.title ?? '') === title,
|
|
853
|
+
) ?? null;
|
|
854
|
+
if (match) return match;
|
|
855
|
+
if (items.length < usedLimit) return null;
|
|
856
|
+
if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
|
|
857
|
+
throw Object.assign(new Error('Open issue idempotency scan incomplete'), {
|
|
858
|
+
forgeError: issueOpenIdempotencyScanIncompleteError(page, usedLimit),
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function statusSetIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
|
|
866
|
+
return forgeError(
|
|
867
|
+
ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
|
|
868
|
+
'Cannot prove no commit status exists for sha+context within scan limit; retry or set manually',
|
|
869
|
+
null,
|
|
870
|
+
{
|
|
871
|
+
idempotency_scan: {
|
|
872
|
+
pages: pagesScanned,
|
|
873
|
+
max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
874
|
+
page_size: pageSizeUsed,
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Paginated commit-status scan for idempotent status set; fail-closed when scan cap prevents proof of absence. */
|
|
881
|
+
export async function findCommitStatusByContext(ctx, sha, context) {
|
|
882
|
+
requireToken();
|
|
883
|
+
const path = repoApiPath(ctx.config, 'commits', sha, 'statuses');
|
|
884
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
885
|
+
let activeLimit = GITEA_PAGE_SIZE;
|
|
886
|
+
let bestMatch = null;
|
|
887
|
+
|
|
888
|
+
for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
|
|
889
|
+
const { items, usedLimit } = await fetchPageWithIngestBackoff(
|
|
890
|
+
async ({ page: pageNum, limit }) => {
|
|
891
|
+
const body = await giteaFetch(
|
|
892
|
+
ctx.config,
|
|
893
|
+
ctx.parsed,
|
|
894
|
+
`${path}${pageSep}limit=${limit}&page=${pageNum}`,
|
|
895
|
+
);
|
|
896
|
+
if (!Array.isArray(body)) {
|
|
897
|
+
throw Object.assign(new Error('Provider returned non-array commit status list'), {
|
|
898
|
+
forgeError: forgeError(
|
|
899
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
900
|
+
'Provider returned non-array commit status list',
|
|
901
|
+
),
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
return body;
|
|
905
|
+
},
|
|
906
|
+
page,
|
|
907
|
+
activeLimit,
|
|
908
|
+
);
|
|
909
|
+
activeLimit = usedLimit;
|
|
910
|
+
|
|
911
|
+
for (const record of items) {
|
|
912
|
+
if (record?.context !== context) continue;
|
|
913
|
+
if (!bestMatch || giteaStatusRecordOrder(record, bestMatch) > 0) {
|
|
914
|
+
bestMatch = record;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (items.length < usedLimit) return bestMatch;
|
|
919
|
+
if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
|
|
920
|
+
throw Object.assign(new Error('Commit status idempotency scan incomplete'), {
|
|
921
|
+
forgeError: statusSetIdempotencyScanIncompleteError(page, usedLimit),
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return bestMatch;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export async function statusSet(ctx, args) {
|
|
929
|
+
assertWriteCommandConfigured(ctx.writePolicy ?? ctx.config, 'status_set');
|
|
930
|
+
const { idempotencyFingerprint = null, ...rest } = args;
|
|
931
|
+
const parsed = parseStatusSetArgs(rest, ctx.writeFieldPolicy);
|
|
932
|
+
const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
|
|
933
|
+
if (existing) {
|
|
934
|
+
const existingState = normalizeStatusSetState(existing.status ?? existing.state);
|
|
935
|
+
if (existingState === parsed.state) {
|
|
936
|
+
return buildCommitStatusSetBody(existing, parsed, {
|
|
937
|
+
reusedExisting: true,
|
|
938
|
+
idempotencyFields: idempotencyFingerprint
|
|
939
|
+
? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: true })
|
|
940
|
+
: null,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const payload = {
|
|
945
|
+
state: parsed.state,
|
|
946
|
+
context: parsed.context,
|
|
947
|
+
};
|
|
948
|
+
if (parsed.description != null) payload.description = parsed.description;
|
|
949
|
+
if (parsed.target_url != null) payload.target_url = parsed.target_url;
|
|
950
|
+
const response = await giteaFetch(
|
|
951
|
+
ctx.config,
|
|
952
|
+
ctx.parsed,
|
|
953
|
+
repoApiPath(ctx.config, 'statuses', parsed.sha),
|
|
954
|
+
{
|
|
955
|
+
method: 'POST',
|
|
956
|
+
headers: { 'Content-Type': 'application/json' },
|
|
957
|
+
body: JSON.stringify(payload),
|
|
958
|
+
},
|
|
959
|
+
);
|
|
960
|
+
return buildCommitStatusSetBody(response, parsed, {
|
|
961
|
+
idempotencyFields: idempotencyFingerprint
|
|
962
|
+
? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: false })
|
|
963
|
+
: null,
|
|
964
|
+
} );
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export async function issueOpen(ctx, { title, body: issueBody, idempotencyFingerprint = null }) {
|
|
968
|
+
assertWriteCommandConfigured(ctx.writePolicy ?? ctx.config, 'issue_open');
|
|
969
|
+
const parsed = parseIssueOpenArgs({ title, body: issueBody }, ctx.writeFieldPolicy);
|
|
970
|
+
const existing = await findOpenIssueByTitle(ctx, parsed.title);
|
|
971
|
+
if (existing) {
|
|
972
|
+
return buildIssueOpenedBody(
|
|
973
|
+
existing,
|
|
974
|
+
{ title: parsed.title },
|
|
975
|
+
{
|
|
976
|
+
reusedExisting: true,
|
|
977
|
+
idempotencyFields: idempotencyFingerprint
|
|
978
|
+
? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: true })
|
|
979
|
+
: null,
|
|
980
|
+
},
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
const payload = { title: parsed.title };
|
|
984
|
+
if (parsed.body != null) payload.body = parsed.body;
|
|
985
|
+
const issue = await giteaFetchIssueWriteResponse(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'issues'), {
|
|
986
|
+
method: 'POST',
|
|
987
|
+
headers: { 'Content-Type': 'application/json' },
|
|
988
|
+
body: JSON.stringify(payload),
|
|
989
|
+
});
|
|
990
|
+
return buildIssueOpenedBody(
|
|
991
|
+
issue,
|
|
992
|
+
{ title: parsed.title },
|
|
993
|
+
{
|
|
994
|
+
idempotencyFields: idempotencyFingerprint
|
|
995
|
+
? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: false })
|
|
996
|
+
: null,
|
|
997
|
+
},
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export async function crOpen(ctx, { head, base, title, body: prBody, idempotencyFingerprint = null }) {
|
|
1002
|
+
assertWriteCommandConfigured(ctx.writePolicy ?? ctx.config, 'cr_open');
|
|
1003
|
+
assertGitRef(head, 'head');
|
|
1004
|
+
assertGitRef(base, 'base');
|
|
1005
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
1006
|
+
throw Object.assign(new Error('--title required'), {
|
|
1007
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--title required for cr open'),
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
const writeFieldPolicy = ctx.writeFieldPolicy ?? null;
|
|
1011
|
+
const payload = {
|
|
1012
|
+
title: sanitizeWriteTitle(title, writeFieldPolicy),
|
|
1013
|
+
head: sanitizeField(head),
|
|
1014
|
+
base: sanitizeField(base),
|
|
1015
|
+
};
|
|
1016
|
+
if (prBody != null && String(prBody).trim() !== '') {
|
|
1017
|
+
payload.body = sanitizeWriteBody(String(prBody), writeFieldPolicy);
|
|
1018
|
+
}
|
|
1019
|
+
const existing = await findOpenPullByHeadBase(ctx, payload.head, payload.base);
|
|
1020
|
+
if (existing) {
|
|
1021
|
+
return buildChangeRequestOpenedBody(
|
|
1022
|
+
existing,
|
|
1023
|
+
{ head, base, title },
|
|
1024
|
+
{
|
|
1025
|
+
reusedExisting: true,
|
|
1026
|
+
idempotencyFields: idempotencyFingerprint
|
|
1027
|
+
? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: true })
|
|
1028
|
+
: null,
|
|
1029
|
+
},
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
const pull = await giteaFetchPullWriteResponse(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'pulls'), {
|
|
1033
|
+
method: 'POST',
|
|
1034
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1035
|
+
body: JSON.stringify(payload),
|
|
1036
|
+
});
|
|
1037
|
+
return buildChangeRequestOpenedBody(
|
|
1038
|
+
pull,
|
|
1039
|
+
{ head, base, title },
|
|
1040
|
+
{
|
|
1041
|
+
idempotencyFields: idempotencyFingerprint
|
|
1042
|
+
? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: false })
|
|
1043
|
+
: null,
|
|
1044
|
+
},
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export async function mergeExecute(ctx, { number, method = 'merge', expectedHeadSha }) {
|
|
1049
|
+
assertWriteCommandConfigured(ctx.writePolicy ?? ctx.config, 'merge');
|
|
1050
|
+
if (method !== 'merge') {
|
|
1051
|
+
throw Object.assign(new Error('Unsupported merge method'), {
|
|
1052
|
+
forgeError: forgeError(
|
|
1053
|
+
ERROR_CODES.INVALID_ARGS,
|
|
1054
|
+
'Only --method merge is supported in v1',
|
|
1055
|
+
),
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
const pullIndex = Number(number);
|
|
1059
|
+
if (!Number.isInteger(pullIndex) || pullIndex <= 0) {
|
|
1060
|
+
throw Object.assign(new Error('Invalid PR number'), {
|
|
1061
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'PR number must be a positive integer'),
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
const headCommitId = assertExpectedSha(expectedHeadSha, 'expectedHeadSha');
|
|
1065
|
+
let result;
|
|
1066
|
+
try {
|
|
1067
|
+
result = await giteaFetch(
|
|
1068
|
+
ctx.config,
|
|
1069
|
+
ctx.parsed,
|
|
1070
|
+
repoApiPath(ctx.config, 'pulls', String(pullIndex), 'merge'),
|
|
1071
|
+
{
|
|
1072
|
+
method: 'POST',
|
|
1073
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1074
|
+
body: JSON.stringify({ Do: 'merge', head_commit_id: headCommitId }),
|
|
1075
|
+
},
|
|
1076
|
+
);
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
const status = err.status ?? err.forgeError?.status ?? null;
|
|
1079
|
+
const message = err.forgeError?.message ?? err.message ?? '';
|
|
1080
|
+
if (isGiteaHeadOutOfDate409(err)) {
|
|
1081
|
+
throw Object.assign(new Error(message), {
|
|
1082
|
+
status,
|
|
1083
|
+
mergeBlockedBlockers: ['head_ref_moved'],
|
|
1084
|
+
forgeError: forgeError(
|
|
1085
|
+
ERROR_CODES.MERGE_BLOCKED,
|
|
1086
|
+
sanitizeField(message) || 'Head branch out of date at merge POST',
|
|
1087
|
+
status,
|
|
1088
|
+
),
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
throw err;
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
commit_sha: sanitizeField(result?.sha ?? result?.merge_commit_sha ?? null),
|
|
1095
|
+
provider_status: 200,
|
|
1096
|
+
base_sha: sanitizeField(result?.base_sha ?? null),
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const GITEA_OPEN_PULL_COMPLIANT_MAX =
|
|
1101
|
+
DEFAULT_OPEN_PULL_LIST_PAGE_SIZE * MAX_CHECK_STATUS_PAGES;
|
|
1102
|
+
|
|
1103
|
+
async function probeGiteaOpenPullPageOne(ctx, retainMax, sliceSort) {
|
|
1104
|
+
const maxTrusted = GITEA_OPEN_PULL_COMPLIANT_MAX * 2;
|
|
1105
|
+
let path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
1106
|
+
path = appendSortQuery(path, giteaOpenPullSortQuery(sliceSort));
|
|
1107
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
1108
|
+
const requestLimit = Math.min(retainMax, GITEA_PAGE_SIZE);
|
|
1109
|
+
try {
|
|
1110
|
+
const { body, headers } = await giteaFetchWithMeta(
|
|
1111
|
+
ctx.config,
|
|
1112
|
+
ctx.parsed,
|
|
1113
|
+
`${path}${pageSep}limit=${requestLimit}&page=1`,
|
|
1114
|
+
);
|
|
1115
|
+
if (!Array.isArray(body)) return null;
|
|
1116
|
+
const totalCount = parseTotalCountHeader(headers, 'X-Total-Count', { maxTrusted });
|
|
1117
|
+
if (totalCount == null) return null;
|
|
1118
|
+
const listTruncated = totalCount > GITEA_OPEN_PULL_COMPLIANT_MAX;
|
|
1119
|
+
return { body, totalCount, listTruncated, requestLimit };
|
|
1120
|
+
} catch {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function buildGiteaOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, listTruncated) {
|
|
1126
|
+
const pageItems = prepareGiteaOpenPullPageItems(body, sliceSort);
|
|
1127
|
+
let numbers = orderOpenPullNumbers(pageItems, (pr) => pr?.number, sliceSort);
|
|
1128
|
+
if (numbers.length > retainMax) numbers = numbers.slice(0, retainMax);
|
|
1129
|
+
return buildOpenPullListMeta({
|
|
1130
|
+
totalCount,
|
|
1131
|
+
numbers,
|
|
1132
|
+
listTruncated,
|
|
1133
|
+
sliceSort,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async function fetchGiteaRecentCreatedTailSlice(ctx, retainMax, sliceSort, totalCount) {
|
|
1138
|
+
const tailPage = giteaRecentCreatedTailPage(totalCount, GITEA_PAGE_SIZE);
|
|
1139
|
+
let path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
1140
|
+
path = appendSortQuery(path, giteaOpenPullSortQuery(sliceSort));
|
|
1141
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
1142
|
+
let body;
|
|
1143
|
+
try {
|
|
1144
|
+
body = await giteaFetch(
|
|
1145
|
+
ctx.config,
|
|
1146
|
+
ctx.parsed,
|
|
1147
|
+
`${path}${pageSep}limit=${GITEA_PAGE_SIZE}&page=${tailPage}`,
|
|
1148
|
+
);
|
|
1149
|
+
} catch {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
if (!Array.isArray(body) || body.length === 0) return null;
|
|
1153
|
+
return buildGiteaOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, false);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function giteaProbePaginationOpts(probe, extra = {}) {
|
|
1157
|
+
const { body, totalCount, requestLimit } = probe;
|
|
1158
|
+
return {
|
|
1159
|
+
trustedTotalCount: totalCount,
|
|
1160
|
+
seededFirstPage: { items: body, usedLimit: requestLimit },
|
|
1161
|
+
...extra,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function paginateGiteaOpenPullList(ctx, opts, sliceSort, paginationOpts = {}) {
|
|
1166
|
+
const {
|
|
1167
|
+
trustedTotalCount = null,
|
|
1168
|
+
numberSortFullCollect = false,
|
|
1169
|
+
seededFirstPage = null,
|
|
1170
|
+
startPage = 1,
|
|
1171
|
+
maxPages = MAX_CHECK_STATUS_PAGES,
|
|
1172
|
+
suppressFinalPageProbe = false,
|
|
1173
|
+
} = paginationOpts;
|
|
1174
|
+
const listLimit =
|
|
1175
|
+
opts.limit != null && Number.isInteger(Number(opts.limit)) && Number(opts.limit) > 0
|
|
1176
|
+
? Number(opts.limit)
|
|
1177
|
+
: null;
|
|
1178
|
+
const retainMax =
|
|
1179
|
+
listLimit == null &&
|
|
1180
|
+
opts.retain_max != null &&
|
|
1181
|
+
Number.isInteger(Number(opts.retain_max)) &&
|
|
1182
|
+
Number(opts.retain_max) > 0
|
|
1183
|
+
? Number(opts.retain_max)
|
|
1184
|
+
: null;
|
|
1185
|
+
const pageSize =
|
|
1186
|
+
listLimit != null ? Math.min(listLimit, GITEA_PAGE_SIZE) : GITEA_PAGE_SIZE;
|
|
1187
|
+
let path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
|
|
1188
|
+
path = appendSortQuery(path, giteaOpenPullSortQuery(sliceSort));
|
|
1189
|
+
const pageSep = path.includes('?') ? '&' : '?';
|
|
1190
|
+
const effectiveRetainMax = numberSortFullCollect ? null : retainMax;
|
|
1191
|
+
const {
|
|
1192
|
+
items: all,
|
|
1193
|
+
list_truncated: listTruncated,
|
|
1194
|
+
entry_count: entryCount,
|
|
1195
|
+
walked_count: walkedCount,
|
|
1196
|
+
} = await paginateOffsetListPages({
|
|
1197
|
+
pageSize,
|
|
1198
|
+
listLimit,
|
|
1199
|
+
retainMax: effectiveRetainMax,
|
|
1200
|
+
trustedEntryCount: trustedTotalCount,
|
|
1201
|
+
seededFirstPage,
|
|
1202
|
+
startPage,
|
|
1203
|
+
maxPages,
|
|
1204
|
+
suppressFinalPageProbe,
|
|
1205
|
+
...(listLimit != null && pageSize < listLimit ? { maxPagesTruncatesWithLimit: true } : {}),
|
|
1206
|
+
fetchPage: async ({ page, limit }) => {
|
|
1207
|
+
const body = await giteaFetch(
|
|
1208
|
+
ctx.config,
|
|
1209
|
+
ctx.parsed,
|
|
1210
|
+
`${path}${pageSep}limit=${limit}&page=${page}`,
|
|
1211
|
+
);
|
|
1212
|
+
return Array.isArray(body) ? body : [];
|
|
1213
|
+
},
|
|
1214
|
+
});
|
|
1215
|
+
let numbers = orderOpenPullNumbers(
|
|
1216
|
+
prepareGiteaOpenPullPageItems(all, sliceSort),
|
|
1217
|
+
(pr) => pr?.number,
|
|
1218
|
+
sliceSort,
|
|
1219
|
+
);
|
|
1220
|
+
const outputCap = listLimit ?? retainMax;
|
|
1221
|
+
if (outputCap != null && numbers.length > outputCap) {
|
|
1222
|
+
numbers = numbers.slice(0, outputCap);
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
numbers,
|
|
1226
|
+
list_truncated: resolveListTruncatedWithTrustedTotal({
|
|
1227
|
+
listTruncated,
|
|
1228
|
+
trustedTotalCount,
|
|
1229
|
+
walkedCount,
|
|
1230
|
+
fullCollect: numberSortFullCollect,
|
|
1231
|
+
}),
|
|
1232
|
+
...(entryCount != null ? { entry_count: entryCount } : {}),
|
|
1233
|
+
slice_sort: sliceSort,
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export async function listOpenPullsWithMeta(ctx, opts = {}) {
|
|
1238
|
+
requireToken();
|
|
1239
|
+
const sliceSort = normalizeCrInventorySort(opts.sort ?? DEFAULT_CR_INVENTORY_SLICE_SORT);
|
|
1240
|
+
if (!isCrInventoryFastPathEligible(opts)) {
|
|
1241
|
+
return paginateGiteaOpenPullList(ctx, opts, sliceSort);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const retainMax = Number(opts.retain_max);
|
|
1245
|
+
const probe = await probeGiteaOpenPullPageOne(ctx, retainMax, sliceSort);
|
|
1246
|
+
if (!probe) {
|
|
1247
|
+
return paginateGiteaOpenPullList(ctx, opts, sliceSort);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const { body, totalCount, listTruncated, requestLimit } = probe;
|
|
1251
|
+
|
|
1252
|
+
if (listTruncated) {
|
|
1253
|
+
if (body.length === 0) {
|
|
1254
|
+
return paginateGiteaOpenPullList(ctx, opts, sliceSort, giteaProbePaginationOpts(probe));
|
|
1255
|
+
}
|
|
1256
|
+
return buildGiteaOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, true);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (
|
|
1260
|
+
sliceSort === 'recent_created' &&
|
|
1261
|
+
!isRecentCreatedFastPathEligible(totalCount, retainMax, sliceSort, 'gitea-api')
|
|
1262
|
+
) {
|
|
1263
|
+
const tail = await fetchGiteaRecentCreatedTailSlice(ctx, retainMax, sliceSort, totalCount);
|
|
1264
|
+
if (tail) return tail;
|
|
1265
|
+
const tailRetry = await fetchGiteaRecentCreatedTailSlice(ctx, retainMax, sliceSort, totalCount);
|
|
1266
|
+
if (tailRetry) return tailRetry;
|
|
1267
|
+
const tailPage = giteaRecentCreatedTailPage(totalCount, GITEA_PAGE_SIZE);
|
|
1268
|
+
return paginateGiteaOpenPullList(ctx, opts, sliceSort, {
|
|
1269
|
+
trustedTotalCount: totalCount,
|
|
1270
|
+
startPage: tailPage,
|
|
1271
|
+
maxPages: tailPage,
|
|
1272
|
+
suppressFinalPageProbe: true,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (
|
|
1277
|
+
isRecentCreatedFastPathEligible(totalCount, retainMax, sliceSort, 'gitea-api') &&
|
|
1278
|
+
isNumberSortFastPathEligible(totalCount, retainMax, sliceSort) &&
|
|
1279
|
+
validateFastPathPageLength(totalCount, requestLimit, body.length)
|
|
1280
|
+
) {
|
|
1281
|
+
return buildGiteaOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, false);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const numberSortFullCollect = isNumberSortFullCollectRequired(totalCount, retainMax, sliceSort);
|
|
1285
|
+
return paginateGiteaOpenPullList(
|
|
1286
|
+
ctx,
|
|
1287
|
+
opts,
|
|
1288
|
+
sliceSort,
|
|
1289
|
+
giteaProbePaginationOpts(probe, { numberSortFullCollect }),
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
export async function listOpenPulls(ctx, opts = {}) {
|
|
1294
|
+
const meta = await listOpenPullsWithMeta(ctx, opts);
|
|
1295
|
+
return meta.numbers;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
export async function crInventorySlice(ctx, opts = {}) {
|
|
1299
|
+
return crInventory(ctx, { listOpenPulls, listOpenPullsWithMeta, prView, prChecks }, opts);
|
|
175
1300
|
}
|
|
176
1301
|
|
|
177
1302
|
function mergeability(pr) {
|
|
@@ -182,22 +1307,26 @@ function mergeability(pr) {
|
|
|
182
1307
|
|
|
183
1308
|
export async function prView(ctx, opts) {
|
|
184
1309
|
const pr = await getPull(ctx, opts);
|
|
185
|
-
|
|
1310
|
+
const body = {
|
|
186
1311
|
pr_number: pr.number,
|
|
187
1312
|
url: sanitizeUrl(pr.html_url ?? pr.url),
|
|
188
1313
|
title: sanitizeField(pr.title),
|
|
189
|
-
state:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
1314
|
+
state: normalizeGiteaPrState(pr.state),
|
|
1315
|
+
forge_target_branch_ref: sanitizeField(pr.base?.ref),
|
|
1316
|
+
forge_target_sha: sanitizeField(pr.base?.sha),
|
|
1317
|
+
forge_source_branch_ref: sanitizeField(pr.head?.ref),
|
|
1318
|
+
forge_source_sha: sanitizeField(pr.head?.sha),
|
|
194
1319
|
mergeability: mergeability(pr),
|
|
195
1320
|
};
|
|
1321
|
+
const forgeSourceRepoId = forgeSourceRepoIdFromPull(ctx.config, pr);
|
|
1322
|
+
if (forgeSourceRepoId) body.forge_source_repo_id = forgeSourceRepoId;
|
|
1323
|
+
return body;
|
|
196
1324
|
}
|
|
197
1325
|
|
|
198
1326
|
export async function prChecks(ctx, opts) {
|
|
199
1327
|
requireToken();
|
|
200
1328
|
let sha;
|
|
1329
|
+
let requiredContexts = [];
|
|
201
1330
|
if (opts.ref) {
|
|
202
1331
|
assertGitRef(opts.ref, 'ref');
|
|
203
1332
|
sha = gitRevParse(ctx.cwd, opts.ref);
|
|
@@ -209,27 +1338,37 @@ export async function prChecks(ctx, opts) {
|
|
|
209
1338
|
} else {
|
|
210
1339
|
const pr = await getPull(ctx, opts);
|
|
211
1340
|
sha = pr.head?.sha;
|
|
1341
|
+
const targetBranch = pr.base?.ref;
|
|
1342
|
+
if (targetBranch) {
|
|
1343
|
+
try {
|
|
1344
|
+
const protection = await resolveBranchProtection(ctx, { branchRef: targetBranch });
|
|
1345
|
+
requiredContexts = protection.required_status_contexts ?? [];
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
if (err?.status !== 404) throw err;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
212
1350
|
}
|
|
213
1351
|
if (!sha) {
|
|
214
1352
|
throw Object.assign(new Error('No SHA'), {
|
|
215
1353
|
forgeError: forgeError(ERROR_CODES.MISSING_REF, 'Could not determine head SHA for checks'),
|
|
216
1354
|
});
|
|
217
1355
|
}
|
|
218
|
-
const
|
|
1356
|
+
const { items: statusRecords, truncated: checks_truncated } = await giteaFetchPaginated(
|
|
219
1357
|
ctx.config,
|
|
220
1358
|
ctx.parsed,
|
|
221
1359
|
repoApiPath(ctx.config, 'commits', sha, 'statuses'),
|
|
222
1360
|
);
|
|
223
|
-
const mapped = (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
1361
|
+
const mapped = mapGiteaCommitStatuses(statusRecords, { headSha: sha });
|
|
1362
|
+
return buildPrChecksBody({
|
|
1363
|
+
forge_source_sha: sha,
|
|
1364
|
+
check_conclusion: summarizeChecks(mapped),
|
|
1365
|
+
checks_truncated,
|
|
1366
|
+
statuses: mapped,
|
|
1367
|
+
required_contexts: requiredContexts,
|
|
1368
|
+
});
|
|
230
1369
|
}
|
|
231
1370
|
|
|
232
|
-
function summarizeChecks(statuses) {
|
|
1371
|
+
export function summarizeChecks(statuses) {
|
|
233
1372
|
if (!statuses.length) return 'missing';
|
|
234
1373
|
if (statuses.some((s) => s.state === 'failure' || s.state === 'error')) return 'failure';
|
|
235
1374
|
if (statuses.some((s) => s.state === 'pending')) return 'pending';
|
|
@@ -238,20 +1377,7 @@ function summarizeChecks(statuses) {
|
|
|
238
1377
|
}
|
|
239
1378
|
|
|
240
1379
|
export async function mergePlan(ctx, opts) {
|
|
241
|
-
|
|
242
|
-
const checks = await prChecks(ctx, { number: view.pr_number });
|
|
243
|
-
const blockers = [];
|
|
244
|
-
if (view.mergeability === 'conflicted') blockers.push('merge_conflict');
|
|
245
|
-
if (view.state !== 'open') blockers.push('pr_not_open');
|
|
246
|
-
if (checks.check_conclusion === 'failure') blockers.push('checks_failed');
|
|
247
|
-
if (checks.check_conclusion === 'missing') blockers.push('checks_missing');
|
|
248
|
-
if (checks.check_conclusion === 'pending') blockers.push('checks_pending');
|
|
249
|
-
return {
|
|
250
|
-
pr_number: view.pr_number,
|
|
251
|
-
mergeability: view.mergeability,
|
|
252
|
-
checks_conclusion: checks.check_conclusion,
|
|
253
|
-
blockers,
|
|
254
|
-
};
|
|
1380
|
+
return buildMergePlanFromProviderFacts(ctx, opts, { prView, prChecks, crFiles });
|
|
255
1381
|
}
|
|
256
1382
|
|
|
257
1383
|
export async function syncPlan(ctx, remoteName = 'origin') {
|
|
@@ -284,10 +1410,26 @@ export async function syncPlan(ctx, remoteName = 'origin') {
|
|
|
284
1410
|
export const provider = {
|
|
285
1411
|
id: 'gitea-api',
|
|
286
1412
|
providerCapabilities,
|
|
1413
|
+
apiReachability,
|
|
287
1414
|
repoStatus,
|
|
288
1415
|
refsCompare,
|
|
1416
|
+
refsInventory,
|
|
1417
|
+
listOpenPulls,
|
|
1418
|
+
crInventory: crInventorySlice,
|
|
289
1419
|
prView,
|
|
290
1420
|
prChecks,
|
|
291
1421
|
mergePlan,
|
|
292
1422
|
syncPlan,
|
|
1423
|
+
crOpen,
|
|
1424
|
+
issueOpen,
|
|
1425
|
+
mergeExecute,
|
|
1426
|
+
statusSet,
|
|
1427
|
+
whoami,
|
|
1428
|
+
branchProtection,
|
|
1429
|
+
branchHeadSha,
|
|
1430
|
+
crFiles,
|
|
1431
|
+
crComments,
|
|
1432
|
+
forgeChanges,
|
|
293
1433
|
};
|
|
1434
|
+
|
|
1435
|
+
setBranchProtectionImpl(branchProtection);
|
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.10",
|
|
4
4
|
"description": "Gitea REST API forge provider for remogram",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
"*.js"
|
|
17
17
|
],
|
|
18
18
|
"exports": {
|
|
19
|
-
".": "./index.js"
|
|
19
|
+
".": "./index.js",
|
|
20
|
+
"./branch-protection-internal.js": "./branch-protection-internal.js"
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=20"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@remogram/core": "0.1.0-beta.
|
|
26
|
+
"@remogram/core": "0.1.0-beta.10"
|
|
26
27
|
}
|
|
27
28
|
}
|