@zereight/mcp-gitlab 2.0.0-beta.0 → 2.0.3

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.
@@ -1,1649 +0,0 @@
1
- import { z } from "zod";
2
- import { URL } from "url";
3
- import fs from "fs";
4
- import path from "path";
5
- import { config } from "./config.js";
6
- import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, GitLabNamespaceSchema, GitLabNamespaceExistsResponseSchema, GitLabProjectSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabDiffSchema, GitLabIssueLinkSchema, GitLabIssueWithLinkDetailsSchema, GitLabDiscussionNoteSchema, GitLabDiscussionSchema, PaginatedDiscussionsResponseSchema, GitLabWikiPageSchema, GitLabTreeItemSchema, GitLabPipelineSchema, GitLabPipelineJobSchema, GitLabMilestonesSchema, GitLabCompareResultSchema, GitLabDraftNoteSchema, GitLabProjectMemberSchema, GitLabMarkdownUploadSchema, } from "./schemas.js";
7
- import { GitlabSession } from "./gitlabsession.js";
8
- export class GitlabHandler extends GitlabSession {
9
- /**
10
- * Utility function for handling GitLab API errors
11
- */
12
- async handleGitLabError(response) {
13
- if (!response.ok) {
14
- const errorBody = await response.text();
15
- // Check specifically for Rate Limit error
16
- if (response.status === 403 && errorBody.includes("User API Key Rate limit exceeded")) {
17
- console.error("GitLab API Rate Limit Exceeded:", errorBody);
18
- console.log("User API Key Rate limit exceeded. Please try again later.");
19
- throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
20
- }
21
- else {
22
- // Handle other API errors
23
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
24
- }
25
- }
26
- }
27
- /**
28
- * Get effective project ID based on config or provided ID
29
- */
30
- getEffectiveProjectId(projectId) {
31
- return config.GITLAB_PROJECT_ID || projectId;
32
- }
33
- /**
34
- * Create a fork of a GitLab project
35
- */
36
- async forkProject(projectId, namespace) {
37
- projectId = decodeURIComponent(projectId);
38
- const effectiveProjectId = this.getEffectiveProjectId(projectId);
39
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/fork`);
40
- if (namespace) {
41
- url.searchParams.append("namespace", namespace);
42
- }
43
- const response = await this.fetch(url.toString(), {
44
- method: "POST",
45
- });
46
- if (response.status === 409) {
47
- throw new Error("Project already exists in the target namespace");
48
- }
49
- await this.handleGitLabError(response);
50
- const data = await response.json();
51
- return GitLabForkSchema.parse(data);
52
- }
53
- /**
54
- * Create a new branch in a GitLab project
55
- */
56
- async createBranch(projectId, options) {
57
- projectId = decodeURIComponent(projectId);
58
- const effectiveProjectId = this.getEffectiveProjectId(projectId);
59
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/branches`);
60
- const response = await this.fetch(url.toString(), {
61
- method: "POST",
62
- body: JSON.stringify({
63
- branch: options.name,
64
- ref: options.ref,
65
- }),
66
- });
67
- await this.handleGitLabError(response);
68
- return GitLabReferenceSchema.parse(await response.json());
69
- }
70
- /**
71
- * Get the default branch for a GitLab project
72
- */
73
- async getDefaultBranchRef(projectId) {
74
- projectId = decodeURIComponent(projectId);
75
- const effectiveProjectId = this.getEffectiveProjectId(projectId);
76
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}`);
77
- const response = await this.fetch(url.toString(), {});
78
- await this.handleGitLabError(response);
79
- const project = GitLabRepositorySchema.parse(await response.json());
80
- return project.default_branch ?? "main";
81
- }
82
- /**
83
- * Get the contents of a file from a GitLab project
84
- */
85
- async getFileContents(projectId, filePath, ref) {
86
- projectId = decodeURIComponent(projectId);
87
- const effectiveProjectId = this.getEffectiveProjectId(projectId);
88
- const encodedPath = encodeURIComponent(filePath);
89
- if (!ref) {
90
- ref = await this.getDefaultBranchRef(projectId);
91
- }
92
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/files/${encodedPath}`);
93
- url.searchParams.append("ref", ref);
94
- const response = await this.fetch(url.toString(), {});
95
- if (response.status === 404) {
96
- throw new Error(`File not found: ${filePath}`);
97
- }
98
- await this.handleGitLabError(response);
99
- const data = await response.json();
100
- const parsedData = GitLabContentSchema.parse(data);
101
- // Decode Base64 content to UTF-8
102
- if (!Array.isArray(parsedData) && parsedData.content) {
103
- parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
104
- parsedData.encoding = "utf8";
105
- }
106
- return parsedData;
107
- }
108
- /**
109
- * Create a new issue in a GitLab project
110
- */
111
- async createIssue(projectId, options) {
112
- projectId = decodeURIComponent(projectId);
113
- const effectiveProjectId = this.getEffectiveProjectId(projectId);
114
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
115
- const response = await this.fetch(url.toString(), {
116
- method: "POST",
117
- body: JSON.stringify({
118
- title: options.title,
119
- description: options.description,
120
- assignee_ids: options.assignee_ids,
121
- milestone_id: options.milestone_id,
122
- labels: options.labels?.join(","),
123
- }),
124
- });
125
- if (response.status === 400) {
126
- const errorBody = await response.text();
127
- throw new Error(`Invalid request: ${errorBody}`);
128
- }
129
- await this.handleGitLabError(response);
130
- const data = await response.json();
131
- return GitLabIssueSchema.parse(data);
132
- }
133
- /**
134
- * List issues in a GitLab project
135
- */
136
- async listIssues(projectId, options = {}) {
137
- projectId = decodeURIComponent(projectId);
138
- const effectiveProjectId = this.getEffectiveProjectId(projectId);
139
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
140
- Object.entries(options).forEach(([key, value]) => {
141
- if (value !== undefined && value !== null) {
142
- const keys = ["labels", "assignee_username"];
143
- if (keys.includes(key)) {
144
- if (Array.isArray(value)) {
145
- value.forEach(label => {
146
- url.searchParams.append(`${key}[]`, label.toString());
147
- });
148
- }
149
- else {
150
- url.searchParams.append(`${key}[]`, value.toString());
151
- }
152
- }
153
- else {
154
- url.searchParams.append(key, value.toString());
155
- }
156
- }
157
- });
158
- const response = await this.fetch(url.toString(), {});
159
- await this.handleGitLabError(response);
160
- const data = await response.json();
161
- return z.array(GitLabIssueSchema).parse(data);
162
- }
163
- /**
164
- * List merge requests in a GitLab project with optional filtering
165
- */
166
- async listMergeRequests(projectId, options = {}) {
167
- projectId = decodeURIComponent(projectId);
168
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests`);
169
- Object.entries(options).forEach(([key, value]) => {
170
- if (value !== undefined) {
171
- if (key === "labels" && Array.isArray(value)) {
172
- url.searchParams.append(key, value.join(","));
173
- }
174
- else {
175
- url.searchParams.append(key, value.toString());
176
- }
177
- }
178
- });
179
- const response = await this.fetch(url.toString(), {});
180
- await this.handleGitLabError(response);
181
- const data = await response.json();
182
- return z.array(GitLabMergeRequestSchema).parse(data);
183
- }
184
- /**
185
- * Get a single issue from a GitLab project
186
- */
187
- async getIssue(projectId, issueIid) {
188
- projectId = decodeURIComponent(projectId);
189
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}`);
190
- const response = await this.fetch(url.toString(), {});
191
- await this.handleGitLabError(response);
192
- const data = await response.json();
193
- return GitLabIssueSchema.parse(data);
194
- }
195
- /**
196
- * Update an issue in a GitLab project
197
- */
198
- async updateIssue(projectId, issueIid, options) {
199
- projectId = decodeURIComponent(projectId);
200
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}`);
201
- const body = { ...options };
202
- if (body.labels && Array.isArray(body.labels)) {
203
- body.labels = body.labels.join(",");
204
- }
205
- const response = await this.fetch(url.toString(), {
206
- method: "PUT",
207
- body: JSON.stringify(body),
208
- });
209
- await this.handleGitLabError(response);
210
- const data = await response.json();
211
- return GitLabIssueSchema.parse(data);
212
- }
213
- /**
214
- * Delete an issue from a GitLab project
215
- */
216
- async deleteIssue(projectId, issueIid) {
217
- projectId = decodeURIComponent(projectId);
218
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}`);
219
- const response = await this.fetch(url.toString(), {
220
- method: "DELETE",
221
- });
222
- await this.handleGitLabError(response);
223
- }
224
- /**
225
- * List all issue links for a specific issue
226
- */
227
- async listIssueLinks(projectId, issueIid) {
228
- projectId = decodeURIComponent(projectId);
229
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}/links`);
230
- const response = await this.fetch(url.toString(), {});
231
- await this.handleGitLabError(response);
232
- const data = await response.json();
233
- return z.array(GitLabIssueWithLinkDetailsSchema).parse(data);
234
- }
235
- /**
236
- * Get a specific issue link
237
- */
238
- async getIssueLink(projectId, issueIid, issueLinkId) {
239
- projectId = decodeURIComponent(projectId);
240
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}/links/${issueLinkId}`);
241
- const response = await this.fetch(url.toString(), {});
242
- await this.handleGitLabError(response);
243
- const data = await response.json();
244
- return GitLabIssueLinkSchema.parse(data);
245
- }
246
- /**
247
- * Create an issue link between two issues
248
- */
249
- async createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
250
- projectId = decodeURIComponent(projectId);
251
- targetProjectId = decodeURIComponent(targetProjectId);
252
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}/links`);
253
- const response = await this.fetch(url.toString(), {
254
- method: "POST",
255
- body: JSON.stringify({
256
- target_project_id: targetProjectId,
257
- target_issue_iid: targetIssueIid,
258
- link_type: linkType,
259
- }),
260
- });
261
- await this.handleGitLabError(response);
262
- const data = await response.json();
263
- return GitLabIssueLinkSchema.parse(data);
264
- }
265
- /**
266
- * Delete an issue link
267
- */
268
- async deleteIssueLink(projectId, issueIid, issueLinkId) {
269
- projectId = decodeURIComponent(projectId);
270
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}/links/${issueLinkId}`);
271
- const response = await this.fetch(url.toString(), {
272
- method: "DELETE",
273
- });
274
- await this.handleGitLabError(response);
275
- }
276
- /**
277
- * Create a new merge request in a GitLab project
278
- */
279
- async createMergeRequest(projectId, options) {
280
- projectId = decodeURIComponent(projectId);
281
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests`);
282
- const response = await this.fetch(url.toString(), {
283
- method: "POST",
284
- body: JSON.stringify({
285
- title: options.title,
286
- description: options.description,
287
- source_branch: options.source_branch,
288
- target_branch: options.target_branch,
289
- assignee_ids: options.assignee_ids,
290
- reviewer_ids: options.reviewer_ids,
291
- labels: options.labels?.join(","),
292
- allow_collaboration: options.allow_collaboration,
293
- draft: options.draft,
294
- remove_source_branch: options.remove_source_branch,
295
- squash: options.squash,
296
- }),
297
- });
298
- if (response.status === 400) {
299
- const errorBody = await response.text();
300
- throw new Error(`Invalid request: ${errorBody}`);
301
- }
302
- if (!response.ok) {
303
- const errorBody = await response.text();
304
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
305
- }
306
- const data = await response.json();
307
- return GitLabMergeRequestSchema.parse(data);
308
- }
309
- /**
310
- * Shared helper function for listing discussions
311
- */
312
- async listDiscussions(projectId, resourceType, resourceIid, options = {}) {
313
- projectId = decodeURIComponent(projectId);
314
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/${resourceType}/${resourceIid}/discussions`);
315
- if (options.page) {
316
- url.searchParams.append("page", options.page.toString());
317
- }
318
- if (options.per_page) {
319
- url.searchParams.append("per_page", options.per_page.toString());
320
- }
321
- const response = await this.fetch(url.toString(), {});
322
- await this.handleGitLabError(response);
323
- const discussions = await response.json();
324
- const pagination = {
325
- x_next_page: response.headers.get("x-next-page")
326
- ? parseInt(response.headers.get("x-next-page"))
327
- : null,
328
- x_page: response.headers.get("x-page") ? parseInt(response.headers.get("x-page")) : undefined,
329
- x_per_page: response.headers.get("x-per-page")
330
- ? parseInt(response.headers.get("x-per-page"))
331
- : undefined,
332
- x_prev_page: response.headers.get("x-prev-page")
333
- ? parseInt(response.headers.get("x-prev-page"))
334
- : null,
335
- x_total: response.headers.get("x-total") ? parseInt(response.headers.get("x-total")) : null,
336
- x_total_pages: response.headers.get("x-total-pages")
337
- ? parseInt(response.headers.get("x-total-pages"))
338
- : null,
339
- };
340
- return PaginatedDiscussionsResponseSchema.parse({
341
- items: discussions,
342
- pagination: pagination,
343
- });
344
- }
345
- /**
346
- * List merge request discussion items
347
- */
348
- async listMergeRequestDiscussions(projectId, mergeRequestIid, options = {}) {
349
- return this.listDiscussions(projectId, "merge_requests", mergeRequestIid, options);
350
- }
351
- /**
352
- * List discussions for an issue
353
- */
354
- async listIssueDiscussions(projectId, issueIid, options = {}) {
355
- return this.listDiscussions(projectId, "issues", issueIid, options);
356
- }
357
- /**
358
- * Modify an existing merge request thread note
359
- */
360
- async updateMergeRequestNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
361
- projectId = decodeURIComponent(projectId);
362
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
363
- const payload = {};
364
- if (body !== undefined) {
365
- payload.body = body;
366
- }
367
- else if (resolved !== undefined) {
368
- payload.resolved = resolved;
369
- }
370
- const response = await this.fetch(url.toString(), {
371
- method: "PUT",
372
- body: JSON.stringify(payload),
373
- });
374
- await this.handleGitLabError(response);
375
- const data = await response.json();
376
- return GitLabDiscussionNoteSchema.parse(data);
377
- }
378
- /**
379
- * Update an issue discussion note
380
- */
381
- async updateIssueNote(projectId, issueIid, discussionId, noteId, body) {
382
- projectId = decodeURIComponent(projectId);
383
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}/discussions/${discussionId}/notes/${noteId}`);
384
- const payload = { body };
385
- const response = await this.fetch(url.toString(), {
386
- method: "PUT",
387
- body: JSON.stringify(payload),
388
- });
389
- await this.handleGitLabError(response);
390
- const data = await response.json();
391
- return GitLabDiscussionNoteSchema.parse(data);
392
- }
393
- /**
394
- * Create a note in an issue discussion
395
- */
396
- async createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
397
- projectId = decodeURIComponent(projectId);
398
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/issues/${issueIid}/discussions/${discussionId}/notes`);
399
- const payload = { body };
400
- if (createdAt) {
401
- payload.created_at = createdAt;
402
- }
403
- const response = await this.fetch(url.toString(), {
404
- method: "POST",
405
- body: JSON.stringify(payload),
406
- });
407
- await this.handleGitLabError(response);
408
- const data = await response.json();
409
- return GitLabDiscussionNoteSchema.parse(data);
410
- }
411
- /**
412
- * Add a new note to an existing merge request thread
413
- */
414
- async createMergeRequestNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
415
- projectId = decodeURIComponent(projectId);
416
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes`);
417
- const payload = { body };
418
- if (createdAt) {
419
- payload.created_at = createdAt;
420
- }
421
- const response = await this.fetch(url.toString(), {
422
- method: "POST",
423
- body: JSON.stringify(payload),
424
- });
425
- await this.handleGitLabError(response);
426
- const data = await response.json();
427
- return GitLabDiscussionNoteSchema.parse(data);
428
- }
429
- /**
430
- * Create or update a file in a GitLab project
431
- */
432
- async createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id) {
433
- projectId = decodeURIComponent(projectId);
434
- const encodedPath = encodeURIComponent(filePath);
435
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/repository/files/${encodedPath}`);
436
- const body = {
437
- branch,
438
- content,
439
- commit_message: commitMessage,
440
- encoding: "text",
441
- ...(previousPath ? { previous_path: previousPath } : {}),
442
- };
443
- let method = "POST";
444
- try {
445
- const fileData = await this.getFileContents(projectId, filePath, branch);
446
- method = "PUT";
447
- if (!Array.isArray(fileData)) {
448
- if (!commit_id && fileData.commit_id) {
449
- body.commit_id = fileData.commit_id;
450
- }
451
- else if (commit_id) {
452
- body.commit_id = commit_id;
453
- }
454
- if (!last_commit_id && fileData.last_commit_id) {
455
- body.last_commit_id = fileData.last_commit_id;
456
- }
457
- else if (last_commit_id) {
458
- body.last_commit_id = last_commit_id;
459
- }
460
- }
461
- }
462
- catch (error) {
463
- if (!(error instanceof Error && error.message.includes("File not found"))) {
464
- throw error;
465
- }
466
- if (commit_id) {
467
- body.commit_id = commit_id;
468
- }
469
- if (last_commit_id) {
470
- body.last_commit_id = last_commit_id;
471
- }
472
- }
473
- const response = await this.fetch(url.toString(), {
474
- method,
475
- body: JSON.stringify(body),
476
- });
477
- if (!response.ok) {
478
- const errorBody = await response.text();
479
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
480
- }
481
- const data = await response.json();
482
- return GitLabCreateUpdateFileResponseSchema.parse(data);
483
- }
484
- /**
485
- * Create a tree structure in a GitLab project repository
486
- */
487
- async createTree(projectId, files, ref) {
488
- projectId = decodeURIComponent(projectId);
489
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/repository/tree`);
490
- if (ref) {
491
- url.searchParams.append("ref", ref);
492
- }
493
- const response = await this.fetch(url.toString(), {
494
- method: "POST",
495
- body: JSON.stringify({
496
- files: files.map(file => ({
497
- file_path: file.path,
498
- content: file.content,
499
- encoding: "text",
500
- })),
501
- }),
502
- });
503
- if (response.status === 400) {
504
- const errorBody = await response.text();
505
- throw new Error(`Invalid request: ${errorBody}`);
506
- }
507
- if (!response.ok) {
508
- const errorBody = await response.text();
509
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
510
- }
511
- const data = await response.json();
512
- return GitLabTreeSchema.parse(data);
513
- }
514
- /**
515
- * Create a commit in a GitLab project repository
516
- */
517
- async createCommit(projectId, message, branch, actions) {
518
- projectId = decodeURIComponent(projectId);
519
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/repository/commits`);
520
- const response = await this.fetch(url.toString(), {
521
- method: "POST",
522
- body: JSON.stringify({
523
- branch,
524
- commit_message: message,
525
- actions: actions.map(action => ({
526
- action: "create",
527
- file_path: action.path,
528
- content: action.content,
529
- encoding: "text",
530
- })),
531
- }),
532
- });
533
- if (response.status === 400) {
534
- const errorBody = await response.text();
535
- throw new Error(`Invalid request: ${errorBody}`);
536
- }
537
- if (!response.ok) {
538
- const errorBody = await response.text();
539
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
540
- }
541
- const data = await response.json();
542
- return GitLabCommitSchema.parse(data);
543
- }
544
- /**
545
- * Search for GitLab projects
546
- */
547
- async searchProjects(query, page = 1, perPage = 20) {
548
- const url = new URL(`${config.GITLAB_API_URL}/projects`);
549
- url.searchParams.append("search", query);
550
- url.searchParams.append("page", page.toString());
551
- url.searchParams.append("per_page", perPage.toString());
552
- url.searchParams.append("order_by", "id");
553
- url.searchParams.append("sort", "desc");
554
- const response = await this.fetch(url.toString(), {});
555
- if (!response.ok) {
556
- const errorBody = await response.text();
557
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
558
- }
559
- const projects = (await response.json());
560
- const totalCount = response.headers.get("x-total");
561
- const totalPages = response.headers.get("x-total-pages");
562
- const count = totalCount ? parseInt(totalCount) : projects.length;
563
- return GitLabSearchResponseSchema.parse({
564
- count,
565
- total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
566
- current_page: page,
567
- items: projects,
568
- });
569
- }
570
- /**
571
- * Create a new GitLab repository
572
- */
573
- async createRepository(options) {
574
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects`, {
575
- method: "POST",
576
- body: JSON.stringify({
577
- name: options.name,
578
- description: options.description,
579
- visibility: options.visibility,
580
- initialize_with_readme: options.initialize_with_readme,
581
- default_branch: "main",
582
- path: options.name.toLowerCase().replace(/\s+/g, "-"),
583
- }),
584
- });
585
- if (!response.ok) {
586
- const errorBody = await response.text();
587
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
588
- }
589
- const data = await response.json();
590
- return GitLabRepositorySchema.parse(data);
591
- }
592
- /**
593
- * Get merge request details
594
- */
595
- async getMergeRequest(projectId, mergeRequestIid, branchName) {
596
- projectId = decodeURIComponent(projectId);
597
- let url;
598
- if (mergeRequestIid) {
599
- url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
600
- }
601
- else if (branchName) {
602
- url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
603
- }
604
- else {
605
- throw new Error("Either mergeRequestIid or branchName must be provided");
606
- }
607
- const response = await this.fetch(url.toString(), {});
608
- await this.handleGitLabError(response);
609
- const data = await response.json();
610
- if (Array.isArray(data) && data.length > 0) {
611
- return GitLabMergeRequestSchema.parse(data[0]);
612
- }
613
- return GitLabMergeRequestSchema.parse(data);
614
- }
615
- /**
616
- * Get merge request changes/diffs
617
- */
618
- async getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view) {
619
- projectId = decodeURIComponent(projectId);
620
- if (!mergeRequestIid && !branchName) {
621
- throw new Error("Either mergeRequestIid or branchName must be provided");
622
- }
623
- if (branchName && !mergeRequestIid) {
624
- const mergeRequest = await this.getMergeRequest(projectId, undefined, branchName);
625
- mergeRequestIid = mergeRequest.iid;
626
- }
627
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/changes`);
628
- if (view) {
629
- url.searchParams.append("view", view);
630
- }
631
- const response = await this.fetch(url.toString(), {});
632
- await this.handleGitLabError(response);
633
- const data = (await response.json());
634
- return z.array(GitLabDiffSchema).parse(data.changes);
635
- }
636
- /**
637
- * Get merge request changes with detailed information
638
- */
639
- async listMergeRequestDiffs(projectId, mergeRequestIid, branchName, page, perPage, unidiff) {
640
- projectId = decodeURIComponent(projectId);
641
- if (!mergeRequestIid && !branchName) {
642
- throw new Error("Either mergeRequestIid or branchName must be provided");
643
- }
644
- if (branchName && !mergeRequestIid) {
645
- const mergeRequest = await this.getMergeRequest(projectId, undefined, branchName);
646
- mergeRequestIid = mergeRequest.iid;
647
- }
648
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/diffs`);
649
- if (page) {
650
- url.searchParams.append("page", page.toString());
651
- }
652
- if (perPage) {
653
- url.searchParams.append("per_page", perPage.toString());
654
- }
655
- if (unidiff) {
656
- url.searchParams.append("unidiff", "true");
657
- }
658
- const response = await this.fetch(url.toString(), {});
659
- await this.handleGitLabError(response);
660
- return await response.json();
661
- }
662
- /**
663
- * Get branch comparison diffs
664
- */
665
- async getBranchDiffs(projectId, from, to, straight) {
666
- projectId = decodeURIComponent(projectId);
667
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/repository/compare`);
668
- url.searchParams.append("from", from);
669
- url.searchParams.append("to", to);
670
- if (straight !== undefined) {
671
- url.searchParams.append("straight", straight.toString());
672
- }
673
- const response = await this.fetch(url.toString(), {});
674
- if (!response.ok) {
675
- const errorBody = await response.text();
676
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
677
- }
678
- const data = await response.json();
679
- return GitLabCompareResultSchema.parse(data);
680
- }
681
- /**
682
- * Update a merge request
683
- */
684
- async updateMergeRequest(projectId, options, mergeRequestIid, branchName) {
685
- projectId = decodeURIComponent(projectId);
686
- if (!mergeRequestIid && !branchName) {
687
- throw new Error("Either mergeRequestIid or branchName must be provided");
688
- }
689
- if (branchName && !mergeRequestIid) {
690
- const mergeRequest = await this.getMergeRequest(projectId, undefined, branchName);
691
- mergeRequestIid = mergeRequest.iid;
692
- }
693
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
694
- const response = await this.fetch(url.toString(), {
695
- method: "PUT",
696
- body: JSON.stringify(options),
697
- });
698
- await this.handleGitLabError(response);
699
- return GitLabMergeRequestSchema.parse(await response.json());
700
- }
701
- /**
702
- * Create a new note (comment) on an issue or merge request
703
- */
704
- async createNote(projectId, noteableType, noteableIid, body) {
705
- projectId = decodeURIComponent(projectId);
706
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/${noteableType}s/${noteableIid}/notes`);
707
- const response = await this.fetch(url.toString(), {
708
- method: "POST",
709
- body: JSON.stringify({ body }),
710
- });
711
- if (!response.ok) {
712
- const errorText = await response.text();
713
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
714
- }
715
- return await response.json();
716
- }
717
- /**
718
- * Create a new thread on a merge request
719
- */
720
- async createMergeRequestThread(projectId, mergeRequestIid, body, position, createdAt) {
721
- projectId = decodeURIComponent(projectId);
722
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions`);
723
- const payload = { body };
724
- if (position) {
725
- payload.position = position;
726
- }
727
- if (createdAt) {
728
- payload.created_at = createdAt;
729
- }
730
- const response = await this.fetch(url.toString(), {
731
- method: "POST",
732
- body: JSON.stringify(payload),
733
- });
734
- await this.handleGitLabError(response);
735
- const data = await response.json();
736
- return GitLabDiscussionSchema.parse(data);
737
- }
738
- /**
739
- * List all namespaces
740
- */
741
- async listNamespaces(options) {
742
- const url = new URL(`${config.GITLAB_API_URL}/namespaces`);
743
- if (options.search) {
744
- url.searchParams.append("search", options.search);
745
- }
746
- if (options.owned_only) {
747
- url.searchParams.append("owned_only", "true");
748
- }
749
- if (options.top_level_only) {
750
- url.searchParams.append("top_level_only", "true");
751
- }
752
- const response = await this.fetch(url.toString(), {});
753
- await this.handleGitLabError(response);
754
- const data = await response.json();
755
- return z.array(GitLabNamespaceSchema).parse(data);
756
- }
757
- /**
758
- * Get details on a namespace
759
- */
760
- async getNamespace(id) {
761
- const url = new URL(`${config.GITLAB_API_URL}/namespaces/${encodeURIComponent(id)}`);
762
- const response = await this.fetch(url.toString(), {});
763
- await this.handleGitLabError(response);
764
- const data = await response.json();
765
- return GitLabNamespaceSchema.parse(data);
766
- }
767
- /**
768
- * Verify if a namespace exists
769
- */
770
- async verifyNamespaceExistence(namespacePath, parentId) {
771
- const url = new URL(`${config.GITLAB_API_URL}/namespaces/${encodeURIComponent(namespacePath)}/exists`);
772
- if (parentId) {
773
- url.searchParams.append("parent_id", parentId.toString());
774
- }
775
- const response = await this.fetch(url.toString(), {});
776
- await this.handleGitLabError(response);
777
- const data = await response.json();
778
- return GitLabNamespaceExistsResponseSchema.parse(data);
779
- }
780
- /**
781
- * Get a single project
782
- */
783
- async getProject(projectId, options = {}) {
784
- projectId = decodeURIComponent(projectId);
785
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}`);
786
- if (options.license) {
787
- url.searchParams.append("license", "true");
788
- }
789
- if (options.statistics) {
790
- url.searchParams.append("statistics", "true");
791
- }
792
- if (options.with_custom_attributes) {
793
- url.searchParams.append("with_custom_attributes", "true");
794
- }
795
- const response = await this.fetch(url.toString(), {});
796
- await this.handleGitLabError(response);
797
- const data = await response.json();
798
- return GitLabRepositorySchema.parse(data);
799
- }
800
- /**
801
- * List projects
802
- */
803
- async listProjects(options = {}) {
804
- const params = new URLSearchParams();
805
- for (const [key, value] of Object.entries(options)) {
806
- if (value !== undefined && value !== null) {
807
- if (typeof value === "boolean") {
808
- params.append(key, value ? "true" : "false");
809
- }
810
- else {
811
- params.append(key, String(value));
812
- }
813
- }
814
- }
815
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects?${params.toString()}`, {});
816
- await this.handleGitLabError(response);
817
- const data = await response.json();
818
- return z.array(GitLabProjectSchema).parse(data);
819
- }
820
- /**
821
- * List labels for a project
822
- */
823
- async listLabels(projectId, options = {}) {
824
- projectId = decodeURIComponent(projectId);
825
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/labels`);
826
- Object.entries(options).forEach(([key, value]) => {
827
- if (value !== undefined) {
828
- if (typeof value === "boolean") {
829
- url.searchParams.append(key, value ? "true" : "false");
830
- }
831
- else {
832
- url.searchParams.append(key, String(value));
833
- }
834
- }
835
- });
836
- const response = await this.fetch(url.toString(), {});
837
- await this.handleGitLabError(response);
838
- const data = await response.json();
839
- return data;
840
- }
841
- /**
842
- * Get a single label from a project
843
- */
844
- async getLabel(projectId, labelId, includeAncestorGroups) {
845
- projectId = decodeURIComponent(projectId);
846
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`);
847
- if (includeAncestorGroups !== undefined) {
848
- url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false");
849
- }
850
- const response = await this.fetch(url.toString(), {});
851
- await this.handleGitLabError(response);
852
- const data = await response.json();
853
- return data;
854
- }
855
- /**
856
- * Create a new label in a project
857
- */
858
- async createLabel(projectId, options) {
859
- projectId = decodeURIComponent(projectId);
860
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/labels`, {
861
- method: "POST",
862
- body: JSON.stringify(options),
863
- });
864
- await this.handleGitLabError(response);
865
- const data = await response.json();
866
- return data;
867
- }
868
- /**
869
- * Update an existing label in a project
870
- */
871
- async updateLabel(projectId, labelId, options) {
872
- projectId = decodeURIComponent(projectId);
873
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`, {
874
- method: "PUT",
875
- body: JSON.stringify(options),
876
- });
877
- await this.handleGitLabError(response);
878
- const data = await response.json();
879
- return data;
880
- }
881
- /**
882
- * Delete a label from a project
883
- */
884
- async deleteLabel(projectId, labelId) {
885
- projectId = decodeURIComponent(projectId);
886
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/labels/${encodeURIComponent(String(labelId))}`, {
887
- method: "DELETE",
888
- });
889
- await this.handleGitLabError(response);
890
- }
891
- /**
892
- * List all projects in a GitLab group
893
- */
894
- async listGroupProjects(options) {
895
- const url = new URL(`${config.GITLAB_API_URL}/groups/${encodeURIComponent(options.group_id)}/projects`);
896
- if (options.include_subgroups)
897
- url.searchParams.append("include_subgroups", "true");
898
- if (options.search)
899
- url.searchParams.append("search", options.search);
900
- if (options.order_by)
901
- url.searchParams.append("order_by", options.order_by);
902
- if (options.sort)
903
- url.searchParams.append("sort", options.sort);
904
- if (options.page)
905
- url.searchParams.append("page", options.page.toString());
906
- if (options.per_page)
907
- url.searchParams.append("per_page", options.per_page.toString());
908
- if (options.archived !== undefined)
909
- url.searchParams.append("archived", options.archived.toString());
910
- if (options.visibility)
911
- url.searchParams.append("visibility", options.visibility);
912
- if (options.with_issues_enabled !== undefined)
913
- url.searchParams.append("with_issues_enabled", options.with_issues_enabled.toString());
914
- if (options.with_merge_requests_enabled !== undefined)
915
- url.searchParams.append("with_merge_requests_enabled", options.with_merge_requests_enabled.toString());
916
- if (options.min_access_level !== undefined)
917
- url.searchParams.append("min_access_level", options.min_access_level.toString());
918
- if (options.with_programming_language)
919
- url.searchParams.append("with_programming_language", options.with_programming_language);
920
- if (options.starred !== undefined)
921
- url.searchParams.append("starred", options.starred.toString());
922
- if (options.statistics !== undefined)
923
- url.searchParams.append("statistics", options.statistics.toString());
924
- if (options.with_custom_attributes !== undefined)
925
- url.searchParams.append("with_custom_attributes", options.with_custom_attributes.toString());
926
- if (options.with_security_reports !== undefined)
927
- url.searchParams.append("with_security_reports", options.with_security_reports.toString());
928
- const response = await this.fetch(url.toString(), {});
929
- await this.handleGitLabError(response);
930
- const projects = await response.json();
931
- return GitLabProjectSchema.array().parse(projects);
932
- }
933
- // Wiki API helper methods
934
- /**
935
- * List wiki pages in a project
936
- */
937
- async listWikiPages(projectId, options = {}) {
938
- projectId = decodeURIComponent(projectId);
939
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/wikis`);
940
- if (options.page)
941
- url.searchParams.append("page", options.page.toString());
942
- if (options.per_page)
943
- url.searchParams.append("per_page", options.per_page.toString());
944
- if (options.with_content)
945
- url.searchParams.append("with_content", options.with_content.toString());
946
- const response = await this.fetch(url.toString(), {});
947
- await this.handleGitLabError(response);
948
- const data = await response.json();
949
- return GitLabWikiPageSchema.array().parse(data);
950
- }
951
- /**
952
- * Get a specific wiki page
953
- */
954
- async getWikiPage(projectId, slug) {
955
- projectId = decodeURIComponent(projectId);
956
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {});
957
- await this.handleGitLabError(response);
958
- const data = await response.json();
959
- return GitLabWikiPageSchema.parse(data);
960
- }
961
- /**
962
- * Create a new wiki page
963
- */
964
- async createWikiPage(projectId, title, content, format) {
965
- projectId = decodeURIComponent(projectId);
966
- const body = { title, content };
967
- if (format)
968
- body.format = format;
969
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/wikis`, {
970
- method: "POST",
971
- body: JSON.stringify(body),
972
- });
973
- await this.handleGitLabError(response);
974
- const data = await response.json();
975
- return GitLabWikiPageSchema.parse(data);
976
- }
977
- /**
978
- * Update an existing wiki page
979
- */
980
- async updateWikiPage(projectId, slug, title, content, format) {
981
- projectId = decodeURIComponent(projectId);
982
- const body = {};
983
- if (title)
984
- body.title = title;
985
- if (content)
986
- body.content = content;
987
- if (format)
988
- body.format = format;
989
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {
990
- method: "PUT",
991
- body: JSON.stringify(body),
992
- });
993
- await this.handleGitLabError(response);
994
- const data = await response.json();
995
- return GitLabWikiPageSchema.parse(data);
996
- }
997
- /**
998
- * Delete a wiki page
999
- */
1000
- async deleteWikiPage(projectId, slug) {
1001
- projectId = decodeURIComponent(projectId);
1002
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, {
1003
- method: "DELETE",
1004
- });
1005
- await this.handleGitLabError(response);
1006
- }
1007
- /**
1008
- * List pipelines in a GitLab project
1009
- */
1010
- async listPipelines(projectId, options = {}) {
1011
- projectId = decodeURIComponent(projectId);
1012
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/pipelines`);
1013
- Object.entries(options).forEach(([key, value]) => {
1014
- if (value !== undefined) {
1015
- url.searchParams.append(key, value.toString());
1016
- }
1017
- });
1018
- const response = await this.fetch(url.toString(), {});
1019
- await this.handleGitLabError(response);
1020
- const data = await response.json();
1021
- return z.array(GitLabPipelineSchema).parse(data);
1022
- }
1023
- /**
1024
- * Get details of a specific pipeline
1025
- */
1026
- async getPipeline(projectId, pipelineId) {
1027
- projectId = decodeURIComponent(projectId);
1028
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/pipelines/${pipelineId}`);
1029
- const response = await this.fetch(url.toString(), {});
1030
- if (response.status === 404) {
1031
- throw new Error(`Pipeline not found`);
1032
- }
1033
- await this.handleGitLabError(response);
1034
- const data = await response.json();
1035
- return GitLabPipelineSchema.parse(data);
1036
- }
1037
- /**
1038
- * List all jobs in a specific pipeline
1039
- */
1040
- async listPipelineJobs(projectId, pipelineId, options = {}) {
1041
- projectId = decodeURIComponent(projectId);
1042
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/jobs`);
1043
- Object.entries(options).forEach(([key, value]) => {
1044
- if (value !== undefined) {
1045
- if (typeof value === "boolean") {
1046
- url.searchParams.append(key, value ? "true" : "false");
1047
- }
1048
- else {
1049
- url.searchParams.append(key, value.toString());
1050
- }
1051
- }
1052
- });
1053
- const response = await this.fetch(url.toString(), {});
1054
- if (response.status === 404) {
1055
- throw new Error(`Pipeline not found`);
1056
- }
1057
- await this.handleGitLabError(response);
1058
- const data = await response.json();
1059
- return z.array(GitLabPipelineJobSchema).parse(data);
1060
- }
1061
- /**
1062
- * Get a specific pipeline job
1063
- */
1064
- async getPipelineJob(projectId, jobId) {
1065
- projectId = decodeURIComponent(projectId);
1066
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/jobs/${jobId}`);
1067
- const response = await this.fetch(url.toString(), {});
1068
- if (response.status === 404) {
1069
- throw new Error(`Job not found`);
1070
- }
1071
- await this.handleGitLabError(response);
1072
- const data = await response.json();
1073
- return GitLabPipelineJobSchema.parse(data);
1074
- }
1075
- /**
1076
- * Get the output/trace of a pipeline job
1077
- */
1078
- async getPipelineJobOutput(projectId, jobId, limit, offset) {
1079
- projectId = decodeURIComponent(projectId);
1080
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/jobs/${jobId}/trace`);
1081
- const response = await this.fetch(url.toString(), {
1082
- headers: {
1083
- Accept: "text/plain",
1084
- },
1085
- });
1086
- if (response.status === 404) {
1087
- throw new Error(`Job trace not found or job is not finished yet`);
1088
- }
1089
- await this.handleGitLabError(response);
1090
- const fullTrace = await response.text();
1091
- // Apply client-side pagination
1092
- if (limit !== undefined || offset !== undefined) {
1093
- const lines = fullTrace.split('\n');
1094
- const startOffset = offset || 0;
1095
- const maxLines = limit || 1000;
1096
- const startIndex = Math.max(0, lines.length - startOffset - maxLines);
1097
- const endIndex = lines.length - startOffset;
1098
- const selectedLines = lines.slice(startIndex, endIndex);
1099
- const result = selectedLines.join('\n');
1100
- if (startIndex > 0 || endIndex < lines.length) {
1101
- const totalLines = lines.length;
1102
- const shownLines = selectedLines.length;
1103
- const skippedFromStart = startIndex;
1104
- const skippedFromEnd = startOffset;
1105
- return `[Log truncated: showing ${shownLines} of ${totalLines} lines, skipped ${skippedFromStart} from start, ${skippedFromEnd} from end]\n\n${result}`;
1106
- }
1107
- return result;
1108
- }
1109
- return fullTrace;
1110
- }
1111
- /**
1112
- * Create a new pipeline
1113
- */
1114
- async createPipeline(projectId, ref, variables) {
1115
- projectId = decodeURIComponent(projectId);
1116
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/pipeline`);
1117
- const body = { ref };
1118
- if (variables && variables.length > 0) {
1119
- body.variables = variables;
1120
- }
1121
- const response = await this.fetch(url.toString(), {
1122
- method: "POST",
1123
- body: JSON.stringify(body),
1124
- });
1125
- await this.handleGitLabError(response);
1126
- const data = await response.json();
1127
- return GitLabPipelineSchema.parse(data);
1128
- }
1129
- /**
1130
- * Retry a pipeline
1131
- */
1132
- async retryPipeline(projectId, pipelineId) {
1133
- projectId = decodeURIComponent(projectId);
1134
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/retry`);
1135
- const response = await this.fetch(url.toString(), {
1136
- method: "POST",
1137
- });
1138
- await this.handleGitLabError(response);
1139
- const data = await response.json();
1140
- return GitLabPipelineSchema.parse(data);
1141
- }
1142
- /**
1143
- * Cancel a pipeline
1144
- */
1145
- async cancelPipeline(projectId, pipelineId) {
1146
- projectId = decodeURIComponent(projectId);
1147
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/cancel`);
1148
- const response = await this.fetch(url.toString(), {
1149
- method: "POST",
1150
- });
1151
- await this.handleGitLabError(response);
1152
- const data = await response.json();
1153
- return GitLabPipelineSchema.parse(data);
1154
- }
1155
- /**
1156
- * Get the repository tree for a project
1157
- */
1158
- async getRepositoryTree(options) {
1159
- options.project_id = decodeURIComponent(options.project_id);
1160
- const queryParams = new URLSearchParams();
1161
- if (options.path)
1162
- queryParams.append("path", options.path);
1163
- if (options.ref)
1164
- queryParams.append("ref", options.ref);
1165
- if (options.recursive)
1166
- queryParams.append("recursive", "true");
1167
- if (options.per_page)
1168
- queryParams.append("per_page", options.per_page.toString());
1169
- if (options.page_token)
1170
- queryParams.append("page_token", options.page_token);
1171
- if (options.pagination)
1172
- queryParams.append("pagination", options.pagination);
1173
- const headers = {
1174
- "Content-Type": "application/json",
1175
- };
1176
- if (config.IS_OLD) {
1177
- headers["Private-Token"] = `${config.GITLAB_PERSONAL_ACCESS_TOKEN}`;
1178
- }
1179
- else {
1180
- headers["Authorization"] = `Bearer ${config.GITLAB_PERSONAL_ACCESS_TOKEN}`;
1181
- }
1182
- const response = await this.fetch(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(options.project_id)}/repository/tree?${queryParams.toString()}`, {
1183
- headers,
1184
- });
1185
- if (response.status === 404) {
1186
- throw new Error("Repository or path not found");
1187
- }
1188
- if (!response.ok) {
1189
- throw new Error(`Failed to get repository tree: ${response.statusText}`);
1190
- }
1191
- const data = await response.json();
1192
- return z.array(GitLabTreeItemSchema).parse(data);
1193
- }
1194
- /**
1195
- * List project milestones in a GitLab project
1196
- */
1197
- async listProjectMilestones(projectId, options) {
1198
- projectId = decodeURIComponent(projectId);
1199
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones`);
1200
- Object.entries(options).forEach(([key, value]) => {
1201
- if (value !== undefined) {
1202
- if (key === "iids" && Array.isArray(value) && value.length > 0) {
1203
- value.forEach(iid => {
1204
- url.searchParams.append("iids[]", iid.toString());
1205
- });
1206
- }
1207
- else if (value !== undefined) {
1208
- url.searchParams.append(key, value.toString());
1209
- }
1210
- }
1211
- });
1212
- const response = await this.fetch(url.toString(), {});
1213
- await this.handleGitLabError(response);
1214
- const data = await response.json();
1215
- return z.array(GitLabMilestonesSchema).parse(data);
1216
- }
1217
- /**
1218
- * Get a single milestone in a GitLab project
1219
- */
1220
- async getProjectMilestone(projectId, milestoneId) {
1221
- projectId = decodeURIComponent(projectId);
1222
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
1223
- const response = await this.fetch(url.toString(), {});
1224
- await this.handleGitLabError(response);
1225
- const data = await response.json();
1226
- return GitLabMilestonesSchema.parse(data);
1227
- }
1228
- /**
1229
- * Create a new milestone in a GitLab project
1230
- */
1231
- async createProjectMilestone(projectId, options) {
1232
- projectId = decodeURIComponent(projectId);
1233
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones`);
1234
- const response = await this.fetch(url.toString(), {
1235
- method: "POST",
1236
- body: JSON.stringify(options),
1237
- });
1238
- await this.handleGitLabError(response);
1239
- const data = await response.json();
1240
- return GitLabMilestonesSchema.parse(data);
1241
- }
1242
- /**
1243
- * Edit an existing milestone in a GitLab project
1244
- */
1245
- async editProjectMilestone(projectId, milestoneId, options) {
1246
- projectId = decodeURIComponent(projectId);
1247
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
1248
- const response = await this.fetch(url.toString(), {
1249
- method: "PUT",
1250
- body: JSON.stringify(options),
1251
- });
1252
- await this.handleGitLabError(response);
1253
- const data = await response.json();
1254
- return GitLabMilestonesSchema.parse(data);
1255
- }
1256
- /**
1257
- * Delete a milestone from a GitLab project
1258
- */
1259
- async deleteProjectMilestone(projectId, milestoneId) {
1260
- projectId = decodeURIComponent(projectId);
1261
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
1262
- const response = await this.fetch(url.toString(), {
1263
- method: "DELETE",
1264
- });
1265
- await this.handleGitLabError(response);
1266
- }
1267
- /**
1268
- * Get all issues assigned to a single milestone
1269
- */
1270
- async getMilestoneIssues(projectId, milestoneId) {
1271
- projectId = decodeURIComponent(projectId);
1272
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones/${milestoneId}/issues`);
1273
- const response = await this.fetch(url.toString(), {});
1274
- await this.handleGitLabError(response);
1275
- const data = await response.json();
1276
- return z.array(GitLabIssueSchema).parse(data);
1277
- }
1278
- /**
1279
- * Get all merge requests assigned to a single milestone
1280
- */
1281
- async getMilestoneMergeRequests(projectId, milestoneId) {
1282
- projectId = decodeURIComponent(projectId);
1283
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones/${milestoneId}/merge_requests`);
1284
- const response = await this.fetch(url.toString(), {});
1285
- await this.handleGitLabError(response);
1286
- const data = await response.json();
1287
- return z.array(GitLabMergeRequestSchema).parse(data);
1288
- }
1289
- /**
1290
- * Promote a project milestone to a group milestone
1291
- */
1292
- async promoteProjectMilestone(projectId, milestoneId) {
1293
- projectId = decodeURIComponent(projectId);
1294
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones/${milestoneId}/promote`);
1295
- const response = await this.fetch(url.toString(), {
1296
- method: "POST",
1297
- });
1298
- await this.handleGitLabError(response);
1299
- const data = await response.json();
1300
- return GitLabMilestonesSchema.parse(data);
1301
- }
1302
- /**
1303
- * Get all burndown chart events for a single milestone
1304
- */
1305
- async getMilestoneBurndownEvents(projectId, milestoneId) {
1306
- projectId = decodeURIComponent(projectId);
1307
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/milestones/${milestoneId}/burndown_events`);
1308
- const response = await this.fetch(url.toString(), {});
1309
- await this.handleGitLabError(response);
1310
- const data = await response.json();
1311
- return data;
1312
- }
1313
- /**
1314
- * Get a single user from GitLab
1315
- */
1316
- async getUser(username) {
1317
- try {
1318
- const url = new URL(`${config.GITLAB_API_URL}/users`);
1319
- url.searchParams.append("username", username);
1320
- const response = await this.fetch(url.toString(), {});
1321
- await this.handleGitLabError(response);
1322
- const users = await response.json();
1323
- if (Array.isArray(users) && users.length > 0) {
1324
- const exactMatch = users.find(user => user.username === username);
1325
- if (exactMatch) {
1326
- return GitLabUserSchema.parse(exactMatch);
1327
- }
1328
- }
1329
- return null;
1330
- }
1331
- catch (error) {
1332
- console.error(`Error fetching user by username '${username}':`, error);
1333
- return null;
1334
- }
1335
- }
1336
- /**
1337
- * Get multiple users from GitLab
1338
- */
1339
- async getUsers(usernames) {
1340
- const users = {};
1341
- for (const username of usernames) {
1342
- try {
1343
- const user = await this.getUser(username);
1344
- users[username] = user;
1345
- }
1346
- catch (error) {
1347
- console.error(`Error processing username '${username}':`, error);
1348
- users[username] = null;
1349
- }
1350
- }
1351
- return GitLabUsersResponseSchema.parse(users);
1352
- }
1353
- /**
1354
- * List repository commits
1355
- */
1356
- async listCommits(projectId, options = {}) {
1357
- projectId = decodeURIComponent(projectId);
1358
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/repository/commits`);
1359
- if (options.ref_name)
1360
- url.searchParams.append("ref_name", options.ref_name);
1361
- if (options.since)
1362
- url.searchParams.append("since", options.since);
1363
- if (options.until)
1364
- url.searchParams.append("until", options.until);
1365
- if (options.path)
1366
- url.searchParams.append("path", options.path);
1367
- if (options.author)
1368
- url.searchParams.append("author", options.author);
1369
- if (options.all)
1370
- url.searchParams.append("all", options.all.toString());
1371
- if (options.with_stats)
1372
- url.searchParams.append("with_stats", options.with_stats.toString());
1373
- if (options.first_parent)
1374
- url.searchParams.append("first_parent", options.first_parent.toString());
1375
- if (options.order)
1376
- url.searchParams.append("order", options.order);
1377
- if (options.trailers)
1378
- url.searchParams.append("trailers", options.trailers.toString());
1379
- if (options.page)
1380
- url.searchParams.append("page", options.page.toString());
1381
- if (options.per_page)
1382
- url.searchParams.append("per_page", options.per_page.toString());
1383
- const response = await this.fetch(url.toString(), {});
1384
- await this.handleGitLabError(response);
1385
- const data = await response.json();
1386
- return z.array(GitLabCommitSchema).parse(data);
1387
- }
1388
- /**
1389
- * Get a single commit
1390
- */
1391
- async getCommit(projectId, sha, stats) {
1392
- projectId = decodeURIComponent(projectId);
1393
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}`);
1394
- if (stats) {
1395
- url.searchParams.append("stats", "true");
1396
- }
1397
- const response = await this.fetch(url.toString(), {});
1398
- await this.handleGitLabError(response);
1399
- const data = await response.json();
1400
- return GitLabCommitSchema.parse(data);
1401
- }
1402
- /**
1403
- * Get commit diff
1404
- */
1405
- async getCommitDiff(projectId, sha) {
1406
- projectId = decodeURIComponent(projectId);
1407
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/repository/commits/${encodeURIComponent(sha)}/diff`);
1408
- const response = await this.fetch(url.toString(), {});
1409
- await this.handleGitLabError(response);
1410
- const data = await response.json();
1411
- return z.array(GitLabDiffSchema).parse(data);
1412
- }
1413
- /**
1414
- * Get details of the current authenticated User
1415
- */
1416
- async getCurrentUser() {
1417
- const url = new URL(`${config.GITLAB_API_URL}/user`);
1418
- const response = await this.fetch(url.toString(), {});
1419
- await this.handleGitLabError(response);
1420
- const data = await response.json();
1421
- return GitLabUserSchema.parse(data);
1422
- }
1423
- /**
1424
- * Get a draft note
1425
- */
1426
- async getDraftNote(projectId, mergeRequestIid, draftNoteId) {
1427
- projectId = decodeURIComponent(projectId);
1428
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
1429
- const response = await this.fetch(url.toString(), {});
1430
- await this.handleGitLabError(response);
1431
- const data = await response.json();
1432
- return GitLabDraftNoteSchema.parse(data);
1433
- }
1434
- /**
1435
- * List draft notes
1436
- */
1437
- async listDraftNotes(projectId, mergeRequestIid) {
1438
- projectId = decodeURIComponent(projectId);
1439
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
1440
- const response = await this.fetch(url.toString(), {});
1441
- await this.handleGitLabError(response);
1442
- const data = await response.json();
1443
- return z.array(GitLabDraftNoteSchema).parse(data);
1444
- }
1445
- /**
1446
- * Create a draft note
1447
- */
1448
- async createDraftNote(projectId, mergeRequestIid, body, position, resolveDiscussion) {
1449
- projectId = decodeURIComponent(projectId);
1450
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
1451
- const requestBody = { note: body };
1452
- if (position) {
1453
- requestBody.position = position;
1454
- }
1455
- if (resolveDiscussion !== undefined) {
1456
- requestBody.resolve_discussion = resolveDiscussion;
1457
- }
1458
- const response = await this.fetch(url.toString(), {
1459
- method: "POST",
1460
- body: JSON.stringify(requestBody),
1461
- });
1462
- await this.handleGitLabError(response);
1463
- const data = await response.json();
1464
- return GitLabDraftNoteSchema.parse(data);
1465
- }
1466
- /**
1467
- * Update a draft note
1468
- */
1469
- async updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, position, resolveDiscussion) {
1470
- projectId = decodeURIComponent(projectId);
1471
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
1472
- const requestBody = {};
1473
- if (body !== undefined) {
1474
- requestBody.note = body;
1475
- }
1476
- if (position) {
1477
- requestBody.position = position;
1478
- }
1479
- if (resolveDiscussion !== undefined) {
1480
- requestBody.resolve_discussion = resolveDiscussion;
1481
- }
1482
- const response = await this.fetch(url.toString(), {
1483
- method: "PUT",
1484
- body: JSON.stringify(requestBody),
1485
- });
1486
- await this.handleGitLabError(response);
1487
- const data = await response.json();
1488
- return GitLabDraftNoteSchema.parse(data);
1489
- }
1490
- /**
1491
- * Delete a draft note
1492
- */
1493
- async deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
1494
- projectId = decodeURIComponent(projectId);
1495
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
1496
- const response = await this.fetch(url.toString(), {
1497
- method: "DELETE",
1498
- });
1499
- await this.handleGitLabError(response);
1500
- }
1501
- /**
1502
- * Publish a draft note
1503
- */
1504
- async publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
1505
- projectId = decodeURIComponent(projectId);
1506
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}/publish`);
1507
- const response = await this.fetch(url.toString(), {
1508
- method: "PUT",
1509
- });
1510
- await this.handleGitLabError(response);
1511
- // Handle empty response (204 No Content) or successful response
1512
- const responseText = await response.text();
1513
- if (!responseText || responseText.trim() === '') {
1514
- // Return a success indicator for empty responses
1515
- return {
1516
- id: draftNoteId.toString(),
1517
- body: "Draft note published successfully",
1518
- author: { id: "unknown", username: "unknown" },
1519
- created_at: new Date().toISOString(),
1520
- };
1521
- }
1522
- const data = JSON.parse(responseText);
1523
- return GitLabDiscussionNoteSchema.parse(data);
1524
- }
1525
- /**
1526
- * Bulk publish draft notes
1527
- */
1528
- async bulkPublishDraftNotes(projectId, mergeRequestIid) {
1529
- projectId = decodeURIComponent(projectId);
1530
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/bulk_publish`);
1531
- const response = await this.fetch(url.toString(), {
1532
- method: "POST",
1533
- body: JSON.stringify({}),
1534
- });
1535
- await this.handleGitLabError(response);
1536
- // Handle empty response (204 No Content) or successful response
1537
- const responseText = await response.text();
1538
- if (!responseText || responseText.trim() === '') {
1539
- // Return empty array for successful bulk publish with no content
1540
- return [];
1541
- }
1542
- try {
1543
- const data = JSON.parse(responseText);
1544
- return z.array(GitLabDiscussionNoteSchema).parse(data);
1545
- }
1546
- catch (error) {
1547
- return [];
1548
- }
1549
- }
1550
- /**
1551
- * Merge a merge request
1552
- */
1553
- async mergeMergeRequest(projectId, options, mergeRequestIid) {
1554
- projectId = decodeURIComponent(projectId);
1555
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(this.getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/merge`);
1556
- const response = await this.fetch(url.toString(), {
1557
- method: "PUT",
1558
- body: JSON.stringify(options),
1559
- });
1560
- await this.handleGitLabError(response);
1561
- return GitLabMergeRequestSchema.parse(await response.json());
1562
- }
1563
- /**
1564
- * Get issues assigned to the current user
1565
- */
1566
- async myIssues(options = {}) {
1567
- // Get current user to find their username
1568
- const currentUser = await this.getCurrentUser();
1569
- // Use getEffectiveProjectId to handle project ID resolution
1570
- const effectiveProjectId = this.getEffectiveProjectId(options.project_id || "");
1571
- // Use listIssues with assignee_username filter
1572
- let listIssuesOptions = {
1573
- state: options.state || "opened",
1574
- labels: options.labels,
1575
- milestone: options.milestone,
1576
- search: options.search,
1577
- created_after: options.created_after,
1578
- created_before: options.created_before,
1579
- updated_after: options.updated_after,
1580
- updated_before: options.updated_before,
1581
- per_page: options.per_page,
1582
- page: options.page,
1583
- };
1584
- if (currentUser.username) {
1585
- listIssuesOptions.assignee_username = [currentUser.username];
1586
- }
1587
- else {
1588
- listIssuesOptions.assignee_id = currentUser.id;
1589
- }
1590
- return this.listIssues(effectiveProjectId, listIssuesOptions);
1591
- }
1592
- /**
1593
- * List project members
1594
- */
1595
- async listProjectMembers(projectId, options = {}) {
1596
- projectId = decodeURIComponent(projectId);
1597
- const effectiveProjectId = this.getEffectiveProjectId(projectId);
1598
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/members`);
1599
- // Add query parameters
1600
- if (options.query)
1601
- url.searchParams.append("query", options.query);
1602
- if (options.user_ids) {
1603
- options.user_ids.forEach(id => url.searchParams.append("user_ids[]", id.toString()));
1604
- }
1605
- if (options.skip_users) {
1606
- options.skip_users.forEach(id => url.searchParams.append("skip_users[]", id.toString()));
1607
- }
1608
- if (options.per_page)
1609
- url.searchParams.append("per_page", options.per_page.toString());
1610
- if (options.page)
1611
- url.searchParams.append("page", options.page.toString());
1612
- const response = await this.fetch(url.toString(), {});
1613
- await this.handleGitLabError(response);
1614
- const data = await response.json();
1615
- return z.array(GitLabProjectMemberSchema).parse(data);
1616
- }
1617
- /**
1618
- * Upload a file for markdown usage
1619
- */
1620
- async markdownUpload(projectId, filePath) {
1621
- projectId = decodeURIComponent(projectId);
1622
- const effectiveProjectId = this.getEffectiveProjectId(projectId);
1623
- // Check if file exists
1624
- if (!fs.existsSync(filePath)) {
1625
- throw new Error(`File not found: ${filePath}`);
1626
- }
1627
- // Read the file
1628
- const fileBuffer = fs.readFileSync(filePath);
1629
- const fileName = path.basename(filePath);
1630
- // Create form data
1631
- const FormData = (await import("form-data")).default;
1632
- const form = new FormData();
1633
- form.append("file", fileBuffer, {
1634
- filename: fileName,
1635
- contentType: "application/octet-stream",
1636
- });
1637
- const url = new URL(`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/uploads`);
1638
- // Need to handle form data specially
1639
- const headers = form.getHeaders();
1640
- const response = await this.fetch(url.toString(), {
1641
- method: "POST",
1642
- body: form,
1643
- headers: headers,
1644
- });
1645
- await this.handleGitLabError(response);
1646
- const data = await response.json();
1647
- return GitLabMarkdownUploadSchema.parse(data);
1648
- }
1649
- }