@remogram/provider-gitea-api 0.1.0-beta.5 → 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 +142 -27
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  gitAheadBehind,
11
11
  refsInventory,
12
12
  crInventory,
13
- buildMergePlanBodyFromFacts,
13
+ buildMergePlanFromProviderFacts,
14
14
  buildChangeRequestOpenedBody,
15
15
  buildCommitStatusSetBody,
16
16
  parseStatusSetArgs,
@@ -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,
@@ -56,6 +57,7 @@ import {
56
57
  buildOpenPullListMeta,
57
58
  giteaOpenPullSortQuery,
58
59
  appendSortQuery,
60
+ assertWriteCommandConfigured,
59
61
  } from '@remogram/core';
60
62
  const PUBLIC_GITEA_HOST = 'gitea.com';
61
63
  const PUBLIC_GITEA_API = 'https://gitea.com/api/v1';
@@ -84,6 +86,7 @@ const STRUCTURED_COMMANDS = apiProviderCommands({
84
86
  crFilesImplemented: true,
85
87
  crCommentsImplemented: true,
86
88
  forgeChangesImplemented: true,
89
+ mergeExecuteImplemented: true,
87
90
  });
88
91
 
89
92
  export function giteaToken() {
@@ -164,13 +167,36 @@ export function authHeaders(token) {
164
167
  }
165
168
 
166
169
  export function repoApiPath(config, ...segments) {
167
- const owner = encodeURIComponent(config.owner);
168
- const repo = encodeURIComponent(config.repo);
169
- 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}`;
170
177
  if (!segments.length) return base;
171
178
  return `${base}/${segments.map((s) => encodeURIComponent(String(s))).join('/')}`;
172
179
  }
173
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
+
174
200
  export async function giteaFetch(config, parsed, path, options = {}) {
175
201
  const token = requireToken();
176
202
  const url = `${apiBase(config, parsed)}${path}`;
@@ -301,6 +327,47 @@ export async function branchProtection(ctx, { branchRef }) {
301
327
  return buildBranchProtectionFromGiteaProtection(branchRef, protection);
302
328
  }
303
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
+
304
371
  export async function crFiles(ctx, { number }) {
305
372
  requireToken();
306
373
  if (number == null) {
@@ -484,7 +551,7 @@ export function providerCapabilities() {
484
551
  host_binding: 'verified_remote_host',
485
552
  pagination: 'supported',
486
553
  write_support: true,
487
- write_commands: ['cr_open', 'status_set'],
554
+ write_commands: ['cr_open', 'status_set', 'merge'],
488
555
  ...forgeIngestCapabilityFacts(),
489
556
  ...checkPaginationCapabilityFacts({
490
557
  strategy: 'offset_limit',
@@ -519,10 +586,10 @@ export async function refsCompare(ctx, baseRef, headRef) {
519
586
  }
520
587
  const counts = gitAheadBehind(ctx.cwd, baseSha, headSha);
521
588
  return {
522
- base_ref: sanitizeField(baseRef),
523
- base_sha: baseSha,
524
- head_ref: sanitizeField(headRef),
525
- 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,
526
593
  ...counts,
527
594
  };
528
595
  }
@@ -648,6 +715,7 @@ export async function findCommitStatusByContext(ctx, sha, context) {
648
715
  }
649
716
 
650
717
  export async function statusSet(ctx, args) {
718
+ assertWriteCommandConfigured(ctx.config, 'status_set');
651
719
  const parsed = parseStatusSetArgs(args);
652
720
  const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
653
721
  if (existing) {
@@ -676,6 +744,7 @@ export async function statusSet(ctx, args) {
676
744
  }
677
745
 
678
746
  export async function crOpen(ctx, { head, base, title, body: prBody }) {
747
+ assertWriteCommandConfigured(ctx.config, 'cr_open');
679
748
  assertGitRef(head, 'head');
680
749
  assertGitRef(base, 'base');
681
750
  if (!title || typeof title !== 'string' || !title.trim()) {
@@ -703,6 +772,58 @@ export async function crOpen(ctx, { head, base, title, body: prBody }) {
703
772
  return buildChangeRequestOpenedBody(pull, { head, base, title });
704
773
  }
705
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
+
706
827
  const GITEA_OPEN_PULL_COMPLIANT_MAX =
707
828
  DEFAULT_OPEN_PULL_LIST_PAGE_SIZE * MAX_CHECK_STATUS_PAGES;
708
829
 
@@ -913,17 +1034,20 @@ function mergeability(pr) {
913
1034
 
914
1035
  export async function prView(ctx, opts) {
915
1036
  const pr = await getPull(ctx, opts);
916
- return {
1037
+ const body = {
917
1038
  pr_number: pr.number,
918
1039
  url: sanitizeUrl(pr.html_url ?? pr.url),
919
1040
  title: sanitizeField(pr.title),
920
1041
  state: normalizeGiteaPrState(pr.state),
921
- base_ref: sanitizeField(pr.base?.ref),
922
- base_sha: sanitizeField(pr.base?.sha),
923
- head_ref: sanitizeField(pr.head?.ref),
924
- 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),
925
1046
  mergeability: mergeability(pr),
926
1047
  };
1048
+ const forgeSourceRepoId = forgeSourceRepoIdFromPull(ctx.config, pr);
1049
+ if (forgeSourceRepoId) body.forge_source_repo_id = forgeSourceRepoId;
1050
+ return body;
927
1051
  }
928
1052
 
929
1053
  export async function prChecks(ctx, opts) {
@@ -954,7 +1078,7 @@ export async function prChecks(ctx, opts) {
954
1078
  const mapped = mapGiteaCommitStatuses(statusRecords);
955
1079
  const conclusion = summarizeChecks(mapped);
956
1080
  return {
957
- head_sha: sha,
1081
+ forge_source_sha: sha,
958
1082
  check_conclusion: conclusion,
959
1083
  checks_truncated,
960
1084
  statuses: mapped,
@@ -970,18 +1094,7 @@ export function summarizeChecks(statuses) {
970
1094
  }
971
1095
 
972
1096
  export async function mergePlan(ctx, opts) {
973
- const view = await prView(ctx, opts);
974
- const checks = await prChecks(ctx, { number: view.pr_number });
975
- let mergeOpts = opts;
976
- if (opts.allowed_paths) {
977
- try {
978
- const crFilesBody = await crFiles(ctx, { number: view.pr_number });
979
- mergeOpts = { ...opts, changed_paths: crFilesBody.changed_paths };
980
- } catch {
981
- // Fall back to local git diff in resolveMergePlanPathScope.
982
- }
983
- }
984
- return buildMergePlanBodyFromFacts(ctx, view, checks, mergeOpts);
1097
+ return buildMergePlanFromProviderFacts(ctx, opts, { prView, prChecks, crFiles });
985
1098
  }
986
1099
 
987
1100
  export async function syncPlan(ctx, remoteName = 'origin') {
@@ -1024,9 +1137,11 @@ export const provider = {
1024
1137
  mergePlan,
1025
1138
  syncPlan,
1026
1139
  crOpen,
1140
+ mergeExecute,
1027
1141
  statusSet,
1028
1142
  whoami,
1029
1143
  branchProtection,
1144
+ branchHeadSha,
1030
1145
  crFiles,
1031
1146
  crComments,
1032
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.5",
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.5"
25
+ "@remogram/core": "0.1.0-beta.8"
26
26
  }
27
27
  }