@goodfellow-cms/gitlab 0.7.0 → 0.9.0
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/dist/index.d.ts +8 -1
- package/dist/index.js +121 -49
- package/dist/testing.d.ts +5 -0
- package/dist/testing.js +45 -4
- package/package.json +7 -3
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,14 @@ import { ConnectDomainOptions, DeployStatus, DnsRecord, FileChange, FileVersion,
|
|
|
3
3
|
interface ApiOptions {
|
|
4
4
|
/** The API root, such as `https://gitlab.com/api/v4`. */
|
|
5
5
|
apiUrl: string;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Returns a current access token, refreshing it if needed. `refresh` asks
|
|
8
|
+
* for a new one after GitLab refused `stale`, which another request may
|
|
9
|
+
* already have replaced.
|
|
10
|
+
*/
|
|
7
11
|
token: (options?: {
|
|
8
12
|
refresh?: boolean;
|
|
13
|
+
stale?: string;
|
|
9
14
|
}) => Promise<string>;
|
|
10
15
|
/** Whether the token can be refreshed after a 401. */
|
|
11
16
|
canRefresh: boolean;
|
|
@@ -84,6 +89,8 @@ export declare class GitLabBackend implements GitBackend {
|
|
|
84
89
|
revision(): Promise<string>;
|
|
85
90
|
/** The branch's latest commit, without moving the revision reads are pinned to. */
|
|
86
91
|
private head;
|
|
92
|
+
/** Every item of one of GitLab's lists, page by page, up to `REVIEW_PAGES` pages. */
|
|
93
|
+
private everyPage;
|
|
87
94
|
/** Every file in one folder at a commit, page by page. */
|
|
88
95
|
private listFolder;
|
|
89
96
|
/** Path → blob id for the editable files at a commit. Commits never change, so this is cached. */
|
package/dist/index.js
CHANGED
|
@@ -2,15 +2,22 @@ import { ConflictError, DomainError, DraftError, EDITABLE_FOLDERS, EditorsError,
|
|
|
2
2
|
//#region src/api.ts
|
|
3
3
|
/** Calls the GitLab API. A 401 refreshes the token once; after that it means the sign-in has expired. */
|
|
4
4
|
async function gitlabRequest(api, path, init = {}) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
...
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
let sent;
|
|
6
|
+
const send = async (refresh) => {
|
|
7
|
+
sent = await api.token(refresh ? {
|
|
8
|
+
refresh,
|
|
9
|
+
...sent !== void 0 && { stale: sent }
|
|
10
|
+
} : {});
|
|
11
|
+
return api.fetch(`${api.apiUrl}${path}`, {
|
|
12
|
+
method: init.method ?? "GET",
|
|
13
|
+
headers: {
|
|
14
|
+
authorization: `Bearer ${sent}`,
|
|
15
|
+
...init.body !== void 0 && { "content-type": "application/json" }
|
|
16
|
+
},
|
|
17
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
|
|
18
|
+
cache: "no-store"
|
|
19
|
+
});
|
|
20
|
+
};
|
|
14
21
|
let response = await send(false);
|
|
15
22
|
if (response.status === 401 && api.canRefresh) response = await send(true);
|
|
16
23
|
if (response.status === 401) throw new SignInError("invalid", "GitLab didn't accept the sign-in. It may have expired.");
|
|
@@ -136,6 +143,11 @@ function gitlabEditors(api, project, branch, username) {
|
|
|
136
143
|
await gitlabRequest(api, editor.id.startsWith(INVITATION) ? `${base}/invitations/${encodeURIComponent(editor.id.slice(11))}` : `${base}/members/${editor.id.slice(7)}`, { method: "DELETE" }).catch(notAllowed$2);
|
|
137
144
|
},
|
|
138
145
|
async letEditorsPublish() {
|
|
146
|
+
const read = async () => {
|
|
147
|
+
const answer = await gitlabRequest(api, protectedPath, { allow: [404] }).catch(notAllowed$2);
|
|
148
|
+
return answer.ok ? await answer.json() : void 0;
|
|
149
|
+
};
|
|
150
|
+
const developersMay = (protection) => !protection || ["push_access_levels", "merge_access_levels"].every((kind) => (protection[kind] ?? []).some((level) => level.access_level !== null && level.access_level <= DEVELOPER));
|
|
139
151
|
const protect = (level) => gitlabRequest(api, `${base}/protected_branches`, {
|
|
140
152
|
method: "POST",
|
|
141
153
|
body: {
|
|
@@ -145,6 +157,30 @@ function gitlabEditors(api, project, branch, username) {
|
|
|
145
157
|
allow_force_push: false
|
|
146
158
|
}
|
|
147
159
|
});
|
|
160
|
+
const current = await read();
|
|
161
|
+
if (!current) {
|
|
162
|
+
await protect(DEVELOPER).catch(notAllowed$2);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (developersMay(current)) return;
|
|
166
|
+
const swap = (levels = []) => [{ access_level: DEVELOPER }, ...levels.flatMap((level) => level.id === void 0 ? [] : [{
|
|
167
|
+
id: level.id,
|
|
168
|
+
_destroy: true
|
|
169
|
+
}])];
|
|
170
|
+
if ((await gitlabRequest(api, protectedPath, {
|
|
171
|
+
method: "PATCH",
|
|
172
|
+
body: {
|
|
173
|
+
allow_force_push: false,
|
|
174
|
+
allowed_to_push: swap(current.push_access_levels),
|
|
175
|
+
allowed_to_merge: swap(current.merge_access_levels)
|
|
176
|
+
},
|
|
177
|
+
allow: [
|
|
178
|
+
400,
|
|
179
|
+
404,
|
|
180
|
+
405,
|
|
181
|
+
422
|
|
182
|
+
]
|
|
183
|
+
}).catch(notAllowed$2)).ok && developersMay(await read())) return;
|
|
148
184
|
await gitlabRequest(api, protectedPath, {
|
|
149
185
|
method: "DELETE",
|
|
150
186
|
allow: [404]
|
|
@@ -155,6 +191,11 @@ function gitlabEditors(api, project, branch, username) {
|
|
|
155
191
|
await protect(MAINTAINER).catch(() => void 0);
|
|
156
192
|
notAllowed$2(error);
|
|
157
193
|
}
|
|
194
|
+
const after = await read();
|
|
195
|
+
if (!after || !developersMay(after)) {
|
|
196
|
+
if (!after) await protect(MAINTAINER).catch(() => void 0);
|
|
197
|
+
throw new Error("GitLab didn't keep the main branch's new protection. Try again.");
|
|
198
|
+
}
|
|
158
199
|
}
|
|
159
200
|
};
|
|
160
201
|
}
|
|
@@ -346,7 +387,11 @@ function notAllowed$1(error) {
|
|
|
346
387
|
if (error instanceof GitApiError && (error.status === 401 || error.status === 403)) throw new ScheduledBuildsError("not-allowed", "Adding pipeline schedules on GitLab needs the Developer role, with permission to merge to the main branch.");
|
|
347
388
|
throw error;
|
|
348
389
|
}
|
|
349
|
-
/**
|
|
390
|
+
/**
|
|
391
|
+
* A cron line for one instant, in UTC. GitLab's crons have no year, so it
|
|
392
|
+
* comes again a year later unless `sync()` has removed it since, which only
|
|
393
|
+
* builds the site again, since the schedule's variable skips the update.
|
|
394
|
+
*/
|
|
350
395
|
function cronAt(at) {
|
|
351
396
|
return `${at.getUTCMinutes()} ${at.getUTCHours()} ${at.getUTCDate()} ${at.getUTCMonth() + 1} *`;
|
|
352
397
|
}
|
|
@@ -373,7 +418,12 @@ function gitlabScheduledBuilds(api, project, branch, readCi, now = () => /* @__P
|
|
|
373
418
|
if (wanted.size >= 50) break;
|
|
374
419
|
wanted.set(minute(at), at);
|
|
375
420
|
}
|
|
376
|
-
const found =
|
|
421
|
+
const found = [];
|
|
422
|
+
for (let page = 1; page <= 10; page++) {
|
|
423
|
+
const listed = await gitlabJson(api, `${base}?per_page=100&page=${page}`).catch(notAllowed$1);
|
|
424
|
+
found.push(...listed);
|
|
425
|
+
if (listed.length < 100) break;
|
|
426
|
+
}
|
|
377
427
|
const ours = /* @__PURE__ */ new Map();
|
|
378
428
|
for (const schedule of found) {
|
|
379
429
|
if (!schedule.description?.startsWith("Goodfellow: build the site for a scheduled change at ")) continue;
|
|
@@ -398,14 +448,22 @@ function gitlabScheduledBuilds(api, project, branch, readCi, now = () => /* @__P
|
|
|
398
448
|
active: true
|
|
399
449
|
}
|
|
400
450
|
}).catch(notAllowed$1);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
451
|
+
try {
|
|
452
|
+
await gitlabRequest(api, `${base}/${created.id}/variables`, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
body: {
|
|
455
|
+
key: SCHEDULED_CHANGE_VARIABLE,
|
|
456
|
+
value: time,
|
|
457
|
+
variable_type: "env_var"
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
} catch (error) {
|
|
461
|
+
await gitlabRequest(api, `${base}/${created.id}`, {
|
|
462
|
+
method: "DELETE",
|
|
463
|
+
allow: [403, 404]
|
|
464
|
+
}).catch(() => {});
|
|
465
|
+
notAllowed$1(error);
|
|
466
|
+
}
|
|
409
467
|
}
|
|
410
468
|
}
|
|
411
469
|
};
|
|
@@ -695,6 +753,10 @@ function gitlabCodeUpdates(options) {
|
|
|
695
753
|
//#endregion
|
|
696
754
|
//#region src/backend.ts
|
|
697
755
|
const PAGE_SIZE = 100;
|
|
756
|
+
/** The most files GitLab's compare lists, by default. */
|
|
757
|
+
const COMPARE_FILES_LIMIT = 1e3;
|
|
758
|
+
/** How many pages of drafts' branches and merge requests are read: a thousand of each. */
|
|
759
|
+
const REVIEW_PAGES = 10;
|
|
698
760
|
/** How many files one GraphQL request reads. */
|
|
699
761
|
const GRAPHQL_BATCH = 50;
|
|
700
762
|
/** The color and section codes in GitLab's job logs. */
|
|
@@ -847,11 +909,8 @@ var GitLabBackend = class GitLabBackend {
|
|
|
847
909
|
return (await response.json()).id;
|
|
848
910
|
},
|
|
849
911
|
listBranches: async (prefix) => {
|
|
850
|
-
const query = new URLSearchParams({
|
|
851
|
-
|
|
852
|
-
per_page: "100"
|
|
853
|
-
});
|
|
854
|
-
return (await gitlabJson(this.api, this.repo(`/branches?${query}`))).map((found) => found.name).filter((name) => name.startsWith(prefix));
|
|
912
|
+
const query = new URLSearchParams({ search: `^${prefix}` });
|
|
913
|
+
return (await this.everyPage(this.repo(`/branches?${query}`))).map((found) => found.name).filter((name) => name.startsWith(prefix));
|
|
855
914
|
},
|
|
856
915
|
openReview: async ({ branch, title, description, reviewers = [] }) => {
|
|
857
916
|
const response = await gitlabRequest(this.api, `${project}/merge_requests`, {
|
|
@@ -880,11 +939,7 @@ var GitLabBackend = class GitLabBackend {
|
|
|
880
939
|
return { url: review.web_url };
|
|
881
940
|
},
|
|
882
941
|
listReviews: async (prefix) => {
|
|
883
|
-
|
|
884
|
-
state: "opened",
|
|
885
|
-
per_page: "100"
|
|
886
|
-
});
|
|
887
|
-
return (await gitlabJson(this.api, `${project}/merge_requests?${query}`)).filter((request) => request.source_branch.startsWith(prefix)).map((request) => ({
|
|
942
|
+
return (await this.everyPage(`${project}/merge_requests?state=opened`)).filter((request) => request.source_branch.startsWith(prefix)).map((request) => ({
|
|
888
943
|
branch: request.source_branch,
|
|
889
944
|
url: request.web_url,
|
|
890
945
|
title: request.title
|
|
@@ -910,6 +965,16 @@ var GitLabBackend = class GitLabBackend {
|
|
|
910
965
|
async head() {
|
|
911
966
|
return (await gitlabJson(this.api, this.repo(`/branches/${encodeURIComponent(this.branch)}`))).commit.id;
|
|
912
967
|
}
|
|
968
|
+
/** Every item of one of GitLab's lists, page by page, up to `REVIEW_PAGES` pages. */
|
|
969
|
+
async everyPage(path) {
|
|
970
|
+
const items = [];
|
|
971
|
+
for (let page = 1; page <= REVIEW_PAGES; page++) {
|
|
972
|
+
const found = await gitlabJson(this.api, `${path}&per_page=${PAGE_SIZE}&page=${page}`);
|
|
973
|
+
items.push(...found);
|
|
974
|
+
if (found.length < PAGE_SIZE) break;
|
|
975
|
+
}
|
|
976
|
+
return items;
|
|
977
|
+
}
|
|
913
978
|
/** Every file in one folder at a commit, page by page. */
|
|
914
979
|
async listFolder(sha, dir) {
|
|
915
980
|
const files = [];
|
|
@@ -1132,12 +1197,13 @@ var GitLabBackend = class GitLabBackend {
|
|
|
1132
1197
|
return response.ok ? response.text() : void 0;
|
|
1133
1198
|
}
|
|
1134
1199
|
async changedPaths(from, to) {
|
|
1135
|
-
const
|
|
1200
|
+
const diffs = (await gitlabJson(this.api, this.repo(`/compare?${new URLSearchParams({
|
|
1136
1201
|
from,
|
|
1137
1202
|
to,
|
|
1138
1203
|
straight: "true"
|
|
1139
|
-
})}`));
|
|
1140
|
-
|
|
1204
|
+
})}`))).diffs ?? [];
|
|
1205
|
+
if (diffs.length >= COMPARE_FILES_LIMIT) throw new GitApiError(0, `More files changed between ${from} and ${to} than GitLab lists.`);
|
|
1206
|
+
return [...new Set(diffs.flatMap((diff) => [diff.old_path, diff.new_path]))];
|
|
1141
1207
|
}
|
|
1142
1208
|
/**
|
|
1143
1209
|
* Reads the commit's pipeline and external statuses (GitLab Pages jobs, or
|
|
@@ -1529,19 +1595,25 @@ function gitlab(options) {
|
|
|
1529
1595
|
}
|
|
1530
1596
|
async function connect(initial, remember) {
|
|
1531
1597
|
let credentials = initial;
|
|
1532
|
-
|
|
1598
|
+
let renewing;
|
|
1599
|
+
const renew = (current) => renewing ??= (async () => {
|
|
1600
|
+
credentials = toTokens(await requestTokens({
|
|
1601
|
+
client_id: options.clientId ?? "",
|
|
1602
|
+
refresh_token: current.refresh,
|
|
1603
|
+
grant_type: "refresh_token",
|
|
1604
|
+
redirect_uri: redirectUri(location().href()),
|
|
1605
|
+
code_verifier: current.verifier
|
|
1606
|
+
}), current.verifier);
|
|
1607
|
+
storage.save(JSON.stringify(credentials), remember);
|
|
1608
|
+
return credentials.access;
|
|
1609
|
+
})().finally(() => {
|
|
1610
|
+
renewing = void 0;
|
|
1611
|
+
});
|
|
1612
|
+
const token = async ({ refresh, stale } = {}) => {
|
|
1533
1613
|
if (credentials.type === "token") return credentials.token;
|
|
1534
|
-
if (
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
client_id: options.clientId ?? "",
|
|
1538
|
-
refresh_token: credentials.refresh,
|
|
1539
|
-
grant_type: "refresh_token",
|
|
1540
|
-
redirect_uri: redirectUri(location().href()),
|
|
1541
|
-
code_verifier: verifier
|
|
1542
|
-
}), verifier);
|
|
1543
|
-
storage.save(JSON.stringify(credentials), remember);
|
|
1544
|
-
}
|
|
1614
|
+
if (renewing) return renewing;
|
|
1615
|
+
if (refresh && stale !== void 0 && stale !== credentials.access) return credentials.access;
|
|
1616
|
+
if (refresh || credentials.expiresAt - 6e4 < Date.now()) return renew(credentials);
|
|
1545
1617
|
return credentials.access;
|
|
1546
1618
|
};
|
|
1547
1619
|
const api = {
|
|
@@ -1584,18 +1656,18 @@ function gitlab(options) {
|
|
|
1584
1656
|
const returnedState = url.searchParams.get("state");
|
|
1585
1657
|
const error = url.searchParams.get("error");
|
|
1586
1658
|
if (!code && !error) return null;
|
|
1587
|
-
const saved = pending.load();
|
|
1588
|
-
if (!saved) return null;
|
|
1589
|
-
pending.clear();
|
|
1590
|
-
const redirect = JSON.parse(saved);
|
|
1591
1659
|
for (const key of [
|
|
1592
1660
|
"code",
|
|
1593
1661
|
"state",
|
|
1594
1662
|
"error",
|
|
1595
1663
|
"error_description"
|
|
1596
1664
|
]) url.searchParams.delete(key);
|
|
1597
|
-
|
|
1665
|
+
const saved = pending.load();
|
|
1666
|
+
if (saved) url.hash = JSON.parse(saved).hash;
|
|
1598
1667
|
location().replace(url.toString());
|
|
1668
|
+
if (!saved) return null;
|
|
1669
|
+
pending.clear();
|
|
1670
|
+
const redirect = JSON.parse(saved);
|
|
1599
1671
|
if (error || !code || returnedState !== redirect.state) throw new SignInError("redirect-failed", "Signing in with GitLab was cancelled or didn't finish.");
|
|
1600
1672
|
return connect(toTokens(await requestTokens({
|
|
1601
1673
|
client_id: options.clientId ?? "",
|
package/dist/testing.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface FakeGitLabOptions {
|
|
|
50
50
|
* artifact, or nothing for a job that fails. Without it, task pipelines fail.
|
|
51
51
|
*/
|
|
52
52
|
tasks?: (variables: Record<string, string>) => string | undefined | Promise<string | undefined>;
|
|
53
|
+
/** GitLab before 15.6, which can't change a protected branch's access levels, only remove and add it. */
|
|
54
|
+
noProtectionChanges?: boolean;
|
|
53
55
|
}
|
|
54
56
|
/** A pipeline schedule, such as the nightly one or one the admin panel adds for a scheduled change. */
|
|
55
57
|
export interface FakeSchedule {
|
|
@@ -120,6 +122,7 @@ export declare function fakeGitLab(options: FakeGitLabOptions): {
|
|
|
120
122
|
merge: number;
|
|
121
123
|
forcePush: boolean;
|
|
122
124
|
}>;
|
|
125
|
+
protectionChanges: string[];
|
|
123
126
|
failBuild: (sha: string, failure?: {
|
|
124
127
|
job?: string;
|
|
125
128
|
failureReason?: string;
|
|
@@ -172,6 +175,8 @@ export declare function fakeGitLab(options: FakeGitLabOptions): {
|
|
|
172
175
|
requests: Request[];
|
|
173
176
|
commit: (changes: FileChange[], message: string, author?: string, branch?: string) => string;
|
|
174
177
|
authorize: (authorizeUrl: string) => string;
|
|
178
|
+
/** Ends every OAuth access token GitLab has given, as their two hours running out does; refresh tokens still work. */
|
|
179
|
+
expireAccessTokens: () => void;
|
|
175
180
|
handle: (request: Request) => Promise<Response>;
|
|
176
181
|
fetch: typeof fetch;
|
|
177
182
|
};
|
package/dist/testing.js
CHANGED
|
@@ -17,6 +17,12 @@ async function sha256Base64Url(text) {
|
|
|
17
17
|
for (const byte of digest) binary += String.fromCharCode(byte);
|
|
18
18
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
19
19
|
}
|
|
20
|
+
/** One page of a list, as GitLab answers with `per_page` and `page`. */
|
|
21
|
+
function onePage(url, items) {
|
|
22
|
+
const perPage = Number(url.searchParams.get("per_page") ?? 20);
|
|
23
|
+
const page = Number(url.searchParams.get("page") ?? 1);
|
|
24
|
+
return items.slice((page - 1) * perPage, page * perPage);
|
|
25
|
+
}
|
|
20
26
|
function fakeGitLab(options) {
|
|
21
27
|
const webUrl = options.url ?? "https://gitlab.com";
|
|
22
28
|
const repo = new FakeRepo(options.files);
|
|
@@ -47,6 +53,8 @@ function fakeGitLab(options) {
|
|
|
47
53
|
merge: 40,
|
|
48
54
|
forcePush: false
|
|
49
55
|
}]]);
|
|
56
|
+
/** Each change to a protected branch, in order: `DELETE main`, `POST main`, `PATCH main`. */
|
|
57
|
+
const protectionChanges = [];
|
|
50
58
|
/** The project's members and invitations, and its protected branches. */
|
|
51
59
|
async function handleMembers(request, url, rest, accessLevel) {
|
|
52
60
|
const forbidden = () => json({ message: "403 Forbidden" }, 403);
|
|
@@ -128,20 +136,48 @@ function fakeGitLab(options) {
|
|
|
128
136
|
if (!found) return json({ message: "404 Not found" }, 404);
|
|
129
137
|
if (request.method === "GET") return json({
|
|
130
138
|
name,
|
|
131
|
-
push_access_levels: [{
|
|
132
|
-
|
|
139
|
+
push_access_levels: [{
|
|
140
|
+
id: 1,
|
|
141
|
+
access_level: found.push
|
|
142
|
+
}],
|
|
143
|
+
merge_access_levels: [{
|
|
144
|
+
id: 2,
|
|
145
|
+
access_level: found.merge
|
|
146
|
+
}],
|
|
133
147
|
allow_force_push: found.forcePush
|
|
134
148
|
});
|
|
135
149
|
if (request.method === "DELETE") {
|
|
136
150
|
if (accessLevel < 40) return forbidden();
|
|
137
151
|
protectedBranches.delete(name);
|
|
152
|
+
protectionChanges.push(`DELETE ${name}`);
|
|
138
153
|
return new Response(null, { status: 204 });
|
|
139
154
|
}
|
|
155
|
+
if (request.method === "PATCH" && !options.noProtectionChanges) {
|
|
156
|
+
if (accessLevel < 40) return forbidden();
|
|
157
|
+
const body = await request.json();
|
|
158
|
+
const levelFrom = (levels, id, now) => {
|
|
159
|
+
if (!levels) return now;
|
|
160
|
+
const kept = levels.some((level) => level.id === id && level._destroy) ? [] : [now];
|
|
161
|
+
const added = levels.flatMap((level) => level.access_level === void 0 ? [] : [level.access_level]);
|
|
162
|
+
return kept.length + added.length === 1 ? kept[0] ?? added[0] ?? now : void 0;
|
|
163
|
+
};
|
|
164
|
+
const push = levelFrom(body.allowed_to_push, 1, found.push);
|
|
165
|
+
const merge = levelFrom(body.allowed_to_merge, 2, found.merge);
|
|
166
|
+
if (push === void 0 || merge === void 0) return json({ message: "Only one access level allowed" }, 422);
|
|
167
|
+
protectedBranches.set(name, {
|
|
168
|
+
push,
|
|
169
|
+
merge,
|
|
170
|
+
forcePush: body.allow_force_push ?? found.forcePush
|
|
171
|
+
});
|
|
172
|
+
protectionChanges.push(`PATCH ${name}`);
|
|
173
|
+
return json({ name });
|
|
174
|
+
}
|
|
140
175
|
}
|
|
141
176
|
if (rest === "/protected_branches" && request.method === "POST") {
|
|
142
177
|
if (accessLevel < 40) return forbidden();
|
|
143
178
|
const body = await request.json();
|
|
144
179
|
if (protectedBranches.has(body.name)) return json({ message: "Protected branch already exists" }, 409);
|
|
180
|
+
protectionChanges.push(`POST ${body.name}`);
|
|
145
181
|
protectedBranches.set(body.name, {
|
|
146
182
|
push: body.push_access_level ?? 40,
|
|
147
183
|
merge: body.merge_access_level ?? 40,
|
|
@@ -515,7 +551,7 @@ function fakeGitLab(options) {
|
|
|
515
551
|
if (rest === "/repository/branches" && request.method === "GET") {
|
|
516
552
|
const search = url.searchParams.get("search") ?? "";
|
|
517
553
|
const matches = (name) => search.startsWith("^") ? name.startsWith(search.slice(1)) : name.includes(search);
|
|
518
|
-
return json([...repo.branches].filter(([name]) => matches(name)).map(([name, sha]) => ({
|
|
554
|
+
return json(onePage(url, [...repo.branches].filter(([name]) => matches(name))).map(([name, sha]) => ({
|
|
519
555
|
name,
|
|
520
556
|
commit: { id: sha }
|
|
521
557
|
})));
|
|
@@ -564,7 +600,7 @@ function fakeGitLab(options) {
|
|
|
564
600
|
}
|
|
565
601
|
if (rest === "/merge_requests" && request.method === "GET") {
|
|
566
602
|
const source = url.searchParams.get("source_branch");
|
|
567
|
-
return json(repo.reviews.filter((review) => review.state === "open" && (!source || review.branch === source)).map((review) => ({
|
|
603
|
+
return json(onePage(url, repo.reviews.filter((review) => review.state === "open" && (!source || review.branch === source))).map((review) => ({
|
|
568
604
|
iid: review.number,
|
|
569
605
|
title: review.title,
|
|
570
606
|
source_branch: review.branch,
|
|
@@ -892,6 +928,7 @@ function fakeGitLab(options) {
|
|
|
892
928
|
members,
|
|
893
929
|
invitations,
|
|
894
930
|
protectedBranches,
|
|
931
|
+
protectionChanges,
|
|
895
932
|
failBuild,
|
|
896
933
|
cancelBuild,
|
|
897
934
|
/** The project's update settings, and the pipelines the update ran in, which tests add with `addUpdateRun()`. */
|
|
@@ -908,6 +945,10 @@ function fakeGitLab(options) {
|
|
|
908
945
|
requests,
|
|
909
946
|
commit,
|
|
910
947
|
authorize,
|
|
948
|
+
/** Ends every OAuth access token GitLab has given, as their two hours running out does; refresh tokens still work. */
|
|
949
|
+
expireAccessTokens: () => {
|
|
950
|
+
for (const [token] of tokens) if (token.startsWith("oauth-access-")) tokens.delete(token);
|
|
951
|
+
},
|
|
911
952
|
handle,
|
|
912
953
|
fetch: ((input, init) => handle(new Request(input, init)))
|
|
913
954
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/gitlab",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Edit a Goodfellow site stored on GitLab, straight from the browser.",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Edit a Goodfellow CMS site stored on GitLab, straight from the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
7
7
|
"cms",
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
],
|
|
13
13
|
"homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/hosts",
|
|
14
14
|
"bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
|
|
15
|
+
"funding": {
|
|
16
|
+
"type": "github",
|
|
17
|
+
"url": "https://github.com/sponsors/goodfellow-cms"
|
|
18
|
+
},
|
|
15
19
|
"license": "MIT",
|
|
16
20
|
"repository": {
|
|
17
21
|
"type": "git",
|
|
@@ -35,7 +39,7 @@
|
|
|
35
39
|
"dist"
|
|
36
40
|
],
|
|
37
41
|
"dependencies": {
|
|
38
|
-
"@goodfellow-cms/core": "0.
|
|
42
|
+
"@goodfellow-cms/core": "0.9.0"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@puckeditor/core": "0.23.0"
|