@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.
- package/branch-protection-internal.js +13 -0
- package/index.js +385 -30
- package/package.json +4 -3
|
@@ -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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
+
repoApiPathFor(owner, repo, 'branches', branchRef),
|
|
301
388
|
);
|
|
302
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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.
|
|
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.
|
|
26
|
+
"@remogram/core": "0.1.0-beta.9"
|
|
26
27
|
}
|
|
27
28
|
}
|