@goodfellow-cms/gitlab 0.7.0 → 0.8.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 +87 -49
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +12 -2
- package/package.json +2 -2
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.");
|
|
@@ -346,7 +353,11 @@ function notAllowed$1(error) {
|
|
|
346
353
|
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
354
|
throw error;
|
|
348
355
|
}
|
|
349
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* A cron line for one instant, in UTC. GitLab's crons have no year, so it
|
|
358
|
+
* comes again a year later unless `sync()` has removed it since, which only
|
|
359
|
+
* builds the site again, since the schedule's variable skips the update.
|
|
360
|
+
*/
|
|
350
361
|
function cronAt(at) {
|
|
351
362
|
return `${at.getUTCMinutes()} ${at.getUTCHours()} ${at.getUTCDate()} ${at.getUTCMonth() + 1} *`;
|
|
352
363
|
}
|
|
@@ -373,7 +384,12 @@ function gitlabScheduledBuilds(api, project, branch, readCi, now = () => /* @__P
|
|
|
373
384
|
if (wanted.size >= 50) break;
|
|
374
385
|
wanted.set(minute(at), at);
|
|
375
386
|
}
|
|
376
|
-
const found =
|
|
387
|
+
const found = [];
|
|
388
|
+
for (let page = 1; page <= 10; page++) {
|
|
389
|
+
const listed = await gitlabJson(api, `${base}?per_page=100&page=${page}`).catch(notAllowed$1);
|
|
390
|
+
found.push(...listed);
|
|
391
|
+
if (listed.length < 100) break;
|
|
392
|
+
}
|
|
377
393
|
const ours = /* @__PURE__ */ new Map();
|
|
378
394
|
for (const schedule of found) {
|
|
379
395
|
if (!schedule.description?.startsWith("Goodfellow: build the site for a scheduled change at ")) continue;
|
|
@@ -398,14 +414,22 @@ function gitlabScheduledBuilds(api, project, branch, readCi, now = () => /* @__P
|
|
|
398
414
|
active: true
|
|
399
415
|
}
|
|
400
416
|
}).catch(notAllowed$1);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
417
|
+
try {
|
|
418
|
+
await gitlabRequest(api, `${base}/${created.id}/variables`, {
|
|
419
|
+
method: "POST",
|
|
420
|
+
body: {
|
|
421
|
+
key: SCHEDULED_CHANGE_VARIABLE,
|
|
422
|
+
value: time,
|
|
423
|
+
variable_type: "env_var"
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
await gitlabRequest(api, `${base}/${created.id}`, {
|
|
428
|
+
method: "DELETE",
|
|
429
|
+
allow: [403, 404]
|
|
430
|
+
}).catch(() => {});
|
|
431
|
+
notAllowed$1(error);
|
|
432
|
+
}
|
|
409
433
|
}
|
|
410
434
|
}
|
|
411
435
|
};
|
|
@@ -695,6 +719,10 @@ function gitlabCodeUpdates(options) {
|
|
|
695
719
|
//#endregion
|
|
696
720
|
//#region src/backend.ts
|
|
697
721
|
const PAGE_SIZE = 100;
|
|
722
|
+
/** The most files GitLab's compare lists, by default. */
|
|
723
|
+
const COMPARE_FILES_LIMIT = 1e3;
|
|
724
|
+
/** How many pages of drafts' branches and merge requests are read: a thousand of each. */
|
|
725
|
+
const REVIEW_PAGES = 10;
|
|
698
726
|
/** How many files one GraphQL request reads. */
|
|
699
727
|
const GRAPHQL_BATCH = 50;
|
|
700
728
|
/** The color and section codes in GitLab's job logs. */
|
|
@@ -847,11 +875,8 @@ var GitLabBackend = class GitLabBackend {
|
|
|
847
875
|
return (await response.json()).id;
|
|
848
876
|
},
|
|
849
877
|
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));
|
|
878
|
+
const query = new URLSearchParams({ search: `^${prefix}` });
|
|
879
|
+
return (await this.everyPage(this.repo(`/branches?${query}`))).map((found) => found.name).filter((name) => name.startsWith(prefix));
|
|
855
880
|
},
|
|
856
881
|
openReview: async ({ branch, title, description, reviewers = [] }) => {
|
|
857
882
|
const response = await gitlabRequest(this.api, `${project}/merge_requests`, {
|
|
@@ -880,11 +905,7 @@ var GitLabBackend = class GitLabBackend {
|
|
|
880
905
|
return { url: review.web_url };
|
|
881
906
|
},
|
|
882
907
|
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) => ({
|
|
908
|
+
return (await this.everyPage(`${project}/merge_requests?state=opened`)).filter((request) => request.source_branch.startsWith(prefix)).map((request) => ({
|
|
888
909
|
branch: request.source_branch,
|
|
889
910
|
url: request.web_url,
|
|
890
911
|
title: request.title
|
|
@@ -910,6 +931,16 @@ var GitLabBackend = class GitLabBackend {
|
|
|
910
931
|
async head() {
|
|
911
932
|
return (await gitlabJson(this.api, this.repo(`/branches/${encodeURIComponent(this.branch)}`))).commit.id;
|
|
912
933
|
}
|
|
934
|
+
/** Every item of one of GitLab's lists, page by page, up to `REVIEW_PAGES` pages. */
|
|
935
|
+
async everyPage(path) {
|
|
936
|
+
const items = [];
|
|
937
|
+
for (let page = 1; page <= REVIEW_PAGES; page++) {
|
|
938
|
+
const found = await gitlabJson(this.api, `${path}&per_page=${PAGE_SIZE}&page=${page}`);
|
|
939
|
+
items.push(...found);
|
|
940
|
+
if (found.length < PAGE_SIZE) break;
|
|
941
|
+
}
|
|
942
|
+
return items;
|
|
943
|
+
}
|
|
913
944
|
/** Every file in one folder at a commit, page by page. */
|
|
914
945
|
async listFolder(sha, dir) {
|
|
915
946
|
const files = [];
|
|
@@ -1132,12 +1163,13 @@ var GitLabBackend = class GitLabBackend {
|
|
|
1132
1163
|
return response.ok ? response.text() : void 0;
|
|
1133
1164
|
}
|
|
1134
1165
|
async changedPaths(from, to) {
|
|
1135
|
-
const
|
|
1166
|
+
const diffs = (await gitlabJson(this.api, this.repo(`/compare?${new URLSearchParams({
|
|
1136
1167
|
from,
|
|
1137
1168
|
to,
|
|
1138
1169
|
straight: "true"
|
|
1139
|
-
})}`));
|
|
1140
|
-
|
|
1170
|
+
})}`))).diffs ?? [];
|
|
1171
|
+
if (diffs.length >= COMPARE_FILES_LIMIT) throw new GitApiError(0, `More files changed between ${from} and ${to} than GitLab lists.`);
|
|
1172
|
+
return [...new Set(diffs.flatMap((diff) => [diff.old_path, diff.new_path]))];
|
|
1141
1173
|
}
|
|
1142
1174
|
/**
|
|
1143
1175
|
* Reads the commit's pipeline and external statuses (GitLab Pages jobs, or
|
|
@@ -1529,19 +1561,25 @@ function gitlab(options) {
|
|
|
1529
1561
|
}
|
|
1530
1562
|
async function connect(initial, remember) {
|
|
1531
1563
|
let credentials = initial;
|
|
1532
|
-
|
|
1564
|
+
let renewing;
|
|
1565
|
+
const renew = (current) => renewing ??= (async () => {
|
|
1566
|
+
credentials = toTokens(await requestTokens({
|
|
1567
|
+
client_id: options.clientId ?? "",
|
|
1568
|
+
refresh_token: current.refresh,
|
|
1569
|
+
grant_type: "refresh_token",
|
|
1570
|
+
redirect_uri: redirectUri(location().href()),
|
|
1571
|
+
code_verifier: current.verifier
|
|
1572
|
+
}), current.verifier);
|
|
1573
|
+
storage.save(JSON.stringify(credentials), remember);
|
|
1574
|
+
return credentials.access;
|
|
1575
|
+
})().finally(() => {
|
|
1576
|
+
renewing = void 0;
|
|
1577
|
+
});
|
|
1578
|
+
const token = async ({ refresh, stale } = {}) => {
|
|
1533
1579
|
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
|
-
}
|
|
1580
|
+
if (renewing) return renewing;
|
|
1581
|
+
if (refresh && stale !== void 0 && stale !== credentials.access) return credentials.access;
|
|
1582
|
+
if (refresh || credentials.expiresAt - 6e4 < Date.now()) return renew(credentials);
|
|
1545
1583
|
return credentials.access;
|
|
1546
1584
|
};
|
|
1547
1585
|
const api = {
|
|
@@ -1584,18 +1622,18 @@ function gitlab(options) {
|
|
|
1584
1622
|
const returnedState = url.searchParams.get("state");
|
|
1585
1623
|
const error = url.searchParams.get("error");
|
|
1586
1624
|
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
1625
|
for (const key of [
|
|
1592
1626
|
"code",
|
|
1593
1627
|
"state",
|
|
1594
1628
|
"error",
|
|
1595
1629
|
"error_description"
|
|
1596
1630
|
]) url.searchParams.delete(key);
|
|
1597
|
-
|
|
1631
|
+
const saved = pending.load();
|
|
1632
|
+
if (saved) url.hash = JSON.parse(saved).hash;
|
|
1598
1633
|
location().replace(url.toString());
|
|
1634
|
+
if (!saved) return null;
|
|
1635
|
+
pending.clear();
|
|
1636
|
+
const redirect = JSON.parse(saved);
|
|
1599
1637
|
if (error || !code || returnedState !== redirect.state) throw new SignInError("redirect-failed", "Signing in with GitLab was cancelled or didn't finish.");
|
|
1600
1638
|
return connect(toTokens(await requestTokens({
|
|
1601
1639
|
client_id: options.clientId ?? "",
|
package/dist/testing.d.ts
CHANGED
|
@@ -172,6 +172,8 @@ export declare function fakeGitLab(options: FakeGitLabOptions): {
|
|
|
172
172
|
requests: Request[];
|
|
173
173
|
commit: (changes: FileChange[], message: string, author?: string, branch?: string) => string;
|
|
174
174
|
authorize: (authorizeUrl: string) => string;
|
|
175
|
+
/** Ends every OAuth access token GitLab has given, as their two hours running out does; refresh tokens still work. */
|
|
176
|
+
expireAccessTokens: () => void;
|
|
175
177
|
handle: (request: Request) => Promise<Response>;
|
|
176
178
|
fetch: typeof fetch;
|
|
177
179
|
};
|
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);
|
|
@@ -515,7 +521,7 @@ function fakeGitLab(options) {
|
|
|
515
521
|
if (rest === "/repository/branches" && request.method === "GET") {
|
|
516
522
|
const search = url.searchParams.get("search") ?? "";
|
|
517
523
|
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]) => ({
|
|
524
|
+
return json(onePage(url, [...repo.branches].filter(([name]) => matches(name))).map(([name, sha]) => ({
|
|
519
525
|
name,
|
|
520
526
|
commit: { id: sha }
|
|
521
527
|
})));
|
|
@@ -564,7 +570,7 @@ function fakeGitLab(options) {
|
|
|
564
570
|
}
|
|
565
571
|
if (rest === "/merge_requests" && request.method === "GET") {
|
|
566
572
|
const source = url.searchParams.get("source_branch");
|
|
567
|
-
return json(repo.reviews.filter((review) => review.state === "open" && (!source || review.branch === source)).map((review) => ({
|
|
573
|
+
return json(onePage(url, repo.reviews.filter((review) => review.state === "open" && (!source || review.branch === source))).map((review) => ({
|
|
568
574
|
iid: review.number,
|
|
569
575
|
title: review.title,
|
|
570
576
|
source_branch: review.branch,
|
|
@@ -908,6 +914,10 @@ function fakeGitLab(options) {
|
|
|
908
914
|
requests,
|
|
909
915
|
commit,
|
|
910
916
|
authorize,
|
|
917
|
+
/** Ends every OAuth access token GitLab has given, as their two hours running out does; refresh tokens still work. */
|
|
918
|
+
expireAccessTokens: () => {
|
|
919
|
+
for (const [token] of tokens) if (token.startsWith("oauth-access-")) tokens.delete(token);
|
|
920
|
+
},
|
|
911
921
|
handle,
|
|
912
922
|
fetch: ((input, init) => handle(new Request(input, init)))
|
|
913
923
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/gitlab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Edit a Goodfellow site stored on GitLab, straight from the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@goodfellow-cms/core": "0.
|
|
38
|
+
"@goodfellow-cms/core": "0.8.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@puckeditor/core": "0.23.0"
|