@remogram/provider-gitea-api 0.1.0-beta.1 → 0.1.0-beta.3

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.
Files changed (2) hide show
  1. package/index.js +118 -29
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -7,22 +7,36 @@ import {
7
7
  gitRevParse,
8
8
  gitCurrentBranch,
9
9
  gitAheadBehind,
10
+ refsInventory,
11
+ crInventory,
12
+ mergeBlockersFromFacts,
10
13
  ERROR_CODES,
11
14
  forgeError,
12
15
  forgeIngestCapabilityFacts,
16
+ checkPaginationCapabilityFacts,
17
+ DEFAULT_CHECK_STATUS_PAGE_SIZE,
18
+ MAX_CHECK_STATUS_PAGES,
19
+ paginateCheckStatusPages,
20
+ paginateOffsetListPages,
21
+ fetchWithIngestPageBackoff,
22
+ fetchPageWithIngestBackoff,
23
+ withPerPageParam,
24
+ apiProviderCommands,
13
25
  } from '@remogram/core';
14
26
  const PUBLIC_GITEA_HOST = 'gitea.com';
15
27
  const PUBLIC_GITEA_API = 'https://gitea.com/api/v1';
16
28
  const AUTH_CAPABILITIES = [
17
29
  'repo_status',
18
30
  'ref_compare',
31
+ 'ref_inventory',
32
+ 'cr_inventory',
19
33
  'pr_status',
20
34
  'pr_checks',
21
35
  'merge_plan',
22
36
  'sync_plan',
23
37
  ];
24
38
 
25
- const STRUCTURED_COMMANDS = AUTH_CAPABILITIES.map((name) => ({ name, implemented: true }));
39
+ const STRUCTURED_COMMANDS = apiProviderCommands();
26
40
 
27
41
  export function giteaToken() {
28
42
  return process.env.GITEA_TOKEN || null;
@@ -118,23 +132,21 @@ export async function giteaFetch(config, parsed, path, options = {}) {
118
132
  });
119
133
  }
120
134
 
121
- const MAX_CHECK_PAGES = 50;
135
+ const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
122
136
  const GITEA_PAGE_SIZE = 100;
123
137
 
124
138
  export async function giteaFetchPaginated(config, parsed, path) {
125
- const all = [];
126
- for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
127
- const separator = path.includes('?') ? '&' : '?';
128
- const body = await giteaFetch(
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;
136
- }
137
- return all;
139
+ return paginateCheckStatusPages({
140
+ fetchPage: async ({ page, limit }) => {
141
+ const separator = path.includes('?') ? '&' : '?';
142
+ const body = await giteaFetch(
143
+ config,
144
+ parsed,
145
+ `${path}${separator}limit=${limit}&page=${page}`,
146
+ );
147
+ return Array.isArray(body) ? body : [];
148
+ },
149
+ });
138
150
  }
139
151
 
140
152
  export function normalizeGiteaStatusState(state) {
@@ -149,6 +161,36 @@ export function normalizeGiteaStatusState(state) {
149
161
  return 'unknown';
150
162
  }
151
163
 
164
+ function giteaStatusRecordOrder(a, b) {
165
+ const aUpdated = Date.parse(a.updated_at ?? '') || 0;
166
+ const bUpdated = Date.parse(b.updated_at ?? '') || 0;
167
+ if (aUpdated !== bUpdated) return aUpdated - bUpdated;
168
+ const aId = Number(a.id) || 0;
169
+ const bId = Number(b.id) || 0;
170
+ return aId - bId;
171
+ }
172
+
173
+ export function dedupeGiteaStatusRecords(records) {
174
+ const latestByContext = new Map();
175
+ for (const record of records) {
176
+ const context = record?.context;
177
+ if (context == null || context === '') continue;
178
+ const existing = latestByContext.get(context);
179
+ if (!existing || giteaStatusRecordOrder(record, existing) > 0) {
180
+ latestByContext.set(context, record);
181
+ }
182
+ }
183
+ return Array.from(latestByContext.values());
184
+ }
185
+
186
+ export function mapGiteaCommitStatuses(records) {
187
+ return dedupeGiteaStatusRecords(records).map((s) => ({
188
+ context: sanitizeField(s.context),
189
+ state: normalizeGiteaStatusState(s.status ?? s.state),
190
+ description: sanitizeField(s.description),
191
+ }));
192
+ }
193
+
152
194
  export function normalizeGiteaPrState(state) {
153
195
  const normalized = String(state ?? '').toLowerCase();
154
196
  if (normalized === 'open') return 'open';
@@ -172,15 +214,21 @@ export async function repoStatus(ctx) {
172
214
  }
173
215
 
174
216
  export function providerCapabilities() {
217
+ const check_sources = ['commit_statuses'];
175
218
  return {
176
219
  commands: STRUCTURED_COMMANDS,
177
220
  auth_envs: ['GITEA_TOKEN'],
178
- check_sources: ['commit_statuses'],
221
+ check_sources,
179
222
  mergeability_confidence: 'direct',
180
223
  host_binding: 'verified_remote_host',
181
224
  pagination: 'supported',
182
225
  write_support: false,
183
226
  ...forgeIngestCapabilityFacts(),
227
+ ...checkPaginationCapabilityFacts({
228
+ strategy: 'offset_limit',
229
+ pageSizeParam: 'limit',
230
+ sourceCount: check_sources.length,
231
+ }),
184
232
  };
185
233
  }
186
234
 
@@ -213,6 +261,48 @@ export async function getPull(ctx, { number }) {
213
261
  return giteaFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'pulls', number));
214
262
  }
