@remogram/provider-gitea-api 0.1.0-beta.6 → 0.1.0-beta.9

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.
@@ -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
@@ -12,11 +12,15 @@ import {
12
12
  crInventory,
13
13
  buildMergePlanFromProviderFacts,
14
14
  buildChangeRequestOpenedBody,
15
+ buildIssueOpenedBody,
16
+ parseIssueOpenArgs,
15
17
  buildCommitStatusSetBody,
18
+ idempotencyPacketFields,
16
19
  parseStatusSetArgs,
17
20
  normalizeStatusSetState,
18
21
  buildProviderIdentityFromGiteaUser,
19
22
  buildBranchProtectionFromGiteaProtection,
23
+ buildPrChecksBody,
20
24
  buildCrFilesBody,
21
25
  buildCrFilesFromGiteaFiles,
22
26
  buildCrCommentsBody,
@@ -27,6 +31,8 @@ import {
27
31
  parseSinceObservedAt,
28
32
  ERROR_CODES,
29
33
  forgeError,
34
+ assertExpectedSha,
35
+ LIVE_REACHABILITY_TIMEOUT_MS,
30
36
  forgeIngestCapabilityFacts,
31
37
  checkPaginationCapabilityFacts,
32
38
  idempotencyScanCapabilityFacts,
@@ -57,7 +63,14 @@ import {
57
63
  giteaOpenPullSortQuery,
58
64
  appendSortQuery,
59
65
  assertWriteCommandConfigured,
66
+ fetchWithTimeout,
67
+ readStreamCapped,
68
+ getEffectiveIngestMaxBytes,
60
69
  } from '@remogram/core';
70
+ import {
71
+ resolveBranchProtection,
72
+ setBranchProtectionImpl,
73
+ } from './branch-protection-internal.js';
61
74
  const PUBLIC_GITEA_HOST = 'gitea.com';
62
75
  const PUBLIC_GITEA_API = 'https://gitea.com/api/v1';
63
76
  const AUTH_CAPABILITIES = [
@@ -80,11 +93,13 @@ const AUTH_CAPABILITIES = [
80
93
 
81
94
  const STRUCTURED_COMMANDS = apiProviderCommands({
82
95
  writeCommandsImplemented: true,
96
+ issueOpenImplemented: true,
83
97
  statusSetImplemented: true,
84
98
  branchProtectionImplemented: true,
85
99
  crFilesImplemented: true,
86
100
  crCommentsImplemented: true,
87
101
  forgeChangesImplemented: true,
102
+ mergeExecuteImplemented: true,
88
103
  });
89
104
 
90
105
  export function giteaToken() {
@@ -165,13 +180,36 @@ export function authHeaders(token) {
165
180
  }
166
181
 
167
182
  export function repoApiPath(config, ...segments) {
168
- const owner = encodeURIComponent(config.owner);
169
- const repo = encodeURIComponent(config.repo);
170
- const base = `/repos/${owner}/${repo}`;
183
+ return repoApiPathFor(config.owner, config.repo, ...segments);
184
+ }
185
+
186
+ export function repoApiPathFor(owner, repo, ...segments) {
187
+ const encodedOwner = encodeURIComponent(owner);
188
+ const encodedRepo = encodeURIComponent(repo);
189
+ const base = `/repos/${encodedOwner}/${encodedRepo}`;
171
190
  if (!segments.length) return base;
172
191
  return `${base}/${segments.map((s) => encodeURIComponent(String(s))).join('/')}`;
173
192
  }
174
193
 
194
+ export function forgeSourceRepoIdFromPull(config, pr) {
195
+ const headOwner = sanitizeField(pr.head?.repo?.owner?.login ?? pr.head?.repo?.owner?.name);
196
+ const headRepo = sanitizeField(pr.head?.repo?.name);
197
+ if (!headOwner || !headRepo) return null;
198
+ const configOwner = String(config.owner ?? '').toLowerCase();
199
+ const configRepo = String(config.repo ?? '').toLowerCase();
200
+ if (headOwner.toLowerCase() === configOwner && headRepo.toLowerCase() === configRepo) return null;
201
+ return `${headOwner}/${headRepo}`;
202
+ }
203
+
204
+ export function isGiteaHeadOutOfDate409(err) {
205
+ const status = err.status ?? err.forgeError?.status ?? null;
206
+ if (status !== 409) return false;
207
+ const message = err.forgeError?.message ?? err.message ?? '';
208
+ if (/head out of date/i.test(message)) return true;
209
+ if (/sha mismatch/i.test(message)) return true;
210
+ return false;
211
+ }
212
+
175
213
  export async function giteaFetch(config, parsed, path, options = {}) {
176
214
  const token = requireToken();
177
215
  const url = `${apiBase(config, parsed)}${path}`;
@@ -190,6 +228,22 @@ export async function giteaFetchWithMeta(config, parsed, path, options = {}) {
190
228
  });
191
229
  }
192
230
 
231
+ export async function apiReachability(ctx) {
232
+ if (!giteaToken()) {
233
+ throw Object.assign(new Error('GITEA_TOKEN not set'), {
234
+ forgeError: forgeError(ERROR_CODES.UNAUTHENTICATED_PROVIDER, 'GITEA_TOKEN not set'),
235
+ });
236
+ }
237
+ const token = requireToken();
238
+ const url = `${apiBase(ctx.config, ctx.parsed)}${repoApiPath(ctx.config)}`;
239
+ await fetchJson(
240
+ url,
241
+ { headers: authHeaders(token) },
242
+ LIVE_REACHABILITY_TIMEOUT_MS,
243
+ );
244
+ return { repo_accessible: true };
245
+ }
246
+
193
247
  const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
194
248
  const GITEA_PAGE_SIZE = DEFAULT_OPEN_PULL_LIST_PAGE_SIZE;
195
249
 
@@ -256,11 +310,14 @@ export function dedupeGiteaStatusRecords(records) {
256
310
  return Array.from(latestByContext.values());
257
311
  }
258
312
 
259
- export function mapGiteaCommitStatuses(records) {
313
+ export function mapGiteaCommitStatuses(records, { headSha } = {}) {
260
314
  return dedupeGiteaStatusRecords(records).map((s) => ({
261
315
  context: sanitizeField(s.context),
262
316
  state: normalizeGiteaStatusState(s.status ?? s.state),
263
317
  description: sanitizeField(s.description),
318
+ ...(s.target_url ? { target_url: sanitizeField(s.target_url) } : {}),
319
+ ...(headSha ? { sha: headSha } : {}),
320
+ source: 'commit_status',
264
321
  }));
265
322
  }
266
323
 
@@ -294,12 +351,60 @@ export async function whoami(ctx) {
294
351
 
295
352
  export async function branchProtection(ctx, { branchRef }) {
296
353
  requireToken();
297
- const protection = await giteaFetch(
354
+ try {
355
+ const protection = await giteaFetch(
356
+ ctx.config,
357
+ ctx.parsed,
358
+ repoApiPath(ctx.config, 'branch_protections', branchRef),
359
+ );
360
+ return buildBranchProtectionFromGiteaProtection(branchRef, protection);
361
+ } catch (err) {
362
+ if (err?.status === 404) {
363
+ return buildBranchProtectionFromGiteaProtection(branchRef, null);
364
+ }
365
+ throw err;
366
+ }
367
+ }
368
+
369
+ export async function branchHeadSha(ctx, branchRef, { repoId } = {}) {
370
+ requireToken();
371
+ assertGitRef(branchRef, 'head_ref');
372
+ let owner = ctx.config.owner;
373
+ let repo = ctx.config.repo;
374
+ if (repoId) {
375
+ const parts = String(repoId).split('/');
376
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
377
+ throw Object.assign(new Error('Invalid repoId'), {
378
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'repoId must be owner/repo'),
379
+ });
380
+ }
381
+ owner = parts[0];
382
+ repo = parts[1];
383
+ }
384
+ const branch = await giteaFetch(
298
385
  ctx.config,
299
386
  ctx.parsed,
300
- repoApiPath(ctx.config, 'branch_protections', branchRef),
387
+ repoApiPathFor(owner, repo, 'branches', branchRef),
301
388
  );
302
- return buildBranchProtectionFromGiteaProtection(branchRef, protection);
389
+ const rawSha = sanitizeField(branch?.commit?.id);
390
+ if (!rawSha) {
391
+ throw Object.assign(new Error('Branch commit id missing'), {
392
+ forgeError: forgeError(
393
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
394
+ 'Gitea branch response missing commit id',
395
+ ),
396
+ });
397
+ }
398
+ try {
399
+ return assertExpectedSha(rawSha, 'branch commit id');
400
+ } catch (err) {
401
+ throw Object.assign(new Error('Branch commit id invalid'), {
402
+ forgeError: forgeError(
403
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
404
+ sanitizeField(err.invalidArgs) || 'Gitea branch response commit id is not a valid SHA',
405
+ ),
406
+ });
407
+ }
303
408
  }
304
409
 
305
410
  export async function crFiles(ctx, { number }) {
@@ -485,7 +590,7 @@ export function providerCapabilities() {
485
590
  host_binding: 'verified_remote_host',
486
591
  pagination: 'supported',
487
592
  write_support: true,
488
- write_commands: ['cr_open', 'status_set'],
593
+ write_commands: ['cr_open', 'status_set', 'merge', 'issue_open'],
489
594
  ...forgeIngestCapabilityFacts(),
490
595
  ...checkPaginationCapabilityFacts({
491
596
  strategy: 'offset_limit',
@@ -520,10 +625,10 @@ export async function refsCompare(ctx, baseRef, headRef) {
520
625
  }
521
626
  const counts = gitAheadBehind(ctx.cwd, baseSha, headSha);
522
627
  return {
523
- base_ref: sanitizeField(baseRef),
524
- base_sha: baseSha,
525
- head_ref: sanitizeField(headRef),
526
- head_sha: headSha,
628
+ compare_base_ref: sanitizeField(baseRef),
629
+ compare_base_sha: baseSha,
630
+ compare_head_ref: sanitizeField(headRef),
631
+ compare_head_sha: headSha,
527
632
  ...counts,
528
633
  };
529
634
  }
@@ -534,7 +639,63 @@ export async function getPull(ctx, { number }) {
534
639
  forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR lookup'),
535
640
  });
536
641
  }
537
- return giteaFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'pulls', number));
642
+ return giteaFetchPullForView(ctx.config, ctx.parsed, number);
643
+ }
644
+
645
+ /** Raw read bound for pull view before stripping bulky fields (#478). */
646
+ const GITEA_PULL_VIEW_RAW_READ_MAX = 256 * 1024;
647
+
648
+ function stripGiteaPullBulkJsonFields(raw) {
649
+ return String(raw)
650
+ .replace(/"body"\s*:\s*"(?:\\.|[^"\\])*"/g, '"body":""')
651
+ .replace(/"body_html"\s*:\s*"(?:\\.|[^"\\])*"/g, '"body_html":""')
652
+ .replace(/"diff"\s*:\s*"(?:\\.|[^"\\])*"/g, '"diff":""')
653
+ .replace(/"patch"\s*:\s*"(?:\\.|[^"\\])*"/g, '"patch":""');
654
+ }
655
+
656
+ async function giteaFetchPullForView(config, parsed, number) {
657
+ const token = requireToken();
658
+ const url = `${apiBase(config, parsed)}${repoApiPath(config, 'pulls', number)}`;
659
+ const res = await fetchWithTimeout(url, { headers: authHeaders(token) });
660
+ if (res.status >= 300 && res.status < 400) {
661
+ const message = 'HTTP redirect rejected';
662
+ throw Object.assign(new Error(message), {
663
+ forgeError: forgeError(ERROR_CODES.API_ERROR, message, res.status),
664
+ status: res.status,
665
+ });
666
+ }
667
+ const capped = await readStreamCapped(res.body, GITEA_PULL_VIEW_RAW_READ_MAX);
668
+ if (capped.truncated) {
669
+ throw Object.assign(new Error('Provider output exceeded cap'), {
670
+ forgeError: forgeError(ERROR_CODES.OVERSIZED_RAW_OUTPUT, 'Provider response exceeded byte cap'),
671
+ status: res.status,
672
+ });
673
+ }
674
+ const stripped = stripGiteaPullBulkJsonFields(capped.text);
675
+ if (Buffer.byteLength(stripped, 'utf8') > getEffectiveIngestMaxBytes().bytes) {
676
+ throw Object.assign(new Error('Provider output exceeded cap after projection'), {
677
+ forgeError: forgeError(ERROR_CODES.OVERSIZED_RAW_OUTPUT, 'Provider response exceeded byte cap'),
678
+ status: res.status,
679
+ });
680
+ }
681
+ let body;
682
+ try {
683
+ body = stripped ? JSON.parse(stripped) : null;
684
+ } catch {
685
+ throw Object.assign(new Error('Unparseable JSON from provider'), {
686
+ forgeError: forgeError(ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT, 'Provider returned invalid JSON'),
687
+ status: res.status,
688
+ });
689
+ }
690
+ if (!res.ok) {
691
+ const raw = body?.message || body?.error || res.statusText || 'API error';
692
+ const message = sanitizeField(raw) || 'API error';
693
+ throw Object.assign(new Error(message), {
694
+ forgeError: forgeError(ERROR_CODES.API_ERROR, message, res.status),
695
+ status: res.status,
696
+ });
697
+ }
698
+ return body;
538
699
  }
