@remogram/provider-gitlab-api 0.1.0-beta.2 → 0.1.0-beta.4

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 +232 -27
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  fetchJson,
3
+ fetchJsonWithMeta,
3
4
  sanitizeField,
4
5
  sanitizeUrl,
5
6
  assertGitRef,
@@ -7,10 +8,35 @@ import {
7
8
  gitRevParse,
8
9
  gitCurrentBranch,
9
10
  gitAheadBehind,
11
+ refsInventory,
12
+ crInventory,
13
+ buildMergePlanBodyFromFacts,
10
14
  ERROR_CODES,
11
15
  forgeError,
12
16
  forgeIngestCapabilityFacts,
17
+ checkPaginationCapabilityFacts,
18
+ openPullListCapabilityFacts,
19
+ DEFAULT_CHECK_STATUS_PAGE_SIZE,
20
+ MAX_CHECK_STATUS_PAGES,
21
+ DEFAULT_OPEN_PULL_LIST_PAGE_SIZE,
22
+ paginateCheckStatusPages,
23
+ paginateOffsetListPages,
24
+ fetchWithIngestPageBackoff,
25
+ fetchPageWithIngestBackoff,
26
+ withPerPageParam,
13
27
  apiProviderCommands,
28
+ normalizeCrInventorySort,
29
+ DEFAULT_CR_INVENTORY_SLICE_SORT,
30
+ parseTotalCountHeader,
31
+ isCrInventoryFastPathEligible,
32
+ validateFastPathPageLength,
33
+ isNumberSortFastPathEligible,
34
+ isNumberSortFullCollectRequired,
35
+ resolveListTruncatedWithTrustedTotal,
36
+ orderOpenPullNumbers,
37
+ buildOpenPullListMeta,
38
+ gitlabOpenPullSortQuery,
39
+ appendSortQuery,
14
40
  } from '@remogram/core';
15
41
 
16
42
  const PUBLIC_GITLAB_HOST = 'gitlab.com';
@@ -18,6 +44,8 @@ const PUBLIC_GITLAB_API = 'https://gitlab.com/api/v4';
18
44
  const AUTH_CAPABILITIES = [
19
45
  'repo_status',
20
46
  'ref_compare',
47
+ 'ref_inventory',
48
+ 'cr_inventory',
21
49
  'pr_status',
22
50
  'pr_checks',
23
51
  'merge_plan',
@@ -116,35 +144,53 @@ export async function gitlabFetch(config, parsed, path, options = {}) {
116
144
  });
117
145
  }
118
146
 
119
- const MAX_CHECK_PAGES = 50;
147
+ export async function gitlabFetchWithMeta(config, parsed, path, options = {}) {
148
+ const base = apiBase(config, parsed);
149
+ const token = requireToken();
150
+ const url = `${base}${path}`;
151
+ return fetchJsonWithMeta(url, {
152
+ ...options,
153
+ headers: { ...authHeaders(token), ...(options.headers || {}) },
154
+ });
155
+ }
156
+
157
+ const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
120
158
  const GITLAB_PAGE_SIZE = 100;
121
159
 
122
160
  export async function gitlabFetchPaginated(config, parsed, path) {
123
- const all = [];
124
- for (let page = 1; page <= MAX_CHECK_PAGES; page += 1) {
125
- const separator = path.includes('?') ? '&' : '?';
126
- const body = await gitlabFetch(
127
- config,
128
- parsed,
129
- `${path}${separator}per_page=${GITLAB_PAGE_SIZE}&page=${page}`,
130
- );
131
- const items = Array.isArray(body) ? body : [];
132
- all.push(...items);
133
- if (items.length < GITLAB_PAGE_SIZE) break;
134
- }
135
- return all;
161
+ return paginateCheckStatusPages({
162
+ fetchPage: async ({ page, limit }) => {
163
+ const separator = path.includes('?') ? '&' : '?';
164
+ const body = await gitlabFetch(
165
+ config,
166
+ parsed,
167
+ `${path}${separator}per_page=${limit}&page=${page}`,
168
+ );
169
+ return Array.isArray(body) ? body : [];
170
+ },
171
+ });
136
172
  }
