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

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 +137 -14
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  parseSinceObservedAt,
28
28
  ERROR_CODES,
29
29
  forgeError,
30
+ assertExpectedSha,
30
31
  forgeIngestCapabilityFacts,
31
32
  checkPaginationCapabilityFacts,
32
33
  idempotencyScanCapabilityFacts,
@@ -85,6 +86,7 @@ const STRUCTURED_COMMANDS = apiProviderCommands({
85
86
  crFilesImplemented: true,
86
87
  crCommentsImplemented: true,
87
88
  forgeChangesImplemented: true,
89
+ mergeExecuteImplemented: true,
88
90
  });
89
91
 
90
92
  export function giteaToken() {
@@ -165,13 +167,36 @@ export function authHeaders(token) {
165
167
  }
166
168
 
167
169
  export function repoApiPath(config, ...segments) {
168
- const owner = encodeURIComponent(config.owner);
169
- const repo = encodeURIComponent(config.repo);
170
- const base = `/repos/${owner}/${repo}`;
170
+ return repoApiPathFor(config.owner, config.repo, ...segments);
171
+ }
172
+
173
+ export function repoApiPathFor(owner, repo, ...segments) {
174
+ const encodedOwner = encodeURIComponent(owner);
175
+ const encodedRepo = encodeURIComponent(repo);
176
+ const base = `/repos/${encodedOwner}/${encodedRepo}`;
171
177
  if (!segments.length) return base;
172
178
  return `${base}/${segments.map((s) => encodeURIComponent(String(s))).join('/')}`;
173
179
  }
174
180
 
181
+ export function forgeSourceRepoIdFromPull(config, pr) {
182
+ const headOwner = sanitizeField(pr.head?.repo?.owner?.login ?? pr.head?.repo?.owner?.name);
183
+ const headRepo = sanitizeField(pr.head?.repo?.name);
184
+ if (!headOwner || !headRepo) return null;
185
+ const configOwner = String(config.owner ?? '').toLowerCase();
186
+ const configRepo = String(config.repo ?? '').toLowerCase();
187
+ if (headOwner.toLowerCase() === configOwner && headRepo.toLowerCase() === configRepo) return null;
188
+ return `${headOwner}/${headRepo}`;
189
+ }
190
+
191
+ export function isGiteaHeadOutOfDate409(err) {
192
+ const status = err.status ?? err.forgeError?.status ?? null;
193
+ if (status !== 409) return false;
194
+ const message = err.forgeError?.message ?? err.message ?? '';
195
+ if (/head out of date/i.test(message)) return true;
196
+ if (/sha mismatch/i.test(message)) return true;
197
+ return false;
198
+ }
199
+
175
200
  export async function giteaFetch(config, parsed, path, options = {}) {
176
201
  const token = requireToken();
177
202
  const url = `${apiBase(config, parsed)}${path}`;
@@ -302,6 +327,47 @@ export async function branchProtection(ctx, { branchRef }) {
302
327
  return buildBranchProtectionFromGiteaProtection(branchRef, protection);
303
328
  }
304
329
 
330
+ export async function branchHeadSha(ctx, branchRef, { repoId } = {}) {
331
+ requireToken();
332
+ assertGitRef(branchRef, 'head_ref');
333
+ let owner = ctx.config.owner;
334
+ let repo = ctx.config.repo;
335
+ if (repoId) {
336
+ const parts = String(repoId).split('/');
337
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
338
+ throw Object.assign(new Error('Invalid repoId'), {
339
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'repoId must be owner/repo'),
340
+ });
341
+ }
342
+ owner = parts[0];
343
+ repo = parts[1];
344
+ }
345
+ const branch = await giteaFetch(
346
+ ctx.config,
347
+ ctx.parsed,
348
+ repoApiPathFor(owner, repo, 'branches', branchRef),
349
+ );
350
+ const rawSha = sanitizeField(branch?.commit?.id);
351
+ if (!rawSha) {
352
+ throw Object.assign(new Error('Branch commit id missing'), {
353
+ forgeError: forgeError(
354
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
355
+ 'Gitea branch response missing commit id',
356
+ ),
357
+ });
358
+ }
359
+ try {
360
+ return assertExpectedSha(rawSha, 'branch commit id');
361
+ } catch (err) {
362
+ throw Object.assign(new Error('Branch commit id invalid'), {
363
+ forgeError: forgeError(
364
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
365
+ sanitizeField(err.invalidArgs) || 'Gitea branch response commit id is not a valid SHA',
366
+ ),
367
+ });
368
+ }
369
+ }
370
+
305
371
  export async function crFiles(ctx, { number }) {
306
372
  requireToken();
307
373
  if (number == null) {
@@ -485,7 +551,7 @@ export function providerCapabilities() {
485
551
  host_binding: 'verified_remote_host',
486
552
  pagination: 'supported',
487
553
  write_support: true,
488
- write_commands: ['cr_open', 'status_set'],
554
+ write_commands: ['cr_open', 'status_set', 'merge'],
489
555
  ...forgeIngestCapabilityFacts(),
490
556
  ...checkPaginationCapabilityFacts({
491
557
  strategy: 'offset_limit',
@@ -520,10 +586,10 @@ export async function refsCompare(ctx, baseRef, headRef) {
520
586
  }
521
587
  const counts = gitAheadBehind(ctx.cwd, baseSha, headSha);
522
588
  return {
523
- base_ref: sanitizeField(baseRef),
524
- base_sha: baseSha,
525
- head_ref: sanitizeField(headRef),
526
- head_sha: headSha,
589
+ compare_base_ref: sanitizeField(baseRef),
590
+ compare_base_sha: baseSha,
591
+ compare_head_ref: sanitizeField(headRef),
592
+ compare_head_sha: headSha,
527
593
  ...counts,
528
594
  };
529
595
  }
@@ -706,6 +772,58 @@ export async function crOpen(ctx, { head, base, title, body: prBody }) {
706
772
  return buildChangeRequestOpenedBody(pull, { head, base, title });
707
773
  }
708
774
 
775
+ export async function mergeExecute(ctx, { number, method = 'merge', expectedHeadSha }) {
776
+ assertWriteCommandConfigured(ctx.config, 'merge');
777
+ if (method !== 'merge') {
778
+ throw Object.assign(new Error('Unsupported merge method'), {
779
+ forgeError: forgeError(
780
+ ERROR_CODES.INVALID_ARGS,
781
+ 'Only --method merge is supported in v1',
782
+ ),
783
+ });
784
+ }
785
+ const pullIndex = Number(number);
786
+ if (!Number.isInteger(pullIndex) || pullIndex <= 0) {
787
+ throw Object.assign(new Error('Invalid PR number'), {
788
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'PR number must be a positive integer'),
789
+ });
790
+ }
791
+ const headCommitId = assertExpectedSha(expectedHeadSha, 'expectedHeadSha');
792
+ let result;
793
+ try {
794
+ result = await giteaFetch(
795
+ ctx.config,
796
+ ctx.parsed,
797
+ repoApiPath(ctx.config, 'pulls', String(pullIndex), 'merge'),
798
+ {
799
+ method: 'POST',
800
+ headers: { 'Content-Type': 'application/json' },
801
+ body: JSON.stringify({ Do: 'merge', head_commit_id: headCommitId }),
802
+ },
803
+ );
804
+ } catch (err) {
805
+ const status = err.status ?? err.forgeError?.status ?? null;
806
+ const message = err.forgeError?.message ?? err.message ?? '';
807
+ if (isGiteaHeadOutOfDate409(err)) {
808
+ throw Object.assign(new Error(message), {
809
+ status,
810
+ mergeBlockedBlockers: ['head_ref_moved'],
811
+ forgeError: forgeError(
812
+ ERROR_CODES.MERGE_BLOCKED,
813
+ sanitizeField(message) || 'Head branch out of date at merge POST',
814
+ status,
815
+ ),
816
+ });
817
+ }
818
+ throw err;
819
+ }
820
+ return {
821
+ commit_sha: sanitizeField(result?.sha ?? result?.merge_commit_sha ?? null),
822
+ provider_status: 200,
823
+ base_sha: sanitizeField(result?.base_sha ?? null),
824
+ };
825
+ }
826
+
709
827
  const GITEA_OPEN_PULL_COMPLIANT_MAX =
710
828
  DEFAULT_OPEN_PULL_LIST_PAGE_SIZE * MAX_CHECK_STATUS_PAGES;
711
829
 
@@ -916,17 +1034,20 @@ function mergeability(pr) {
916
1034
 
917
1035
  export async function prView(ctx, opts) {
918
1036
  const pr = await getPull(ctx, opts);
919
- return {
1037
+ const body = {
920
1038
  pr_number: pr.number,
921
1039
  url: sanitizeUrl(pr.html_url ?? pr.url),
922
1040
  title: sanitizeField(pr.title),
923
1041
  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),
1042
+ forge_target_branch_ref: sanitizeField(pr.base?.ref),
1043
+ forge_target_sha: sanitizeField(pr.base?.sha),
1044
+ forge_source_branch_ref: sanitizeField(pr.head?.ref),
1045
+ forge_source_sha: sanitizeField(pr.head?.sha),
928
1046
  mergeability: mergeability(pr),
929
1047
  };
1048
+ const forgeSourceRepoId = forgeSourceRepoIdFromPull(ctx.config, pr);
1049
+ if (forgeSourceRepoId) body.forge_source_repo_id = forgeSourceRepoId;
1050
+ return body;
930
1051
  }
931
1052
 
932
1053
  export async function prChecks(ctx, opts) {
@@ -957,7 +1078,7 @@ export async function prChecks(ctx, opts) {
957
1078
  const mapped = mapGiteaCommitStatuses(statusRecords);
958
1079
  const conclusion = summarizeChecks(mapped);
959
1080
  return {
960
- head_sha: sha,
1081
+ forge_source_sha: sha,
961
1082
  check_conclusion: conclusion,
962
1083
  checks_truncated,
963
1084
  statuses: mapped,
@@ -1016,9 +1137,11 @@ export const provider = {
1016
1137
  mergePlan,
1017
1138
  syncPlan,
1018
1139
  crOpen,
1140
+ mergeExecute,
1019
1141
  statusSet,
1020
1142
  whoami,
1021
1143
  branchProtection,
1144
+ branchHeadSha,
1022
1145
  crFiles,
1023
1146
  crComments,
1024
1147
  forgeChanges,
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.8",
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.6"
25
+ "@remogram/core": "0.1.0-beta.8"
26
26
  }
27
27
  }