539
700
 
540
701
  /** Paginated open-pull scan for idempotent cr open; fail-closed when scan cap prevents proof of absence. */
@@ -585,6 +746,68 @@ export async function findOpenPullByHeadBase(ctx, head, base) {
585
746
  return null;
586
747
  }
587
748
 
749
+ function issueOpenIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
750
+ return forgeError(
751
+ ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
752
+ 'Cannot prove no open issue exists for title within scan limit; retry or open manually',
753
+ null,
754
+ {
755
+ idempotency_scan: {
756
+ pages: pagesScanned,
757
+ max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
758
+ page_size: pageSizeUsed,
759
+ },
760
+ },
761
+ );
762
+ }
763
+
764
+ /** Paginated open-issue scan for idempotent issue open; fail-closed when scan cap prevents proof of absence. */
765
+ export async function findOpenIssueByTitle(ctx, title) {
766
+ requireToken();
767
+ const path = `${repoApiPath(ctx.config, 'issues')}?state=open`;
768
+ const pageSep = path.includes('?') ? '&' : '?';
769
+ let activeLimit = GITEA_PAGE_SIZE;
770
+
771
+ for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
772
+ const { items, usedLimit } = await fetchPageWithIngestBackoff(
773
+ async ({ page: pageNum, limit }) => {
774
+ const body = await giteaFetch(
775
+ ctx.config,
776
+ ctx.parsed,
777
+ `${path}${pageSep}limit=${limit}&page=${pageNum}`,
778
+ );
779
+ if (!Array.isArray(body)) {
780
+ throw Object.assign(new Error('Provider returned non-array open issue list'), {
781
+ forgeError: forgeError(
782
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
783
+ 'Provider returned non-array open issue list',
784
+ ),
785
+ });
786
+ }
787
+ return body;
788
+ },
789
+ page,
790
+ activeLimit,
791
+ );
792
+ activeLimit = usedLimit;
793
+
794
+ const match =
795
+ items.find(
796
+ (issue) =>
797
+ String(issue?.state ?? '').toLowerCase() === 'open' &&
798
+ sanitizeField(issue?.title ?? '') === title,
799
+ ) ?? null;
800
+ if (match) return match;
801
+ if (items.length < usedLimit) return null;
802
+ if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
803
+ throw Object.assign(new Error('Open issue idempotency scan incomplete'), {
804
+ forgeError: issueOpenIdempotencyScanIncompleteError(page, usedLimit),
805
+ });
806
+ }
807
+ }
808
+ return null;
809
+ }
810
+
588
811
  function statusSetIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