137
173
 
138
174
  export function providerCapabilities() {
175
+ const check_sources = ['commit_statuses', 'pipelines'];
139
176
  return {
140
177
  commands: STRUCTURED_COMMANDS,
141
178
  auth_envs: ['GITLAB_TOKEN'],
142
- check_sources: ['commit_statuses', 'pipelines'],
179
+ check_sources,
143
180
  mergeability_confidence: 'derived',
144
181
  host_binding: 'verified_remote_host',
145
182
  pagination: 'supported',
146
183
  write_support: false,
147
184
  ...forgeIngestCapabilityFacts(),
185
+ ...checkPaginationCapabilityFacts({
186
+ strategy: 'offset_limit',
187
+ pageSizeParam: 'per_page',
188
+ sourceCount: check_sources.length,
189
+ }),
190
+ ...openPullListCapabilityFacts({
191
+ totalCountSource: 'response_header',
192
+ totalCountHeader: 'X-Total',
193
+ }),
148
194
  };
149
195
  }
150
196
 
@@ -260,7 +306,7 @@ export async function prChecks(ctx, opts) {
260
306
  });
261
307
  }
262
308
 
263
- const [statusRecords, pipelineRecords] = await Promise.all([
309
+ const [statusResult, pipelineResult] = await Promise.all([
264
310
  gitlabFetchPaginated(
265
311
  ctx.config,
266
312
  ctx.parsed,
@@ -272,6 +318,8 @@ export async function prChecks(ctx, opts) {
272
318
  `${projectApiPath(ctx.config, 'pipelines')}?sha=${encodeURIComponent(sha)}`,
273
319
  ),
274
320
  ]);
321
+ const statusRecords = statusResult.items;
322
+ const pipelineRecords = pipelineResult.items;
275
323
  const mappedStatuses = statusRecords.map((status) => ({
276
324
  context: sanitizeField(status.name || status.context),
277
325
  state: normalizeStatusState(status.status),
@@ -283,26 +331,180 @@ export async function prChecks(ctx, opts) {
283
331
  description: sanitizeField(pipeline.status),
284
332
  }));
285
333
  const mapped = [...mappedStatuses, ...mappedPipelines];
286
- return { head_sha: sha, check_conclusion: summarizeChecks(mapped), statuses: mapped };
334
+ const checks_truncated = statusResult.truncated || pipelineResult.truncated;
335
+ return {
336
+ head_sha: sha,
337
+ check_conclusion: summarizeChecks(mapped),
338
+ checks_truncated,
339
+ statuses: mapped,
340
+ };
287
341
  }
288
342
 
289
343
  export async function mergePlan(ctx, opts) {
290
344
  const view = await prView(ctx, opts);
291
345
  const checks = await prChecks(ctx, { number: view.pr_number });
292
- const blockers = [];
293
- if (view.mergeability === 'conflicted') blockers.push('merge_conflict');
294
- if (view.state !== 'open') blockers.push('pr_not_open');
295
- if (checks.check_conclusion === 'failure') blockers.push('checks_failed');
296
- if (checks.check_conclusion === 'missing') blockers.push('checks_missing');
297
- if (checks.check_conclusion === 'pending') blockers.push('checks_pending');
346
+ return buildMergePlanBodyFromFacts(ctx, view, checks, opts);
347
+ }
348
+
349
+ const GITLAB_OPEN_PULL_COMPLIANT_MAX =
350
+ DEFAULT_OPEN_PULL_LIST_PAGE_SIZE * MAX_CHECK_STATUS_PAGES;
351
+
352
+ async function probeGitlabOpenPullPageOne(ctx, retainMax, sliceSort) {
353
+ const maxTrusted = GITLAB_OPEN_PULL_COMPLIANT_MAX * 2;
354
+ let path = `${projectApiPath(ctx.config, 'merge_requests')}?state=opened`;
355
+ path = appendSortQuery(path, gitlabOpenPullSortQuery(sliceSort));
356
+ const separator = path.includes('?') ? '&' : '?';
357
+ const requestLimit = Math.min(retainMax, GITLAB_PAGE_SIZE);
358
+ try {
359
+ const { body, headers } = await gitlabFetchWithMeta(
360
+ ctx.config,
361
+ ctx.parsed,
362
+ `${path}${separator}per_page=${requestLimit}&page=1`,
363
+ );
364
+ if (!Array.isArray(body)) return null;
365
+ const totalCount = parseTotalCountHeader(headers, 'X-Total', { maxTrusted });
366
+ if (totalCount == null) return null;
367
+ const listTruncated = totalCount > GITLAB_OPEN_PULL_COMPLIANT_MAX;
368
+ return { body, totalCount, listTruncated, requestLimit };
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+
374
+ function buildGitlabOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, listTruncated) {
375
+ let numbers = orderOpenPullNumbers(body, (mr) => mr?.iid, sliceSort);
376
+ if (numbers.length > retainMax) numbers = numbers.slice(0, retainMax);
377
+ return buildOpenPullListMeta({
378
+ totalCount,
379
+ numbers,
380
+ listTruncated,
381
+ sliceSort,
382
+ });
383
+ }
384
+
385
+ function gitlabProbePaginationOpts(probe, extra = {}) {
386
+ const { body, totalCount, requestLimit } = probe;
298
387
  return {
299
- pr_number: view.pr_number,
300
- mergeability: view.mergeability,
301
- checks_conclusion: checks.check_conclusion,
302
- blockers,
388
+ trustedTotalCount: totalCount,
389
+ seededFirstPage: { items: body, usedLimit: requestLimit },
390
+ ...extra,
303
391
  };
304
392
  }
305
393
 
394
+ async function paginateGitlabOpenPullList(ctx, opts, sliceSort, paginationOpts = {}) {
395
+ const {
396
+ trustedTotalCount = null,
397
+ numberSortFullCollect = false,
398
+ seededFirstPage = null,
399
+ startPage = 1,
400
+ maxPages = MAX_CHECK_STATUS_PAGES,
401
+ suppressFinalPageProbe = false,
402
+ } = paginationOpts;
403
+ const listLimit =
404
+ opts.limit != null && Number.isInteger(Number(opts.limit)) && Number(opts.limit) > 0
405
+ ? Number(opts.limit)
406
+ : null;
407
+ const retainMax =
408
+ listLimit == null &&
409
+ opts.retain_max != null &&
410
+ Number.isInteger(Number(opts.retain_max)) &&
411
+ Number(opts.retain_max) > 0
412
+ ? Number(opts.retain_max)
413
+ : null;
414
+ let path = `${projectApiPath(ctx.config, 'merge_requests')}?state=opened`;
415
+ path = appendSortQuery(path, gitlabOpenPullSortQuery(sliceSort));
416
+ const separator = path.includes('?') ? '&' : '?';
417
+ const effectiveRetainMax = numberSortFullCollect ? null : retainMax;
418
+ const {
419
+ items: all,
420
+ list_truncated: listTruncated,
421
+ entry_count: entryCount,
422
+ walked_count: walkedCount,
423
+ } = await paginateOffsetListPages({
424
+ pageSize: GITLAB_PAGE_SIZE,
425
+ listLimit,
426
+ retainMax: effectiveRetainMax,
427
+ trustedEntryCount: trustedTotalCount,
428
+ seededFirstPage,
429
+ startPage,
430
+ maxPages,
431
+ suppressFinalPageProbe,
432
+ ...(listLimit != null ? { maxPagesTruncatesWithLimit: true } : {}),
433
+ fetchPage: async ({ page, limit }) => {
434
+ const body = await gitlabFetch(
435
+ ctx.config,
436
+ ctx.parsed,
437
+ `${path}${separator}per_page=${limit}&page=${page}`,
438
+ );
439
+ return Array.isArray(body) ? body : [];
440
+ },
441
+ });
442
+ let numbers = orderOpenPullNumbers(all, (mr) => mr?.iid, sliceSort);
443
+ const outputCap = listLimit ?? retainMax;
444
+ if (outputCap != null && numbers.length > outputCap) {
445
+ numbers = numbers.slice(0, outputCap);
446
+ }
447
+ return {
448
+ numbers,
449
+ list_truncated: resolveListTruncatedWithTrustedTotal({
450
+ listTruncated,
451
+ trustedTotalCount,
452
+ walkedCount,
453
+ fullCollect: numberSortFullCollect,
454
+ }),
455
+ ...(entryCount != null ? { entry_count: entryCount } : {}),
456
+ slice_sort: sliceSort,
457
+ };
458
+ }
459
+
460
+ export async function listOpenPullsWithMeta(ctx, opts = {}) {
461
+ apiBase(ctx.config, ctx.parsed);
462
+ requireToken();
463
+ const sliceSort = normalizeCrInventorySort(opts.sort ?? DEFAULT_CR_INVENTORY_SLICE_SORT);
464
+ if (!isCrInventoryFastPathEligible(opts)) {
465
+ return paginateGitlabOpenPullList(ctx, opts, sliceSort);
466
+ }
467
+
468
+ const retainMax = Number(opts.retain_max);
469
+ const probe = await probeGitlabOpenPullPageOne(ctx, retainMax, sliceSort);
470
+ if (!probe) {
471
+ return paginateGitlabOpenPullList(ctx, opts, sliceSort);
472
+ }
473
+
474
+ const { body, totalCount, listTruncated, requestLimit } = probe;
475
+
476
+ if (listTruncated) {
477
+ if (body.length === 0) {
478
+ return paginateGitlabOpenPullList(ctx, opts, sliceSort, gitlabProbePaginationOpts(probe));
479
+ }
480
+ return buildGitlabOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, true);
481
+ }
482
+
483
+ if (
484
+ isNumberSortFastPathEligible(totalCount, retainMax, sliceSort) &&
485
+ validateFastPathPageLength(totalCount, requestLimit, body.length)
486
+ ) {
487
+ return buildGitlabOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, false);
488
+ }
489
+
490
+ const numberSortFullCollect = isNumberSortFullCollectRequired(totalCount, retainMax, sliceSort);
491
+ return paginateGitlabOpenPullList(
492
+ ctx,
493
+ opts,
494
+ sliceSort,
495
+ gitlabProbePaginationOpts(probe, { numberSortFullCollect }),
496
+ );
497
+ }
498
+
499
+ export async function listOpenPulls(ctx, opts = {}) {
500
+ const meta = await listOpenPullsWithMeta(ctx, opts);
501
+ return meta.numbers;
502
+ }
503
+
504
+ export async function crInventorySlice(ctx, opts = {}) {
505
+ return crInventory(ctx, { listOpenPulls, listOpenPullsWithMeta, prView, prChecks }, opts);
506
+ }
507
+
306
508
  export async function syncPlan(ctx, remoteName = 'origin') {
307
509
  assertGitRemote(remoteName, 'remote');
308
510
  apiBase(ctx.config, ctx.parsed);
@@ -336,6 +538,9 @@ export const provider = {
336
538
  providerCapabilities,
337
539
  repoStatus,
338
540
  refsCompare,
541
+ refsInventory,
542
+ listOpenPulls,
543
+ crInventory: crInventorySlice,
339
544
  prView,
340
545
  prChecks,
341
546
  mergePlan,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remogram/provider-gitlab-api",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "GitLab API 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.2"
25
+ "@remogram/core": "0.1.0-beta.4"
26
26
  }
27
27
  }