@remogram/provider-gitea-api 0.1.0-beta.1 → 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 +1157 -54
- 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,28 +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,
|
|
12
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,
|
|
13
72
|
} from '@remogram/core';
|
|
73
|
+
import {
|
|
74
|
+
resolveBranchProtection,
|
|
75
|
+
setBranchProtectionImpl,
|
|
76
|
+
} from './branch-protection-internal.js';
|
|
14
77
|
const PUBLIC_GITEA_HOST = 'gitea.com';
|
|
15
78
|
const PUBLIC_GITEA_API = 'https://gitea.com/api/v1';
|
|
16
79
|
const AUTH_CAPABILITIES = [
|
|
17
80
|
'repo_status',
|
|
18
81
|
'ref_compare',
|
|
82
|
+
'ref_inventory',
|
|
83
|
+
'cr_inventory',
|
|
19
84
|
'pr_status',
|
|
20
85
|
'pr_checks',
|
|
21
86
|
'merge_plan',
|
|
22
87
|
'sync_plan',
|
|
88
|
+
'cr_open',
|
|
89
|
+
'status_set',
|
|
90
|
+
'whoami',
|
|
91
|
+
'branch_protection',
|
|
92
|
+
'cr_files',
|
|
93
|
+
'cr_comments',
|
|
94
|
+
'forge_changes',
|
|
23
95
|
];
|
|
24
96
|
|
|
25
|
-
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
|
+
});
|
|
26
107
|
|
|
27
108
|
export function giteaToken() {
|
|
28
109
|
return process.env.GITEA_TOKEN || null;
|
|
@@ -102,13 +183,36 @@ export function authHeaders(token) {
|
|
|
102
183
|
}
|
|
103
184
|
|
|
104
185
|
export function repoApiPath(config, ...segments) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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}`;
|
|
108
193
|
if (!segments.length) return base;
|
|
109
194
|
return `${base}/${segments.map((s) => encodeURIComponent(String(s))).join('/')}`;
|
|
110
195
|
}
|
|
111
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
|
+
|
|
112
216
|
export async function giteaFetch(config, parsed, path, options = {}) {
|
|
113
217
|
const token = requireToken();
|
|
114
218
|
const url = `${apiBase(config, parsed)}${path}`;
|
|
@@ -118,23 +222,61 @@ export async function giteaFetch(config, parsed, path, options = {}) {
|
|
|
118
222
|
});
|
|
119
223
|
}
|
|
120
224
|
|
|
121
|
-
|
|
122
|
-
const
|
|
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
|
+
}
|
|
123
233
|
|
|
124
|
-
export async function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
config,
|
|
130
|
-
parsed,
|
|
131
|
-
`${path}${separator}limit=${GITEA_PAGE_SIZE}&page=${page}`,
|
|
132
|
-
);
|
|
133
|
-
const items = Array.isArray(body) ? body : [];
|
|
134
|
-
all.push(...items);
|
|
135
|
-
if (items.length < GITEA_PAGE_SIZE) break;
|
|
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
|
+
});
|
|
136
239
|
}
|
|
137
|
-
|
|
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
|
+
});
|
|
138
280
|
}
|
|
139
281
|
|
|
140
282
|
export function normalizeGiteaStatusState(state) {
|
|
@@ -149,6 +291,39 @@ export function normalizeGiteaStatusState(state) {
|
|
|
149
291
|
return 'unknown';
|
|
150
292
|
}
|
|
151
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
|
+
|
|
152
327
|
export function normalizeGiteaPrState(state) {
|
|
153
328
|
const normalized = String(state ?? '').toLowerCase();
|
|
154
329
|
if (normalized === 'open') return 'open';
|
|
@@ -171,16 +346,274 @@ export async function repoStatus(ctx) {
|
|
|
171
346
|
};
|
|
172
347
|
}
|
|
173
348
|
|
|
174
|
-
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'];
|
|
175
588
|
return {
|
|
176
589
|
commands: STRUCTURED_COMMANDS,
|
|
177
590
|
auth_envs: ['GITEA_TOKEN'],
|
|
178
|
-
check_sources
|
|
591
|
+
check_sources,
|
|
179
592
|
mergeability_confidence: 'direct',
|
|
180
593
|
host_binding: 'verified_remote_host',
|
|
181
594
|
pagination: 'supported',
|
|
182
|
-
write_support:
|
|
595
|
+
write_support: true,
|
|
596
|
+
write_commands: ['cr_open', 'status_set', 'merge', 'issue_open'],
|
|
183
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
|
+
}),
|
|
184
617
|
};
|
|
185
618
|
}
|
|
186
619
|
|
|
@@ -196,10 +629,10 @@ export async function refsCompare(ctx, baseRef, headRef) {
|
|
|
196
629
|
}
|
|
197
630
|
const counts = gitAheadBehind(ctx.cwd, baseSha, headSha);
|
|
198
631
|
return {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
632
|
+
compare_base_ref: sanitizeField(baseRef),
|
|
633
|
+
compare_base_sha: baseSha,
|
|
634
|
+
compare_head_ref: sanitizeField(headRef),
|
|
635
|
+
compare_head_sha: headSha,
|
|
203
636
|
...counts,
|
|
204
637
|
};
|
|
205
638
|
}
|
|
@@ -210,7 +643,660 @@ export async function getPull(ctx, { number }) {
|
|
|
210
643
|
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR lookup'),
|
|
211
644
|
});
|
|
212
645
|
}
|
|
213
|
-
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);
|
|
214
1300
|
}
|
|
215
1301
|
|
|
216
1302
|
function mergeability(pr) {
|
|
@@ -221,22 +1307,26 @@ function mergeability(pr) {
|
|
|
221
1307
|
|
|
222
1308
|
export async function prView(ctx, opts) {
|
|
223
1309
|
const pr = await getPull(ctx, opts);
|
|
224
|
-
|
|
1310
|
+
const body = {
|
|
225
1311
|
pr_number: pr.number,
|
|
226
1312
|
url: sanitizeUrl(pr.html_url ?? pr.url),
|
|
227
1313
|
title: sanitizeField(pr.title),
|
|
228
1314
|
state: normalizeGiteaPrState(pr.state),
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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),
|
|
233
1319
|
mergeability: mergeability(pr),
|
|
234
1320
|
};
|
|
1321
|
+
const forgeSourceRepoId = forgeSourceRepoIdFromPull(ctx.config, pr);
|
|
1322
|
+
if (forgeSourceRepoId) body.forge_source_repo_id = forgeSourceRepoId;
|
|
1323
|
+
return body;
|
|
235
1324
|
}
|
|
236
1325
|
|
|
237
1326
|
export async function prChecks(ctx, opts) {
|
|
238
1327
|
requireToken();
|
|
239
1328
|
let sha;
|
|
1329
|
+
let requiredContexts = [];
|
|
240
1330
|
if (opts.ref) {
|
|
241
1331
|
assertGitRef(opts.ref, 'ref');
|
|
242
1332
|
sha = gitRevParse(ctx.cwd, opts.ref);
|
|
@@ -248,24 +1338,34 @@ export async function prChecks(ctx, opts) {
|
|
|
248
1338
|
} else {
|
|
249
1339
|
const pr = await getPull(ctx, opts);
|
|
250
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
|
+
}
|
|
251
1350
|
}
|
|
252
1351
|
if (!sha) {
|
|
253
1352
|
throw Object.assign(new Error('No SHA'), {
|
|
254
1353
|
forgeError: forgeError(ERROR_CODES.MISSING_REF, 'Could not determine head SHA for checks'),
|
|
255
1354
|
});
|
|
256
1355
|
}
|
|
257
|
-
const statusRecords = await giteaFetchPaginated(
|
|
1356
|
+
const { items: statusRecords, truncated: checks_truncated } = await giteaFetchPaginated(
|
|
258
1357
|
ctx.config,
|
|
259
1358
|
ctx.parsed,
|
|
260
1359
|
repoApiPath(ctx.config, 'commits', sha, 'statuses'),
|
|
261
1360
|
);
|
|
262
|
-
const mapped = statusRecords
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
});
|
|
269
1369
|
}
|
|
270
1370
|
|
|
271
1371
|
export function summarizeChecks(statuses) {
|
|
@@ -277,20 +1377,7 @@ export function summarizeChecks(statuses) {
|
|
|
277
1377
|
}
|
|
278
1378
|
|
|
279
1379
|
export async function mergePlan(ctx, opts) {
|
|
280
|
-
|
|
281
|
-
const checks = await prChecks(ctx, { number: view.pr_number });
|
|
282
|
-
const blockers = [];
|
|
283
|
-
if (view.mergeability === 'conflicted') blockers.push('merge_conflict');
|
|
284
|
-
if (view.state !== 'open') blockers.push('pr_not_open');
|
|
285
|
-
if (checks.check_conclusion === 'failure') blockers.push('checks_failed');
|
|
286
|
-
if (checks.check_conclusion === 'missing') blockers.push('checks_missing');
|
|
287
|
-
if (checks.check_conclusion === 'pending') blockers.push('checks_pending');
|
|
288
|
-
return {
|
|
289
|
-
pr_number: view.pr_number,
|
|
290
|
-
mergeability: view.mergeability,
|
|
291
|
-
checks_conclusion: checks.check_conclusion,
|
|
292
|
-
blockers,
|
|
293
|
-
};
|
|
1380
|
+
return buildMergePlanFromProviderFacts(ctx, opts, { prView, prChecks, crFiles });
|
|
294
1381
|
}
|
|
295
1382
|
|
|
296
1383
|
export async function syncPlan(ctx, remoteName = 'origin') {
|
|
@@ -323,10 +1410,26 @@ export async function syncPlan(ctx, remoteName = 'origin') {
|
|
|
323
1410
|
export const provider = {
|
|
324
1411
|
id: 'gitea-api',
|
|
325
1412
|
providerCapabilities,
|
|
1413
|
+
apiReachability,
|
|
326
1414
|
repoStatus,
|
|
327
1415
|
refsCompare,
|
|
1416
|
+
refsInventory,
|
|
1417
|
+
listOpenPulls,
|
|
1418
|
+
crInventory: crInventorySlice,
|
|
328
1419
|
prView,
|
|
329
1420
|
prChecks,
|
|
330
1421
|
mergePlan,
|
|
331
1422
|
syncPlan,
|
|
1423
|
+
crOpen,
|
|
1424
|
+
issueOpen,
|
|
1425
|
+
mergeExecute,
|
|
1426
|
+
statusSet,
|
|
1427
|
+
whoami,
|
|
1428
|
+
branchProtection,
|
|
1429
|
+
branchHeadSha,
|
|
1430
|
+
crFiles,
|
|
1431
|
+
crComments,
|
|
1432
|
+
forgeChanges,
|
|
332
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
|
}
|