589
812
  return forgeError(
590
813
  ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
@@ -650,12 +873,18 @@ export async function findCommitStatusByContext(ctx, sha, context) {
650
873
 
651
874
  export async function statusSet(ctx, args) {
652
875
  assertWriteCommandConfigured(ctx.config, 'status_set');
653
- const parsed = parseStatusSetArgs(args);
876
+ const { idempotencyFingerprint = null, ...rest } = args;
877
+ const parsed = parseStatusSetArgs(rest);
654
878
  const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
655
879
  if (existing) {
656
880
  const existingState = normalizeStatusSetState(existing.status ?? existing.state);
657
881
  if (existingState === parsed.state) {
658
- return buildCommitStatusSetBody(existing, parsed, { reusedExisting: true });
882
+ return buildCommitStatusSetBody(existing, parsed, {
883
+ reusedExisting: true,
884
+ idempotencyFields: idempotencyFingerprint
885
+ ? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: true })
886
+ : null,
887
+ });
659
888
  }
660
889
  }
661
890
  const payload = {
@@ -674,10 +903,48 @@ export async function statusSet(ctx, args) {
674
903
  body: JSON.stringify(payload),
675
904
  },
676
905
  );
677
- return buildCommitStatusSetBody(response, parsed);
906
+ return buildCommitStatusSetBody(response, parsed, {
907
+ idempotencyFields: idempotencyFingerprint
908
+ ? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: false })
909
+ : null,
910
+ } );
911
+ }
912
+
913
+ export async function issueOpen(ctx, { title, body: issueBody, idempotencyFingerprint = null }) {
914
+ assertWriteCommandConfigured(ctx.config, 'issue_open');
915
+ const parsed = parseIssueOpenArgs({ title, body: issueBody });
916
+ const existing = await findOpenIssueByTitle(ctx, parsed.title);
917
+ if (existing) {
918
+ return buildIssueOpenedBody(
919
+ existing,
920
+ { title: parsed.title },
921
+ {
922
+ reusedExisting: true,
923
+ idempotencyFields: idempotencyFingerprint
924
+ ? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: true })
925
+ : null,
926
+ },
927
+ );
928
+ }
929
+ const payload = { title: parsed.title };
930
+ if (parsed.body != null) payload.body = parsed.body;
931
+ const issue = await giteaFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'issues'), {
932
+ method: 'POST',
933
+ headers: { 'Content-Type': 'application/json' },
934
+ body: JSON.stringify(payload),
935
+ });
936
+ return buildIssueOpenedBody(
937
+ issue,
938
+ { title: parsed.title },
939
+ {
940
+ idempotencyFields: idempotencyFingerprint
941
+ ? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: false })
942
+ : null,
943
+ },
944
+ );
678
945
  }