215
263
 
264
+ export async function listOpenPullsWithMeta(ctx, opts = {}) {
265
+ requireToken();
266
+ const listLimit =
267
+ opts.limit != null && Number.isInteger(Number(opts.limit)) && Number(opts.limit) > 0
268
+ ? Number(opts.limit)
269
+ : null;
270
+ const pageSize =
271
+ listLimit != null ? Math.min(listLimit, GITEA_PAGE_SIZE) : GITEA_PAGE_SIZE;
272
+ const path = `${repoApiPath(ctx.config, 'pulls')}?state=open`;
273
+ const pageSep = path.includes('?') ? '&' : '?';
274
+ const { items: all, list_truncated: listTruncated } = await paginateOffsetListPages({
275
+ pageSize,
276
+ listLimit,
277
+ ...(listLimit != null && pageSize < listLimit ? { maxPagesTruncatesWithLimit: true } : {}),
278
+ fetchPage: async ({ page, limit }) => {
279
+ const body = await giteaFetch(
280
+ ctx.config,
281
+ ctx.parsed,
282
+ `${path}${pageSep}limit=${limit}&page=${page}`,
283
+ );
284
+ return Array.isArray(body) ? body : [];
285
+ },
286
+ });
287
+ let numbers = all
288
+ .map((pr) => pr.number)
289
+ .filter((number) => Number.isInteger(number))
290
+ .sort((a, b) => a - b);
291
+ if (listLimit != null && numbers.length > listLimit) {
292
+ numbers = numbers.slice(0, listLimit);
293
+ }
294
+ return { numbers, list_truncated: listTruncated };
295
+ }
296
+
297
+ export async function listOpenPulls(ctx, opts = {}) {
298
+ const meta = await listOpenPullsWithMeta(ctx, opts);
299
+ return meta.numbers;
300
+ }
301
+
302
+ export async function crInventorySlice(ctx, opts = {}) {
303
+ return crInventory(ctx, { listOpenPulls, listOpenPullsWithMeta, prView, prChecks }, opts);
304
+ }
305
+
216
306
  function mergeability(pr) {
217
307
  if (pr.mergeable === true) return 'clean';
218
308
  if (pr.mergeable === false) return 'conflicted';
@@ -254,18 +344,19 @@ export async function prChecks(ctx, opts) {
254
344
  forgeError: forgeError(ERROR_CODES.MISSING_REF, 'Could not determine head SHA for checks'),
255
345
  });
256
346
  }
257
- const statusRecords = await giteaFetchPaginated(
347
+ const { items: statusRecords, truncated: checks_truncated } = await giteaFetchPaginated(
258
348
  ctx.config,
259
349
  ctx.parsed,
260
350
  repoApiPath(ctx.config, 'commits', sha, 'statuses'),
261
351
  );
262
- const mapped = statusRecords.map((s) => ({
263
- context: sanitizeField(s.context),
264
- state: normalizeGiteaStatusState(s.state),
265
- description: sanitizeField(s.description),
266
- }));
352
+ const mapped = mapGiteaCommitStatuses(statusRecords);
267
353
  const conclusion = summarizeChecks(mapped);
268
- return { head_sha: sha, check_conclusion: conclusion, statuses: mapped };
354
+ return {
355
+ head_sha: sha,
356
+ check_conclusion: conclusion,
357
+ checks_truncated,
358
+ statuses: mapped,
359
+ };
269
360
  }
270
361
 
271
362
  export function summarizeChecks(statuses) {
@@ -279,12 +370,7 @@ export function summarizeChecks(statuses) {
279
370
  export async function mergePlan(ctx, opts) {
280
371
  const view = await prView(ctx, opts);
281
372
  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');
373
+ const blockers = mergeBlockersFromFacts(view, checks);
288
374
  return {
289
375
  pr_number: view.pr_number,
290
376
  mergeability: view.mergeability,
@@ -325,6 +411,9 @@ export const provider = {
325
411
  providerCapabilities,
326
412
  repoStatus,
327
413
  refsCompare,
414
+ refsInventory,
415
+ listOpenPulls,
416
+ crInventory: crInventorySlice,
328
417
  prView,
329
418
  prChecks,
330
419
  mergePlan,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remogram/provider-gitea-api",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Gitea REST API forge provider for remogram",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,6 +22,6 @@
22
22
  "node": ">=20"
23
23
  },
24
24
  "dependencies": {
25
- "@remogram/core": "0.1.0-beta.1"
25
+ "@remogram/core": "0.1.0-beta.3"
26
26
  }
27
27
  }