@ttpears/gitlab-mcp-server 1.7.3 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/dist/gitlab-client.d.ts +91 -9
- package/dist/gitlab-client.d.ts.map +1 -1
- package/dist/gitlab-client.js +1095 -40
- package/dist/gitlab-client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +729 -31
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/gitlab-client.js
CHANGED
|
@@ -192,6 +192,35 @@ export class GitLabGraphQLClient {
|
|
|
192
192
|
const client = this.getClient(userConfig, requiresWrite);
|
|
193
193
|
return this.executeWithRetry(() => client.request(query, variables), 'GraphQL query');
|
|
194
194
|
}
|
|
195
|
+
async fetchAllPages(query, variables, connectionPath, options = {}) {
|
|
196
|
+
const maxItems = options.maxItems ?? 100;
|
|
197
|
+
const pageSize = Math.min(options.pageSize ?? 50, this.config.maxPageSize);
|
|
198
|
+
const allNodes = [];
|
|
199
|
+
let after = undefined;
|
|
200
|
+
let lastPageInfo = { hasNextPage: false, hasPreviousPage: false, startCursor: null, endCursor: null };
|
|
201
|
+
while (allNodes.length < maxItems) {
|
|
202
|
+
const remaining = maxItems - allNodes.length;
|
|
203
|
+
const currentPageSize = Math.min(pageSize, remaining);
|
|
204
|
+
const result = await this.query(query, { ...variables, first: currentPageSize, after }, options.userConfig);
|
|
205
|
+
// Navigate to the connection using the dot-separated path
|
|
206
|
+
const connection = connectionPath.split('.').reduce((obj, key) => obj?.[key], result);
|
|
207
|
+
if (!connection || !connection.nodes) {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
allNodes.push(...connection.nodes);
|
|
211
|
+
lastPageInfo = connection.pageInfo || lastPageInfo;
|
|
212
|
+
if (!lastPageInfo.hasNextPage || allNodes.length >= maxItems) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
after = lastPageInfo.endCursor ?? undefined;
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
nodes: allNodes,
|
|
219
|
+
totalFetched: allNodes.length,
|
|
220
|
+
hasMore: lastPageInfo.hasNextPage && allNodes.length >= maxItems,
|
|
221
|
+
pageInfo: lastPageInfo,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
195
224
|
async getCurrentUser(userConfig) {
|
|
196
225
|
const query = gql `
|
|
197
226
|
query getCurrentUser {
|
|
@@ -233,10 +262,10 @@ export class GitLabGraphQLClient {
|
|
|
233
262
|
`;
|
|
234
263
|
return this.query(query, { fullPath }, userConfig);
|
|
235
264
|
}
|
|
236
|
-
async getProjects(first = 20, after, userConfig) {
|
|
265
|
+
async getProjects(first = 20, after, fetchAll = false, userConfig, sort) {
|
|
237
266
|
const query = gql `
|
|
238
|
-
query getProjects($first: Int!, $after: String) {
|
|
239
|
-
projects(first: $first, after: $after) {
|
|
267
|
+
query getProjects($first: Int!, $after: String, $sort: String) {
|
|
268
|
+
projects(first: $first, after: $after, sort: $sort) {
|
|
240
269
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
241
270
|
nodes {
|
|
242
271
|
id
|
|
@@ -249,13 +278,20 @@ export class GitLabGraphQLClient {
|
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
`;
|
|
252
|
-
|
|
281
|
+
if (fetchAll) {
|
|
282
|
+
return this.fetchAllPages(query, { sort }, 'projects', {
|
|
283
|
+
maxItems: first,
|
|
284
|
+
pageSize: this.config.maxPageSize,
|
|
285
|
+
userConfig,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return this.query(query, { first: Math.min(first, this.config.maxPageSize), after, sort }, userConfig);
|
|
253
289
|
}
|
|
254
|
-
async getIssues(projectPath, first = 20, after, userConfig) {
|
|
290
|
+
async getIssues(projectPath, first = 20, after, fetchAll = false, userConfig, sort) {
|
|
255
291
|
const query = gql `
|
|
256
|
-
query getIssues($projectPath: ID!, $first: Int!, $after: String) {
|
|
292
|
+
query getIssues($projectPath: ID!, $first: Int!, $after: String, $sort: IssueSort) {
|
|
257
293
|
project(fullPath: $projectPath) {
|
|
258
|
-
issues(first: $first, after: $after) {
|
|
294
|
+
issues(first: $first, after: $after, sort: $sort) {
|
|
259
295
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
260
296
|
nodes {
|
|
261
297
|
id
|
|
@@ -271,13 +307,20 @@ export class GitLabGraphQLClient {
|
|
|
271
307
|
}
|
|
272
308
|
}
|
|
273
309
|
`;
|
|
274
|
-
|
|
310
|
+
if (fetchAll) {
|
|
311
|
+
return this.fetchAllPages(query, { projectPath, sort: sort || 'UPDATED_DESC' }, 'project.issues', {
|
|
312
|
+
maxItems: first,
|
|
313
|
+
pageSize: this.config.maxPageSize,
|
|
314
|
+
userConfig,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return this.query(query, { projectPath, first: Math.min(first, this.config.maxPageSize), after, sort: sort || 'UPDATED_DESC' }, userConfig);
|
|
275
318
|
}
|
|
276
|
-
async getMergeRequests(projectPath, first = 20, after, userConfig) {
|
|
319
|
+
async getMergeRequests(projectPath, first = 20, after, fetchAll = false, userConfig, sort) {
|
|
277
320
|
const query = gql `
|
|
278
|
-
query getMergeRequests($projectPath: ID!, $first: Int!, $after: String) {
|
|
321
|
+
query getMergeRequests($projectPath: ID!, $first: Int!, $after: String, $sort: MergeRequestSort) {
|
|
279
322
|
project(fullPath: $projectPath) {
|
|
280
|
-
mergeRequests(first: $first, after: $after) {
|
|
323
|
+
mergeRequests(first: $first, after: $after, sort: $sort) {
|
|
281
324
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
282
325
|
nodes {
|
|
283
326
|
id
|
|
@@ -296,7 +339,108 @@ export class GitLabGraphQLClient {
|
|
|
296
339
|
}
|
|
297
340
|
}
|
|
298
341
|
`;
|
|
299
|
-
|
|
342
|
+
if (fetchAll) {
|
|
343
|
+
return this.fetchAllPages(query, { projectPath, sort: sort || 'UPDATED_DESC' }, 'project.mergeRequests', {
|
|
344
|
+
maxItems: first,
|
|
345
|
+
pageSize: this.config.maxPageSize,
|
|
346
|
+
userConfig,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return this.query(query, { projectPath, first: Math.min(first, this.config.maxPageSize), after, sort: sort || 'UPDATED_DESC' }, userConfig);
|
|
350
|
+
}
|
|
351
|
+
async getWorkItem(id, userConfig) {
|
|
352
|
+
const gid = id.startsWith('gid://') ? id : `gid://gitlab/WorkItem/${id}`;
|
|
353
|
+
const query = gql `
|
|
354
|
+
query getWorkItem($id: WorkItemID!) {
|
|
355
|
+
workItem(id: $id) {
|
|
356
|
+
id
|
|
357
|
+
iid
|
|
358
|
+
title
|
|
359
|
+
state
|
|
360
|
+
confidential
|
|
361
|
+
createdAt
|
|
362
|
+
updatedAt
|
|
363
|
+
closedAt
|
|
364
|
+
webUrl
|
|
365
|
+
workItemType { id name iconName }
|
|
366
|
+
author { username name }
|
|
367
|
+
namespace { id fullPath }
|
|
368
|
+
widgets {
|
|
369
|
+
type
|
|
370
|
+
... on WorkItemWidgetDescription { description descriptionHtml }
|
|
371
|
+
... on WorkItemWidgetAssignees {
|
|
372
|
+
assignees { nodes { username name webUrl } }
|
|
373
|
+
}
|
|
374
|
+
... on WorkItemWidgetLabels {
|
|
375
|
+
labels { nodes { id title color description } }
|
|
376
|
+
}
|
|
377
|
+
... on WorkItemWidgetHierarchy {
|
|
378
|
+
hasChildren
|
|
379
|
+
hasParent
|
|
380
|
+
parent { id iid title workItemType { name } webUrl }
|
|
381
|
+
children {
|
|
382
|
+
nodes { id iid title state workItemType { name } webUrl }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
... on WorkItemWidgetMilestone {
|
|
386
|
+
milestone { id title state dueDate webPath }
|
|
387
|
+
}
|
|
388
|
+
... on WorkItemWidgetStartAndDueDate { startDate dueDate }
|
|
389
|
+
... on WorkItemWidgetNotes {
|
|
390
|
+
discussions(first: 5) {
|
|
391
|
+
nodes {
|
|
392
|
+
id
|
|
393
|
+
notes { nodes { id body author { username } createdAt } }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
`;
|
|
401
|
+
return this.query(query, { id: gid }, userConfig);
|
|
402
|
+
}
|
|
403
|
+
async listWorkItems(fullPath, opts = {}, userConfig) {
|
|
404
|
+
const { first = 20, after, fetchAll = false, types, state, sort = 'UPDATED_DESC' } = opts;
|
|
405
|
+
const query = gql `
|
|
406
|
+
query listWorkItems(
|
|
407
|
+
$fullPath: ID!
|
|
408
|
+
$first: Int!
|
|
409
|
+
$after: String
|
|
410
|
+
$types: [IssueType!]
|
|
411
|
+
$state: IssuableState
|
|
412
|
+
$sort: WorkItemSort
|
|
413
|
+
) {
|
|
414
|
+
namespace(fullPath: $fullPath) {
|
|
415
|
+
id
|
|
416
|
+
fullPath
|
|
417
|
+
workItems(first: $first, after: $after, types: $types, state: $state, sort: $sort) {
|
|
418
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
419
|
+
nodes {
|
|
420
|
+
id
|
|
421
|
+
iid
|
|
422
|
+
title
|
|
423
|
+
state
|
|
424
|
+
confidential
|
|
425
|
+
createdAt
|
|
426
|
+
updatedAt
|
|
427
|
+
webUrl
|
|
428
|
+
workItemType { name iconName }
|
|
429
|
+
author { username name }
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
const variables = { fullPath, types, state, sort };
|
|
436
|
+
if (fetchAll) {
|
|
437
|
+
return this.fetchAllPages(query, variables, 'namespace.workItems', {
|
|
438
|
+
maxItems: first,
|
|
439
|
+
pageSize: this.config.maxPageSize,
|
|
440
|
+
userConfig,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return this.query(query, { ...variables, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
300
444
|
}
|
|
301
445
|
async createIssue(projectPath, title, description, userConfig) {
|
|
302
446
|
await this.introspectSchema(userConfig);
|
|
@@ -623,10 +767,11 @@ export class GitLabGraphQLClient {
|
|
|
623
767
|
const fields = type.getFields();
|
|
624
768
|
return Object.keys(fields);
|
|
625
769
|
}
|
|
626
|
-
async globalSearch(searchTerm,
|
|
770
|
+
async globalSearch(searchTerm, first = 20, after, userConfig) {
|
|
627
771
|
const query = gql `
|
|
628
|
-
query globalSearch($search: String, $first: Int
|
|
629
|
-
projects(search: $search, first: $first) {
|
|
772
|
+
query globalSearch($search: String, $first: Int!, $after: String) {
|
|
773
|
+
projects(search: $search, first: $first, after: $after) {
|
|
774
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
630
775
|
nodes {
|
|
631
776
|
id
|
|
632
777
|
name
|
|
@@ -635,7 +780,8 @@ export class GitLabGraphQLClient {
|
|
|
635
780
|
visibility
|
|
636
781
|
}
|
|
637
782
|
}
|
|
638
|
-
issues(search: $search, first: $first) {
|
|
783
|
+
issues(search: $search, first: $first, after: $after) {
|
|
784
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
639
785
|
nodes {
|
|
640
786
|
id
|
|
641
787
|
iid
|
|
@@ -649,10 +795,34 @@ export class GitLabGraphQLClient {
|
|
|
649
795
|
`;
|
|
650
796
|
return this.query(query, {
|
|
651
797
|
search: searchTerm || undefined,
|
|
652
|
-
first: Math.min(this.config.maxPageSize,
|
|
798
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
799
|
+
after
|
|
653
800
|
}, userConfig);
|
|
654
801
|
}
|
|
655
|
-
async
|
|
802
|
+
async globalSearchAll(searchTerm, maxItems = 100, userConfig) {
|
|
803
|
+
const projectsQuery = gql `
|
|
804
|
+
query globalSearchProjects($search: String, $first: Int!, $after: String) {
|
|
805
|
+
projects(search: $search, first: $first, after: $after) {
|
|
806
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
807
|
+
nodes { id name fullPath webUrl visibility }
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
`;
|
|
811
|
+
const issuesQuery = gql `
|
|
812
|
+
query globalSearchIssues($search: String, $first: Int!, $after: String) {
|
|
813
|
+
issues(search: $search, first: $first, after: $after) {
|
|
814
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
815
|
+
nodes { id iid title state webUrl createdAt }
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
`;
|
|
819
|
+
const [projects, issues] = await Promise.all([
|
|
820
|
+
this.fetchAllPages(projectsQuery, { search: searchTerm || undefined }, 'projects', { maxItems, userConfig }),
|
|
821
|
+
this.fetchAllPages(issuesQuery, { search: searchTerm || undefined }, 'issues', { maxItems, userConfig }),
|
|
822
|
+
]);
|
|
823
|
+
return { projects, issues };
|
|
824
|
+
}
|
|
825
|
+
async searchProjects(searchTerm, first = 20, after, fetchAll = false, userConfig) {
|
|
656
826
|
const query = gql `
|
|
657
827
|
query searchProjects($search: String!, $first: Int!, $after: String) {
|
|
658
828
|
projects(search: $search, first: $first, after: $after) {
|
|
@@ -669,7 +839,14 @@ export class GitLabGraphQLClient {
|
|
|
669
839
|
}
|
|
670
840
|
}
|
|
671
841
|
`;
|
|
672
|
-
|
|
842
|
+
if (fetchAll) {
|
|
843
|
+
return this.fetchAllPages(query, { search: searchTerm }, 'projects', {
|
|
844
|
+
maxItems: first,
|
|
845
|
+
pageSize: this.config.maxPageSize,
|
|
846
|
+
userConfig,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
673
850
|
}
|
|
674
851
|
getTypeName(t) {
|
|
675
852
|
if (!t)
|
|
@@ -683,7 +860,7 @@ export class GitLabGraphQLClient {
|
|
|
683
860
|
const values = (enumType && typeof enumType.getValues === 'function') ? enumType.getValues() : [];
|
|
684
861
|
return Array.isArray(values) ? values.map((v) => v.name) : [];
|
|
685
862
|
}
|
|
686
|
-
async searchIssues(searchTerm, projectPath, state, first = 20, after, userConfig, assigneeUsernames, authorUsername, labelNames) {
|
|
863
|
+
async searchIssues(searchTerm, projectPath, state, first = 20, after, fetchAll = false, userConfig, assigneeUsernames, authorUsername, labelNames, sort) {
|
|
687
864
|
await this.introspectSchema(userConfig);
|
|
688
865
|
const mappedState = state && state.toLowerCase() !== 'all' ? state.toUpperCase() : undefined;
|
|
689
866
|
if (projectPath) {
|
|
@@ -697,9 +874,9 @@ export class GitLabGraphQLClient {
|
|
|
697
874
|
? (allowed.find(v => v.toLowerCase() === state.toLowerCase()) || undefined)
|
|
698
875
|
: undefined;
|
|
699
876
|
const query = gql `
|
|
700
|
-
query searchIssuesProject($projectPath: ID!, $search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!]) {
|
|
877
|
+
query searchIssuesProject($projectPath: ID!, $search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!], $sort: IssueSort) {
|
|
701
878
|
project(fullPath: $projectPath) {
|
|
702
|
-
issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName) {
|
|
879
|
+
issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName, sort: $sort) {
|
|
703
880
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
704
881
|
nodes {
|
|
705
882
|
id
|
|
@@ -716,6 +893,21 @@ export class GitLabGraphQLClient {
|
|
|
716
893
|
}
|
|
717
894
|
}
|
|
718
895
|
`;
|
|
896
|
+
if (fetchAll) {
|
|
897
|
+
return this.fetchAllPages(query, {
|
|
898
|
+
projectPath,
|
|
899
|
+
search: searchTerm,
|
|
900
|
+
state: mapped,
|
|
901
|
+
assigneeUsernames,
|
|
902
|
+
authorUsername,
|
|
903
|
+
labelName: labelNames,
|
|
904
|
+
sort: sort || 'UPDATED_DESC'
|
|
905
|
+
}, 'project.issues', {
|
|
906
|
+
maxItems: first,
|
|
907
|
+
pageSize: this.config.maxPageSize,
|
|
908
|
+
userConfig,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
719
911
|
return this.query(query, {
|
|
720
912
|
projectPath,
|
|
721
913
|
search: searchTerm,
|
|
@@ -724,7 +916,8 @@ export class GitLabGraphQLClient {
|
|
|
724
916
|
after,
|
|
725
917
|
assigneeUsernames,
|
|
726
918
|
authorUsername,
|
|
727
|
-
labelName: labelNames
|
|
919
|
+
labelName: labelNames,
|
|
920
|
+
sort: sort || 'UPDATED_DESC'
|
|
728
921
|
}, userConfig);
|
|
729
922
|
}
|
|
730
923
|
else {
|
|
@@ -742,8 +935,8 @@ export class GitLabGraphQLClient {
|
|
|
742
935
|
// We keep description for AI context but limit nested collections (assignees/labels)
|
|
743
936
|
// Note: Global Issue type doesn't have 'project' field - only project-scoped queries do
|
|
744
937
|
const query = gql `
|
|
745
|
-
query searchIssuesGlobal($search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!]) {
|
|
746
|
-
issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName) {
|
|
938
|
+
query searchIssuesGlobal($search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!], $sort: IssueSort) {
|
|
939
|
+
issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName, sort: $sort) {
|
|
747
940
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
748
941
|
nodes {
|
|
749
942
|
id
|
|
@@ -762,6 +955,20 @@ export class GitLabGraphQLClient {
|
|
|
762
955
|
`;
|
|
763
956
|
// Note: Global search returns streamlined fields (no assignees/labels/project) for performance.
|
|
764
957
|
// For full details and project attribution, search within a specific project using projectPath.
|
|
958
|
+
if (fetchAll) {
|
|
959
|
+
return this.fetchAllPages(query, {
|
|
960
|
+
search: searchTerm,
|
|
961
|
+
state: mapped,
|
|
962
|
+
assigneeUsernames,
|
|
963
|
+
authorUsername,
|
|
964
|
+
labelName: labelNames,
|
|
965
|
+
sort: sort || 'UPDATED_DESC'
|
|
966
|
+
}, 'issues', {
|
|
967
|
+
maxItems: first,
|
|
968
|
+
pageSize: this.config.maxPageSize,
|
|
969
|
+
userConfig,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
765
972
|
return this.query(query, {
|
|
766
973
|
search: searchTerm,
|
|
767
974
|
state: mapped,
|
|
@@ -769,17 +976,18 @@ export class GitLabGraphQLClient {
|
|
|
769
976
|
after,
|
|
770
977
|
assigneeUsernames,
|
|
771
978
|
authorUsername,
|
|
772
|
-
labelName: labelNames
|
|
979
|
+
labelName: labelNames,
|
|
980
|
+
sort: sort || 'UPDATED_DESC'
|
|
773
981
|
}, userConfig);
|
|
774
982
|
}
|
|
775
983
|
}
|
|
776
|
-
async searchMergeRequests(searchTerm, projectPath, state, first = 20, after, userConfig) {
|
|
984
|
+
async searchMergeRequests(searchTerm, projectPath, state, first = 20, after, fetchAll = false, userConfig, sort) {
|
|
777
985
|
const mappedState = state && state.toLowerCase() !== 'all' ? state.toUpperCase() : undefined;
|
|
778
986
|
if (projectPath) {
|
|
779
987
|
const query = gql `
|
|
780
|
-
query searchMergeRequestsProject($projectPath: ID!, $search: String, $state: MergeRequestState, $first: Int!, $after: String) {
|
|
988
|
+
query searchMergeRequestsProject($projectPath: ID!, $search: String, $state: MergeRequestState, $first: Int!, $after: String, $sort: MergeRequestSort) {
|
|
781
989
|
project(fullPath: $projectPath) {
|
|
782
|
-
mergeRequests(search: $search, state: $state, first: $first, after: $after) {
|
|
990
|
+
mergeRequests(search: $search, state: $state, first: $first, after: $after, sort: $sort) {
|
|
783
991
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
784
992
|
nodes {
|
|
785
993
|
id
|
|
@@ -798,12 +1006,25 @@ export class GitLabGraphQLClient {
|
|
|
798
1006
|
}
|
|
799
1007
|
}
|
|
800
1008
|
`;
|
|
1009
|
+
if (fetchAll) {
|
|
1010
|
+
return this.fetchAllPages(query, {
|
|
1011
|
+
projectPath,
|
|
1012
|
+
search: searchTerm,
|
|
1013
|
+
state: mappedState,
|
|
1014
|
+
sort: sort || 'UPDATED_DESC'
|
|
1015
|
+
}, 'project.mergeRequests', {
|
|
1016
|
+
maxItems: first,
|
|
1017
|
+
pageSize: this.config.maxPageSize,
|
|
1018
|
+
userConfig,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
801
1021
|
return this.query(query, {
|
|
802
1022
|
projectPath,
|
|
803
1023
|
search: searchTerm,
|
|
804
1024
|
state: mappedState,
|
|
805
|
-
first: Math.min(first,
|
|
806
|
-
after
|
|
1025
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1026
|
+
after,
|
|
1027
|
+
sort: sort || 'UPDATED_DESC'
|
|
807
1028
|
}, userConfig);
|
|
808
1029
|
}
|
|
809
1030
|
else {
|
|
@@ -851,9 +1072,20 @@ export class GitLabGraphQLClient {
|
|
|
851
1072
|
}
|
|
852
1073
|
`;
|
|
853
1074
|
try {
|
|
1075
|
+
if (fetchAll) {
|
|
1076
|
+
const connectionPath = `user.${fieldName}`;
|
|
1077
|
+
return this.fetchAllPages(query, {
|
|
1078
|
+
username,
|
|
1079
|
+
state: mappedState
|
|
1080
|
+
}, connectionPath, {
|
|
1081
|
+
maxItems: first,
|
|
1082
|
+
pageSize: this.config.maxPageSize,
|
|
1083
|
+
userConfig,
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
854
1086
|
const result = await this.query(query, {
|
|
855
1087
|
username,
|
|
856
|
-
first: Math.min(first,
|
|
1088
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
857
1089
|
after,
|
|
858
1090
|
state: mappedState
|
|
859
1091
|
}, userConfig);
|
|
@@ -997,10 +1229,16 @@ export class GitLabGraphQLClient {
|
|
|
997
1229
|
ref: ref || "HEAD"
|
|
998
1230
|
}, userConfig);
|
|
999
1231
|
}
|
|
1000
|
-
async searchUsers(searchTerm, first = 20, userConfig) {
|
|
1232
|
+
async searchUsers(searchTerm, first = 20, after, fetchAll = false, userConfig) {
|
|
1001
1233
|
const query = gql `
|
|
1002
|
-
query searchUsers($search: String!, $first: Int
|
|
1003
|
-
users(search: $search, first: $first) {
|
|
1234
|
+
query searchUsers($search: String!, $first: Int!, $after: String) {
|
|
1235
|
+
users(search: $search, first: $first, after: $after) {
|
|
1236
|
+
pageInfo {
|
|
1237
|
+
hasNextPage
|
|
1238
|
+
hasPreviousPage
|
|
1239
|
+
startCursor
|
|
1240
|
+
endCursor
|
|
1241
|
+
}
|
|
1004
1242
|
nodes {
|
|
1005
1243
|
id
|
|
1006
1244
|
username
|
|
@@ -1012,12 +1250,25 @@ export class GitLabGraphQLClient {
|
|
|
1012
1250
|
}
|
|
1013
1251
|
}
|
|
1014
1252
|
`;
|
|
1015
|
-
|
|
1253
|
+
if (fetchAll) {
|
|
1254
|
+
return this.fetchAllPages(query, { search: searchTerm }, 'users', {
|
|
1255
|
+
maxItems: first,
|
|
1256
|
+
pageSize: this.config.maxPageSize,
|
|
1257
|
+
userConfig,
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1016
1261
|
}
|
|
1017
|
-
async searchGroups(searchTerm, first = 20, userConfig) {
|
|
1262
|
+
async searchGroups(searchTerm, first = 20, after, fetchAll = false, userConfig) {
|
|
1018
1263
|
const query = gql `
|
|
1019
|
-
query searchGroups($search: String!, $first: Int
|
|
1020
|
-
groups(search: $search, first: $first) {
|
|
1264
|
+
query searchGroups($search: String!, $first: Int!, $after: String) {
|
|
1265
|
+
groups(search: $search, first: $first, after: $after) {
|
|
1266
|
+
pageInfo {
|
|
1267
|
+
hasNextPage
|
|
1268
|
+
hasPreviousPage
|
|
1269
|
+
startCursor
|
|
1270
|
+
endCursor
|
|
1271
|
+
}
|
|
1021
1272
|
nodes {
|
|
1022
1273
|
id
|
|
1023
1274
|
name
|
|
@@ -1030,7 +1281,811 @@ export class GitLabGraphQLClient {
|
|
|
1030
1281
|
}
|
|
1031
1282
|
}
|
|
1032
1283
|
`;
|
|
1033
|
-
|
|
1284
|
+
if (fetchAll) {
|
|
1285
|
+
return this.fetchAllPages(query, { search: searchTerm }, 'groups', {
|
|
1286
|
+
maxItems: first,
|
|
1287
|
+
pageSize: this.config.maxPageSize,
|
|
1288
|
+
userConfig,
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1292
|
+
}
|
|
1293
|
+
// ── CI/CD Pipelines ──────────────────────────────────────────────────
|
|
1294
|
+
async getMergeRequestPipelines(projectPath, iid, first = 20, after, fetchAll = false, userConfig) {
|
|
1295
|
+
const query = gql `
|
|
1296
|
+
query getMergeRequestPipelines($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1297
|
+
project(fullPath: $projectPath) {
|
|
1298
|
+
mergeRequest(iid: $iid) {
|
|
1299
|
+
pipelines(first: $first, after: $after) {
|
|
1300
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1301
|
+
nodes {
|
|
1302
|
+
id
|
|
1303
|
+
iid
|
|
1304
|
+
status
|
|
1305
|
+
duration
|
|
1306
|
+
createdAt
|
|
1307
|
+
finishedAt
|
|
1308
|
+
ref
|
|
1309
|
+
sha
|
|
1310
|
+
detailedStatus {
|
|
1311
|
+
text
|
|
1312
|
+
label
|
|
1313
|
+
icon
|
|
1314
|
+
group
|
|
1315
|
+
detailsPath
|
|
1316
|
+
}
|
|
1317
|
+
stages {
|
|
1318
|
+
nodes {
|
|
1319
|
+
name
|
|
1320
|
+
status
|
|
1321
|
+
detailedStatus {
|
|
1322
|
+
text
|
|
1323
|
+
label
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
`;
|
|
1333
|
+
if (fetchAll) {
|
|
1334
|
+
return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.pipelines', {
|
|
1335
|
+
maxItems: first,
|
|
1336
|
+
pageSize: this.config.maxPageSize,
|
|
1337
|
+
userConfig,
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1341
|
+
}
|
|
1342
|
+
async getPipelineJobs(projectPath, pipelineIid, first = 20, after, fetchAll = false, userConfig) {
|
|
1343
|
+
const query = gql `
|
|
1344
|
+
query getPipelineJobs($projectPath: ID!, $pipelineIid: ID!, $first: Int!, $after: String) {
|
|
1345
|
+
project(fullPath: $projectPath) {
|
|
1346
|
+
pipeline(iid: $pipelineIid) {
|
|
1347
|
+
id
|
|
1348
|
+
iid
|
|
1349
|
+
status
|
|
1350
|
+
jobs(first: $first, after: $after) {
|
|
1351
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1352
|
+
nodes {
|
|
1353
|
+
id
|
|
1354
|
+
name
|
|
1355
|
+
status
|
|
1356
|
+
stage {
|
|
1357
|
+
name
|
|
1358
|
+
}
|
|
1359
|
+
duration
|
|
1360
|
+
queuedDuration
|
|
1361
|
+
createdAt
|
|
1362
|
+
startedAt
|
|
1363
|
+
finishedAt
|
|
1364
|
+
retryable
|
|
1365
|
+
cancelable
|
|
1366
|
+
webPath
|
|
1367
|
+
detailedStatus {
|
|
1368
|
+
text
|
|
1369
|
+
label
|
|
1370
|
+
icon
|
|
1371
|
+
group
|
|
1372
|
+
detailsPath
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
`;
|
|
1380
|
+
if (fetchAll) {
|
|
1381
|
+
return this.fetchAllPages(query, { projectPath, pipelineIid }, 'project.pipeline.jobs', {
|
|
1382
|
+
maxItems: first,
|
|
1383
|
+
pageSize: this.config.maxPageSize,
|
|
1384
|
+
userConfig,
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return this.query(query, { projectPath, pipelineIid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1388
|
+
}
|
|
1389
|
+
async getPipelineId(projectPath, pipelineIid, userConfig) {
|
|
1390
|
+
const query = gql `
|
|
1391
|
+
query pipelineId($projectPath: ID!, $pipelineIid: ID!) {
|
|
1392
|
+
project(fullPath: $projectPath) {
|
|
1393
|
+
pipeline(iid: $pipelineIid) { id }
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
`;
|
|
1397
|
+
const result = await this.query(query, { projectPath, pipelineIid }, userConfig);
|
|
1398
|
+
const id = result?.project?.pipeline?.id;
|
|
1399
|
+
if (!id)
|
|
1400
|
+
throw new Error('Pipeline not found');
|
|
1401
|
+
return id;
|
|
1402
|
+
}
|
|
1403
|
+
async managePipeline(projectPath, pipelineIid, action, userConfig) {
|
|
1404
|
+
await this.introspectSchema(userConfig);
|
|
1405
|
+
const mutationType = this.schema?.getMutationType();
|
|
1406
|
+
const fields = mutationType ? mutationType.getFields() : {};
|
|
1407
|
+
const pipelineGlobalId = await this.getPipelineId(projectPath, pipelineIid, userConfig);
|
|
1408
|
+
if (action === 'retry') {
|
|
1409
|
+
const fieldName = fields['pipelineRetry'] ? 'pipelineRetry' : null;
|
|
1410
|
+
if (!fieldName) {
|
|
1411
|
+
throw new Error('pipelineRetry mutation is not available on this GitLab instance');
|
|
1412
|
+
}
|
|
1413
|
+
const mutation = gql `
|
|
1414
|
+
mutation retryPipeline($input: PipelineRetryInput!) {
|
|
1415
|
+
pipelineRetry(input: $input) {
|
|
1416
|
+
pipeline { id iid status }
|
|
1417
|
+
errors
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
`;
|
|
1421
|
+
const result = await this.query(mutation, { input: { id: pipelineGlobalId } }, userConfig, true);
|
|
1422
|
+
const payload = result.pipelineRetry;
|
|
1423
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
1424
|
+
throw new Error(`Failed to retry pipeline: ${payload.errors.join(', ')}`);
|
|
1425
|
+
}
|
|
1426
|
+
return payload;
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
const fieldName = fields['pipelineCancel'] ? 'pipelineCancel' : null;
|
|
1430
|
+
if (!fieldName) {
|
|
1431
|
+
throw new Error('pipelineCancel mutation is not available on this GitLab instance');
|
|
1432
|
+
}
|
|
1433
|
+
const mutation = gql `
|
|
1434
|
+
mutation cancelPipeline($input: PipelineCancelInput!) {
|
|
1435
|
+
pipelineCancel(input: $input) {
|
|
1436
|
+
errors
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
`;
|
|
1440
|
+
const result = await this.query(mutation, { input: { id: pipelineGlobalId } }, userConfig, true);
|
|
1441
|
+
const payload = result.pipelineCancel;
|
|
1442
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
1443
|
+
throw new Error(`Failed to cancel pipeline: ${payload.errors.join(', ')}`);
|
|
1444
|
+
}
|
|
1445
|
+
return payload;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
// ── MR Diffs & Commits ───────────────────────────────────────────────
|
|
1449
|
+
async getMergeRequestDiffs(projectPath, iid, userConfig) {
|
|
1450
|
+
const query = gql `
|
|
1451
|
+
query getMergeRequestDiffs($projectPath: ID!, $iid: String!) {
|
|
1452
|
+
project(fullPath: $projectPath) {
|
|
1453
|
+
mergeRequest(iid: $iid) {
|
|
1454
|
+
diffStatsSummary {
|
|
1455
|
+
additions
|
|
1456
|
+
deletions
|
|
1457
|
+
fileCount
|
|
1458
|
+
}
|
|
1459
|
+
diffStats {
|
|
1460
|
+
path
|
|
1461
|
+
additions
|
|
1462
|
+
deletions
|
|
1463
|
+
}
|
|
1464
|
+
diffRefs {
|
|
1465
|
+
baseSha
|
|
1466
|
+
headSha
|
|
1467
|
+
startSha
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
`;
|
|
1473
|
+
return this.query(query, { projectPath, iid }, userConfig);
|
|
1474
|
+
}
|
|
1475
|
+
async getMergeRequestCommits(projectPath, iid, first = 20, after, fetchAll = false, userConfig) {
|
|
1476
|
+
const query = gql `
|
|
1477
|
+
query getMergeRequestCommits($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1478
|
+
project(fullPath: $projectPath) {
|
|
1479
|
+
mergeRequest(iid: $iid) {
|
|
1480
|
+
commitCount
|
|
1481
|
+
commitsWithoutMergeCommits(first: $first, after: $after) {
|
|
1482
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1483
|
+
nodes {
|
|
1484
|
+
sha
|
|
1485
|
+
shortId
|
|
1486
|
+
title
|
|
1487
|
+
message
|
|
1488
|
+
authorName
|
|
1489
|
+
authoredDate
|
|
1490
|
+
webUrl
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
`;
|
|
1497
|
+
if (fetchAll) {
|
|
1498
|
+
return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.commitsWithoutMergeCommits', {
|
|
1499
|
+
maxItems: first,
|
|
1500
|
+
pageSize: this.config.maxPageSize,
|
|
1501
|
+
userConfig,
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1505
|
+
}
|
|
1506
|
+
// ── Work Item Notes ──────────────────────────────────────────────────
|
|
1507
|
+
async getNotes(projectPath, noteableType, iid, first = 20, after, fetchAll = false, userConfig) {
|
|
1508
|
+
if (noteableType === 'issue') {
|
|
1509
|
+
const query = gql `
|
|
1510
|
+
query getIssueNotes($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1511
|
+
project(fullPath: $projectPath) {
|
|
1512
|
+
issue(iid: $iid) {
|
|
1513
|
+
notes(first: $first, after: $after) {
|
|
1514
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1515
|
+
nodes {
|
|
1516
|
+
id
|
|
1517
|
+
body
|
|
1518
|
+
author { username name }
|
|
1519
|
+
createdAt
|
|
1520
|
+
updatedAt
|
|
1521
|
+
system
|
|
1522
|
+
internal
|
|
1523
|
+
url
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
`;
|
|
1530
|
+
if (fetchAll) {
|
|
1531
|
+
return this.fetchAllPages(query, { projectPath, iid }, 'project.issue.notes', {
|
|
1532
|
+
maxItems: first,
|
|
1533
|
+
pageSize: this.config.maxPageSize,
|
|
1534
|
+
userConfig,
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1538
|
+
}
|
|
1539
|
+
else {
|
|
1540
|
+
const query = gql `
|
|
1541
|
+
query getMergeRequestNotes($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1542
|
+
project(fullPath: $projectPath) {
|
|
1543
|
+
mergeRequest(iid: $iid) {
|
|
1544
|
+
notes(first: $first, after: $after) {
|
|
1545
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1546
|
+
nodes {
|
|
1547
|
+
id
|
|
1548
|
+
body
|
|
1549
|
+
author { username name }
|
|
1550
|
+
createdAt
|
|
1551
|
+
updatedAt
|
|
1552
|
+
system
|
|
1553
|
+
internal
|
|
1554
|
+
url
|
|
1555
|
+
position {
|
|
1556
|
+
filePath
|
|
1557
|
+
newLine
|
|
1558
|
+
oldLine
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
`;
|
|
1566
|
+
if (fetchAll) {
|
|
1567
|
+
return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.notes', {
|
|
1568
|
+
maxItems: first,
|
|
1569
|
+
pageSize: this.config.maxPageSize,
|
|
1570
|
+
userConfig,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
async createNote(projectPath, noteableType, iid, body, internal = false, userConfig) {
|
|
1577
|
+
await this.introspectSchema(userConfig);
|
|
1578
|
+
const mutationType = this.schema?.getMutationType();
|
|
1579
|
+
const fields = mutationType ? mutationType.getFields() : {};
|
|
1580
|
+
const fieldName = fields['createNote'] ? 'createNote' : null;
|
|
1581
|
+
if (!fieldName) {
|
|
1582
|
+
throw new Error('createNote mutation is not available on this GitLab instance');
|
|
1583
|
+
}
|
|
1584
|
+
// Resolve the noteable IID to a global ID
|
|
1585
|
+
const noteableId = noteableType === 'issue'
|
|
1586
|
+
? await this.getIssueId(projectPath, iid, userConfig)
|
|
1587
|
+
: await this.getMergeRequestId(projectPath, iid, userConfig);
|
|
1588
|
+
const mutation = gql `
|
|
1589
|
+
mutation createNote($input: CreateNoteInput!) {
|
|
1590
|
+
createNote(input: $input) {
|
|
1591
|
+
note {
|
|
1592
|
+
id
|
|
1593
|
+
body
|
|
1594
|
+
author { username name }
|
|
1595
|
+
createdAt
|
|
1596
|
+
url
|
|
1597
|
+
internal
|
|
1598
|
+
}
|
|
1599
|
+
errors
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
`;
|
|
1603
|
+
const input = {
|
|
1604
|
+
noteableId,
|
|
1605
|
+
body,
|
|
1606
|
+
};
|
|
1607
|
+
if (internal) {
|
|
1608
|
+
input.internal = true;
|
|
1609
|
+
}
|
|
1610
|
+
const result = await this.query(mutation, { input }, userConfig, true);
|
|
1611
|
+
return result.createNote;
|
|
1612
|
+
}
|
|
1613
|
+
// ── Milestones ─────────────────────────────────────────────────────
|
|
1614
|
+
async listMilestones(fullPath, isProject, state, search, includeAncestors = false, first = 20, after, fetchAll = false, userConfig) {
|
|
1615
|
+
const milestoneFields = `
|
|
1616
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1617
|
+
nodes {
|
|
1618
|
+
id
|
|
1619
|
+
iid
|
|
1620
|
+
title
|
|
1621
|
+
description
|
|
1622
|
+
state
|
|
1623
|
+
dueDate
|
|
1624
|
+
startDate
|
|
1625
|
+
expired
|
|
1626
|
+
upcoming
|
|
1627
|
+
webPath
|
|
1628
|
+
stats {
|
|
1629
|
+
totalIssuesCount
|
|
1630
|
+
closedIssuesCount
|
|
1631
|
+
}
|
|
1632
|
+
createdAt
|
|
1633
|
+
updatedAt
|
|
1634
|
+
}
|
|
1635
|
+
`;
|
|
1636
|
+
if (isProject) {
|
|
1637
|
+
const query = gql `
|
|
1638
|
+
query listProjectMilestones($fullPath: ID!, $state: MilestoneStateEnum, $searchTitle: String, $includeAncestors: Boolean, $first: Int!, $after: String) {
|
|
1639
|
+
project(fullPath: $fullPath) {
|
|
1640
|
+
milestones(state: $state, searchTitle: $searchTitle, includeAncestors: $includeAncestors, first: $first, after: $after) {
|
|
1641
|
+
${milestoneFields}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
`;
|
|
1646
|
+
if (fetchAll) {
|
|
1647
|
+
return this.fetchAllPages(query, {
|
|
1648
|
+
fullPath,
|
|
1649
|
+
state: state?.toLowerCase(),
|
|
1650
|
+
searchTitle: search,
|
|
1651
|
+
includeAncestors,
|
|
1652
|
+
}, 'project.milestones', {
|
|
1653
|
+
maxItems: first,
|
|
1654
|
+
pageSize: this.config.maxPageSize,
|
|
1655
|
+
userConfig,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
return this.query(query, {
|
|
1659
|
+
fullPath,
|
|
1660
|
+
state: state?.toLowerCase(),
|
|
1661
|
+
searchTitle: search,
|
|
1662
|
+
includeAncestors,
|
|
1663
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1664
|
+
after,
|
|
1665
|
+
}, userConfig);
|
|
1666
|
+
}
|
|
1667
|
+
else {
|
|
1668
|
+
const query = gql `
|
|
1669
|
+
query listGroupMilestones($fullPath: ID!, $state: MilestoneStateEnum, $searchTitle: String, $includeAncestors: Boolean, $first: Int!, $after: String) {
|
|
1670
|
+
group(fullPath: $fullPath) {
|
|
1671
|
+
milestones(state: $state, searchTitle: $searchTitle, includeAncestors: $includeAncestors, first: $first, after: $after) {
|
|
1672
|
+
${milestoneFields}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
`;
|
|
1677
|
+
if (fetchAll) {
|
|
1678
|
+
return this.fetchAllPages(query, {
|
|
1679
|
+
fullPath,
|
|
1680
|
+
state: state?.toLowerCase(),
|
|
1681
|
+
searchTitle: search,
|
|
1682
|
+
includeAncestors,
|
|
1683
|
+
}, 'group.milestones', {
|
|
1684
|
+
maxItems: first,
|
|
1685
|
+
pageSize: this.config.maxPageSize,
|
|
1686
|
+
userConfig,
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
return this.query(query, {
|
|
1690
|
+
fullPath,
|
|
1691
|
+
state: state?.toLowerCase(),
|
|
1692
|
+
searchTitle: search,
|
|
1693
|
+
includeAncestors,
|
|
1694
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1695
|
+
after,
|
|
1696
|
+
}, userConfig);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
// ── Iterations ────────────────────────────────────────────────────
|
|
1700
|
+
async listIterations(groupPath, state, first = 20, after, fetchAll = false, userConfig) {
|
|
1701
|
+
const query = gql `
|
|
1702
|
+
query listIterations($groupPath: ID!, $state: IterationState, $first: Int!, $after: String) {
|
|
1703
|
+
group(fullPath: $groupPath) {
|
|
1704
|
+
iterations(state: $state, first: $first, after: $after) {
|
|
1705
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1706
|
+
nodes {
|
|
1707
|
+
id
|
|
1708
|
+
iid
|
|
1709
|
+
title
|
|
1710
|
+
description
|
|
1711
|
+
state
|
|
1712
|
+
startDate
|
|
1713
|
+
dueDate
|
|
1714
|
+
webUrl
|
|
1715
|
+
scopedPath
|
|
1716
|
+
iterationCadence {
|
|
1717
|
+
id
|
|
1718
|
+
title
|
|
1719
|
+
active
|
|
1720
|
+
durationInWeeks
|
|
1721
|
+
}
|
|
1722
|
+
createdAt
|
|
1723
|
+
updatedAt
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
`;
|
|
1729
|
+
if (fetchAll) {
|
|
1730
|
+
return this.fetchAllPages(query, {
|
|
1731
|
+
groupPath,
|
|
1732
|
+
state: state?.toLowerCase(),
|
|
1733
|
+
}, 'group.iterations', {
|
|
1734
|
+
maxItems: first,
|
|
1735
|
+
pageSize: this.config.maxPageSize,
|
|
1736
|
+
userConfig,
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
return this.query(query, {
|
|
1740
|
+
groupPath,
|
|
1741
|
+
state: state?.toLowerCase(),
|
|
1742
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1743
|
+
after,
|
|
1744
|
+
}, userConfig);
|
|
1745
|
+
}
|
|
1746
|
+
// ── Time Tracking ─────────────────────────────────────────────────
|
|
1747
|
+
async getTimeTracking(projectPath, resourceType, iid, includeTimelogs = true, first = 20, after, userConfig) {
|
|
1748
|
+
const timelogFields = includeTimelogs ? `
|
|
1749
|
+
timelogs(first: $first, after: $after) {
|
|
1750
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1751
|
+
nodes {
|
|
1752
|
+
id
|
|
1753
|
+
timeSpent
|
|
1754
|
+
summary
|
|
1755
|
+
spentAt
|
|
1756
|
+
user {
|
|
1757
|
+
username
|
|
1758
|
+
name
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
` : '';
|
|
1763
|
+
if (resourceType === 'issue') {
|
|
1764
|
+
const query = gql `
|
|
1765
|
+
query getIssueTimeTracking($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1766
|
+
project(fullPath: $projectPath) {
|
|
1767
|
+
issue(iid: $iid) {
|
|
1768
|
+
iid
|
|
1769
|
+
title
|
|
1770
|
+
webUrl
|
|
1771
|
+
timeEstimate
|
|
1772
|
+
totalTimeSpent
|
|
1773
|
+
humanTimeEstimate
|
|
1774
|
+
humanTotalTimeSpent
|
|
1775
|
+
${timelogFields}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
`;
|
|
1780
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1781
|
+
}
|
|
1782
|
+
else {
|
|
1783
|
+
const query = gql `
|
|
1784
|
+
query getMergeRequestTimeTracking($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1785
|
+
project(fullPath: $projectPath) {
|
|
1786
|
+
mergeRequest(iid: $iid) {
|
|
1787
|
+
iid
|
|
1788
|
+
title
|
|
1789
|
+
webUrl
|
|
1790
|
+
timeEstimate
|
|
1791
|
+
totalTimeSpent
|
|
1792
|
+
humanTimeEstimate
|
|
1793
|
+
humanTotalTimeSpent
|
|
1794
|
+
${timelogFields}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
`;
|
|
1799
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
// ── MR Reviewers & Approvals ──────────────────────────────────────
|
|
1803
|
+
async getMergeRequestReviewers(projectPath, iid, userConfig) {
|
|
1804
|
+
const query = gql `
|
|
1805
|
+
query getMergeRequestReviewers($projectPath: ID!, $iid: String!) {
|
|
1806
|
+
project(fullPath: $projectPath) {
|
|
1807
|
+
mergeRequest(iid: $iid) {
|
|
1808
|
+
iid
|
|
1809
|
+
title
|
|
1810
|
+
webUrl
|
|
1811
|
+
state
|
|
1812
|
+
approved
|
|
1813
|
+
approvalsRequired
|
|
1814
|
+
approvalsLeft
|
|
1815
|
+
approvedBy {
|
|
1816
|
+
nodes {
|
|
1817
|
+
username
|
|
1818
|
+
name
|
|
1819
|
+
avatarUrl
|
|
1820
|
+
webUrl
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
reviewers {
|
|
1824
|
+
nodes {
|
|
1825
|
+
username
|
|
1826
|
+
name
|
|
1827
|
+
avatarUrl
|
|
1828
|
+
webUrl
|
|
1829
|
+
mergeRequestInteraction {
|
|
1830
|
+
reviewState
|
|
1831
|
+
approved
|
|
1832
|
+
reviewed
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
`;
|
|
1840
|
+
return this.query(query, { projectPath, iid }, userConfig);
|
|
1841
|
+
}
|
|
1842
|
+
// ── Project Statistics ────────────────────────────────────────────
|
|
1843
|
+
async getProjectStatistics(projectPath, userConfig) {
|
|
1844
|
+
const query = gql `
|
|
1845
|
+
query getProjectStatistics($projectPath: ID!) {
|
|
1846
|
+
project(fullPath: $projectPath) {
|
|
1847
|
+
id
|
|
1848
|
+
name
|
|
1849
|
+
fullPath
|
|
1850
|
+
webUrl
|
|
1851
|
+
starCount
|
|
1852
|
+
forksCount
|
|
1853
|
+
openIssuesCount
|
|
1854
|
+
statistics {
|
|
1855
|
+
repositorySize
|
|
1856
|
+
lfsObjectsSize
|
|
1857
|
+
buildArtifactsSize
|
|
1858
|
+
packagesSize
|
|
1859
|
+
wikiSize
|
|
1860
|
+
snippetsSize
|
|
1861
|
+
uploadsSize
|
|
1862
|
+
containerRegistrySize
|
|
1863
|
+
commitCount
|
|
1864
|
+
}
|
|
1865
|
+
openMergeRequests: mergeRequests(state: opened) {
|
|
1866
|
+
count
|
|
1867
|
+
}
|
|
1868
|
+
lastPipeline: pipelines(first: 1) {
|
|
1869
|
+
nodes {
|
|
1870
|
+
id
|
|
1871
|
+
iid
|
|
1872
|
+
status
|
|
1873
|
+
createdAt
|
|
1874
|
+
finishedAt
|
|
1875
|
+
ref
|
|
1876
|
+
detailedStatus {
|
|
1877
|
+
text
|
|
1878
|
+
label
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
releaseCount: releases {
|
|
1883
|
+
count
|
|
1884
|
+
}
|
|
1885
|
+
languages {
|
|
1886
|
+
name
|
|
1887
|
+
share
|
|
1888
|
+
color
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
`;
|
|
1893
|
+
return this.query(query, { projectPath }, userConfig);
|
|
1894
|
+
}
|
|
1895
|
+
// ── Group Members ─────────────────────────────────────────────────
|
|
1896
|
+
async listGroupMembers(groupPath, search, first = 20, after, fetchAll = false, userConfig) {
|
|
1897
|
+
const query = gql `
|
|
1898
|
+
query listGroupMembers($groupPath: ID!, $search: String, $first: Int!, $after: String) {
|
|
1899
|
+
group(fullPath: $groupPath) {
|
|
1900
|
+
groupMembers(search: $search, first: $first, after: $after) {
|
|
1901
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1902
|
+
nodes {
|
|
1903
|
+
id
|
|
1904
|
+
accessLevel {
|
|
1905
|
+
stringValue
|
|
1906
|
+
integerValue
|
|
1907
|
+
}
|
|
1908
|
+
createdAt
|
|
1909
|
+
expiresAt
|
|
1910
|
+
user {
|
|
1911
|
+
username
|
|
1912
|
+
name
|
|
1913
|
+
avatarUrl
|
|
1914
|
+
webUrl
|
|
1915
|
+
state
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
`;
|
|
1922
|
+
if (fetchAll) {
|
|
1923
|
+
return this.fetchAllPages(query, {
|
|
1924
|
+
groupPath,
|
|
1925
|
+
search: search || undefined,
|
|
1926
|
+
}, 'group.groupMembers', {
|
|
1927
|
+
maxItems: first,
|
|
1928
|
+
pageSize: this.config.maxPageSize,
|
|
1929
|
+
userConfig,
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
return this.query(query, {
|
|
1933
|
+
groupPath,
|
|
1934
|
+
search: search || undefined,
|
|
1935
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1936
|
+
after,
|
|
1937
|
+
}, userConfig);
|
|
1938
|
+
}
|
|
1939
|
+
// ── Label Search ─────────────────────────────────────────────────────
|
|
1940
|
+
async searchLabels(fullPath, isProject, search, first = 20, after, fetchAll = false, userConfig) {
|
|
1941
|
+
if (isProject) {
|
|
1942
|
+
const query = gql `
|
|
1943
|
+
query searchProjectLabels($fullPath: ID!, $search: String, $first: Int!, $after: String) {
|
|
1944
|
+
project(fullPath: $fullPath) {
|
|
1945
|
+
labels(searchTerm: $search, first: $first, after: $after) {
|
|
1946
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1947
|
+
nodes {
|
|
1948
|
+
id
|
|
1949
|
+
title
|
|
1950
|
+
description
|
|
1951
|
+
color
|
|
1952
|
+
textColor
|
|
1953
|
+
createdAt
|
|
1954
|
+
updatedAt
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
`;
|
|
1960
|
+
if (fetchAll) {
|
|
1961
|
+
return this.fetchAllPages(query, { fullPath, search }, 'project.labels', {
|
|
1962
|
+
maxItems: first,
|
|
1963
|
+
pageSize: this.config.maxPageSize,
|
|
1964
|
+
userConfig,
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
return this.query(query, { fullPath, search, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
const query = gql `
|
|
1971
|
+
query searchGroupLabels($fullPath: ID!, $search: String, $first: Int!, $after: String) {
|
|
1972
|
+
group(fullPath: $fullPath) {
|
|
1973
|
+
labels(searchTerm: $search, first: $first, after: $after) {
|
|
1974
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1975
|
+
nodes {
|
|
1976
|
+
id
|
|
1977
|
+
title
|
|
1978
|
+
description
|
|
1979
|
+
color
|
|
1980
|
+
textColor
|
|
1981
|
+
createdAt
|
|
1982
|
+
updatedAt
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
`;
|
|
1988
|
+
if (fetchAll) {
|
|
1989
|
+
return this.fetchAllPages(query, { fullPath, search }, 'group.labels', {
|
|
1990
|
+
maxItems: first,
|
|
1991
|
+
pageSize: this.config.maxPageSize,
|
|
1992
|
+
userConfig,
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
return this.query(query, { fullPath, search, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Resolve the GitLab base URL and access token for a REST call, honoring
|
|
2000
|
+
* per-request user credentials and falling back to the shared token.
|
|
2001
|
+
*/
|
|
2002
|
+
resolveRestAuth(userConfig, requiresWrite = false) {
|
|
2003
|
+
if (userConfig) {
|
|
2004
|
+
return {
|
|
2005
|
+
baseUrl: userConfig.gitlabUrl || this.config.gitlabUrl,
|
|
2006
|
+
token: userConfig.accessToken,
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
if (requiresWrite) {
|
|
2010
|
+
throw new Error('Write operations require user authentication. Please provide your GitLab credentials.');
|
|
2011
|
+
}
|
|
2012
|
+
if (this.config.sharedAccessToken && this.config.authMode !== 'per-user') {
|
|
2013
|
+
return { baseUrl: this.config.gitlabUrl, token: this.config.sharedAccessToken };
|
|
2014
|
+
}
|
|
2015
|
+
throw new Error('This operation requires user authentication. Please provide your GitLab credentials.');
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Perform a GitLab REST API v4 request. Used for endpoints not exposed via GraphQL
|
|
2019
|
+
* (e.g., broadcast messages).
|
|
2020
|
+
*/
|
|
2021
|
+
async restRequest(method, path, options = {}) {
|
|
2022
|
+
const { baseUrl, token } = this.resolveRestAuth(options.userConfig, options.requiresWrite);
|
|
2023
|
+
const url = new URL(`${baseUrl.replace(/\/$/, '')}/api/v4${path}`);
|
|
2024
|
+
if (options.query) {
|
|
2025
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
2026
|
+
if (v !== undefined && v !== null)
|
|
2027
|
+
url.searchParams.set(k, String(v));
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
const timeoutMs = this.config.defaultTimeout || 30000;
|
|
2031
|
+
return this.executeWithRetry(async () => {
|
|
2032
|
+
const controller = new AbortController();
|
|
2033
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2034
|
+
try {
|
|
2035
|
+
const response = await fetch(url.toString(), {
|
|
2036
|
+
method,
|
|
2037
|
+
headers: {
|
|
2038
|
+
Authorization: `Bearer ${token}`,
|
|
2039
|
+
'Content-Type': 'application/json',
|
|
2040
|
+
Accept: 'application/json',
|
|
2041
|
+
},
|
|
2042
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
2043
|
+
signal: controller.signal,
|
|
2044
|
+
});
|
|
2045
|
+
if (!response.ok) {
|
|
2046
|
+
const statusCode = response.status;
|
|
2047
|
+
let message = `${response.statusText}`;
|
|
2048
|
+
try {
|
|
2049
|
+
const errBody = await response.json();
|
|
2050
|
+
message = errBody?.message || errBody?.error || JSON.stringify(errBody);
|
|
2051
|
+
}
|
|
2052
|
+
catch {
|
|
2053
|
+
// non-JSON body
|
|
2054
|
+
}
|
|
2055
|
+
const retryable = RETRY_CONFIG.retryableStatusCodes.includes(statusCode);
|
|
2056
|
+
throw new GitLabAPIError(`GitLab REST ${method} ${path} failed (${statusCode}): ${message}`, {
|
|
2057
|
+
code: statusCode === 401 ? 'AUTH_FAILED' : statusCode === 403 ? 'FORBIDDEN' : 'HTTP_ERROR',
|
|
2058
|
+
statusCode,
|
|
2059
|
+
isRetryable: retryable,
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
if (response.status === 204)
|
|
2063
|
+
return undefined;
|
|
2064
|
+
const text = await response.text();
|
|
2065
|
+
return (text ? JSON.parse(text) : undefined);
|
|
2066
|
+
}
|
|
2067
|
+
finally {
|
|
2068
|
+
clearTimeout(timeoutId);
|
|
2069
|
+
}
|
|
2070
|
+
}, `REST ${method} ${path}`);
|
|
2071
|
+
}
|
|
2072
|
+
async listBroadcastMessages(page = 1, perPage = 20, userConfig) {
|
|
2073
|
+
return this.restRequest('GET', '/broadcast_messages', {
|
|
2074
|
+
query: { page, per_page: Math.min(perPage, this.config.maxPageSize) },
|
|
2075
|
+
userConfig,
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
async getBroadcastMessage(id, userConfig) {
|
|
2079
|
+
return this.restRequest('GET', `/broadcast_messages/${id}`, { userConfig });
|
|
2080
|
+
}
|
|
2081
|
+
async createBroadcastMessage(input, userConfig) {
|
|
2082
|
+
return this.restRequest('POST', '/broadcast_messages', { body: input, userConfig, requiresWrite: true });
|
|
2083
|
+
}
|
|
2084
|
+
async updateBroadcastMessage(id, input, userConfig) {
|
|
2085
|
+
return this.restRequest('PUT', `/broadcast_messages/${id}`, { body: input, userConfig, requiresWrite: true });
|
|
2086
|
+
}
|
|
2087
|
+
async deleteBroadcastMessage(id, userConfig) {
|
|
2088
|
+
return this.restRequest('DELETE', `/broadcast_messages/${id}`, { userConfig, requiresWrite: true });
|
|
1034
2089
|
}
|
|
1035
2090
|
}
|
|
1036
2091
|
//# sourceMappingURL=gitlab-client.js.map
|