@goodfellow-cms/gitlab 0.1.0 → 0.3.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/README.md +2 -2
- package/dist/index.d.ts +49 -2
- package/dist/index.js +778 -7
- package/dist/testing.d.ts +100 -1
- package/dist/testing.js +528 -10
- package/package.json +5 -5
package/dist/testing.js
CHANGED
|
@@ -24,14 +24,319 @@ function fakeGitLab(options) {
|
|
|
24
24
|
username: "editor",
|
|
25
25
|
name: "Test Editor"
|
|
26
26
|
} }));
|
|
27
|
+
let nextUserId = 1;
|
|
28
|
+
/** Every GitLab account, by username, with its id. */
|
|
29
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const name of [...[...tokens.values(), ...options.oauth ? [options.oauth.user] : []].map((user) => user.username), ...options.accounts ?? []]) if (!accounts.has(name)) accounts.set(name, nextUserId++);
|
|
31
|
+
const members = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const user of [...tokens.values(), ...options.oauth ? [options.oauth.user] : []]) {
|
|
33
|
+
const id = accounts.get(user.username) ?? 0;
|
|
34
|
+
members.set(id, {
|
|
35
|
+
id,
|
|
36
|
+
username: user.username,
|
|
37
|
+
name: user.name,
|
|
38
|
+
accessLevel: user.accessLevel ?? 40
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const invitations = /* @__PURE__ */ new Map();
|
|
42
|
+
/** Protected branches and who may push to them: GitLab protects the default branch for Maintainers. */
|
|
43
|
+
const protectedBranches = /* @__PURE__ */ new Map([[repo.defaultBranch, {
|
|
44
|
+
push: 40,
|
|
45
|
+
merge: 40,
|
|
46
|
+
forcePush: false
|
|
47
|
+
}]]);
|
|
48
|
+
/** The project's members and invitations, and its protected branches. */
|
|
49
|
+
async function handleMembers(request, url, rest, accessLevel) {
|
|
50
|
+
const forbidden = () => json({ message: "403 Forbidden" }, 403);
|
|
51
|
+
const list = (items) => {
|
|
52
|
+
const perPage = Number(url.searchParams.get("per_page") ?? 20);
|
|
53
|
+
const page = Number(url.searchParams.get("page") ?? 1);
|
|
54
|
+
return json(items.slice((page - 1) * perPage, page * perPage));
|
|
55
|
+
};
|
|
56
|
+
const shown = (member) => ({
|
|
57
|
+
id: member.id,
|
|
58
|
+
username: member.username,
|
|
59
|
+
name: member.name ?? member.username,
|
|
60
|
+
avatar_url: `https://avatars.example/${member.username}`,
|
|
61
|
+
access_level: member.accessLevel
|
|
62
|
+
});
|
|
63
|
+
if (rest === "/members/all" && request.method === "GET") return list([...members.values()].map(shown));
|
|
64
|
+
if (rest === "/members" && request.method === "GET") return list([...members.values()].filter((member) => !member.inherited).map(shown));
|
|
65
|
+
if (rest === "/members" && request.method === "POST") {
|
|
66
|
+
if (accessLevel < 40) return forbidden();
|
|
67
|
+
const body = await request.json();
|
|
68
|
+
const username = [...accounts].find(([, id]) => id === body.user_id)?.[0];
|
|
69
|
+
if (!username) return json({ message: "404 User Not Found" }, 404);
|
|
70
|
+
if (members.has(body.user_id)) return json({ message: "Member already exists" }, 409);
|
|
71
|
+
members.set(body.user_id, {
|
|
72
|
+
id: body.user_id,
|
|
73
|
+
username,
|
|
74
|
+
accessLevel: body.access_level
|
|
75
|
+
});
|
|
76
|
+
return json(shown(members.get(body.user_id)), 201);
|
|
77
|
+
}
|
|
78
|
+
const member = rest.match(/^\/members\/(\d+)$/);
|
|
79
|
+
if (member) {
|
|
80
|
+
if (accessLevel < 40) return forbidden();
|
|
81
|
+
const found = members.get(Number(member[1]));
|
|
82
|
+
if (!found || found.inherited) return json({ message: "404 Member Not Found" }, 404);
|
|
83
|
+
if (request.method === "DELETE") {
|
|
84
|
+
members.delete(found.id);
|
|
85
|
+
return new Response(null, { status: 204 });
|
|
86
|
+
}
|
|
87
|
+
if (request.method === "PUT") {
|
|
88
|
+
found.accessLevel = (await request.json()).access_level;
|
|
89
|
+
return json(shown(found));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (rest === "/invitations") {
|
|
93
|
+
if (accessLevel < 40) return forbidden();
|
|
94
|
+
if (request.method === "GET") return list([...invitations].map(([email, level]) => ({
|
|
95
|
+
invite_email: email,
|
|
96
|
+
access_level: level
|
|
97
|
+
})));
|
|
98
|
+
if (request.method === "POST") {
|
|
99
|
+
const body = await request.json();
|
|
100
|
+
if (invitations.has(body.email)) return json({
|
|
101
|
+
status: "error",
|
|
102
|
+
message: { [body.email]: "Invite email has already been taken" }
|
|
103
|
+
}, 201);
|
|
104
|
+
invitations.set(body.email, body.access_level);
|
|
105
|
+
return json({ status: "success" }, 201);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const invitation = rest.match(/^\/invitations\/([^/]+)$/);
|
|
109
|
+
if (invitation) {
|
|
110
|
+
if (accessLevel < 40) return forbidden();
|
|
111
|
+
const email = decodeURIComponent(invitation[1] ?? "");
|
|
112
|
+
if (!invitations.has(email)) return json({ message: "404 Not Found" }, 404);
|
|
113
|
+
if (request.method === "DELETE") {
|
|
114
|
+
invitations.delete(email);
|
|
115
|
+
return new Response(null, { status: 204 });
|
|
116
|
+
}
|
|
117
|
+
if (request.method === "PUT") {
|
|
118
|
+
invitations.set(email, (await request.json()).access_level);
|
|
119
|
+
return json({ invite_email: email });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const protection = rest.match(/^\/protected_branches\/([^/]+)$/);
|
|
123
|
+
if (protection) {
|
|
124
|
+
const name = decodeURIComponent(protection[1] ?? "");
|
|
125
|
+
const found = protectedBranches.get(name);
|
|
126
|
+
if (!found) return json({ message: "404 Not found" }, 404);
|
|
127
|
+
if (request.method === "GET") return json({
|
|
128
|
+
name,
|
|
129
|
+
push_access_levels: [{ access_level: found.push }],
|
|
130
|
+
merge_access_levels: [{ access_level: found.merge }],
|
|
131
|
+
allow_force_push: found.forcePush
|
|
132
|
+
});
|
|
133
|
+
if (request.method === "DELETE") {
|
|
134
|
+
if (accessLevel < 40) return forbidden();
|
|
135
|
+
protectedBranches.delete(name);
|
|
136
|
+
return new Response(null, { status: 204 });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (rest === "/protected_branches" && request.method === "POST") {
|
|
140
|
+
if (accessLevel < 40) return forbidden();
|
|
141
|
+
const body = await request.json();
|
|
142
|
+
if (protectedBranches.has(body.name)) return json({ message: "Protected branch already exists" }, 409);
|
|
143
|
+
protectedBranches.set(body.name, {
|
|
144
|
+
push: body.push_access_level ?? 40,
|
|
145
|
+
merge: body.merge_access_level ?? 40,
|
|
146
|
+
forcePush: body.allow_force_push ?? false
|
|
147
|
+
});
|
|
148
|
+
return json({ name: body.name }, 201);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
27
151
|
const codes = /* @__PURE__ */ new Map();
|
|
28
152
|
const refreshTokens = /* @__PURE__ */ new Map();
|
|
29
153
|
const pipelines = /* @__PURE__ */ new Map();
|
|
154
|
+
/** The project's settings for updates: whether job tokens may push, and its pipeline schedules. */
|
|
155
|
+
const updateSettings = {
|
|
156
|
+
jobTokenPush: false,
|
|
157
|
+
schedules: []
|
|
158
|
+
};
|
|
159
|
+
/** Pipelines the update ran in: scheduled, or started through the API, with the pages job's log. */
|
|
160
|
+
const updateRuns = [];
|
|
161
|
+
/** Simulates a scheduled (or API) pipeline whose pages job logged `trace`. */
|
|
162
|
+
function addUpdateRun(run) {
|
|
163
|
+
const added = {
|
|
164
|
+
id: 1e3 + updateRuns.length,
|
|
165
|
+
source: "schedule",
|
|
166
|
+
status: "success",
|
|
167
|
+
trace: "",
|
|
168
|
+
...run
|
|
169
|
+
};
|
|
170
|
+
updateRuns.push(added);
|
|
171
|
+
return added;
|
|
172
|
+
}
|
|
173
|
+
/** Failed jobs, by commit, which tests add with `failBuild()`. */
|
|
174
|
+
const failures = /* @__PURE__ */ new Map();
|
|
175
|
+
/**
|
|
176
|
+
* Simulates the pipeline for a commit failing: its `pages` job (or another,
|
|
177
|
+
* such as `pages:deploy`) fails for a GitLab failure reason, with a log.
|
|
178
|
+
*/
|
|
179
|
+
function failBuild(sha, failure = {}) {
|
|
180
|
+
if (!pipelines.has(sha)) pipelines.set(sha, { checks: 0 });
|
|
181
|
+
failures.set(sha, {
|
|
182
|
+
job: failure.job ?? "pages",
|
|
183
|
+
failureReason: failure.failureReason ?? "script_failure",
|
|
184
|
+
trace: failure.trace ?? ""
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const pipelineId = (sha) => [...pipelines.keys()].indexOf(sha) + 1;
|
|
30
188
|
const requests = [];
|
|
31
189
|
let counter = 0;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
190
|
+
let graphqlReads = 0;
|
|
191
|
+
const created = /* @__PURE__ */ new Map();
|
|
192
|
+
const pages = options.pages ? {
|
|
193
|
+
uniqueDomain: options.pages.uniqueDomain ?? true,
|
|
194
|
+
forceHttps: true,
|
|
195
|
+
primaryDomain: null,
|
|
196
|
+
domains: /* @__PURE__ */ new Map(),
|
|
197
|
+
dnsReady: /* @__PURE__ */ new Set()
|
|
198
|
+
} : void 0;
|
|
199
|
+
function pagesUrl() {
|
|
200
|
+
const [namespace = "", ...rest] = options.project.split("/");
|
|
201
|
+
return pages?.uniqueDomain ? `https://${rest.join("-")}-1a2b3c.gitlab.io` : `https://${namespace}.gitlab.io/${rest.join("/")}`;
|
|
202
|
+
}
|
|
203
|
+
/** GitLab Pages' settings and custom domains, for the site's own project. */
|
|
204
|
+
async function handlePages(request, rest, accessLevel) {
|
|
205
|
+
if (!rest.startsWith("/pages")) return void 0;
|
|
206
|
+
if (!pages) return json({ message: "404 Not Found" }, 404);
|
|
207
|
+
if (accessLevel < 40) return json({ message: "403 Forbidden" }, 403);
|
|
208
|
+
const method = request.method;
|
|
209
|
+
const domainJson = (name, domain) => ({
|
|
210
|
+
domain: name,
|
|
211
|
+
url: `https://${name}`,
|
|
212
|
+
verified: domain.verified,
|
|
213
|
+
verification_code: domain.verificationCode,
|
|
214
|
+
auto_ssl_enabled: true,
|
|
215
|
+
certificate: domain.certificate ? {
|
|
216
|
+
subject: `/CN=${name}`,
|
|
217
|
+
expired: false
|
|
218
|
+
} : null
|
|
219
|
+
});
|
|
220
|
+
if (rest === "/pages" && method === "GET") return json({
|
|
221
|
+
url: pagesUrl(),
|
|
222
|
+
is_unique_domain_enabled: pages.uniqueDomain,
|
|
223
|
+
force_https: pages.forceHttps,
|
|
224
|
+
primary_domain: pages.primaryDomain
|
|
225
|
+
});
|
|
226
|
+
if (rest === "/pages" && method === "PATCH") {
|
|
227
|
+
const body = await request.json();
|
|
228
|
+
if (body.pages_primary_domain !== void 0 && !pages.domains.has(body.pages_primary_domain)) return json({ message: "Primary domain must be one of the project's domains" }, 400);
|
|
229
|
+
if (body.pages_unique_domain_enabled !== void 0) pages.uniqueDomain = body.pages_unique_domain_enabled;
|
|
230
|
+
if (body.pages_https_only !== void 0) pages.forceHttps = body.pages_https_only;
|
|
231
|
+
if (body.pages_primary_domain !== void 0) pages.primaryDomain = body.pages_primary_domain;
|
|
232
|
+
return json({
|
|
233
|
+
url: pagesUrl(),
|
|
234
|
+
is_unique_domain_enabled: pages.uniqueDomain,
|
|
235
|
+
force_https: pages.forceHttps
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (rest === "/pages/domains" && method === "GET") return json([...pages.domains].map(([name, domain]) => domainJson(name, domain)));
|
|
239
|
+
if (rest === "/pages/domains" && method === "POST") {
|
|
240
|
+
const body = await request.json();
|
|
241
|
+
if (pages.domains.has(body.domain)) return json({ message: { domain: ["has already been taken"] } }, 400);
|
|
242
|
+
if (!/^[a-z0-9.-]+\.[a-z]+$/.test(body.domain)) return json({ message: { domain: ["is invalid"] } }, 400);
|
|
243
|
+
counter += 1;
|
|
244
|
+
const domain = {
|
|
245
|
+
verified: false,
|
|
246
|
+
verificationCode: `code-${counter}`,
|
|
247
|
+
certificate: false
|
|
248
|
+
};
|
|
249
|
+
pages.domains.set(body.domain, domain);
|
|
250
|
+
return json(domainJson(body.domain, domain), 201);
|
|
251
|
+
}
|
|
252
|
+
const one = rest.match(/^\/pages\/domains\/([^/]+)(\/verify)?$/);
|
|
253
|
+
const name = decodeURIComponent(one?.[1] ?? "");
|
|
254
|
+
const domain = pages.domains.get(name);
|
|
255
|
+
if (!one || !domain) return json({ message: "404 Not Found" }, 404);
|
|
256
|
+
if (one[2] && method === "PUT") {
|
|
257
|
+
if (!pages.dnsReady.has(name)) return json({ message: "Failed to verify domain ownership" }, 400);
|
|
258
|
+
domain.verified = true;
|
|
259
|
+
return json(domainJson(name, domain));
|
|
260
|
+
}
|
|
261
|
+
if (method === "GET") return json(domainJson(name, domain));
|
|
262
|
+
if (method === "DELETE") {
|
|
263
|
+
pages.domains.delete(name);
|
|
264
|
+
if (pages.primaryDomain === name) pages.primaryDomain = null;
|
|
265
|
+
return new Response(null, { status: 204 });
|
|
266
|
+
}
|
|
267
|
+
return json({ message: "404 Not Found" }, 404);
|
|
268
|
+
}
|
|
269
|
+
const groups = (options.groups ?? []).map((group, index) => ({
|
|
270
|
+
id: 100 + index,
|
|
271
|
+
path: group
|
|
272
|
+
}));
|
|
273
|
+
/** Creating projects, and committing to the projects created, as the setup page does. */
|
|
274
|
+
async function handleCreated(request, path, user) {
|
|
275
|
+
if (path === "/namespaces") return json([{
|
|
276
|
+
id: 1,
|
|
277
|
+
kind: "user",
|
|
278
|
+
full_path: user.username,
|
|
279
|
+
name: user.name ?? user.username
|
|
280
|
+
}, ...groups.map((group) => ({
|
|
281
|
+
id: group.id,
|
|
282
|
+
kind: "group",
|
|
283
|
+
full_path: group.path,
|
|
284
|
+
name: group.path
|
|
285
|
+
}))]);
|
|
286
|
+
if (path === "/projects" && request.method === "POST") {
|
|
287
|
+
const body = await request.json();
|
|
288
|
+
const namespace = body.namespace_id === void 0 || body.namespace_id === 1 ? user.username : groups.find((group) => group.id === body.namespace_id)?.path;
|
|
289
|
+
if (!namespace) return json({ message: "404 Namespace Not Found" }, 404);
|
|
290
|
+
if (!/^[\w.-]+$/.test(body.path)) return json({ message: { path: ["is invalid"] } }, 400);
|
|
291
|
+
const fullPath = `${namespace}/${body.path}`;
|
|
292
|
+
if (created.has(fullPath) || fullPath === options.project) return json({ message: { name: ["has already been taken"] } }, 400);
|
|
293
|
+
counter += 1;
|
|
294
|
+
const project = {
|
|
295
|
+
id: 1e3 + counter,
|
|
296
|
+
path: fullPath,
|
|
297
|
+
visibility: body.visibility,
|
|
298
|
+
description: body.description,
|
|
299
|
+
pagesAccessLevel: body.pages_access_level,
|
|
300
|
+
repo: new FakeRepo({})
|
|
301
|
+
};
|
|
302
|
+
created.set(fullPath, project);
|
|
303
|
+
return json({
|
|
304
|
+
id: project.id,
|
|
305
|
+
path_with_namespace: fullPath,
|
|
306
|
+
web_url: `${webUrl}/${fullPath}`
|
|
307
|
+
}, 201);
|
|
308
|
+
}
|
|
309
|
+
const match = path.match(/^\/projects\/([^/]+)(\/.*)?$/);
|
|
310
|
+
const key = decodeURIComponent(match?.[1] ?? "");
|
|
311
|
+
const project = created.get(key) ?? [...created.values()].find((candidate) => String(candidate.id) === key);
|
|
312
|
+
if (!match || !project) return void 0;
|
|
313
|
+
const rest = match[2] ?? "";
|
|
314
|
+
if (rest === "") return json({
|
|
315
|
+
id: project.id,
|
|
316
|
+
path_with_namespace: project.path,
|
|
317
|
+
default_branch: "main"
|
|
318
|
+
});
|
|
319
|
+
if (rest === "/repository/commits" && request.method === "POST") {
|
|
320
|
+
const body = await request.json();
|
|
321
|
+
return json({
|
|
322
|
+
id: project.repo.commit(body.actions.map((action) => action.encoding === "base64" ? {
|
|
323
|
+
path: action.file_path,
|
|
324
|
+
bytes: decodeBase64(action.content ?? "")
|
|
325
|
+
} : {
|
|
326
|
+
path: action.file_path,
|
|
327
|
+
content: action.content ?? ""
|
|
328
|
+
}), body.commit_message, { branch: body.branch }),
|
|
329
|
+
message: body.commit_message
|
|
330
|
+
}, 201);
|
|
331
|
+
}
|
|
332
|
+
return json({ message: "404 Not Found" }, 404);
|
|
333
|
+
}
|
|
334
|
+
function commit(changes, message, author, branch = repo.defaultBranch) {
|
|
335
|
+
const sha = repo.commit(changes, message, {
|
|
336
|
+
author,
|
|
337
|
+
branch
|
|
338
|
+
});
|
|
339
|
+
if (options.deployAfterChecks !== void 0 && branch === repo.defaultBranch) pipelines.set(sha, { checks: 0 });
|
|
35
340
|
return sha;
|
|
36
341
|
}
|
|
37
342
|
function issueTokens(user) {
|
|
@@ -89,6 +394,22 @@ function fakeGitLab(options) {
|
|
|
89
394
|
}
|
|
90
395
|
return json({ error: "unsupported_grant_type" }, 400);
|
|
91
396
|
}
|
|
397
|
+
if (url.pathname === "/api/graphql" && request.method === "POST") {
|
|
398
|
+
if (!tokens.has(request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "")) return json({ message: "401 Unauthorized" }, 401);
|
|
399
|
+
const { variables } = await request.json();
|
|
400
|
+
if (variables.project !== options.project) return json({ data: { project: null } });
|
|
401
|
+
graphqlReads += 1;
|
|
402
|
+
const files = repo.files(variables.ref);
|
|
403
|
+
return json({ data: { project: { repository: { blobs: { nodes: variables.paths.flatMap((path) => {
|
|
404
|
+
const content = files.get(path);
|
|
405
|
+
if (content === void 0) return [];
|
|
406
|
+
return [{
|
|
407
|
+
path,
|
|
408
|
+
rawTextBlob: typeof content === "string" ? content : null,
|
|
409
|
+
size: String(fakeFileBytes(content).length)
|
|
410
|
+
}];
|
|
411
|
+
}) } } } } });
|
|
412
|
+
}
|
|
92
413
|
if (!url.pathname.startsWith("/api/v4/")) return json({ message: "404 Not Found" }, 404);
|
|
93
414
|
const user = tokens.get(request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "");
|
|
94
415
|
if (!user) return json({ message: "401 Unauthorized" }, 401);
|
|
@@ -98,22 +419,89 @@ function fakeGitLab(options) {
|
|
|
98
419
|
name: user.name,
|
|
99
420
|
avatar_url: `https://avatars.example/${user.username}`
|
|
100
421
|
});
|
|
422
|
+
const setup = await handleCreated(request, path, user);
|
|
423
|
+
if (setup) return setup;
|
|
424
|
+
if (path === "/projects" && request.method === "GET") {
|
|
425
|
+
const level = members.get(accounts.get(user.username) ?? 0)?.accessLevel ?? 0;
|
|
426
|
+
const min = Number(url.searchParams.get("min_access_level") ?? "0");
|
|
427
|
+
return json(level > 0 && level >= min ? [{
|
|
428
|
+
path_with_namespace: options.project,
|
|
429
|
+
default_branch: repo.defaultBranch
|
|
430
|
+
}] : []);
|
|
431
|
+
}
|
|
432
|
+
if (path === "/users") {
|
|
433
|
+
const name = url.searchParams.get("username") ?? "";
|
|
434
|
+
const id = [...accounts].find(([username]) => username.toLowerCase() === name.toLowerCase())?.[1];
|
|
435
|
+
return json(id === void 0 ? [] : [{
|
|
436
|
+
id,
|
|
437
|
+
username: name
|
|
438
|
+
}]);
|
|
439
|
+
}
|
|
101
440
|
const projectMatch = path.match(/^\/projects\/([^/]+)(\/.*)?$/);
|
|
102
441
|
if (!projectMatch || decodeURIComponent(projectMatch[1] ?? "") !== options.project) return json({ message: "404 Project Not Found" }, 404);
|
|
103
442
|
const rest = projectMatch[2] ?? "";
|
|
104
|
-
const accessLevel = user.accessLevel ??
|
|
105
|
-
|
|
443
|
+
const accessLevel = members.get(accounts.get(user.username) ?? 0)?.accessLevel ?? 0;
|
|
444
|
+
const pagesResponse = await handlePages(request, rest, accessLevel);
|
|
445
|
+
if (pagesResponse) return pagesResponse;
|
|
446
|
+
const membersResponse = await handleMembers(request, url, rest, accessLevel);
|
|
447
|
+
if (membersResponse) return membersResponse;
|
|
448
|
+
if (rest === "" && request.method !== "PUT") return json({
|
|
106
449
|
default_branch: repo.defaultBranch,
|
|
107
450
|
permissions: {
|
|
108
451
|
project_access: { access_level: accessLevel },
|
|
109
452
|
group_access: null
|
|
110
|
-
}
|
|
453
|
+
},
|
|
454
|
+
ci_push_repository_for_job_token_allowed: updateSettings.jobTokenPush
|
|
111
455
|
});
|
|
456
|
+
if (rest === "/repository/branches" && request.method === "POST") {
|
|
457
|
+
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
458
|
+
const name = url.searchParams.get("branch") ?? "";
|
|
459
|
+
const ref = url.searchParams.get("ref") ?? "";
|
|
460
|
+
if (repo.branches.has(name)) return json({ message: "Branch already exists" }, 400);
|
|
461
|
+
const sha = repo.branches.get(ref) ?? (repo.commitAt(ref) ? ref : void 0);
|
|
462
|
+
if (!sha) return json({ message: "Invalid reference name" }, 400);
|
|
463
|
+
repo.branches.set(name, sha);
|
|
464
|
+
return json({
|
|
465
|
+
name,
|
|
466
|
+
commit: { id: sha }
|
|
467
|
+
}, 201);
|
|
468
|
+
}
|
|
112
469
|
const branch = rest.match(/^\/repository\/branches\/(.+)$/);
|
|
470
|
+
if (branch && request.method === "DELETE") {
|
|
471
|
+
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
472
|
+
const name = decodeURIComponent(branch[1] ?? "");
|
|
473
|
+
if (!repo.branches.has(name)) return json({ message: "404 Branch Not Found" }, 404);
|
|
474
|
+
repo.branches.delete(name);
|
|
475
|
+
return new Response(null, { status: 204 });
|
|
476
|
+
}
|
|
113
477
|
if (branch) {
|
|
114
478
|
const sha = repo.branches.get(decodeURIComponent(branch[1] ?? ""));
|
|
115
479
|
return sha ? json({ commit: { id: sha } }) : json({ message: "404 Branch Not Found" }, 404);
|
|
116
480
|
}
|
|
481
|
+
if (rest === "/repository/merge_base") {
|
|
482
|
+
const refs = url.searchParams.getAll("refs[]").map((ref) => repo.branches.get(ref) ?? ref);
|
|
483
|
+
if (refs.length !== 2 || refs.some((ref) => !repo.commitAt(ref))) return json({ message: "Could not find ref" }, 400);
|
|
484
|
+
const base = repo.mergeBase(refs[0] ?? "", refs[1] ?? "");
|
|
485
|
+
return base ? json({ id: base }) : json({ message: "404 Not Found" }, 404);
|
|
486
|
+
}
|
|
487
|
+
if (rest === "/merge_requests" && request.method === "POST") {
|
|
488
|
+
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
489
|
+
const body = await request.json();
|
|
490
|
+
if (!repo.branches.has(body.source_branch) || !repo.branches.has(body.target_branch)) return json({ message: ["Source branch does not exist"] }, 422);
|
|
491
|
+
if (repo.reviews.some((review) => review.branch === body.source_branch && review.state === "open")) return json({ message: ["Another open merge request already exists for this source branch"] }, 409);
|
|
492
|
+
const review = repo.openReview(body.source_branch, body.title, body.description ?? "", body.target_branch);
|
|
493
|
+
return json({
|
|
494
|
+
iid: review.number,
|
|
495
|
+
web_url: `${webUrl}/${options.project}/-/merge_requests/${review.number}`
|
|
496
|
+
}, 201);
|
|
497
|
+
}
|
|
498
|
+
if (rest === "/merge_requests" && request.method === "GET") {
|
|
499
|
+
const source = url.searchParams.get("source_branch");
|
|
500
|
+
return json(repo.reviews.filter((review) => review.state === "open" && (!source || review.branch === source)).map((review) => ({
|
|
501
|
+
iid: review.number,
|
|
502
|
+
web_url: `${webUrl}/${options.project}/-/merge_requests/${review.number}`
|
|
503
|
+
})));
|
|
504
|
+
}
|
|
117
505
|
if (rest === "/repository/tree") {
|
|
118
506
|
const ref = url.searchParams.get("ref") ?? repo.defaultBranch;
|
|
119
507
|
const dir = url.searchParams.get("path") ?? "";
|
|
@@ -133,6 +521,13 @@ function fakeGitLab(options) {
|
|
|
133
521
|
const content = repo.blobs.get(blob[1] ?? "");
|
|
134
522
|
return content === void 0 ? json({ message: "404 Blob Not Found" }, 404) : new Response(fakeFileBytes(content));
|
|
135
523
|
}
|
|
524
|
+
const raw = rest.match(/^\/repository\/files\/([^/]+)\/raw$/);
|
|
525
|
+
if (raw) {
|
|
526
|
+
const ref = url.searchParams.get("ref") ?? repo.defaultBranch;
|
|
527
|
+
if (!repo.commitAt(repo.branches.get(ref) ?? ref)) return json({ message: "404 Commit Not Found" }, 404);
|
|
528
|
+
const content = repo.files(ref).get(decodeURIComponent(raw[1] ?? ""));
|
|
529
|
+
return content === void 0 ? json({ message: "404 File Not Found" }, 404) : new Response(fakeFileBytes(content));
|
|
530
|
+
}
|
|
136
531
|
const file = rest.match(/^\/repository\/files\/([^/]+)$/);
|
|
137
532
|
if (file) {
|
|
138
533
|
const filePath = decodeURIComponent(file[1] ?? "");
|
|
@@ -145,6 +540,8 @@ function fakeGitLab(options) {
|
|
|
145
540
|
}
|
|
146
541
|
if (rest === "/repository/commits" && request.method === "POST") {
|
|
147
542
|
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
543
|
+
const pushTo = await request.clone().json();
|
|
544
|
+
if (accessLevel < (protectedBranches.get(pushTo.branch)?.push ?? 0)) return json({ message: "You are not allowed to push into this branch" }, 403);
|
|
148
545
|
const body = await request.json();
|
|
149
546
|
const files = repo.files(body.branch);
|
|
150
547
|
for (const action of body.actions) {
|
|
@@ -162,10 +559,25 @@ function fakeGitLab(options) {
|
|
|
162
559
|
} : {
|
|
163
560
|
path: action.file_path,
|
|
164
561
|
content: action.content ?? ""
|
|
165
|
-
}), body.commit_message),
|
|
562
|
+
}), body.commit_message, user.name ?? user.username, body.branch),
|
|
166
563
|
message: body.commit_message
|
|
167
564
|
}, 201);
|
|
168
565
|
}
|
|
566
|
+
if (rest === "/repository/commits" && request.method === "GET") {
|
|
567
|
+
const ref = url.searchParams.get("ref_name") ?? repo.defaultBranch;
|
|
568
|
+
const file = url.searchParams.get("path");
|
|
569
|
+
if (!repo.commitAt(repo.branches.get(ref) ?? ref)) return json({ message: "404 Reference Not Found" }, 404);
|
|
570
|
+
const perPage = Number(url.searchParams.get("per_page") ?? 20);
|
|
571
|
+
const page = Number(url.searchParams.get("page") ?? 1);
|
|
572
|
+
return json((file ? repo.history(file, ref) : []).slice((page - 1) * perPage, page * perPage).map((commit) => ({
|
|
573
|
+
id: commit.sha,
|
|
574
|
+
title: commit.message.split("\n")[0],
|
|
575
|
+
message: commit.message,
|
|
576
|
+
author_name: commit.author ?? "GitLab",
|
|
577
|
+
authored_date: commit.date,
|
|
578
|
+
committed_date: commit.date
|
|
579
|
+
})));
|
|
580
|
+
}
|
|
169
581
|
if (rest === "/repository/compare") return json({ diffs: repo.changedPaths(url.searchParams.get("from") ?? "", url.searchParams.get("to") ?? "").map((changedPath) => ({
|
|
170
582
|
old_path: changedPath,
|
|
171
583
|
new_path: changedPath
|
|
@@ -174,6 +586,15 @@ function fakeGitLab(options) {
|
|
|
174
586
|
if (statuses) {
|
|
175
587
|
const pipeline = pipelines.get(statuses[1] ?? "");
|
|
176
588
|
if (!pipeline) return json([]);
|
|
589
|
+
const failure = failures.get(statuses[1] ?? "");
|
|
590
|
+
if (failure) {
|
|
591
|
+
const id = pipelineId(statuses[1] ?? "");
|
|
592
|
+
return json([{
|
|
593
|
+
name: failure.job,
|
|
594
|
+
status: "failed",
|
|
595
|
+
target_url: `${webUrl}/${options.project}/-/jobs/${id}`
|
|
596
|
+
}]);
|
|
597
|
+
}
|
|
177
598
|
pipeline.checks += 1;
|
|
178
599
|
return json([{
|
|
179
600
|
name: "pages",
|
|
@@ -181,14 +602,111 @@ function fakeGitLab(options) {
|
|
|
181
602
|
target_url: `${webUrl}/${options.project}/-/jobs/1`
|
|
182
603
|
}]);
|
|
183
604
|
}
|
|
184
|
-
if (rest === "/pipelines") return json([...
|
|
185
|
-
id:
|
|
186
|
-
|
|
605
|
+
if (rest === "/pipelines" && url.searchParams.has("ref")) return json([...updateRuns].reverse().map((run) => ({
|
|
606
|
+
id: run.id,
|
|
607
|
+
source: run.source,
|
|
608
|
+
status: run.status,
|
|
609
|
+
created_at: "2026-01-05T04:17:00Z",
|
|
610
|
+
web_url: `${webUrl}/${options.project}/-/pipelines/${run.id}`
|
|
187
611
|
})));
|
|
612
|
+
if (rest === "/pipeline" && request.method === "POST") {
|
|
613
|
+
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
614
|
+
const run = addUpdateRun({
|
|
615
|
+
source: "api",
|
|
616
|
+
status: "created",
|
|
617
|
+
variables: (await request.json()).variables
|
|
618
|
+
});
|
|
619
|
+
return json({
|
|
620
|
+
id: run.id,
|
|
621
|
+
status: run.status
|
|
622
|
+
}, 201);
|
|
623
|
+
}
|
|
624
|
+
if (rest === "/pipeline_schedules") {
|
|
625
|
+
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
626
|
+
if (request.method === "POST") {
|
|
627
|
+
const body = await request.json();
|
|
628
|
+
const schedule = {
|
|
629
|
+
id: updateSettings.schedules.length + 1,
|
|
630
|
+
ref: body.ref,
|
|
631
|
+
cron: body.cron,
|
|
632
|
+
active: body.active ?? true
|
|
633
|
+
};
|
|
634
|
+
updateSettings.schedules.push(schedule);
|
|
635
|
+
return json(schedule, 201);
|
|
636
|
+
}
|
|
637
|
+
return json(updateSettings.schedules);
|
|
638
|
+
}
|
|
639
|
+
if (rest === "" && request.method === "PUT") {
|
|
640
|
+
if (accessLevel < 40) return json({ message: "403 Forbidden" }, 403);
|
|
641
|
+
const body = await request.json();
|
|
642
|
+
if (body.ci_push_repository_for_job_token_allowed !== void 0) updateSettings.jobTokenPush = body.ci_push_repository_for_job_token_allowed;
|
|
643
|
+
return json({ ci_push_repository_for_job_token_allowed: updateSettings.jobTokenPush });
|
|
644
|
+
}
|
|
645
|
+
const updateJobs = rest.match(/^\/pipelines\/(\d{4,})\/jobs$/);
|
|
646
|
+
if (updateJobs) {
|
|
647
|
+
const run = updateRuns.find((candidate) => candidate.id === Number(updateJobs[1]));
|
|
648
|
+
return run ? json([{
|
|
649
|
+
id: run.id,
|
|
650
|
+
name: "pages",
|
|
651
|
+
status: run.status
|
|
652
|
+
}]) : json({ message: "404 Not Found" }, 404);
|
|
653
|
+
}
|
|
654
|
+
const updateTrace = rest.match(/^\/jobs\/(\d{4,})\/trace$/);
|
|
655
|
+
if (updateTrace) {
|
|
656
|
+
const run = updateRuns.find((candidate) => candidate.id === Number(updateTrace[1]));
|
|
657
|
+
return run ? new Response(run.trace) : json({ message: "404 Not Found" }, 404);
|
|
658
|
+
}
|
|
659
|
+
if (rest === "/pipelines") {
|
|
660
|
+
const sha = url.searchParams.get("sha");
|
|
661
|
+
return json([...pipelines.keys()].map((pipelineSha, index) => ({
|
|
662
|
+
id: index + 1,
|
|
663
|
+
sha: pipelineSha
|
|
664
|
+
})).filter((pipeline) => !sha || pipeline.sha === sha));
|
|
665
|
+
}
|
|
666
|
+
const pipelineJobs = rest.match(/^\/pipelines\/(\d+)\/jobs$/);
|
|
667
|
+
if (pipelineJobs) {
|
|
668
|
+
const id = Number(pipelineJobs[1]);
|
|
669
|
+
const sha = [...pipelines.keys()][id - 1];
|
|
670
|
+
if (!sha) return json({ message: "404 Not Found" }, 404);
|
|
671
|
+
const failure = failures.get(sha);
|
|
672
|
+
return json([failure ? {
|
|
673
|
+
id,
|
|
674
|
+
name: failure.job,
|
|
675
|
+
status: "failed",
|
|
676
|
+
failure_reason: failure.failureReason
|
|
677
|
+
} : {
|
|
678
|
+
id,
|
|
679
|
+
name: "pages",
|
|
680
|
+
status: "success"
|
|
681
|
+
}]);
|
|
682
|
+
}
|
|
683
|
+
const trace = rest.match(/^\/jobs\/(\d+)\/trace$/);
|
|
684
|
+
if (trace) {
|
|
685
|
+
const sha = [...pipelines.keys()][Number(trace[1]) - 1];
|
|
686
|
+
const failure = sha ? failures.get(sha) : void 0;
|
|
687
|
+
return failure ? new Response(failure.trace) : json({ message: "404 Not Found" }, 404);
|
|
688
|
+
}
|
|
188
689
|
return json({ message: "404 Not Found" }, 404);
|
|
189
690
|
}
|
|
190
691
|
return {
|
|
692
|
+
/** How many GraphQL requests read files, as `readFiles()` makes. */
|
|
693
|
+
get graphqlReads() {
|
|
694
|
+
return graphqlReads;
|
|
695
|
+
},
|
|
696
|
+
/** The project's members by user id, invitations by email, and protected branches, which tests can change. */
|
|
697
|
+
members,
|
|
698
|
+
invitations,
|
|
699
|
+
protectedBranches,
|
|
700
|
+
failBuild,
|
|
701
|
+
/** The project's update settings, and the pipelines the update ran in, which tests add with `addUpdateRun()`. */
|
|
702
|
+
updateSettings,
|
|
703
|
+
updateRuns,
|
|
704
|
+
addUpdateRun,
|
|
191
705
|
repo,
|
|
706
|
+
/** Projects created through the API, by full path. */
|
|
707
|
+
created,
|
|
708
|
+
/** The site's GitLab Pages, if it has them, which tests can change. */
|
|
709
|
+
pages,
|
|
192
710
|
requests,
|
|
193
711
|
commit,
|
|
194
712
|
authorize,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/gitlab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Edit a Goodfellow site stored on GitLab, straight from the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"gitlab",
|
|
11
11
|
"git-based-cms"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://
|
|
14
|
-
"bugs": "https://github.com/
|
|
13
|
+
"homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/hosts",
|
|
14
|
+
"bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/
|
|
18
|
+
"url": "git+https://github.com/goodfellow-cms/goodfellow-cms.git",
|
|
19
19
|
"directory": "packages/gitlab"
|
|
20
20
|
},
|
|
21
21
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@goodfellow-cms/core": "0.
|
|
38
|
+
"@goodfellow-cms/core": "0.3.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@puckeditor/core": "0.23.0"
|