679
946
 
680
- export async function crOpen(ctx, { head, base, title, body: prBody }) {
947
+ export async function crOpen(ctx, { head, base, title, body: prBody, idempotencyFingerprint = null }) {
681
948
  assertWriteCommandConfigured(ctx.config, 'cr_open');
682
949
  assertGitRef(head, 'head');
683
950
  assertGitRef(base, 'base');
@@ -696,14 +963,83 @@ export async function crOpen(ctx, { head, base, title, body: prBody }) {
696
963
  }
697
964
  const existing = await findOpenPullByHeadBase(ctx, payload.head, payload.base);
698
965
  if (existing) {
699
- return buildChangeRequestOpenedBody(existing, { head, base, title }, { reusedExisting: true });
966
+ return buildChangeRequestOpenedBody(
967
+ existing,
968
+ { head, base, title },
969
+ {
970
+ reusedExisting: true,
971
+ idempotencyFields: idempotencyFingerprint
972
+ ? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: true })
973
+ : null,
974
+ },
975
+ );
700
976
  }
701
977
  const pull = await giteaFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'pulls'), {
702
978
  method: 'POST',
703
979
  headers: { 'Content-Type': 'application/json' },
704
980
  body: JSON.stringify(payload),
705
981
  });
706
- return buildChangeRequestOpenedBody(pull, { head, base, title });
982
+ return buildChangeRequestOpenedBody(
983
+ pull,
984
+ { head, base, title },
985
+ {
986
+ idempotencyFields: idempotencyFingerprint
987
+ ? idempotencyPacketFields(idempotencyFingerprint, { reusedExisting: false })
988
+ : null,
989
+ },
990
+ );
991
+ }
992
+
993
+ export async function mergeExecute(ctx, { number, method = 'merge', expectedHeadSha }) {
994
+ assertWriteCommandConfigured(ctx.config, 'merge');
995
+ if (method !== 'merge') {
996
+ throw Object.assign(new Error('Unsupported merge method'), {
997
+ forgeError: forgeError(
998
+ ERROR_CODES.INVALID_ARGS,
999
+ 'Only --method merge is supported in v1',
1000
+ ),
1001
+ });
1002
+ }
1003
+ const pullIndex = Number(number);
1004
+ if (!Number.isInteger(pullIndex) || pullIndex <= 0) {
1005
+ throw Object.assign(new Error('Invalid PR number'), {
1006
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'PR number must be a positive integer'),
1007
+ });
1008
+ }
1009
+ const headCommitId = assertExpectedSha(expectedHeadSha, 'expectedHeadSha');
1010
+ let result;
1011
+ try {
1012
+ result = await giteaFetch(
1013
+ ctx.config,
1014
+ ctx.parsed,
1015
+ repoApiPath(ctx.config, 'pulls', String(pullIndex), 'merge'),
1016
+ {
1017
+ method: 'POST',
1018
+ headers: { 'Content-Type': 'application/json' },
1019
+ body: JSON.stringify({ Do: 'merge', head_commit_id: headCommitId }),
1020
+ },
1021
+ );
1022
+ } catch (err) {
1023
+ const status = err.status ?? err.forgeError?.status ?? null;
1024
+ const message = err.forgeError?.message ?? err.message ?? '';
1025
+ if (isGiteaHeadOutOfDate409(err)) {
1026
+ throw Object.assign(new Error(message), {
1027
+ status,
1028
+ mergeBlockedBlockers: ['head_ref_moved'],
1029
+ forgeError: forgeError(
1030
+ ERROR_CODES.MERGE_BLOCKED,
1031
+ sanitizeField(message) || 'Head branch out of date at merge POST',
1032
+ status,
1033
+ ),
1034
+ });
1035
+ }
1036
+ throw err;
1037
+ }
1038
+ return {
1039
+ commit_sha: sanitizeField(result?.sha ?? result?.merge_commit_sha ?? null),
1040
+ provider_status: 200,
1041
+ base_sha: sanitizeField(result?.base_sha ?? null),
1042
+ };
707
1043
  }
