@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.
- package/index.js +142 -27
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
gitAheadBehind,
|
|
11
11
|
refsInventory,
|
|
12
12
|
crInventory,
|
|
13
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
25
|
+
"@remogram/core": "0.1.0-beta.8"
|
|
26
26
|
}
|
|
27
27
|
}
|