gitlab-mcp 1.4.0 → 1.5.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 +45 -34
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +17 -0
- package/dist/config/env.js.map +1 -1
- package/dist/http-app.js +422 -17
- package/dist/http-app.js.map +1 -1
- package/dist/http.js +10 -1
- package/dist/http.js.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/download-token.d.ts +24 -0
- package/dist/lib/download-token.js +84 -0
- package/dist/lib/download-token.js.map +1 -0
- package/dist/lib/gitlab-client.d.ts +78 -1
- package/dist/lib/gitlab-client.js +451 -7
- package/dist/lib/gitlab-client.js.map +1 -1
- package/dist/lib/http-auth-guard.d.ts +8 -0
- package/dist/lib/http-auth-guard.js +19 -0
- package/dist/lib/http-auth-guard.js.map +1 -0
- package/dist/lib/mcp-oauth-provider.d.ts +2 -0
- package/dist/lib/mcp-oauth-provider.js +61 -0
- package/dist/lib/mcp-oauth-provider.js.map +1 -0
- package/dist/lib/oauth.d.ts +3 -1
- package/dist/lib/oauth.js +5 -5
- package/dist/lib/oauth.js.map +1 -1
- package/dist/lib/patch-helper.d.ts +13 -0
- package/dist/lib/patch-helper.js +156 -0
- package/dist/lib/patch-helper.js.map +1 -0
- package/dist/lib/request-runtime.d.ts +1 -0
- package/dist/lib/request-runtime.js +42 -4
- package/dist/lib/request-runtime.js.map +1 -1
- package/dist/lib/tool-schema.js +1 -1
- package/dist/lib/tool-schema.js.map +1 -1
- package/dist/tools/gitlab.js +2690 -52
- package/dist/tools/gitlab.js.map +1 -1
- package/docs/authentication.md +37 -8
- package/docs/configuration.md +7 -4
- package/docs/tools.md +153 -51
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ export class GitLabClient {
|
|
|
21
21
|
apiUrls;
|
|
22
22
|
nextApiUrlIndex = 0;
|
|
23
23
|
defaultToken;
|
|
24
|
+
defaultAuthHeader;
|
|
24
25
|
timeoutMs;
|
|
25
26
|
maxAttachmentBytes;
|
|
26
27
|
maxLocalFileBytes;
|
|
@@ -33,6 +34,7 @@ export class GitLabClient {
|
|
|
33
34
|
.filter((item) => item.length > 0) ?? [this.baseApiUrl];
|
|
34
35
|
this.apiUrls = configuredApiUrls.length > 0 ? configuredApiUrls : [this.baseApiUrl];
|
|
35
36
|
this.defaultToken = defaultToken;
|
|
37
|
+
this.defaultAuthHeader = options.defaultAuthHeader;
|
|
36
38
|
this.timeoutMs = options.timeoutMs ?? 20_000;
|
|
37
39
|
this.maxAttachmentBytes =
|
|
38
40
|
options.maxAttachmentBytes ?? GitLabClient.DEFAULT_MAX_ATTACHMENT_BYTES;
|
|
@@ -58,6 +60,16 @@ export class GitLabClient {
|
|
|
58
60
|
}
|
|
59
61
|
});
|
|
60
62
|
}
|
|
63
|
+
createGroup(payload, options = {}) {
|
|
64
|
+
return this.post("/groups", {
|
|
65
|
+
...options,
|
|
66
|
+
body: JSON.stringify(payload),
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
...(options.headers ?? {})
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
61
73
|
listProjectMembers(projectId, options = {}) {
|
|
62
74
|
return this.get(`/projects/${encode(projectId)}/members/all`, options);
|
|
63
75
|
}
|
|
@@ -95,6 +107,16 @@ export class GitLabClient {
|
|
|
95
107
|
}
|
|
96
108
|
});
|
|
97
109
|
}
|
|
110
|
+
searchCode(search, options = {}) {
|
|
111
|
+
return this.get("/search", {
|
|
112
|
+
...options,
|
|
113
|
+
query: {
|
|
114
|
+
scope: "blobs",
|
|
115
|
+
search,
|
|
116
|
+
...(options.query ?? {})
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
98
120
|
searchCodeBlobs(projectId, search, options = {}) {
|
|
99
121
|
return this.get(`/projects/${encode(projectId)}/search`, {
|
|
100
122
|
...options,
|
|
@@ -105,9 +127,67 @@ export class GitLabClient {
|
|
|
105
127
|
}
|
|
106
128
|
});
|
|
107
129
|
}
|
|
130
|
+
searchGroupCodeBlobs(groupId, search, options = {}) {
|
|
131
|
+
return this.get(`/groups/${encode(groupId)}/search`, {
|
|
132
|
+
...options,
|
|
133
|
+
query: {
|
|
134
|
+
scope: "blobs",
|
|
135
|
+
search,
|
|
136
|
+
...(options.query ?? {})
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
108
140
|
// repository/files
|
|
109
|
-
getRepositoryTree(projectId, options = {}) {
|
|
110
|
-
|
|
141
|
+
async getRepositoryTree(projectId, options = {}) {
|
|
142
|
+
const config = this.resolveRequestConfig(options);
|
|
143
|
+
const url = new URL(`projects/${encode(projectId)}/repository/tree`, `${config.apiUrl}/`);
|
|
144
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
145
|
+
if (value !== undefined && value !== null) {
|
|
146
|
+
url.searchParams.set(key, String(value));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const response = await this.fetchRawResponse(url, {
|
|
150
|
+
method: "GET",
|
|
151
|
+
headers: {
|
|
152
|
+
Accept: "application/json",
|
|
153
|
+
...(options.headers ?? {})
|
|
154
|
+
},
|
|
155
|
+
token: config.token,
|
|
156
|
+
authHeader: config.authHeader
|
|
157
|
+
});
|
|
158
|
+
let body;
|
|
159
|
+
try {
|
|
160
|
+
body = await this.parseResponseBody(response);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (response.ok) {
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, {
|
|
167
|
+
message: error instanceof Error ? error.message : "Failed to read GitLab error response"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, body);
|
|
172
|
+
}
|
|
173
|
+
const usesKeyset = options.query?.pagination === "keyset";
|
|
174
|
+
const nextPageToken = response.headers.get("x-next-page-token") ??
|
|
175
|
+
(usesKeyset ? response.headers.get("x-next-page") : null) ??
|
|
176
|
+
undefined;
|
|
177
|
+
if (!usesKeyset && !nextPageToken) {
|
|
178
|
+
return body;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
items: Array.isArray(body) ? body : [],
|
|
182
|
+
...(nextPageToken
|
|
183
|
+
? {
|
|
184
|
+
next_page_token: nextPageToken,
|
|
185
|
+
pagination_note: "Pass next_page_token as page_token with pagination=keyset to retrieve the next page."
|
|
186
|
+
}
|
|
187
|
+
: {
|
|
188
|
+
pagination_note: "No next_page_token was returned; this is the final keyset page."
|
|
189
|
+
})
|
|
190
|
+
};
|
|
111
191
|
}
|
|
112
192
|
getFileContents(projectId, filePath, ref, options = {}) {
|
|
113
193
|
return this.get(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}`, {
|
|
@@ -118,6 +198,15 @@ export class GitLabClient {
|
|
|
118
198
|
}
|
|
119
199
|
});
|
|
120
200
|
}
|
|
201
|
+
getFileBlame(projectId, filePath, ref, options = {}) {
|
|
202
|
+
return this.get(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}/blame`, {
|
|
203
|
+
...options,
|
|
204
|
+
query: {
|
|
205
|
+
ref,
|
|
206
|
+
...(options.query ?? {})
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
121
210
|
createOrUpdateFile(projectId, filePath, payload, options = {}) {
|
|
122
211
|
return this.put(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}`, {
|
|
123
212
|
...options,
|
|
@@ -144,6 +233,15 @@ export class GitLabClient {
|
|
|
144
233
|
query: payload
|
|
145
234
|
});
|
|
146
235
|
}
|
|
236
|
+
listBranches(projectId, options = {}) {
|
|
237
|
+
return this.get(`/projects/${encode(projectId)}/repository/branches`, options);
|
|
238
|
+
}
|
|
239
|
+
getBranch(projectId, branch, options = {}) {
|
|
240
|
+
return this.get(`/projects/${encode(projectId)}/repository/branches/${encode(branch)}`, options);
|
|
241
|
+
}
|
|
242
|
+
deleteBranch(projectId, branch, options = {}) {
|
|
243
|
+
return this.delete(`/projects/${encode(projectId)}/repository/branches/${encode(branch)}`, options);
|
|
244
|
+
}
|
|
147
245
|
getBranchDiffs(projectId, payload, options = {}) {
|
|
148
246
|
return this.get(`/projects/${encode(projectId)}/repository/compare`, {
|
|
149
247
|
...options,
|
|
@@ -159,6 +257,18 @@ export class GitLabClient {
|
|
|
159
257
|
getCommitDiff(projectId, sha, options = {}) {
|
|
160
258
|
return this.get(`/projects/${encode(projectId)}/repository/commits/${encode(sha)}/diff`, options);
|
|
161
259
|
}
|
|
260
|
+
listCommitStatuses(projectId, sha, options = {}) {
|
|
261
|
+
return this.get(`/projects/${encode(projectId)}/repository/commits/${encode(sha)}/statuses`, options);
|
|
262
|
+
}
|
|
263
|
+
createCommitStatus(projectId, sha, payload, options = {}) {
|
|
264
|
+
return this.post(`/projects/${encode(projectId)}/statuses/${encode(sha)}`, {
|
|
265
|
+
...options,
|
|
266
|
+
query: {
|
|
267
|
+
...payload,
|
|
268
|
+
...(options.query ?? {})
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
162
272
|
// merge requests
|
|
163
273
|
listMergeRequests(projectId, options = {}) {
|
|
164
274
|
return this.get(`/projects/${encode(projectId)}/merge_requests`, options);
|
|
@@ -167,7 +277,59 @@ export class GitLabClient {
|
|
|
167
277
|
return this.get("/merge_requests", options);
|
|
168
278
|
}
|
|
169
279
|
getMergeRequest(projectId, mergeRequestIid, options = {}) {
|
|
170
|
-
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}`,
|
|
280
|
+
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}`, {
|
|
281
|
+
...options,
|
|
282
|
+
query: {
|
|
283
|
+
include_diverged_commits_count: true,
|
|
284
|
+
...(options.query ?? {})
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async countMergeRequestCommits(projectId, mergeRequestIid, options = {}) {
|
|
289
|
+
const config = this.resolveRequestConfig(options);
|
|
290
|
+
let page = 1;
|
|
291
|
+
let totalCount = 0;
|
|
292
|
+
while (true) {
|
|
293
|
+
const url = new URL(`projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/commits`, `${config.apiUrl}/`);
|
|
294
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
295
|
+
if (value !== undefined && value !== null) {
|
|
296
|
+
url.searchParams.set(key, String(value));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!url.searchParams.has("per_page")) {
|
|
300
|
+
url.searchParams.set("per_page", "100");
|
|
301
|
+
}
|
|
302
|
+
url.searchParams.set("page", String(page));
|
|
303
|
+
const response = await this.fetchRawResponse(url, {
|
|
304
|
+
method: "GET",
|
|
305
|
+
headers: {
|
|
306
|
+
Accept: "application/json",
|
|
307
|
+
...(options.headers ?? {})
|
|
308
|
+
},
|
|
309
|
+
token: config.token,
|
|
310
|
+
authHeader: config.authHeader
|
|
311
|
+
});
|
|
312
|
+
const body = await this.parseApiResponse(response);
|
|
313
|
+
if (!Array.isArray(body)) {
|
|
314
|
+
throw new Error("Unexpected merge request commits response format");
|
|
315
|
+
}
|
|
316
|
+
totalCount += body.length;
|
|
317
|
+
const nextPage = response.headers.get("x-next-page");
|
|
318
|
+
if (!nextPage) {
|
|
319
|
+
return totalCount;
|
|
320
|
+
}
|
|
321
|
+
const parsedNextPage = Number.parseInt(nextPage, 10);
|
|
322
|
+
if (!Number.isFinite(parsedNextPage) || parsedNextPage <= page) {
|
|
323
|
+
return totalCount;
|
|
324
|
+
}
|
|
325
|
+
page = parsedNextPage;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
listMergeRequestCommits(projectId, mergeRequestIid, options = {}) {
|
|
329
|
+
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/commits`, options);
|
|
330
|
+
}
|
|
331
|
+
listMergeRequestPipelines(projectId, mergeRequestIid, options = {}) {
|
|
332
|
+
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/pipelines`, options);
|
|
171
333
|
}
|
|
172
334
|
createMergeRequest(projectId, payload, options = {}) {
|
|
173
335
|
return this.post(`/projects/${encode(projectId)}/merge_requests`, {
|
|
@@ -231,8 +393,28 @@ export class GitLabClient {
|
|
|
231
393
|
}
|
|
232
394
|
});
|
|
233
395
|
}
|
|
234
|
-
getMergeRequestApprovalState(projectId, mergeRequestIid, options = {}) {
|
|
235
|
-
|
|
396
|
+
async getMergeRequestApprovalState(projectId, mergeRequestIid, options = {}) {
|
|
397
|
+
const config = this.resolveRequestConfig(options);
|
|
398
|
+
const url = new URL(`projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approval_state`, `${config.apiUrl}/`);
|
|
399
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
400
|
+
if (value !== undefined && value !== null) {
|
|
401
|
+
url.searchParams.set(key, String(value));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const response = await this.fetchRawResponse(url, {
|
|
405
|
+
method: "GET",
|
|
406
|
+
headers: {
|
|
407
|
+
Accept: "application/json",
|
|
408
|
+
...(options.headers ?? {})
|
|
409
|
+
},
|
|
410
|
+
token: config.token,
|
|
411
|
+
authHeader: config.authHeader
|
|
412
|
+
});
|
|
413
|
+
if (response.status === 404) {
|
|
414
|
+
const approvals = await this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approvals`, options);
|
|
415
|
+
return normalizeMergeRequestApprovalsFallback(approvals);
|
|
416
|
+
}
|
|
417
|
+
return normalizeMergeRequestApprovalState(await this.parseApiResponse(response));
|
|
236
418
|
}
|
|
237
419
|
getMergeRequestConflicts(projectId, mergeRequestIid, options = {}) {
|
|
238
420
|
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/conflicts`, options);
|
|
@@ -299,6 +481,34 @@ export class GitLabClient {
|
|
|
299
481
|
}
|
|
300
482
|
});
|
|
301
483
|
}
|
|
484
|
+
listMergeRequestEmojiReactions(projectId, mergeRequestIid, options = {}) {
|
|
485
|
+
return this.get(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid), options);
|
|
486
|
+
}
|
|
487
|
+
createMergeRequestEmojiReaction(projectId, mergeRequestIid, name, options = {}) {
|
|
488
|
+
return this.createAwardEmoji(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid), name, options);
|
|
489
|
+
}
|
|
490
|
+
deleteMergeRequestEmojiReaction(projectId, mergeRequestIid, awardId, options = {}) {
|
|
491
|
+
return this.delete(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid, { awardId }), options);
|
|
492
|
+
}
|
|
493
|
+
listMergeRequestNoteEmojiReactions(projectId, mergeRequestIid, noteId, payload = {}, options = {}) {
|
|
494
|
+
return this.get(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid, {
|
|
495
|
+
noteId,
|
|
496
|
+
discussionId: payload.discussion_id
|
|
497
|
+
}), options);
|
|
498
|
+
}
|
|
499
|
+
createMergeRequestNoteEmojiReaction(projectId, mergeRequestIid, noteId, payload, options = {}) {
|
|
500
|
+
return this.createAwardEmoji(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid, {
|
|
501
|
+
noteId,
|
|
502
|
+
discussionId: payload.discussion_id
|
|
503
|
+
}), payload.name, options);
|
|
504
|
+
}
|
|
505
|
+
deleteMergeRequestNoteEmojiReaction(projectId, mergeRequestIid, noteId, payload, options = {}) {
|
|
506
|
+
return this.delete(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid, {
|
|
507
|
+
noteId,
|
|
508
|
+
discussionId: payload.discussion_id,
|
|
509
|
+
awardId: payload.award_id
|
|
510
|
+
}), options);
|
|
511
|
+
}
|
|
302
512
|
getDraftNote(projectId, mergeRequestIid, draftNoteId, options = {}) {
|
|
303
513
|
return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}`, options);
|
|
304
514
|
}
|
|
@@ -424,6 +634,15 @@ export class GitLabClient {
|
|
|
424
634
|
}
|
|
425
635
|
});
|
|
426
636
|
}
|
|
637
|
+
listTodos(options = {}) {
|
|
638
|
+
return this.get("/todos", options);
|
|
639
|
+
}
|
|
640
|
+
markTodoDone(todoId, options = {}) {
|
|
641
|
+
return this.post(`/todos/${encode(todoId)}/mark_as_done`, options);
|
|
642
|
+
}
|
|
643
|
+
markAllTodosDone(options = {}) {
|
|
644
|
+
return this.post("/todos/mark_as_done", options);
|
|
645
|
+
}
|
|
427
646
|
listIssueDiscussions(projectId, issueIid, options = {}) {
|
|
428
647
|
return this.get(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/discussions`, options);
|
|
429
648
|
}
|
|
@@ -443,6 +662,34 @@ export class GitLabClient {
|
|
|
443
662
|
}
|
|
444
663
|
});
|
|
445
664
|
}
|
|
665
|
+
listIssueEmojiReactions(projectId, issueIid, options = {}) {
|
|
666
|
+
return this.get(this.awardEmojiPath("issues", projectId, issueIid), options);
|
|
667
|
+
}
|
|
668
|
+
createIssueEmojiReaction(projectId, issueIid, name, options = {}) {
|
|
669
|
+
return this.createAwardEmoji(this.awardEmojiPath("issues", projectId, issueIid), name, options);
|
|
670
|
+
}
|
|
671
|
+
deleteIssueEmojiReaction(projectId, issueIid, awardId, options = {}) {
|
|
672
|
+
return this.delete(this.awardEmojiPath("issues", projectId, issueIid, { awardId }), options);
|
|
673
|
+
}
|
|
674
|
+
listIssueNoteEmojiReactions(projectId, issueIid, noteId, payload = {}, options = {}) {
|
|
675
|
+
return this.get(this.awardEmojiPath("issues", projectId, issueIid, {
|
|
676
|
+
noteId,
|
|
677
|
+
discussionId: payload.discussion_id
|
|
678
|
+
}), options);
|
|
679
|
+
}
|
|
680
|
+
createIssueNoteEmojiReaction(projectId, issueIid, noteId, payload, options = {}) {
|
|
681
|
+
return this.createAwardEmoji(this.awardEmojiPath("issues", projectId, issueIid, {
|
|
682
|
+
noteId,
|
|
683
|
+
discussionId: payload.discussion_id
|
|
684
|
+
}), payload.name, options);
|
|
685
|
+
}
|
|
686
|
+
deleteIssueNoteEmojiReaction(projectId, issueIid, noteId, payload, options = {}) {
|
|
687
|
+
return this.delete(this.awardEmojiPath("issues", projectId, issueIid, {
|
|
688
|
+
noteId,
|
|
689
|
+
discussionId: payload.discussion_id,
|
|
690
|
+
awardId: payload.award_id
|
|
691
|
+
}), options);
|
|
692
|
+
}
|
|
446
693
|
updateIssueNote(projectId, issueIid, discussionId, noteId, payload, options = {}) {
|
|
447
694
|
return this.put(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`, {
|
|
448
695
|
...options,
|
|
@@ -502,6 +749,35 @@ export class GitLabClient {
|
|
|
502
749
|
deleteWikiPage(projectId, slug, options = {}) {
|
|
503
750
|
return this.delete(`/projects/${encode(projectId)}/wikis/${encode(slug)}`, options);
|
|
504
751
|
}
|
|
752
|
+
listGroupWikiPages(groupId, options = {}) {
|
|
753
|
+
return this.get(`/groups/${encode(groupId)}/wikis`, options);
|
|
754
|
+
}
|
|
755
|
+
getGroupWikiPage(groupId, slug, options = {}) {
|
|
756
|
+
return this.get(`/groups/${encode(groupId)}/wikis/${encode(slug)}`, options);
|
|
757
|
+
}
|
|
758
|
+
createGroupWikiPage(groupId, payload, options = {}) {
|
|
759
|
+
return this.post(`/groups/${encode(groupId)}/wikis`, {
|
|
760
|
+
...options,
|
|
761
|
+
body: JSON.stringify(payload),
|
|
762
|
+
headers: {
|
|
763
|
+
"Content-Type": "application/json",
|
|
764
|
+
...(options.headers ?? {})
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
updateGroupWikiPage(groupId, slug, payload, options = {}) {
|
|
769
|
+
return this.put(`/groups/${encode(groupId)}/wikis/${encode(slug)}`, {
|
|
770
|
+
...options,
|
|
771
|
+
body: JSON.stringify(payload),
|
|
772
|
+
headers: {
|
|
773
|
+
"Content-Type": "application/json",
|
|
774
|
+
...(options.headers ?? {})
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
deleteGroupWikiPage(groupId, slug, options = {}) {
|
|
779
|
+
return this.delete(`/groups/${encode(groupId)}/wikis/${encode(slug)}`, options);
|
|
780
|
+
}
|
|
505
781
|
// pipelines
|
|
506
782
|
listPipelines(projectId, options = {}) {
|
|
507
783
|
return this.get(`/projects/${encode(projectId)}/pipelines`, options);
|
|
@@ -533,6 +809,19 @@ export class GitLabClient {
|
|
|
533
809
|
getPipelineJobOutput(projectId, jobId, options = {}) {
|
|
534
810
|
return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/trace`, options);
|
|
535
811
|
}
|
|
812
|
+
validateCiLint(projectId, payload, options = {}) {
|
|
813
|
+
return this.post(`/projects/${encode(projectId)}/ci/lint`, {
|
|
814
|
+
...options,
|
|
815
|
+
body: JSON.stringify(payload),
|
|
816
|
+
headers: {
|
|
817
|
+
"Content-Type": "application/json",
|
|
818
|
+
...(options.headers ?? {})
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
validateProjectCiLint(projectId, options = {}) {
|
|
823
|
+
return this.get(`/projects/${encode(projectId)}/ci/lint`, options);
|
|
824
|
+
}
|
|
536
825
|
listJobArtifacts(projectId, jobId, options = {}) {
|
|
537
826
|
return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/artifacts/tree`, options);
|
|
538
827
|
}
|
|
@@ -675,8 +964,37 @@ export class GitLabClient {
|
|
|
675
964
|
return this.post(`/projects/${encode(projectId)}/releases/${encode(tagName)}/evidence`, options);
|
|
676
965
|
}
|
|
677
966
|
downloadReleaseAsset(projectId, tagName, directAssetPath, options = {}) {
|
|
967
|
+
const requestConfig = this.resolveRequestConfig(options);
|
|
678
968
|
const safePath = encodeSlashPath(directAssetPath);
|
|
679
|
-
|
|
969
|
+
const url = new URL(`projects/${encode(projectId)}/releases/${encode(tagName)}/downloads/${safePath}`, `${requestConfig.apiUrl}/`);
|
|
970
|
+
return this.downloadFile(url, {
|
|
971
|
+
headers: options.headers,
|
|
972
|
+
token: requestConfig.token,
|
|
973
|
+
authHeader: requestConfig.authHeader
|
|
974
|
+
}, "Release asset", path.basename(directAssetPath) || "release-asset");
|
|
975
|
+
}
|
|
976
|
+
// tags
|
|
977
|
+
listTags(projectId, options = {}) {
|
|
978
|
+
return this.get(`/projects/${encode(projectId)}/repository/tags`, options);
|
|
979
|
+
}
|
|
980
|
+
getTag(projectId, tagName, options = {}) {
|
|
981
|
+
return this.get(`/projects/${encode(projectId)}/repository/tags/${encode(tagName)}`, options);
|
|
982
|
+
}
|
|
983
|
+
createTag(projectId, payload, options = {}) {
|
|
984
|
+
return this.post(`/projects/${encode(projectId)}/repository/tags`, {
|
|
985
|
+
...options,
|
|
986
|
+
body: JSON.stringify(payload),
|
|
987
|
+
headers: {
|
|
988
|
+
"Content-Type": "application/json",
|
|
989
|
+
...(options.headers ?? {})
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
deleteTag(projectId, tagName, options = {}) {
|
|
994
|
+
return this.delete(`/projects/${encode(projectId)}/repository/tags/${encode(tagName)}`, options);
|
|
995
|
+
}
|
|
996
|
+
getTagSignature(projectId, tagName, options = {}) {
|
|
997
|
+
return this.get(`/projects/${encode(projectId)}/repository/tags/${encode(tagName)}/signature`, options);
|
|
680
998
|
}
|
|
681
999
|
// labels
|
|
682
1000
|
listLabels(projectId, options = {}) {
|
|
@@ -730,12 +1048,24 @@ export class GitLabClient {
|
|
|
730
1048
|
getUsers(options = {}) {
|
|
731
1049
|
return this.get("/users", options);
|
|
732
1050
|
}
|
|
1051
|
+
getUser(userId, options = {}) {
|
|
1052
|
+
return this.get(`/users/${encode(userId)}`, options);
|
|
1053
|
+
}
|
|
1054
|
+
whoami(options = {}) {
|
|
1055
|
+
return this.get("/user", options);
|
|
1056
|
+
}
|
|
733
1057
|
listEvents(options = {}) {
|
|
734
1058
|
return this.get("/events", options);
|
|
735
1059
|
}
|
|
736
1060
|
getProjectEvents(projectId, options = {}) {
|
|
737
1061
|
return this.get(`/projects/${encode(projectId)}/events`, options);
|
|
738
1062
|
}
|
|
1063
|
+
listWebhooks(scope, options = {}) {
|
|
1064
|
+
return this.get(`${this.webhookBasePath(scope)}/hooks`, options);
|
|
1065
|
+
}
|
|
1066
|
+
listWebhookEvents(scope, hookId, options = {}) {
|
|
1067
|
+
return this.get(`${this.webhookBasePath(scope)}/hooks/${encode(hookId)}/events`, options);
|
|
1068
|
+
}
|
|
739
1069
|
// attachments / markdown
|
|
740
1070
|
uploadMarkdown(projectId, content, filename, options = {}) {
|
|
741
1071
|
const form = new FormData();
|
|
@@ -1003,6 +1333,24 @@ export class GitLabClient {
|
|
|
1003
1333
|
}
|
|
1004
1334
|
return body;
|
|
1005
1335
|
}
|
|
1336
|
+
async parseApiResponse(response) {
|
|
1337
|
+
let body;
|
|
1338
|
+
try {
|
|
1339
|
+
body = await this.parseResponseBody(response);
|
|
1340
|
+
}
|
|
1341
|
+
catch (error) {
|
|
1342
|
+
if (response.ok) {
|
|
1343
|
+
throw error;
|
|
1344
|
+
}
|
|
1345
|
+
throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, {
|
|
1346
|
+
message: error instanceof Error ? error.message : "Failed to read GitLab error response"
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
if (!response.ok) {
|
|
1350
|
+
throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, body);
|
|
1351
|
+
}
|
|
1352
|
+
return body;
|
|
1353
|
+
}
|
|
1006
1354
|
async parseResponseBody(response) {
|
|
1007
1355
|
assertContentLengthWithinLimit(response, this.maxResponseBodyBytes, "Response body");
|
|
1008
1356
|
const text = await readResponseTextWithLimit(response, this.maxResponseBodyBytes, "Response body");
|
|
@@ -1021,7 +1369,7 @@ export class GitLabClient {
|
|
|
1021
1369
|
const sessionAuth = getSessionAuth();
|
|
1022
1370
|
const apiUrl = options.apiUrl ?? sessionAuth?.apiUrl ?? this.pickApiUrl();
|
|
1023
1371
|
const token = options.token ?? sessionAuth?.token ?? this.defaultToken;
|
|
1024
|
-
const authHeader = options.authHeader ?? sessionAuth?.header;
|
|
1372
|
+
const authHeader = options.authHeader ?? sessionAuth?.header ?? this.defaultAuthHeader;
|
|
1025
1373
|
return {
|
|
1026
1374
|
apiUrl: normalizeApiUrl(apiUrl),
|
|
1027
1375
|
token,
|
|
@@ -1036,6 +1384,38 @@ export class GitLabClient {
|
|
|
1036
1384
|
this.nextApiUrlIndex = (this.nextApiUrlIndex + 1) % this.apiUrls.length;
|
|
1037
1385
|
return this.apiUrls[index] ?? this.baseApiUrl;
|
|
1038
1386
|
}
|
|
1387
|
+
webhookBasePath(scope) {
|
|
1388
|
+
if (scope.projectId) {
|
|
1389
|
+
return `/projects/${encode(scope.projectId)}`;
|
|
1390
|
+
}
|
|
1391
|
+
if (scope.groupId) {
|
|
1392
|
+
return `/groups/${encode(scope.groupId)}`;
|
|
1393
|
+
}
|
|
1394
|
+
throw new Error("Either projectId or groupId is required");
|
|
1395
|
+
}
|
|
1396
|
+
awardEmojiPath(entity, projectId, entityIid, options = {}) {
|
|
1397
|
+
let path = `/projects/${encode(projectId)}/${entity}/${encode(entityIid)}`;
|
|
1398
|
+
if (options.noteId) {
|
|
1399
|
+
path += options.discussionId
|
|
1400
|
+
? `/discussions/${encode(options.discussionId)}/notes/${encode(options.noteId)}`
|
|
1401
|
+
: `/notes/${encode(options.noteId)}`;
|
|
1402
|
+
}
|
|
1403
|
+
path += "/award_emoji";
|
|
1404
|
+
if (options.awardId) {
|
|
1405
|
+
path += `/${encode(options.awardId)}`;
|
|
1406
|
+
}
|
|
1407
|
+
return path;
|
|
1408
|
+
}
|
|
1409
|
+
createAwardEmoji(path, name, options) {
|
|
1410
|
+
return this.post(path, {
|
|
1411
|
+
...options,
|
|
1412
|
+
body: JSON.stringify({ name }),
|
|
1413
|
+
headers: {
|
|
1414
|
+
"Content-Type": "application/json",
|
|
1415
|
+
...(options.headers ?? {})
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1039
1419
|
resolveAbsoluteUrl(raw, apiUrl) {
|
|
1040
1420
|
if (/^https?:\/\//i.test(raw)) {
|
|
1041
1421
|
return new URL(raw);
|
|
@@ -1093,6 +1473,70 @@ function encodeSlashPath(pathValue) {
|
|
|
1093
1473
|
.map((segment) => encode(segment))
|
|
1094
1474
|
.join("/");
|
|
1095
1475
|
}
|
|
1476
|
+
function normalizeMergeRequestApprovalState(value) {
|
|
1477
|
+
if (!isRecord(value)) {
|
|
1478
|
+
return value;
|
|
1479
|
+
}
|
|
1480
|
+
const approvedBy = uniqueApprovalUsers(extractApprovalRules(value.rules).flatMap((rule) => extractApprovalUsers(rule.approved_by)));
|
|
1481
|
+
return {
|
|
1482
|
+
...value,
|
|
1483
|
+
approved_by: approvedBy,
|
|
1484
|
+
approved_by_usernames: approvedBy
|
|
1485
|
+
.map((user) => user.username)
|
|
1486
|
+
.filter((username) => typeof username === "string"),
|
|
1487
|
+
source_endpoint: "approval_state"
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function normalizeMergeRequestApprovalsFallback(value) {
|
|
1491
|
+
if (!isRecord(value)) {
|
|
1492
|
+
return value;
|
|
1493
|
+
}
|
|
1494
|
+
const approvedBy = uniqueApprovalUsers(extractApprovalEntries(value.approved_by).flatMap((entry) => [entry.user]));
|
|
1495
|
+
return {
|
|
1496
|
+
approved: typeof value.approved === "boolean" ? value.approved : undefined,
|
|
1497
|
+
user_has_approved: typeof value.user_has_approved === "boolean" ? value.user_has_approved : undefined,
|
|
1498
|
+
user_can_approve: typeof value.user_can_approve === "boolean" ? value.user_can_approve : undefined,
|
|
1499
|
+
approved_by: approvedBy,
|
|
1500
|
+
approved_by_usernames: approvedBy
|
|
1501
|
+
.map((user) => user.username)
|
|
1502
|
+
.filter((username) => typeof username === "string"),
|
|
1503
|
+
source_endpoint: "approvals"
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function extractApprovalRules(value) {
|
|
1507
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
1508
|
+
}
|
|
1509
|
+
function extractApprovalUsers(value) {
|
|
1510
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
1511
|
+
}
|
|
1512
|
+
function extractApprovalEntries(value) {
|
|
1513
|
+
if (!Array.isArray(value)) {
|
|
1514
|
+
return [];
|
|
1515
|
+
}
|
|
1516
|
+
return value.filter((item) => isRecord(item) && isRecord(item.user));
|
|
1517
|
+
}
|
|
1518
|
+
function uniqueApprovalUsers(users) {
|
|
1519
|
+
const seen = new Set();
|
|
1520
|
+
const unique = [];
|
|
1521
|
+
for (const user of users) {
|
|
1522
|
+
const id = user.id;
|
|
1523
|
+
const username = user.username;
|
|
1524
|
+
const key = typeof id === "string" || typeof id === "number"
|
|
1525
|
+
? `id:${id}`
|
|
1526
|
+
: typeof username === "string"
|
|
1527
|
+
? `username:${username}`
|
|
1528
|
+
: undefined;
|
|
1529
|
+
if (!key || seen.has(key)) {
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
seen.add(key);
|
|
1533
|
+
unique.push(user);
|
|
1534
|
+
}
|
|
1535
|
+
return unique;
|
|
1536
|
+
}
|
|
1537
|
+
function isRecord(value) {
|
|
1538
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1539
|
+
}
|
|
1096
1540
|
function normalizeApiUrl(rawUrl) {
|
|
1097
1541
|
const url = new URL(rawUrl);
|
|
1098
1542
|
const pathname = url.pathname.replace(/\/+$/, "");
|