708
1044
 
709
1045
  const GITEA_OPEN_PULL_COMPLIANT_MAX =
@@ -916,22 +1252,26 @@ function mergeability(pr) {
916
1252
 
917
1253
  export async function prView(ctx, opts) {
918
1254
  const pr = await getPull(ctx, opts);
919
- return {
1255
+ const body = {
920
1256
  pr_number: pr.number,
921
1257
  url: sanitizeUrl(pr.html_url ?? pr.url),
922
1258
  title: sanitizeField(pr.title),
923
1259
  state: normalizeGiteaPrState(pr.state),
924
- base_ref: sanitizeField(pr.base?.ref),
925
- base_sha: sanitizeField(pr.base?.sha),
926
- head_ref: sanitizeField(pr.head?.ref),
927
- head_sha: sanitizeField(pr.head?.sha),
1260
+ forge_target_branch_ref: sanitizeField(pr.base?.ref),
1261
+ forge_target_sha: sanitizeField(pr.base?.sha),
1262
+ forge_source_branch_ref: sanitizeField(pr.head?.ref),
1263
+ forge_source_sha: sanitizeField(pr.head?.sha),
928
1264
  mergeability: mergeability(pr),
929
1265
  };
1266
+ const forgeSourceRepoId = forgeSourceRepoIdFromPull(ctx.config, pr);
1267
+ if (forgeSourceRepoId) body.forge_source_repo_id = forgeSourceRepoId;
1268
+ return body;
930
1269
  }
931
1270
 
932
1271
  export async function prChecks(ctx, opts) {
933
1272
  requireToken();
934
1273
  let sha;
1274
+ let requiredContexts = [];
935
1275
  if (opts.ref) {
936
1276
  assertGitRef(opts.ref, 'ref');
937
1277
  sha = gitRevParse(ctx.cwd, opts.ref);
@@ -943,6 +1283,15 @@ export async function prChecks(ctx, opts) {
943
1283
  } else {
944
1284
  const pr = await getPull(ctx, opts);
945
1285
  sha = pr.head?.sha;
1286
+ const targetBranch = pr.base?.ref;
1287
+ if (targetBranch) {
1288
+ try {
1289
+ const protection = await resolveBranchProtection(ctx, { branchRef: targetBranch });
1290
+ requiredContexts = protection.required_status_contexts ?? [];
1291
+ } catch (err) {
1292
+ if (err?.status !== 404) throw err;
1293
+ }
1294
+ }
946
1295
  }
947
1296
  if (!sha) {
948
1297
  throw Object.assign(new Error('No SHA'), {
@@ -954,14 +1303,14 @@ export async function prChecks(ctx, opts) {
954
1303
  ctx.parsed,
955
1304
  repoApiPath(ctx.config, 'commits', sha, 'statuses'),
956
1305
  );
957
- const mapped = mapGiteaCommitStatuses(statusRecords);
958
- const conclusion = summarizeChecks(mapped);
959
- return {
960
- head_sha: sha,
961
- check_conclusion: conclusion,
1306
+ const mapped = mapGiteaCommitStatuses(statusRecords, { headSha: sha });
1307
+ return buildPrChecksBody({
1308
+ forge_source_sha: sha,
1309
+ check_conclusion: summarizeChecks(mapped),
962
1310
  checks_truncated,
963
1311
  statuses: mapped,
964
- };
1312
+ required_contexts: requiredContexts,
1313
+ });
965
1314
  }
966
1315
 
967
1316
  export function summarizeChecks(statuses) {
@@ -1006,6 +1355,7 @@ export async function syncPlan(ctx, remoteName = 'origin') {
1006
1355
  export const provider = {
1007
1356
  id: 'gitea-api',
1008
1357
  providerCapabilities,
1358
+ apiReachability,
1009
1359
  repoStatus,
1010
1360
  refsCompare,
1011
1361
  refsInventory,
@@ -1016,10 +1366,15 @@ export const provider = {
1016
1366
  mergePlan,
1017
1367
  syncPlan,
1018
1368
  crOpen,
1369
+ issueOpen,
1370
+ mergeExecute,
1019
1371
  statusSet,
1020
1372
  whoami,
1021
1373
  branchProtection,
1374
+ branchHeadSha,
1022
1375
  crFiles,
1023
1376
  crComments,
1024
1377
  forgeChanges,
1025
1378
  };
1379
+
1380
+ 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.6",
3
+ "version": "0.1.0-beta.9",
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.6"
26
+ "@remogram/core": "0.1.0-beta.9"
26
27
  }
27
28
  }