@ttpears/gitlab-mcp-server 1.7.3 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/dist/gitlab-client.d.ts +82 -9
- package/dist/gitlab-client.d.ts.map +1 -1
- package/dist/gitlab-client.js +1001 -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 +672 -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,14 @@ 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);
|
|
300
350
|
}
|
|
301
351
|
async createIssue(projectPath, title, description, userConfig) {
|
|
302
352
|
await this.introspectSchema(userConfig);
|
|
@@ -623,10 +673,11 @@ export class GitLabGraphQLClient {
|
|
|
623
673
|
const fields = type.getFields();
|
|
624
674
|
return Object.keys(fields);
|
|
625
675
|
}
|
|
626
|
-
async globalSearch(searchTerm,
|
|
676
|
+
async globalSearch(searchTerm, first = 20, after, userConfig) {
|
|
627
677
|
const query = gql `
|
|
628
|
-
query globalSearch($search: String, $first: Int
|
|
629
|
-
projects(search: $search, first: $first) {
|
|
678
|
+
query globalSearch($search: String, $first: Int!, $after: String) {
|
|
679
|
+
projects(search: $search, first: $first, after: $after) {
|
|
680
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
630
681
|
nodes {
|
|
631
682
|
id
|
|
632
683
|
name
|
|
@@ -635,7 +686,8 @@ export class GitLabGraphQLClient {
|
|
|
635
686
|
visibility
|
|
636
687
|
}
|
|
637
688
|
}
|
|
638
|
-
issues(search: $search, first: $first) {
|
|
689
|
+
issues(search: $search, first: $first, after: $after) {
|
|
690
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
639
691
|
nodes {
|
|
640
692
|
id
|
|
641
693
|
iid
|
|
@@ -649,10 +701,34 @@ export class GitLabGraphQLClient {
|
|
|
649
701
|
`;
|
|
650
702
|
return this.query(query, {
|
|
651
703
|
search: searchTerm || undefined,
|
|
652
|
-
first: Math.min(this.config.maxPageSize,
|
|
704
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
705
|
+
after
|
|
653
706
|
}, userConfig);
|
|
654
707
|
}
|
|
655
|
-
async
|
|
708
|
+
async globalSearchAll(searchTerm, maxItems = 100, userConfig) {
|
|
709
|
+
const projectsQuery = gql `
|
|
710
|
+
query globalSearchProjects($search: String, $first: Int!, $after: String) {
|
|
711
|
+
projects(search: $search, first: $first, after: $after) {
|
|
712
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
713
|
+
nodes { id name fullPath webUrl visibility }
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
`;
|
|
717
|
+
const issuesQuery = gql `
|
|
718
|
+
query globalSearchIssues($search: String, $first: Int!, $after: String) {
|
|
719
|
+
issues(search: $search, first: $first, after: $after) {
|
|
720
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
721
|
+
nodes { id iid title state webUrl createdAt }
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
`;
|
|
725
|
+
const [projects, issues] = await Promise.all([
|
|
726
|
+
this.fetchAllPages(projectsQuery, { search: searchTerm || undefined }, 'projects', { maxItems, userConfig }),
|
|
727
|
+
this.fetchAllPages(issuesQuery, { search: searchTerm || undefined }, 'issues', { maxItems, userConfig }),
|
|
728
|
+
]);
|
|
729
|
+
return { projects, issues };
|
|
730
|
+
}
|
|
731
|
+
async searchProjects(searchTerm, first = 20, after, fetchAll = false, userConfig) {
|
|
656
732
|
const query = gql `
|
|
657
733
|
query searchProjects($search: String!, $first: Int!, $after: String) {
|
|
658
734
|
projects(search: $search, first: $first, after: $after) {
|
|
@@ -669,7 +745,14 @@ export class GitLabGraphQLClient {
|
|
|
669
745
|
}
|
|
670
746
|
}
|
|
671
747
|
`;
|
|
672
|
-
|
|
748
|
+
if (fetchAll) {
|
|
749
|
+
return this.fetchAllPages(query, { search: searchTerm }, 'projects', {
|
|
750
|
+
maxItems: first,
|
|
751
|
+
pageSize: this.config.maxPageSize,
|
|
752
|
+
userConfig,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
673
756
|
}
|
|
674
757
|
getTypeName(t) {
|
|
675
758
|
if (!t)
|
|
@@ -683,7 +766,7 @@ export class GitLabGraphQLClient {
|
|
|
683
766
|
const values = (enumType && typeof enumType.getValues === 'function') ? enumType.getValues() : [];
|
|
684
767
|
return Array.isArray(values) ? values.map((v) => v.name) : [];
|
|
685
768
|
}
|
|
686
|
-
async searchIssues(searchTerm, projectPath, state, first = 20, after, userConfig, assigneeUsernames, authorUsername, labelNames) {
|
|
769
|
+
async searchIssues(searchTerm, projectPath, state, first = 20, after, fetchAll = false, userConfig, assigneeUsernames, authorUsername, labelNames, sort) {
|
|
687
770
|
await this.introspectSchema(userConfig);
|
|
688
771
|
const mappedState = state && state.toLowerCase() !== 'all' ? state.toUpperCase() : undefined;
|
|
689
772
|
if (projectPath) {
|
|
@@ -697,9 +780,9 @@ export class GitLabGraphQLClient {
|
|
|
697
780
|
? (allowed.find(v => v.toLowerCase() === state.toLowerCase()) || undefined)
|
|
698
781
|
: undefined;
|
|
699
782
|
const query = gql `
|
|
700
|
-
query searchIssuesProject($projectPath: ID!, $search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!]) {
|
|
783
|
+
query searchIssuesProject($projectPath: ID!, $search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!], $sort: IssueSort) {
|
|
701
784
|
project(fullPath: $projectPath) {
|
|
702
|
-
issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName) {
|
|
785
|
+
issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName, sort: $sort) {
|
|
703
786
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
704
787
|
nodes {
|
|
705
788
|
id
|
|
@@ -716,6 +799,21 @@ export class GitLabGraphQLClient {
|
|
|
716
799
|
}
|
|
717
800
|
}
|
|
718
801
|
`;
|
|
802
|
+
if (fetchAll) {
|
|
803
|
+
return this.fetchAllPages(query, {
|
|
804
|
+
projectPath,
|
|
805
|
+
search: searchTerm,
|
|
806
|
+
state: mapped,
|
|
807
|
+
assigneeUsernames,
|
|
808
|
+
authorUsername,
|
|
809
|
+
labelName: labelNames,
|
|
810
|
+
sort: sort || 'UPDATED_DESC'
|
|
811
|
+
}, 'project.issues', {
|
|
812
|
+
maxItems: first,
|
|
813
|
+
pageSize: this.config.maxPageSize,
|
|
814
|
+
userConfig,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
719
817
|
return this.query(query, {
|
|
720
818
|
projectPath,
|
|
721
819
|
search: searchTerm,
|
|
@@ -724,7 +822,8 @@ export class GitLabGraphQLClient {
|
|
|
724
822
|
after,
|
|
725
823
|
assigneeUsernames,
|
|
726
824
|
authorUsername,
|
|
727
|
-
labelName: labelNames
|
|
825
|
+
labelName: labelNames,
|
|
826
|
+
sort: sort || 'UPDATED_DESC'
|
|
728
827
|
}, userConfig);
|
|
729
828
|
}
|
|
730
829
|
else {
|
|
@@ -742,8 +841,8 @@ export class GitLabGraphQLClient {
|
|
|
742
841
|
// We keep description for AI context but limit nested collections (assignees/labels)
|
|
743
842
|
// Note: Global Issue type doesn't have 'project' field - only project-scoped queries do
|
|
744
843
|
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) {
|
|
844
|
+
query searchIssuesGlobal($search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!], $sort: IssueSort) {
|
|
845
|
+
issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName, sort: $sort) {
|
|
747
846
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
748
847
|
nodes {
|
|
749
848
|
id
|
|
@@ -762,6 +861,20 @@ export class GitLabGraphQLClient {
|
|
|
762
861
|
`;
|
|
763
862
|
// Note: Global search returns streamlined fields (no assignees/labels/project) for performance.
|
|
764
863
|
// For full details and project attribution, search within a specific project using projectPath.
|
|
864
|
+
if (fetchAll) {
|
|
865
|
+
return this.fetchAllPages(query, {
|
|
866
|
+
search: searchTerm,
|
|
867
|
+
state: mapped,
|
|
868
|
+
assigneeUsernames,
|
|
869
|
+
authorUsername,
|
|
870
|
+
labelName: labelNames,
|
|
871
|
+
sort: sort || 'UPDATED_DESC'
|
|
872
|
+
}, 'issues', {
|
|
873
|
+
maxItems: first,
|
|
874
|
+
pageSize: this.config.maxPageSize,
|
|
875
|
+
userConfig,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
765
878
|
return this.query(query, {
|
|
766
879
|
search: searchTerm,
|
|
767
880
|
state: mapped,
|
|
@@ -769,17 +882,18 @@ export class GitLabGraphQLClient {
|
|
|
769
882
|
after,
|
|
770
883
|
assigneeUsernames,
|
|
771
884
|
authorUsername,
|
|
772
|
-
labelName: labelNames
|
|
885
|
+
labelName: labelNames,
|
|
886
|
+
sort: sort || 'UPDATED_DESC'
|
|
773
887
|
}, userConfig);
|
|
774
888
|
}
|
|
775
889
|
}
|
|
776
|
-
async searchMergeRequests(searchTerm, projectPath, state, first = 20, after, userConfig) {
|
|
890
|
+
async searchMergeRequests(searchTerm, projectPath, state, first = 20, after, fetchAll = false, userConfig, sort) {
|
|
777
891
|
const mappedState = state && state.toLowerCase() !== 'all' ? state.toUpperCase() : undefined;
|
|
778
892
|
if (projectPath) {
|
|
779
893
|
const query = gql `
|
|
780
|
-
query searchMergeRequestsProject($projectPath: ID!, $search: String, $state: MergeRequestState, $first: Int!, $after: String) {
|
|
894
|
+
query searchMergeRequestsProject($projectPath: ID!, $search: String, $state: MergeRequestState, $first: Int!, $after: String, $sort: MergeRequestSort) {
|
|
781
895
|
project(fullPath: $projectPath) {
|
|
782
|
-
mergeRequests(search: $search, state: $state, first: $first, after: $after) {
|
|
896
|
+
mergeRequests(search: $search, state: $state, first: $first, after: $after, sort: $sort) {
|
|
783
897
|
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
784
898
|
nodes {
|
|
785
899
|
id
|
|
@@ -798,12 +912,25 @@ export class GitLabGraphQLClient {
|
|
|
798
912
|
}
|
|
799
913
|
}
|
|
800
914
|
`;
|
|
915
|
+
if (fetchAll) {
|
|
916
|
+
return this.fetchAllPages(query, {
|
|
917
|
+
projectPath,
|
|
918
|
+
search: searchTerm,
|
|
919
|
+
state: mappedState,
|
|
920
|
+
sort: sort || 'UPDATED_DESC'
|
|
921
|
+
}, 'project.mergeRequests', {
|
|
922
|
+
maxItems: first,
|
|
923
|
+
pageSize: this.config.maxPageSize,
|
|
924
|
+
userConfig,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
801
927
|
return this.query(query, {
|
|
802
928
|
projectPath,
|
|
803
929
|
search: searchTerm,
|
|
804
930
|
state: mappedState,
|
|
805
|
-
first: Math.min(first,
|
|
806
|
-
after
|
|
931
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
932
|
+
after,
|
|
933
|
+
sort: sort || 'UPDATED_DESC'
|
|
807
934
|
}, userConfig);
|
|
808
935
|
}
|
|
809
936
|
else {
|
|
@@ -851,9 +978,20 @@ export class GitLabGraphQLClient {
|
|
|
851
978
|
}
|
|
852
979
|
`;
|
|
853
980
|
try {
|
|
981
|
+
if (fetchAll) {
|
|
982
|
+
const connectionPath = `user.${fieldName}`;
|
|
983
|
+
return this.fetchAllPages(query, {
|
|
984
|
+
username,
|
|
985
|
+
state: mappedState
|
|
986
|
+
}, connectionPath, {
|
|
987
|
+
maxItems: first,
|
|
988
|
+
pageSize: this.config.maxPageSize,
|
|
989
|
+
userConfig,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
854
992
|
const result = await this.query(query, {
|
|
855
993
|
username,
|
|
856
|
-
first: Math.min(first,
|
|
994
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
857
995
|
after,
|
|
858
996
|
state: mappedState
|
|
859
997
|
}, userConfig);
|
|
@@ -997,10 +1135,16 @@ export class GitLabGraphQLClient {
|
|
|
997
1135
|
ref: ref || "HEAD"
|
|
998
1136
|
}, userConfig);
|
|
999
1137
|
}
|
|
1000
|
-
async searchUsers(searchTerm, first = 20, userConfig) {
|
|
1138
|
+
async searchUsers(searchTerm, first = 20, after, fetchAll = false, userConfig) {
|
|
1001
1139
|
const query = gql `
|
|
1002
|
-
query searchUsers($search: String!, $first: Int
|
|
1003
|
-
users(search: $search, first: $first) {
|
|
1140
|
+
query searchUsers($search: String!, $first: Int!, $after: String) {
|
|
1141
|
+
users(search: $search, first: $first, after: $after) {
|
|
1142
|
+
pageInfo {
|
|
1143
|
+
hasNextPage
|
|
1144
|
+
hasPreviousPage
|
|
1145
|
+
startCursor
|
|
1146
|
+
endCursor
|
|
1147
|
+
}
|
|
1004
1148
|
nodes {
|
|
1005
1149
|
id
|
|
1006
1150
|
username
|
|
@@ -1012,12 +1156,25 @@ export class GitLabGraphQLClient {
|
|
|
1012
1156
|
}
|
|
1013
1157
|
}
|
|
1014
1158
|
`;
|
|
1015
|
-
|
|
1159
|
+
if (fetchAll) {
|
|
1160
|
+
return this.fetchAllPages(query, { search: searchTerm }, 'users', {
|
|
1161
|
+
maxItems: first,
|
|
1162
|
+
pageSize: this.config.maxPageSize,
|
|
1163
|
+
userConfig,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1016
1167
|
}
|
|
1017
|
-
async searchGroups(searchTerm, first = 20, userConfig) {
|
|
1168
|
+
async searchGroups(searchTerm, first = 20, after, fetchAll = false, userConfig) {
|
|
1018
1169
|
const query = gql `
|
|
1019
|
-
query searchGroups($search: String!, $first: Int
|
|
1020
|
-
groups(search: $search, first: $first) {
|
|
1170
|
+
query searchGroups($search: String!, $first: Int!, $after: String) {
|
|
1171
|
+
groups(search: $search, first: $first, after: $after) {
|
|
1172
|
+
pageInfo {
|
|
1173
|
+
hasNextPage
|
|
1174
|
+
hasPreviousPage
|
|
1175
|
+
startCursor
|
|
1176
|
+
endCursor
|
|
1177
|
+
}
|
|
1021
1178
|
nodes {
|
|
1022
1179
|
id
|
|
1023
1180
|
name
|
|
@@ -1030,7 +1187,811 @@ export class GitLabGraphQLClient {
|
|
|
1030
1187
|
}
|
|
1031
1188
|
}
|
|
1032
1189
|
`;
|
|
1033
|
-
|
|
1190
|
+
if (fetchAll) {
|
|
1191
|
+
return this.fetchAllPages(query, { search: searchTerm }, 'groups', {
|
|
1192
|
+
maxItems: first,
|
|
1193
|
+
pageSize: this.config.maxPageSize,
|
|
1194
|
+
userConfig,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1198
|
+
}
|
|
1199
|
+
// ── CI/CD Pipelines ──────────────────────────────────────────────────
|
|
1200
|
+
async getMergeRequestPipelines(projectPath, iid, first = 20, after, fetchAll = false, userConfig) {
|
|
1201
|
+
const query = gql `
|
|
1202
|
+
query getMergeRequestPipelines($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1203
|
+
project(fullPath: $projectPath) {
|
|
1204
|
+
mergeRequest(iid: $iid) {
|
|
1205
|
+
pipelines(first: $first, after: $after) {
|
|
1206
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1207
|
+
nodes {
|
|
1208
|
+
id
|
|
1209
|
+
iid
|
|
1210
|
+
status
|
|
1211
|
+
duration
|
|
1212
|
+
createdAt
|
|
1213
|
+
finishedAt
|
|
1214
|
+
ref
|
|
1215
|
+
sha
|
|
1216
|
+
detailedStatus {
|
|
1217
|
+
text
|
|
1218
|
+
label
|
|
1219
|
+
icon
|
|
1220
|
+
group
|
|
1221
|
+
detailsPath
|
|
1222
|
+
}
|
|
1223
|
+
stages {
|
|
1224
|
+
nodes {
|
|
1225
|
+
name
|
|
1226
|
+
status
|
|
1227
|
+
detailedStatus {
|
|
1228
|
+
text
|
|
1229
|
+
label
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
`;
|
|
1239
|
+
if (fetchAll) {
|
|
1240
|
+
return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.pipelines', {
|
|
1241
|
+
maxItems: first,
|
|
1242
|
+
pageSize: this.config.maxPageSize,
|
|
1243
|
+
userConfig,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1247
|
+
}
|
|
1248
|
+
async getPipelineJobs(projectPath, pipelineIid, first = 20, after, fetchAll = false, userConfig) {
|
|
1249
|
+
const query = gql `
|
|
1250
|
+
query getPipelineJobs($projectPath: ID!, $pipelineIid: ID!, $first: Int!, $after: String) {
|
|
1251
|
+
project(fullPath: $projectPath) {
|
|
1252
|
+
pipeline(iid: $pipelineIid) {
|
|
1253
|
+
id
|
|
1254
|
+
iid
|
|
1255
|
+
status
|
|
1256
|
+
jobs(first: $first, after: $after) {
|
|
1257
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1258
|
+
nodes {
|
|
1259
|
+
id
|
|
1260
|
+
name
|
|
1261
|
+
status
|
|
1262
|
+
stage {
|
|
1263
|
+
name
|
|
1264
|
+
}
|
|
1265
|
+
duration
|
|
1266
|
+
queuedDuration
|
|
1267
|
+
createdAt
|
|
1268
|
+
startedAt
|
|
1269
|
+
finishedAt
|
|
1270
|
+
retryable
|
|
1271
|
+
cancelable
|
|
1272
|
+
webPath
|
|
1273
|
+
detailedStatus {
|
|
1274
|
+
text
|
|
1275
|
+
label
|
|
1276
|
+
icon
|
|
1277
|
+
group
|
|
1278
|
+
detailsPath
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
`;
|
|
1286
|
+
if (fetchAll) {
|
|
1287
|
+
return this.fetchAllPages(query, { projectPath, pipelineIid }, 'project.pipeline.jobs', {
|
|
1288
|
+
maxItems: first,
|
|
1289
|
+
pageSize: this.config.maxPageSize,
|
|
1290
|
+
userConfig,
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
return this.query(query, { projectPath, pipelineIid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1294
|
+
}
|
|
1295
|
+
async getPipelineId(projectPath, pipelineIid, userConfig) {
|
|
1296
|
+
const query = gql `
|
|
1297
|
+
query pipelineId($projectPath: ID!, $pipelineIid: ID!) {
|
|
1298
|
+
project(fullPath: $projectPath) {
|
|
1299
|
+
pipeline(iid: $pipelineIid) { id }
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
`;
|
|
1303
|
+
const result = await this.query(query, { projectPath, pipelineIid }, userConfig);
|
|
1304
|
+
const id = result?.project?.pipeline?.id;
|
|
1305
|
+
if (!id)
|
|
1306
|
+
throw new Error('Pipeline not found');
|
|
1307
|
+
return id;
|
|
1308
|
+
}
|
|
1309
|
+
async managePipeline(projectPath, pipelineIid, action, userConfig) {
|
|
1310
|
+
await this.introspectSchema(userConfig);
|
|
1311
|
+
const mutationType = this.schema?.getMutationType();
|
|
1312
|
+
const fields = mutationType ? mutationType.getFields() : {};
|
|
1313
|
+
const pipelineGlobalId = await this.getPipelineId(projectPath, pipelineIid, userConfig);
|
|
1314
|
+
if (action === 'retry') {
|
|
1315
|
+
const fieldName = fields['pipelineRetry'] ? 'pipelineRetry' : null;
|
|
1316
|
+
if (!fieldName) {
|
|
1317
|
+
throw new Error('pipelineRetry mutation is not available on this GitLab instance');
|
|
1318
|
+
}
|
|
1319
|
+
const mutation = gql `
|
|
1320
|
+
mutation retryPipeline($input: PipelineRetryInput!) {
|
|
1321
|
+
pipelineRetry(input: $input) {
|
|
1322
|
+
pipeline { id iid status }
|
|
1323
|
+
errors
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
`;
|
|
1327
|
+
const result = await this.query(mutation, { input: { id: pipelineGlobalId } }, userConfig, true);
|
|
1328
|
+
const payload = result.pipelineRetry;
|
|
1329
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
1330
|
+
throw new Error(`Failed to retry pipeline: ${payload.errors.join(', ')}`);
|
|
1331
|
+
}
|
|
1332
|
+
return payload;
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
const fieldName = fields['pipelineCancel'] ? 'pipelineCancel' : null;
|
|
1336
|
+
if (!fieldName) {
|
|
1337
|
+
throw new Error('pipelineCancel mutation is not available on this GitLab instance');
|
|
1338
|
+
}
|
|
1339
|
+
const mutation = gql `
|
|
1340
|
+
mutation cancelPipeline($input: PipelineCancelInput!) {
|
|
1341
|
+
pipelineCancel(input: $input) {
|
|
1342
|
+
errors
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
`;
|
|
1346
|
+
const result = await this.query(mutation, { input: { id: pipelineGlobalId } }, userConfig, true);
|
|
1347
|
+
const payload = result.pipelineCancel;
|
|
1348
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
1349
|
+
throw new Error(`Failed to cancel pipeline: ${payload.errors.join(', ')}`);
|
|
1350
|
+
}
|
|
1351
|
+
return payload;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
// ── MR Diffs & Commits ───────────────────────────────────────────────
|
|
1355
|
+
async getMergeRequestDiffs(projectPath, iid, userConfig) {
|
|
1356
|
+
const query = gql `
|
|
1357
|
+
query getMergeRequestDiffs($projectPath: ID!, $iid: String!) {
|
|
1358
|
+
project(fullPath: $projectPath) {
|
|
1359
|
+
mergeRequest(iid: $iid) {
|
|
1360
|
+
diffStatsSummary {
|
|
1361
|
+
additions
|
|
1362
|
+
deletions
|
|
1363
|
+
fileCount
|
|
1364
|
+
}
|
|
1365
|
+
diffStats {
|
|
1366
|
+
path
|
|
1367
|
+
additions
|
|
1368
|
+
deletions
|
|
1369
|
+
}
|
|
1370
|
+
diffRefs {
|
|
1371
|
+
baseSha
|
|
1372
|
+
headSha
|
|
1373
|
+
startSha
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
`;
|
|
1379
|
+
return this.query(query, { projectPath, iid }, userConfig);
|
|
1380
|
+
}
|
|
1381
|
+
async getMergeRequestCommits(projectPath, iid, first = 20, after, fetchAll = false, userConfig) {
|
|
1382
|
+
const query = gql `
|
|
1383
|
+
query getMergeRequestCommits($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1384
|
+
project(fullPath: $projectPath) {
|
|
1385
|
+
mergeRequest(iid: $iid) {
|
|
1386
|
+
commitCount
|
|
1387
|
+
commitsWithoutMergeCommits(first: $first, after: $after) {
|
|
1388
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1389
|
+
nodes {
|
|
1390
|
+
sha
|
|
1391
|
+
shortId
|
|
1392
|
+
title
|
|
1393
|
+
message
|
|
1394
|
+
authorName
|
|
1395
|
+
authoredDate
|
|
1396
|
+
webUrl
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
`;
|
|
1403
|
+
if (fetchAll) {
|
|
1404
|
+
return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.commitsWithoutMergeCommits', {
|
|
1405
|
+
maxItems: first,
|
|
1406
|
+
pageSize: this.config.maxPageSize,
|
|
1407
|
+
userConfig,
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1411
|
+
}
|
|
1412
|
+
// ── Work Item Notes ──────────────────────────────────────────────────
|
|
1413
|
+
async getNotes(projectPath, noteableType, iid, first = 20, after, fetchAll = false, userConfig) {
|
|
1414
|
+
if (noteableType === 'issue') {
|
|
1415
|
+
const query = gql `
|
|
1416
|
+
query getIssueNotes($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1417
|
+
project(fullPath: $projectPath) {
|
|
1418
|
+
issue(iid: $iid) {
|
|
1419
|
+
notes(first: $first, after: $after) {
|
|
1420
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1421
|
+
nodes {
|
|
1422
|
+
id
|
|
1423
|
+
body
|
|
1424
|
+
author { username name }
|
|
1425
|
+
createdAt
|
|
1426
|
+
updatedAt
|
|
1427
|
+
system
|
|
1428
|
+
internal
|
|
1429
|
+
url
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
`;
|
|
1436
|
+
if (fetchAll) {
|
|
1437
|
+
return this.fetchAllPages(query, { projectPath, iid }, 'project.issue.notes', {
|
|
1438
|
+
maxItems: first,
|
|
1439
|
+
pageSize: this.config.maxPageSize,
|
|
1440
|
+
userConfig,
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
const query = gql `
|
|
1447
|
+
query getMergeRequestNotes($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1448
|
+
project(fullPath: $projectPath) {
|
|
1449
|
+
mergeRequest(iid: $iid) {
|
|
1450
|
+
notes(first: $first, after: $after) {
|
|
1451
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1452
|
+
nodes {
|
|
1453
|
+
id
|
|
1454
|
+
body
|
|
1455
|
+
author { username name }
|
|
1456
|
+
createdAt
|
|
1457
|
+
updatedAt
|
|
1458
|
+
system
|
|
1459
|
+
internal
|
|
1460
|
+
url
|
|
1461
|
+
position {
|
|
1462
|
+
filePath
|
|
1463
|
+
newLine
|
|
1464
|
+
oldLine
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
`;
|
|
1472
|
+
if (fetchAll) {
|
|
1473
|
+
return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.notes', {
|
|
1474
|
+
maxItems: first,
|
|
1475
|
+
pageSize: this.config.maxPageSize,
|
|
1476
|
+
userConfig,
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async createNote(projectPath, noteableType, iid, body, internal = false, userConfig) {
|
|
1483
|
+
await this.introspectSchema(userConfig);
|
|
1484
|
+
const mutationType = this.schema?.getMutationType();
|
|
1485
|
+
const fields = mutationType ? mutationType.getFields() : {};
|
|
1486
|
+
const fieldName = fields['createNote'] ? 'createNote' : null;
|
|
1487
|
+
if (!fieldName) {
|
|
1488
|
+
throw new Error('createNote mutation is not available on this GitLab instance');
|
|
1489
|
+
}
|
|
1490
|
+
// Resolve the noteable IID to a global ID
|
|
1491
|
+
const noteableId = noteableType === 'issue'
|
|
1492
|
+
? await this.getIssueId(projectPath, iid, userConfig)
|
|
1493
|
+
: await this.getMergeRequestId(projectPath, iid, userConfig);
|
|
1494
|
+
const mutation = gql `
|
|
1495
|
+
mutation createNote($input: CreateNoteInput!) {
|
|
1496
|
+
createNote(input: $input) {
|
|
1497
|
+
note {
|
|
1498
|
+
id
|
|
1499
|
+
body
|
|
1500
|
+
author { username name }
|
|
1501
|
+
createdAt
|
|
1502
|
+
url
|
|
1503
|
+
internal
|
|
1504
|
+
}
|
|
1505
|
+
errors
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
`;
|
|
1509
|
+
const input = {
|
|
1510
|
+
noteableId,
|
|
1511
|
+
body,
|
|
1512
|
+
};
|
|
1513
|
+
if (internal) {
|
|
1514
|
+
input.internal = true;
|
|
1515
|
+
}
|
|
1516
|
+
const result = await this.query(mutation, { input }, userConfig, true);
|
|
1517
|
+
return result.createNote;
|
|
1518
|
+
}
|
|
1519
|
+
// ── Milestones ─────────────────────────────────────────────────────
|
|
1520
|
+
async listMilestones(fullPath, isProject, state, search, includeAncestors = false, first = 20, after, fetchAll = false, userConfig) {
|
|
1521
|
+
const milestoneFields = `
|
|
1522
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1523
|
+
nodes {
|
|
1524
|
+
id
|
|
1525
|
+
iid
|
|
1526
|
+
title
|
|
1527
|
+
description
|
|
1528
|
+
state
|
|
1529
|
+
dueDate
|
|
1530
|
+
startDate
|
|
1531
|
+
expired
|
|
1532
|
+
upcoming
|
|
1533
|
+
webPath
|
|
1534
|
+
stats {
|
|
1535
|
+
totalIssuesCount
|
|
1536
|
+
closedIssuesCount
|
|
1537
|
+
}
|
|
1538
|
+
createdAt
|
|
1539
|
+
updatedAt
|
|
1540
|
+
}
|
|
1541
|
+
`;
|
|
1542
|
+
if (isProject) {
|
|
1543
|
+
const query = gql `
|
|
1544
|
+
query listProjectMilestones($fullPath: ID!, $state: MilestoneStateEnum, $searchTitle: String, $includeAncestors: Boolean, $first: Int!, $after: String) {
|
|
1545
|
+
project(fullPath: $fullPath) {
|
|
1546
|
+
milestones(state: $state, searchTitle: $searchTitle, includeAncestors: $includeAncestors, first: $first, after: $after) {
|
|
1547
|
+
${milestoneFields}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
`;
|
|
1552
|
+
if (fetchAll) {
|
|
1553
|
+
return this.fetchAllPages(query, {
|
|
1554
|
+
fullPath,
|
|
1555
|
+
state: state?.toLowerCase(),
|
|
1556
|
+
searchTitle: search,
|
|
1557
|
+
includeAncestors,
|
|
1558
|
+
}, 'project.milestones', {
|
|
1559
|
+
maxItems: first,
|
|
1560
|
+
pageSize: this.config.maxPageSize,
|
|
1561
|
+
userConfig,
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
return this.query(query, {
|
|
1565
|
+
fullPath,
|
|
1566
|
+
state: state?.toLowerCase(),
|
|
1567
|
+
searchTitle: search,
|
|
1568
|
+
includeAncestors,
|
|
1569
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1570
|
+
after,
|
|
1571
|
+
}, userConfig);
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
const query = gql `
|
|
1575
|
+
query listGroupMilestones($fullPath: ID!, $state: MilestoneStateEnum, $searchTitle: String, $includeAncestors: Boolean, $first: Int!, $after: String) {
|
|
1576
|
+
group(fullPath: $fullPath) {
|
|
1577
|
+
milestones(state: $state, searchTitle: $searchTitle, includeAncestors: $includeAncestors, first: $first, after: $after) {
|
|
1578
|
+
${milestoneFields}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
`;
|
|
1583
|
+
if (fetchAll) {
|
|
1584
|
+
return this.fetchAllPages(query, {
|
|
1585
|
+
fullPath,
|
|
1586
|
+
state: state?.toLowerCase(),
|
|
1587
|
+
searchTitle: search,
|
|
1588
|
+
includeAncestors,
|
|
1589
|
+
}, 'group.milestones', {
|
|
1590
|
+
maxItems: first,
|
|
1591
|
+
pageSize: this.config.maxPageSize,
|
|
1592
|
+
userConfig,
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
return this.query(query, {
|
|
1596
|
+
fullPath,
|
|
1597
|
+
state: state?.toLowerCase(),
|
|
1598
|
+
searchTitle: search,
|
|
1599
|
+
includeAncestors,
|
|
1600
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1601
|
+
after,
|
|
1602
|
+
}, userConfig);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
// ── Iterations ────────────────────────────────────────────────────
|
|
1606
|
+
async listIterations(groupPath, state, first = 20, after, fetchAll = false, userConfig) {
|
|
1607
|
+
const query = gql `
|
|
1608
|
+
query listIterations($groupPath: ID!, $state: IterationState, $first: Int!, $after: String) {
|
|
1609
|
+
group(fullPath: $groupPath) {
|
|
1610
|
+
iterations(state: $state, first: $first, after: $after) {
|
|
1611
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1612
|
+
nodes {
|
|
1613
|
+
id
|
|
1614
|
+
iid
|
|
1615
|
+
title
|
|
1616
|
+
description
|
|
1617
|
+
state
|
|
1618
|
+
startDate
|
|
1619
|
+
dueDate
|
|
1620
|
+
webUrl
|
|
1621
|
+
scopedPath
|
|
1622
|
+
iterationCadence {
|
|
1623
|
+
id
|
|
1624
|
+
title
|
|
1625
|
+
active
|
|
1626
|
+
durationInWeeks
|
|
1627
|
+
}
|
|
1628
|
+
createdAt
|
|
1629
|
+
updatedAt
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
`;
|
|
1635
|
+
if (fetchAll) {
|
|
1636
|
+
return this.fetchAllPages(query, {
|
|
1637
|
+
groupPath,
|
|
1638
|
+
state: state?.toLowerCase(),
|
|
1639
|
+
}, 'group.iterations', {
|
|
1640
|
+
maxItems: first,
|
|
1641
|
+
pageSize: this.config.maxPageSize,
|
|
1642
|
+
userConfig,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
return this.query(query, {
|
|
1646
|
+
groupPath,
|
|
1647
|
+
state: state?.toLowerCase(),
|
|
1648
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1649
|
+
after,
|
|
1650
|
+
}, userConfig);
|
|
1651
|
+
}
|
|
1652
|
+
// ── Time Tracking ─────────────────────────────────────────────────
|
|
1653
|
+
async getTimeTracking(projectPath, resourceType, iid, includeTimelogs = true, first = 20, after, userConfig) {
|
|
1654
|
+
const timelogFields = includeTimelogs ? `
|
|
1655
|
+
timelogs(first: $first, after: $after) {
|
|
1656
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1657
|
+
nodes {
|
|
1658
|
+
id
|
|
1659
|
+
timeSpent
|
|
1660
|
+
summary
|
|
1661
|
+
spentAt
|
|
1662
|
+
user {
|
|
1663
|
+
username
|
|
1664
|
+
name
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
` : '';
|
|
1669
|
+
if (resourceType === 'issue') {
|
|
1670
|
+
const query = gql `
|
|
1671
|
+
query getIssueTimeTracking($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1672
|
+
project(fullPath: $projectPath) {
|
|
1673
|
+
issue(iid: $iid) {
|
|
1674
|
+
iid
|
|
1675
|
+
title
|
|
1676
|
+
webUrl
|
|
1677
|
+
timeEstimate
|
|
1678
|
+
totalTimeSpent
|
|
1679
|
+
humanTimeEstimate
|
|
1680
|
+
humanTotalTimeSpent
|
|
1681
|
+
${timelogFields}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
`;
|
|
1686
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
const query = gql `
|
|
1690
|
+
query getMergeRequestTimeTracking($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
|
|
1691
|
+
project(fullPath: $projectPath) {
|
|
1692
|
+
mergeRequest(iid: $iid) {
|
|
1693
|
+
iid
|
|
1694
|
+
title
|
|
1695
|
+
webUrl
|
|
1696
|
+
timeEstimate
|
|
1697
|
+
totalTimeSpent
|
|
1698
|
+
humanTimeEstimate
|
|
1699
|
+
humanTotalTimeSpent
|
|
1700
|
+
${timelogFields}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
`;
|
|
1705
|
+
return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
// ── MR Reviewers & Approvals ──────────────────────────────────────
|
|
1709
|
+
async getMergeRequestReviewers(projectPath, iid, userConfig) {
|
|
1710
|
+
const query = gql `
|
|
1711
|
+
query getMergeRequestReviewers($projectPath: ID!, $iid: String!) {
|
|
1712
|
+
project(fullPath: $projectPath) {
|
|
1713
|
+
mergeRequest(iid: $iid) {
|
|
1714
|
+
iid
|
|
1715
|
+
title
|
|
1716
|
+
webUrl
|
|
1717
|
+
state
|
|
1718
|
+
approved
|
|
1719
|
+
approvalsRequired
|
|
1720
|
+
approvalsLeft
|
|
1721
|
+
approvedBy {
|
|
1722
|
+
nodes {
|
|
1723
|
+
username
|
|
1724
|
+
name
|
|
1725
|
+
avatarUrl
|
|
1726
|
+
webUrl
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
reviewers {
|
|
1730
|
+
nodes {
|
|
1731
|
+
username
|
|
1732
|
+
name
|
|
1733
|
+
avatarUrl
|
|
1734
|
+
webUrl
|
|
1735
|
+
mergeRequestInteraction {
|
|
1736
|
+
reviewState
|
|
1737
|
+
approved
|
|
1738
|
+
reviewed
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
`;
|
|
1746
|
+
return this.query(query, { projectPath, iid }, userConfig);
|
|
1747
|
+
}
|
|
1748
|
+
// ── Project Statistics ────────────────────────────────────────────
|
|
1749
|
+
async getProjectStatistics(projectPath, userConfig) {
|
|
1750
|
+
const query = gql `
|
|
1751
|
+
query getProjectStatistics($projectPath: ID!) {
|
|
1752
|
+
project(fullPath: $projectPath) {
|
|
1753
|
+
id
|
|
1754
|
+
name
|
|
1755
|
+
fullPath
|
|
1756
|
+
webUrl
|
|
1757
|
+
starCount
|
|
1758
|
+
forksCount
|
|
1759
|
+
openIssuesCount
|
|
1760
|
+
statistics {
|
|
1761
|
+
repositorySize
|
|
1762
|
+
lfsObjectsSize
|
|
1763
|
+
buildArtifactsSize
|
|
1764
|
+
packagesSize
|
|
1765
|
+
wikiSize
|
|
1766
|
+
snippetsSize
|
|
1767
|
+
uploadsSize
|
|
1768
|
+
containerRegistrySize
|
|
1769
|
+
commitCount
|
|
1770
|
+
}
|
|
1771
|
+
openMergeRequests: mergeRequests(state: opened) {
|
|
1772
|
+
count
|
|
1773
|
+
}
|
|
1774
|
+
lastPipeline: pipelines(first: 1) {
|
|
1775
|
+
nodes {
|
|
1776
|
+
id
|
|
1777
|
+
iid
|
|
1778
|
+
status
|
|
1779
|
+
createdAt
|
|
1780
|
+
finishedAt
|
|
1781
|
+
ref
|
|
1782
|
+
detailedStatus {
|
|
1783
|
+
text
|
|
1784
|
+
label
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
releaseCount: releases {
|
|
1789
|
+
count
|
|
1790
|
+
}
|
|
1791
|
+
languages {
|
|
1792
|
+
name
|
|
1793
|
+
share
|
|
1794
|
+
color
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
`;
|
|
1799
|
+
return this.query(query, { projectPath }, userConfig);
|
|
1800
|
+
}
|
|
1801
|
+
// ── Group Members ─────────────────────────────────────────────────
|
|
1802
|
+
async listGroupMembers(groupPath, search, first = 20, after, fetchAll = false, userConfig) {
|
|
1803
|
+
const query = gql `
|
|
1804
|
+
query listGroupMembers($groupPath: ID!, $search: String, $first: Int!, $after: String) {
|
|
1805
|
+
group(fullPath: $groupPath) {
|
|
1806
|
+
groupMembers(search: $search, first: $first, after: $after) {
|
|
1807
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1808
|
+
nodes {
|
|
1809
|
+
id
|
|
1810
|
+
accessLevel {
|
|
1811
|
+
stringValue
|
|
1812
|
+
integerValue
|
|
1813
|
+
}
|
|
1814
|
+
createdAt
|
|
1815
|
+
expiresAt
|
|
1816
|
+
user {
|
|
1817
|
+
username
|
|
1818
|
+
name
|
|
1819
|
+
avatarUrl
|
|
1820
|
+
webUrl
|
|
1821
|
+
state
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
`;
|
|
1828
|
+
if (fetchAll) {
|
|
1829
|
+
return this.fetchAllPages(query, {
|
|
1830
|
+
groupPath,
|
|
1831
|
+
search: search || undefined,
|
|
1832
|
+
}, 'group.groupMembers', {
|
|
1833
|
+
maxItems: first,
|
|
1834
|
+
pageSize: this.config.maxPageSize,
|
|
1835
|
+
userConfig,
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
return this.query(query, {
|
|
1839
|
+
groupPath,
|
|
1840
|
+
search: search || undefined,
|
|
1841
|
+
first: Math.min(first, this.config.maxPageSize),
|
|
1842
|
+
after,
|
|
1843
|
+
}, userConfig);
|
|
1844
|
+
}
|
|
1845
|
+
// ── Label Search ─────────────────────────────────────────────────────
|
|
1846
|
+
async searchLabels(fullPath, isProject, search, first = 20, after, fetchAll = false, userConfig) {
|
|
1847
|
+
if (isProject) {
|
|
1848
|
+
const query = gql `
|
|
1849
|
+
query searchProjectLabels($fullPath: ID!, $search: String, $first: Int!, $after: String) {
|
|
1850
|
+
project(fullPath: $fullPath) {
|
|
1851
|
+
labels(searchTerm: $search, first: $first, after: $after) {
|
|
1852
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1853
|
+
nodes {
|
|
1854
|
+
id
|
|
1855
|
+
title
|
|
1856
|
+
description
|
|
1857
|
+
color
|
|
1858
|
+
textColor
|
|
1859
|
+
createdAt
|
|
1860
|
+
updatedAt
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
`;
|
|
1866
|
+
if (fetchAll) {
|
|
1867
|
+
return this.fetchAllPages(query, { fullPath, search }, 'project.labels', {
|
|
1868
|
+
maxItems: first,
|
|
1869
|
+
pageSize: this.config.maxPageSize,
|
|
1870
|
+
userConfig,
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
return this.query(query, { fullPath, search, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1874
|
+
}
|
|
1875
|
+
else {
|
|
1876
|
+
const query = gql `
|
|
1877
|
+
query searchGroupLabels($fullPath: ID!, $search: String, $first: Int!, $after: String) {
|
|
1878
|
+
group(fullPath: $fullPath) {
|
|
1879
|
+
labels(searchTerm: $search, first: $first, after: $after) {
|
|
1880
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
1881
|
+
nodes {
|
|
1882
|
+
id
|
|
1883
|
+
title
|
|
1884
|
+
description
|
|
1885
|
+
color
|
|
1886
|
+
textColor
|
|
1887
|
+
createdAt
|
|
1888
|
+
updatedAt
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
`;
|
|
1894
|
+
if (fetchAll) {
|
|
1895
|
+
return this.fetchAllPages(query, { fullPath, search }, 'group.labels', {
|
|
1896
|
+
maxItems: first,
|
|
1897
|
+
pageSize: this.config.maxPageSize,
|
|
1898
|
+
userConfig,
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
return this.query(query, { fullPath, search, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Resolve the GitLab base URL and access token for a REST call, honoring
|
|
1906
|
+
* per-request user credentials and falling back to the shared token.
|
|
1907
|
+
*/
|
|
1908
|
+
resolveRestAuth(userConfig, requiresWrite = false) {
|
|
1909
|
+
if (userConfig) {
|
|
1910
|
+
return {
|
|
1911
|
+
baseUrl: userConfig.gitlabUrl || this.config.gitlabUrl,
|
|
1912
|
+
token: userConfig.accessToken,
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
if (requiresWrite) {
|
|
1916
|
+
throw new Error('Write operations require user authentication. Please provide your GitLab credentials.');
|
|
1917
|
+
}
|
|
1918
|
+
if (this.config.sharedAccessToken && this.config.authMode !== 'per-user') {
|
|
1919
|
+
return { baseUrl: this.config.gitlabUrl, token: this.config.sharedAccessToken };
|
|
1920
|
+
}
|
|
1921
|
+
throw new Error('This operation requires user authentication. Please provide your GitLab credentials.');
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Perform a GitLab REST API v4 request. Used for endpoints not exposed via GraphQL
|
|
1925
|
+
* (e.g., broadcast messages).
|
|
1926
|
+
*/
|
|
1927
|
+
async restRequest(method, path, options = {}) {
|
|
1928
|
+
const { baseUrl, token } = this.resolveRestAuth(options.userConfig, options.requiresWrite);
|
|
1929
|
+
const url = new URL(`${baseUrl.replace(/\/$/, '')}/api/v4${path}`);
|
|
1930
|
+
if (options.query) {
|
|
1931
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
1932
|
+
if (v !== undefined && v !== null)
|
|
1933
|
+
url.searchParams.set(k, String(v));
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
const timeoutMs = this.config.defaultTimeout || 30000;
|
|
1937
|
+
return this.executeWithRetry(async () => {
|
|
1938
|
+
const controller = new AbortController();
|
|
1939
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1940
|
+
try {
|
|
1941
|
+
const response = await fetch(url.toString(), {
|
|
1942
|
+
method,
|
|
1943
|
+
headers: {
|
|
1944
|
+
Authorization: `Bearer ${token}`,
|
|
1945
|
+
'Content-Type': 'application/json',
|
|
1946
|
+
Accept: 'application/json',
|
|
1947
|
+
},
|
|
1948
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
1949
|
+
signal: controller.signal,
|
|
1950
|
+
});
|
|
1951
|
+
if (!response.ok) {
|
|
1952
|
+
const statusCode = response.status;
|
|
1953
|
+
let message = `${response.statusText}`;
|
|
1954
|
+
try {
|
|
1955
|
+
const errBody = await response.json();
|
|
1956
|
+
message = errBody?.message || errBody?.error || JSON.stringify(errBody);
|
|
1957
|
+
}
|
|
1958
|
+
catch {
|
|
1959
|
+
// non-JSON body
|
|
1960
|
+
}
|
|
1961
|
+
const retryable = RETRY_CONFIG.retryableStatusCodes.includes(statusCode);
|
|
1962
|
+
throw new GitLabAPIError(`GitLab REST ${method} ${path} failed (${statusCode}): ${message}`, {
|
|
1963
|
+
code: statusCode === 401 ? 'AUTH_FAILED' : statusCode === 403 ? 'FORBIDDEN' : 'HTTP_ERROR',
|
|
1964
|
+
statusCode,
|
|
1965
|
+
isRetryable: retryable,
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
if (response.status === 204)
|
|
1969
|
+
return undefined;
|
|
1970
|
+
const text = await response.text();
|
|
1971
|
+
return (text ? JSON.parse(text) : undefined);
|
|
1972
|
+
}
|
|
1973
|
+
finally {
|
|
1974
|
+
clearTimeout(timeoutId);
|
|
1975
|
+
}
|
|
1976
|
+
}, `REST ${method} ${path}`);
|
|
1977
|
+
}
|
|
1978
|
+
async listBroadcastMessages(page = 1, perPage = 20, userConfig) {
|
|
1979
|
+
return this.restRequest('GET', '/broadcast_messages', {
|
|
1980
|
+
query: { page, per_page: Math.min(perPage, this.config.maxPageSize) },
|
|
1981
|
+
userConfig,
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
async getBroadcastMessage(id, userConfig) {
|
|
1985
|
+
return this.restRequest('GET', `/broadcast_messages/${id}`, { userConfig });
|
|
1986
|
+
}
|
|
1987
|
+
async createBroadcastMessage(input, userConfig) {
|
|
1988
|
+
return this.restRequest('POST', '/broadcast_messages', { body: input, userConfig, requiresWrite: true });
|
|
1989
|
+
}
|
|
1990
|
+
async updateBroadcastMessage(id, input, userConfig) {
|
|
1991
|
+
return this.restRequest('PUT', `/broadcast_messages/${id}`, { body: input, userConfig, requiresWrite: true });
|
|
1992
|
+
}
|
|
1993
|
+
async deleteBroadcastMessage(id, userConfig) {
|
|
1994
|
+
return this.restRequest('DELETE', `/broadcast_messages/${id}`, { userConfig, requiresWrite: true });
|
|
1034
1995
|
}
|
|
1035
1996
|
}
|
|
1036
1997
|
//# sourceMappingURL=gitlab-client.js.map
|