@gitkraken/core-gitlens 0.3.1 → 0.4.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.
Files changed (230) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +13 -10
  3. package/dist/git/context.d.ts +4 -3
  4. package/dist/git/context.d.ts.map +1 -1
  5. package/dist/git/context.js.map +1 -1
  6. package/dist/git/providers/operations.d.ts +1 -0
  7. package/dist/git/providers/operations.d.ts.map +1 -1
  8. package/dist/git/providers/worktrees.d.ts +2 -0
  9. package/dist/git/providers/worktrees.d.ts.map +1 -1
  10. package/dist/git/utils/conflictResolution.utils.d.ts +32 -0
  11. package/dist/git/utils/conflictResolution.utils.d.ts.map +1 -1
  12. package/dist/git/utils/conflictResolution.utils.js +95 -0
  13. package/dist/git/utils/conflictResolution.utils.js.map +1 -1
  14. package/dist/git-cli/providers/operations.d.ts +1 -0
  15. package/dist/git-cli/providers/operations.d.ts.map +1 -1
  16. package/dist/git-cli/providers/operations.js +22 -1
  17. package/dist/git-cli/providers/operations.js.map +1 -1
  18. package/dist/git-cli/providers/worktrees.d.ts +2 -0
  19. package/dist/git-cli/providers/worktrees.d.ts.map +1 -1
  20. package/dist/git-cli/providers/worktrees.js +3 -0
  21. package/dist/git-cli/providers/worktrees.js.map +1 -1
  22. package/dist/plus/ai/utils/ai.utils.js +5 -5
  23. package/dist/plus/ai/utils/ai.utils.js.map +1 -1
  24. package/dist/plus/git-github/api/github.d.ts +6 -13
  25. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  26. package/dist/plus/git-github/api/github.js +5 -10
  27. package/dist/plus/git-github/api/github.js.map +1 -1
  28. package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts +11 -0
  29. package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts.map +1 -0
  30. package/dist/plus/integrations/authentication/cloudIntegrationService.js +90 -0
  31. package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -0
  32. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +67 -0
  33. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts.map +1 -0
  34. package/dist/plus/integrations/authentication/configuredIntegrationService.js +295 -0
  35. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -0
  36. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts +92 -0
  37. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts.map +1 -0
  38. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +185 -0
  39. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -0
  40. package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts +62 -0
  41. package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts.map +1 -0
  42. package/dist/plus/integrations/authentication/integrationAuthenticationService.js +122 -0
  43. package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -0
  44. package/dist/plus/integrations/authentication/manualTokenProvider.d.ts +15 -0
  45. package/dist/plus/integrations/authentication/manualTokenProvider.d.ts.map +1 -0
  46. package/dist/plus/integrations/authentication/manualTokenProvider.js +49 -0
  47. package/dist/plus/integrations/authentication/manualTokenProvider.js.map +1 -0
  48. package/dist/plus/integrations/authentication/models.d.ts +87 -0
  49. package/dist/plus/integrations/authentication/models.d.ts.map +1 -0
  50. package/dist/plus/integrations/authentication/models.js +64 -0
  51. package/dist/plus/integrations/authentication/models.js.map +1 -0
  52. package/dist/plus/integrations/constants.d.ts +47 -0
  53. package/dist/plus/integrations/constants.d.ts.map +1 -0
  54. package/dist/plus/integrations/constants.js +128 -0
  55. package/dist/plus/integrations/constants.js.map +1 -0
  56. package/dist/plus/integrations/context.d.ts +354 -0
  57. package/dist/plus/integrations/context.d.ts.map +1 -0
  58. package/dist/plus/integrations/context.js +2 -0
  59. package/dist/plus/integrations/context.js.map +1 -0
  60. package/dist/plus/integrations/errors.d.ts +14 -0
  61. package/dist/plus/integrations/errors.d.ts.map +1 -0
  62. package/dist/plus/integrations/errors.js +19 -0
  63. package/dist/plus/integrations/errors.js.map +1 -0
  64. package/dist/plus/integrations/index.d.ts +32 -0
  65. package/dist/plus/integrations/index.d.ts.map +1 -0
  66. package/dist/plus/integrations/index.js +62 -0
  67. package/dist/plus/integrations/index.js.map +1 -0
  68. package/dist/plus/integrations/integrationService.d.ts +98 -0
  69. package/dist/plus/integrations/integrationService.d.ts.map +1 -0
  70. package/dist/plus/integrations/integrationService.js +627 -0
  71. package/dist/plus/integrations/integrationService.js.map +1 -0
  72. package/dist/plus/integrations/models/gitHostIntegration.d.ts +82 -0
  73. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -0
  74. package/dist/plus/integrations/models/gitHostIntegration.js +522 -0
  75. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -0
  76. package/dist/plus/integrations/models/integration.d.ts +127 -0
  77. package/dist/plus/integrations/models/integration.d.ts.map +1 -0
  78. package/dist/plus/integrations/models/integration.js +456 -0
  79. package/dist/plus/integrations/models/integration.js.map +1 -0
  80. package/dist/plus/integrations/models/issuesIntegration.d.ts +28 -0
  81. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -0
  82. package/dist/plus/integrations/models/issuesIntegration.js +101 -0
  83. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -0
  84. package/dist/plus/integrations/providers/apiClients.d.ts +25 -0
  85. package/dist/plus/integrations/providers/apiClients.d.ts.map +1 -0
  86. package/dist/plus/integrations/providers/apiClients.js +39 -0
  87. package/dist/plus/integrations/providers/apiClients.js.map +1 -0
  88. package/dist/plus/integrations/providers/azure/azure.d.ts +47 -0
  89. package/dist/plus/integrations/providers/azure/azure.d.ts.map +1 -0
  90. package/dist/plus/integrations/providers/azure/azure.js +444 -0
  91. package/dist/plus/integrations/providers/azure/azure.js.map +1 -0
  92. package/dist/plus/integrations/providers/azure/models.d.ts +286 -0
  93. package/dist/plus/integrations/providers/azure/models.d.ts.map +1 -0
  94. package/dist/plus/integrations/providers/azure/models.js +203 -0
  95. package/dist/plus/integrations/providers/azure/models.js.map +1 -0
  96. package/dist/plus/integrations/providers/azureDevOps.d.ts +95 -0
  97. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -0
  98. package/dist/plus/integrations/providers/azureDevOps.js +432 -0
  99. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -0
  100. package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +42 -0
  101. package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts.map +1 -0
  102. package/dist/plus/integrations/providers/bitbucket/bitbucket.js +513 -0
  103. package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -0
  104. package/dist/plus/integrations/providers/bitbucket/models.d.ts +222 -0
  105. package/dist/plus/integrations/providers/bitbucket/models.d.ts.map +1 -0
  106. package/dist/plus/integrations/providers/bitbucket/models.js +158 -0
  107. package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -0
  108. package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +120 -0
  109. package/dist/plus/integrations/providers/bitbucket-server/models.d.ts.map +1 -0
  110. package/dist/plus/integrations/providers/bitbucket-server/models.js +111 -0
  111. package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -0
  112. package/dist/plus/integrations/providers/bitbucket-server.d.ts +60 -0
  113. package/dist/plus/integrations/providers/bitbucket-server.d.ts.map +1 -0
  114. package/dist/plus/integrations/providers/bitbucket-server.js +138 -0
  115. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -0
  116. package/dist/plus/integrations/providers/bitbucket.d.ts +50 -0
  117. package/dist/plus/integrations/providers/bitbucket.d.ts.map +1 -0
  118. package/dist/plus/integrations/providers/bitbucket.js +181 -0
  119. package/dist/plus/integrations/providers/bitbucket.js.map +1 -0
  120. package/dist/plus/integrations/providers/github/github.d.ts +5 -0
  121. package/dist/plus/integrations/providers/github/github.d.ts.map +1 -0
  122. package/dist/plus/integrations/providers/github/github.js +47 -0
  123. package/dist/plus/integrations/providers/github/github.js.map +1 -0
  124. package/dist/plus/integrations/providers/github/github.utils.d.ts +11 -0
  125. package/dist/plus/integrations/providers/github/github.utils.d.ts.map +1 -0
  126. package/dist/plus/integrations/providers/github/github.utils.js +6 -0
  127. package/dist/plus/integrations/providers/github/github.utils.js.map +1 -0
  128. package/dist/plus/integrations/providers/github/models.d.ts +3 -0
  129. package/dist/plus/integrations/providers/github/models.d.ts.map +1 -0
  130. package/dist/plus/integrations/providers/github/models.js +2 -0
  131. package/dist/plus/integrations/providers/github/models.js.map +1 -0
  132. package/dist/plus/integrations/providers/github.d.ts +73 -0
  133. package/dist/plus/integrations/providers/github.d.ts.map +1 -0
  134. package/dist/plus/integrations/providers/github.js +160 -0
  135. package/dist/plus/integrations/providers/github.js.map +1 -0
  136. package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +63 -0
  137. package/dist/plus/integrations/providers/gitlab/gitlab.d.ts.map +1 -0
  138. package/dist/plus/integrations/providers/gitlab/gitlab.js +828 -0
  139. package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -0
  140. package/dist/plus/integrations/providers/gitlab/gitlab.utils.d.ts +21 -0
  141. package/dist/plus/integrations/providers/gitlab/gitlab.utils.d.ts.map +1 -0
  142. package/dist/plus/integrations/providers/gitlab/gitlab.utils.js +45 -0
  143. package/dist/plus/integrations/providers/gitlab/gitlab.utils.js.map +1 -0
  144. package/dist/plus/integrations/providers/gitlab/models.d.ts +128 -0
  145. package/dist/plus/integrations/providers/gitlab/models.d.ts.map +1 -0
  146. package/dist/plus/integrations/providers/gitlab/models.js +96 -0
  147. package/dist/plus/integrations/providers/gitlab/models.js.map +1 -0
  148. package/dist/plus/integrations/providers/gitlab.d.ts +79 -0
  149. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -0
  150. package/dist/plus/integrations/providers/gitlab.js +253 -0
  151. package/dist/plus/integrations/providers/gitlab.js.map +1 -0
  152. package/dist/plus/integrations/providers/jira.d.ts +46 -0
  153. package/dist/plus/integrations/providers/jira.d.ts.map +1 -0
  154. package/dist/plus/integrations/providers/jira.js +255 -0
  155. package/dist/plus/integrations/providers/jira.js.map +1 -0
  156. package/dist/plus/integrations/providers/linear.d.ts +47 -0
  157. package/dist/plus/integrations/providers/linear.d.ts.map +1 -0
  158. package/dist/plus/integrations/providers/linear.js +198 -0
  159. package/dist/plus/integrations/providers/linear.js.map +1 -0
  160. package/dist/plus/integrations/providers/models.d.ts +433 -0
  161. package/dist/plus/integrations/providers/models.d.ts.map +1 -0
  162. package/dist/plus/integrations/providers/models.js +602 -0
  163. package/dist/plus/integrations/providers/models.js.map +1 -0
  164. package/dist/plus/integrations/providers/providersApi.d.ts +129 -0
  165. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -0
  166. package/dist/plus/integrations/providers/providersApi.js +602 -0
  167. package/dist/plus/integrations/providers/providersApi.js.map +1 -0
  168. package/dist/plus/integrations/providers/utils.d.ts +54 -0
  169. package/dist/plus/integrations/providers/utils.d.ts.map +1 -0
  170. package/dist/plus/integrations/providers/utils.js +262 -0
  171. package/dist/plus/integrations/providers/utils.js.map +1 -0
  172. package/dist/plus/integrations/telemetry.d.ts +26 -0
  173. package/dist/plus/integrations/telemetry.d.ts.map +1 -0
  174. package/dist/plus/integrations/telemetry.js +11 -0
  175. package/dist/plus/integrations/telemetry.js.map +1 -0
  176. package/dist/plus/integrations/utils/integration.utils.d.ts +14 -0
  177. package/dist/plus/integrations/utils/integration.utils.d.ts.map +1 -0
  178. package/dist/plus/integrations/utils/integration.utils.js +111 -0
  179. package/dist/plus/integrations/utils/integration.utils.js.map +1 -0
  180. package/dist/utils/disposable.d.ts +15 -2
  181. package/dist/utils/disposable.d.ts.map +1 -1
  182. package/dist/utils/disposable.js.map +1 -1
  183. package/package.json +43 -2
  184. package/src/git/context.ts +4 -3
  185. package/src/git/providers/operations.ts +1 -1
  186. package/src/git/providers/worktrees.ts +14 -2
  187. package/src/git/utils/conflictResolution.utils.ts +126 -0
  188. package/src/git-cli/providers/operations.ts +22 -2
  189. package/src/git-cli/providers/worktrees.ts +17 -2
  190. package/src/plus/ai/utils/ai.utils.ts +5 -5
  191. package/src/plus/git-github/api/github.ts +13 -25
  192. package/src/plus/integrations/authentication/cloudIntegrationService.ts +120 -0
  193. package/src/plus/integrations/authentication/configuredIntegrationService.ts +414 -0
  194. package/src/plus/integrations/authentication/integrationAuthenticationProvider.ts +289 -0
  195. package/src/plus/integrations/authentication/integrationAuthenticationService.ts +144 -0
  196. package/src/plus/integrations/authentication/manualTokenProvider.ts +72 -0
  197. package/src/plus/integrations/authentication/models.ts +168 -0
  198. package/src/plus/integrations/constants.ts +169 -0
  199. package/src/plus/integrations/context.ts +383 -0
  200. package/src/plus/integrations/errors.ts +35 -0
  201. package/src/plus/integrations/index.ts +119 -0
  202. package/src/plus/integrations/integrationService.ts +924 -0
  203. package/src/plus/integrations/models/gitHostIntegration.ts +738 -0
  204. package/src/plus/integrations/models/integration.ts +657 -0
  205. package/src/plus/integrations/models/issuesIntegration.ts +124 -0
  206. package/src/plus/integrations/providers/apiClients.ts +72 -0
  207. package/src/plus/integrations/providers/azure/azure.ts +691 -0
  208. package/src/plus/integrations/providers/azure/models.ts +605 -0
  209. package/src/plus/integrations/providers/azureDevOps.ts +677 -0
  210. package/src/plus/integrations/providers/bitbucket/bitbucket.ts +774 -0
  211. package/src/plus/integrations/providers/bitbucket/models.ts +445 -0
  212. package/src/plus/integrations/providers/bitbucket-server/models.ts +252 -0
  213. package/src/plus/integrations/providers/bitbucket-server.ts +282 -0
  214. package/src/plus/integrations/providers/bitbucket.ts +350 -0
  215. package/src/plus/integrations/providers/github/github.ts +64 -0
  216. package/src/plus/integrations/providers/github/github.utils.ts +23 -0
  217. package/src/plus/integrations/providers/github/models.ts +29 -0
  218. package/src/plus/integrations/providers/github.ts +366 -0
  219. package/src/plus/integrations/providers/gitlab/gitlab.ts +1210 -0
  220. package/src/plus/integrations/providers/gitlab/gitlab.utils.ts +71 -0
  221. package/src/plus/integrations/providers/gitlab/models.ts +257 -0
  222. package/src/plus/integrations/providers/gitlab.ts +473 -0
  223. package/src/plus/integrations/providers/jira.ts +359 -0
  224. package/src/plus/integrations/providers/linear.ts +275 -0
  225. package/src/plus/integrations/providers/models.ts +1114 -0
  226. package/src/plus/integrations/providers/providersApi.ts +1126 -0
  227. package/src/plus/integrations/providers/utils.ts +359 -0
  228. package/src/plus/integrations/telemetry.ts +29 -0
  229. package/src/plus/integrations/utils/integration.utils.ts +138 -0
  230. package/src/utils/disposable.ts +15 -2
@@ -0,0 +1,1210 @@
1
+ import type { Account } from '../../../../git/models/author.js';
2
+ import type { DefaultBranch } from '../../../../git/models/defaultBranch.js';
3
+ import type { IssueOrPullRequest } from '../../../../git/models/issueOrPullRequest.js';
4
+ import { PullRequest } from '../../../../git/models/pullRequest.js';
5
+ import type { Provider } from '../../../../git/models/remoteProvider.js';
6
+ import type { RepositoryMetadata } from '../../../../git/models/repositoryMetadata.js';
7
+ import { CancellationError } from '../../../../utils/cancellation.js';
8
+ import { trace } from '../../../../utils/decorators/log.js';
9
+ import type { Disposable } from '../../../../utils/disposable.js';
10
+ import { Logger } from '../../../../utils/logger.js';
11
+ import type { ScopedLogger } from '../../../../utils/logger.scoped.js';
12
+ import { getScopedLogger } from '../../../../utils/logger.scoped.js';
13
+ import { maybeStopWatch } from '../../../../utils/stopwatch.js';
14
+ import { equalsIgnoreCase } from '../../../../utils/string.js';
15
+ import { joinUriPath, parseUri } from '../../../../utils/uri.js';
16
+ import type { TokenWithInfo } from '../../authentication/models.js';
17
+ import type { IntegrationServiceContext } from '../../context.js';
18
+ import {
19
+ AuthenticationError,
20
+ AuthenticationErrorReason,
21
+ ProviderFetchError,
22
+ RequestClientError,
23
+ RequestNotFoundError,
24
+ RequestRateLimitError,
25
+ } from '../../errors.js';
26
+ import { selectGitLabUserForCommit } from './gitlab.utils.js';
27
+ import type {
28
+ GitLabCommit,
29
+ GitLabIssue,
30
+ GitLabMergeRequest,
31
+ GitLabMergeRequestFull,
32
+ GitLabMergeRequestREST,
33
+ GitLabMergeRequestState,
34
+ GitLabProjectREST,
35
+ GitLabUser,
36
+ } from './models.js';
37
+ import { fromGitLabMergeRequest, fromGitLabMergeRequestREST, fromGitLabMergeRequestState } from './models.js';
38
+
39
+ // drop it as soon as we switch to @gitkraken/providers-api
40
+ const gitlabUserIdPrefix = 'gid://gitlab/User/';
41
+ const gitlabMergeRequestIdPrefix = 'gid://gitlab/MergeRequest/';
42
+
43
+ function buildGitLabUserId(id: string | number | undefined): string | undefined {
44
+ return typeof id === 'string' && id?.startsWith(gitlabUserIdPrefix)
45
+ ? id.substring(gitlabUserIdPrefix.length)
46
+ : String(id);
47
+ }
48
+
49
+ export class GitLabApi implements Disposable {
50
+ private readonly _disposable: Disposable;
51
+ private _projectIds = new Map<string, Promise<string | undefined>>();
52
+
53
+ constructor(private readonly ctx: IntegrationServiceContext) {
54
+ this._disposable = ctx.config.onDidChange(e => {
55
+ if (e.httpProxy || e.remotes) {
56
+ this.resetCaches();
57
+ }
58
+ });
59
+ }
60
+
61
+ dispose(): void {
62
+ this._disposable.dispose();
63
+ }
64
+
65
+ private resetCaches(): void {
66
+ this._projectIds.clear();
67
+ }
68
+
69
+ @trace({
70
+ args: (provider, token, owner, repo, rev) => ({
71
+ provider: provider.name,
72
+ token: `<token:${token.microHash}>`,
73
+ owner: owner,
74
+ repo: repo,
75
+ rev: rev,
76
+ }),
77
+ })
78
+ async getAccountForCommit(
79
+ provider: Provider,
80
+ token: TokenWithInfo,
81
+ owner: string,
82
+ repo: string,
83
+ rev: string,
84
+ options?: {
85
+ baseUrl?: string;
86
+ avatarSize?: number;
87
+ },
88
+ cancellation?: AbortSignal,
89
+ ): Promise<Account | undefined> {
90
+ const scope = getScopedLogger();
91
+
92
+ const projectId = await this.getProjectId(provider, token, owner, repo, options?.baseUrl, cancellation);
93
+ if (!projectId) return undefined;
94
+
95
+ try {
96
+ const commit = await this.request<GitLabCommit>(
97
+ provider,
98
+ token,
99
+ options?.baseUrl,
100
+ `v4/projects/${projectId}/repository/commits/${rev}?stats=false`,
101
+ {
102
+ method: 'GET',
103
+ // ...options,
104
+ },
105
+ cancellation,
106
+ scope,
107
+ );
108
+
109
+ let users = await this.findUser(provider, token, commit.author_name, options, cancellation);
110
+ let user = selectGitLabUserForCommit(users, commit.author_name, commit.author_email);
111
+
112
+ // If the name search was ambiguous (multiple users share the name), try an email-scoped search to
113
+ // disambiguate before giving up — otherwise we'd show a stranger's avatar (see #2205)
114
+ if (user == null && commit.author_email) {
115
+ const exactNameMatches = users.filter(u => equalsIgnoreCase(u.name, commit.author_name)).length;
116
+ if (exactNameMatches > 1) {
117
+ users = await this.findUser(provider, token, commit.author_email, options, cancellation);
118
+ user = selectGitLabUserForCommit(users, commit.author_name, commit.author_email);
119
+ }
120
+ }
121
+
122
+ if (user == null) return undefined;
123
+
124
+ // If the avatarUrl is a relative URL, make it absolute using the webUrl (assuming the webUrl is the root URL with the username tacked on)
125
+ if (user.avatarUrl && !/^([a-zA-Z][\w+.-]+):/.test(user.avatarUrl)) {
126
+ user.avatarUrl = joinUriPath(parseUri(user.webUrl), '..', user.avatarUrl).toString();
127
+ }
128
+
129
+ return {
130
+ provider: provider,
131
+ id: String(user.id),
132
+ name: user.name || undefined,
133
+ email: commit.author_email || undefined,
134
+ avatarUrl: user.avatarUrl || undefined,
135
+ username: user.username || undefined,
136
+ };
137
+ } catch (ex) {
138
+ if (ex instanceof RequestNotFoundError) return undefined;
139
+
140
+ throw this.handleException(ex, provider, scope);
141
+ }
142
+ }
143
+
144
+ @trace({
145
+ args: (provider, token, _owner, _repo, email) => ({
146
+ provider: provider.name,
147
+ token: `<token:${token.microHash}>`,
148
+ email: email,
149
+ }),
150
+ })
151
+ async getAccountForEmail(
152
+ provider: Provider,
153
+ token: TokenWithInfo,
154
+ _owner: string,
155
+ _repo: string,
156
+ email: string,
157
+ options?: {
158
+ baseUrl?: string;
159
+ avatarSize?: number;
160
+ },
161
+ ): Promise<Account | undefined> {
162
+ const scope = getScopedLogger();
163
+
164
+ try {
165
+ const [user] = await this.findUser(provider, token, email, options);
166
+ if (user == null) return undefined;
167
+
168
+ return {
169
+ provider: provider,
170
+ id: String(user.id),
171
+ name: user.name || undefined,
172
+ email: user.publicEmail || undefined,
173
+ avatarUrl: user.avatarUrl || undefined,
174
+ username: user.username || undefined,
175
+ };
176
+ } catch (ex) {
177
+ if (ex instanceof RequestNotFoundError) return undefined;
178
+
179
+ throw this.handleException(ex, provider, scope);
180
+ }
181
+ }
182
+
183
+ @trace({
184
+ args: (provider, token, owner, repo) => ({
185
+ provider: provider.name,
186
+ token: `<token:${token.microHash}>`,
187
+ owner: owner,
188
+ repo: repo,
189
+ }),
190
+ })
191
+ async getDefaultBranch(
192
+ provider: Provider,
193
+ token: TokenWithInfo,
194
+ owner: string,
195
+ repo: string,
196
+ options?: {
197
+ baseUrl?: string;
198
+ },
199
+ cancellation?: AbortSignal,
200
+ ): Promise<DefaultBranch | undefined> {
201
+ const scope = getScopedLogger();
202
+
203
+ interface QueryResult {
204
+ data: {
205
+ project:
206
+ | {
207
+ repository: { rootRef: string } | null | undefined;
208
+ }
209
+ | null
210
+ | undefined;
211
+ };
212
+ }
213
+
214
+ try {
215
+ const query = `query getDefaultBranch(
216
+ $fullPath: ID!
217
+ ) {
218
+ project(fullPath: $fullPath) {
219
+ repository {
220
+ rootRef
221
+ }
222
+ }`;
223
+
224
+ const rsp = await this.graphql<QueryResult>(
225
+ provider,
226
+ token,
227
+ options?.baseUrl,
228
+ query,
229
+ {
230
+ fullPath: `${owner}/${repo}`,
231
+ },
232
+ cancellation,
233
+ scope,
234
+ );
235
+
236
+ const defaultBranch = rsp?.data?.project?.repository?.rootRef ?? undefined;
237
+ if (defaultBranch == null) return undefined;
238
+
239
+ return {
240
+ provider: provider,
241
+ name: defaultBranch,
242
+ };
243
+ } catch (ex) {
244
+ if (ex instanceof RequestNotFoundError) return undefined;
245
+
246
+ throw this.handleException(ex, provider, scope);
247
+ }
248
+ }
249
+
250
+ @trace({
251
+ args: (provider, token, owner, repo, number) => ({
252
+ provider: provider.name,
253
+ token: `<token:${token.microHash}>`,
254
+ owner: owner,
255
+ repo: repo,
256
+ number: number,
257
+ }),
258
+ })
259
+ async getIssueOrPullRequest(
260
+ provider: Provider,
261
+ token: TokenWithInfo,
262
+ owner: string,
263
+ repo: string,
264
+ number: number,
265
+ options?: {
266
+ baseUrl?: string;
267
+ },
268
+ cancellation?: AbortSignal,
269
+ ): Promise<IssueOrPullRequest | undefined> {
270
+ const scope = getScopedLogger();
271
+
272
+ interface QueryResult {
273
+ data: {
274
+ project: {
275
+ mergeRequest: GitLabMergeRequest | null;
276
+ issue: GitLabIssue | null;
277
+ } | null;
278
+ };
279
+ }
280
+
281
+ try {
282
+ const query = `query getIssueOrMergeRequest(
283
+ $fullPath: ID!
284
+ $iid: String!
285
+ ) {
286
+ project(fullPath: $fullPath) {
287
+ mergeRequest(iid: $iid) {
288
+ author {
289
+ id
290
+ name
291
+ avatarUrl
292
+ webUrl
293
+ }
294
+ iid
295
+ title
296
+ description
297
+ state
298
+ createdAt
299
+ updatedAt
300
+ mergedAt
301
+ webUrl
302
+ }
303
+ issue(iid: $iid) {
304
+ author {
305
+ id
306
+ name
307
+ avatarUrl
308
+ webUrl
309
+ }
310
+ iid
311
+ title
312
+ description
313
+ state
314
+ createdAt
315
+ updatedAt
316
+ closedAt
317
+ webUrl
318
+ }
319
+ }
320
+ }`;
321
+
322
+ const rsp = await this.graphql<QueryResult>(
323
+ provider,
324
+ token,
325
+ options?.baseUrl,
326
+ query,
327
+ {
328
+ fullPath: `${owner}/${repo}`,
329
+ iid: String(number),
330
+ },
331
+ cancellation,
332
+ scope,
333
+ );
334
+
335
+ if (rsp?.data?.project?.issue != null) {
336
+ const issue = rsp.data.project.issue;
337
+ return {
338
+ provider: provider,
339
+ type: 'issue',
340
+ id: issue.iid,
341
+ nodeId: undefined,
342
+ createdDate: new Date(issue.createdAt),
343
+ updatedDate: new Date(issue.updatedAt),
344
+ title: issue.title,
345
+ closed: issue.state === 'closed',
346
+ closedDate: issue.closedAt == null ? undefined : new Date(issue.closedAt),
347
+ url: issue.webUrl,
348
+ state: issue.state === 'locked' ? 'closed' : issue.state,
349
+ };
350
+ }
351
+
352
+ if (rsp?.data?.project?.mergeRequest != null) {
353
+ const mergeRequest = rsp.data.project.mergeRequest;
354
+ return {
355
+ provider: provider,
356
+ type: 'pullrequest',
357
+ id: mergeRequest.iid,
358
+ nodeId: undefined,
359
+ createdDate: new Date(mergeRequest.createdAt),
360
+ updatedDate: new Date(mergeRequest.updatedAt),
361
+ title: mergeRequest.title,
362
+ closed: mergeRequest.state === 'closed',
363
+ // TODO@eamodio this isn't right, but GitLab doesn't seem to provide a closedAt on merge requests in GraphQL
364
+ closedDate: mergeRequest.state === 'closed' ? new Date(mergeRequest.updatedAt) : undefined,
365
+ url: mergeRequest.webUrl,
366
+ state: mergeRequest.state === 'locked' ? 'closed' : mergeRequest.state,
367
+ };
368
+ }
369
+
370
+ return undefined;
371
+ } catch (ex) {
372
+ if (ex instanceof RequestNotFoundError) return undefined;
373
+
374
+ throw this.handleException(ex, provider, scope);
375
+ }
376
+ }
377
+
378
+ @trace({
379
+ args: (provider, token, owner, repo, branch) => ({
380
+ provider: provider.name,
381
+ token: `<token:${token.microHash}>`,
382
+ owner: owner,
383
+ repo: repo,
384
+ branch: branch,
385
+ }),
386
+ })
387
+ async getPullRequestForBranch(
388
+ provider: Provider,
389
+ token: TokenWithInfo,
390
+ owner: string,
391
+ repo: string,
392
+ branch: string,
393
+ options?: {
394
+ baseUrl?: string;
395
+ avatarSize?: number;
396
+ include?: GitLabMergeRequestState[];
397
+ },
398
+ cancellation?: AbortSignal,
399
+ ): Promise<PullRequest | undefined> {
400
+ const scope = getScopedLogger();
401
+
402
+ interface QueryResult {
403
+ data: {
404
+ project: {
405
+ mergeRequests?: {
406
+ nodes: GitLabMergeRequest[];
407
+ } | null;
408
+ opened?: {
409
+ nodes: GitLabMergeRequest[];
410
+ } | null;
411
+ closed?: {
412
+ nodes: GitLabMergeRequest[];
413
+ } | null;
414
+ merged?: {
415
+ nodes: GitLabMergeRequest[];
416
+ } | null;
417
+ } | null;
418
+ };
419
+ }
420
+
421
+ try {
422
+ const fragment = `
423
+ nodes {
424
+ iid
425
+ author {
426
+ id
427
+ name
428
+ avatarUrl
429
+ webUrl
430
+ }
431
+ title
432
+ description
433
+ state
434
+ createdAt
435
+ updatedAt
436
+ mergedAt
437
+ webUrl
438
+ }`;
439
+
440
+ const query = `query getMergeRequestForBranch(
441
+ $fullPath: ID!
442
+ $branches: [String!]
443
+ ) {
444
+ project(fullPath: $fullPath) {
445
+ ${
446
+ options?.include == null
447
+ ? `mergeRequests(sourceBranches: $branches sort: UPDATED_DESC first: 1) {
448
+ ${fragment}
449
+ }`
450
+ : ''
451
+ }
452
+ ${
453
+ options?.include?.includes('opened')
454
+ ? `opened: mergeRequests(sourceBranches: $branches state: opened sort: UPDATED_DESC first: 1) {
455
+ ${fragment}
456
+ }`
457
+ : ''
458
+ }
459
+ ${
460
+ options?.include?.includes('merged')
461
+ ? `merged: mergeRequests(sourceBranches: $branches state: merged sort: UPDATED_DESC first: 1) {
462
+ ${fragment}
463
+ }`
464
+ : ''
465
+ }
466
+ ${
467
+ options?.include?.includes('closed')
468
+ ? `closed: mergeRequests(sourceBranches: $branches state: closed sort: UPDATED_DESC first: 1) {
469
+ ${fragment}
470
+ }`
471
+ : ''
472
+ }
473
+ }
474
+ }`;
475
+
476
+ const rsp = await this.graphql<QueryResult>(
477
+ provider,
478
+ token,
479
+ options?.baseUrl,
480
+ query,
481
+ {
482
+ fullPath: `${owner}/${repo}`,
483
+ branches: [branch],
484
+ state: options?.include,
485
+ },
486
+ cancellation,
487
+ scope,
488
+ );
489
+
490
+ let pr: GitLabMergeRequest | undefined;
491
+
492
+ if (options?.include == null) {
493
+ pr = rsp?.data?.project?.mergeRequests?.nodes?.[0];
494
+ } else {
495
+ for (const state of options.include) {
496
+ let mr;
497
+ if (state === 'opened') {
498
+ mr = rsp?.data?.project?.opened?.nodes?.[0];
499
+ } else if (state === 'merged') {
500
+ mr = rsp?.data?.project?.merged?.nodes?.[0];
501
+ } else if (state === 'closed') {
502
+ mr = rsp?.data?.project?.closed?.nodes?.[0];
503
+ }
504
+
505
+ if (mr != null && (pr == null || new Date(mr.updatedAt) > new Date(pr.updatedAt))) {
506
+ pr = mr;
507
+ }
508
+ }
509
+ }
510
+
511
+ if (pr == null) return undefined;
512
+
513
+ return new PullRequest(
514
+ provider,
515
+ {
516
+ id: buildGitLabUserId(pr.author?.id) ?? '',
517
+ name: pr.author?.name ?? 'Unknown',
518
+ avatarUrl: pr.author?.avatarUrl ?? '',
519
+ url: pr.author?.webUrl ?? '',
520
+ },
521
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
522
+ String(pr.iid),
523
+ undefined,
524
+ pr.title,
525
+ pr.webUrl,
526
+ { owner: owner, repo: repo },
527
+ fromGitLabMergeRequestState(pr.state),
528
+ new Date(pr.createdAt),
529
+ new Date(pr.updatedAt),
530
+ // TODO@eamodio this isn't right, but GitLab doesn't seem to provide a closedAt on merge requests in GraphQL
531
+ pr.state !== 'closed' ? undefined : new Date(pr.updatedAt),
532
+ pr.mergedAt == null ? undefined : new Date(pr.mergedAt),
533
+ );
534
+ } catch (ex) {
535
+ if (ex instanceof RequestNotFoundError) return undefined;
536
+
537
+ throw this.handleException(ex, provider, scope);
538
+ }
539
+ }
540
+
541
+ @trace({
542
+ args: (provider, token, owner, repo, rev) => ({
543
+ provider: provider.name,
544
+ token: `<token:${token.microHash}>`,
545
+ owner: owner,
546
+ repo: repo,
547
+ rev: rev,
548
+ }),
549
+ })
550
+ async getPullRequestForCommit(
551
+ provider: Provider,
552
+ token: TokenWithInfo,
553
+ owner: string,
554
+ repo: string,
555
+ rev: string,
556
+ options?: {
557
+ baseUrl?: string;
558
+ avatarSize?: number;
559
+ },
560
+ cancellation?: AbortSignal,
561
+ ): Promise<PullRequest | undefined> {
562
+ const scope = getScopedLogger();
563
+
564
+ const projectId = await this.getProjectId(provider, token, owner, repo, options?.baseUrl, cancellation);
565
+ if (!projectId) return undefined;
566
+
567
+ try {
568
+ const mrs = await this.request<GitLabMergeRequestREST[]>(
569
+ provider,
570
+ token,
571
+ options?.baseUrl,
572
+ `v4/projects/${projectId}/repository/commits/${rev}/merge_requests`,
573
+ {
574
+ method: 'GET',
575
+ // ...options,
576
+ },
577
+ cancellation,
578
+ scope,
579
+ );
580
+ if (mrs == null || mrs.length === 0) return undefined;
581
+
582
+ if (mrs.length > 1) {
583
+ mrs.sort(
584
+ (a, b) =>
585
+ (a.state === 'opened' ? -1 : 1) - (b.state === 'opened' ? -1 : 1) ||
586
+ new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
587
+ );
588
+ }
589
+
590
+ return fromGitLabMergeRequestREST(mrs[0], provider, { owner: owner, repo: repo });
591
+ } catch (ex) {
592
+ if (ex instanceof RequestNotFoundError) return undefined;
593
+
594
+ throw this.handleException(ex, provider, scope);
595
+ }
596
+ }
597
+
598
+ @trace({
599
+ args: (provider, token, owner, repo, id) => ({
600
+ provider: provider.name,
601
+ token: `<token:${token.microHash}>`,
602
+ owner: owner,
603
+ repo: repo,
604
+ id: id,
605
+ }),
606
+ })
607
+ async getPullRequest(
608
+ provider: Provider,
609
+ token: TokenWithInfo,
610
+ owner: string,
611
+ repo: string,
612
+ id: number,
613
+ options?: {
614
+ baseUrl?: string;
615
+ },
616
+ cancellation?: AbortSignal,
617
+ ): Promise<PullRequest | undefined> {
618
+ const scope = getScopedLogger();
619
+
620
+ interface QueryResult {
621
+ data: {
622
+ project: {
623
+ mergeRequest: GitLabMergeRequestFull | null;
624
+ } | null;
625
+ };
626
+ }
627
+
628
+ try {
629
+ const query = `query getMergeRequest(
630
+ $fullPath: ID!
631
+ $iid: String!
632
+ ) {
633
+ project(fullPath: $fullPath) {
634
+ mergeRequest(iid: $iid) {
635
+ id,
636
+ iid
637
+ state,
638
+ author {
639
+ id
640
+ name
641
+ avatarUrl
642
+ webUrl
643
+ }
644
+ diffRefs {
645
+ baseSha
646
+ headSha
647
+ }
648
+ title
649
+ description
650
+ webUrl
651
+ createdAt
652
+ updatedAt
653
+ mergedAt
654
+ targetBranch
655
+ sourceBranch
656
+ project {
657
+ id
658
+ fullPath
659
+ webUrl
660
+ }
661
+ sourceProject {
662
+ id
663
+ fullPath
664
+ webUrl
665
+ }
666
+ }
667
+ }
668
+ }`;
669
+
670
+ const rsp = await this.graphql<QueryResult>(
671
+ provider,
672
+ token,
673
+ options?.baseUrl,
674
+ query,
675
+ {
676
+ fullPath: `${owner}/${repo}`,
677
+ iid: String(id),
678
+ },
679
+ cancellation,
680
+ scope,
681
+ );
682
+
683
+ if (rsp?.data?.project?.mergeRequest == null) return undefined;
684
+
685
+ const pr = rsp.data.project.mergeRequest;
686
+ return fromGitLabMergeRequest(pr, provider);
687
+ } catch (ex) {
688
+ if (ex instanceof RequestNotFoundError) return undefined;
689
+
690
+ throw this.handleException(ex, provider, scope);
691
+ }
692
+ }
693
+
694
+ @trace({
695
+ args: (provider, token, owner, repo) => ({
696
+ provider: provider.name,
697
+ token: `<token:${token.microHash}>`,
698
+ owner: owner,
699
+ repo: repo,
700
+ }),
701
+ })
702
+ async getRepositoryMetadata(
703
+ provider: Provider,
704
+ token: TokenWithInfo,
705
+ owner: string,
706
+ repo: string,
707
+ options?: {
708
+ baseUrl?: string;
709
+ },
710
+ cancellation?: AbortSignal,
711
+ ): Promise<RepositoryMetadata | undefined> {
712
+ const scope = getScopedLogger();
713
+
714
+ const projectId = await this.getProjectId(provider, token, owner, repo, options?.baseUrl, cancellation);
715
+ if (!projectId) return undefined;
716
+
717
+ try {
718
+ const proj = await this.request<GitLabProjectREST>(
719
+ provider,
720
+ token,
721
+ options?.baseUrl,
722
+ `v4/projects/${projectId}`,
723
+ {
724
+ method: 'GET',
725
+ // ...options,
726
+ },
727
+ cancellation,
728
+ scope,
729
+ );
730
+ if (proj == null) return undefined;
731
+
732
+ return {
733
+ provider: provider,
734
+ owner: proj.namespace.full_path,
735
+ name: proj.path,
736
+ isFork: proj.forked_from_project != null,
737
+ parent:
738
+ proj.forked_from_project != null
739
+ ? {
740
+ owner: proj.forked_from_project.namespace.full_path,
741
+ name: proj.forked_from_project.path,
742
+ }
743
+ : undefined,
744
+ } satisfies RepositoryMetadata;
745
+ } catch (ex) {
746
+ if (ex instanceof RequestNotFoundError) return undefined;
747
+
748
+ throw this.handleException(ex, provider, scope);
749
+ }
750
+ }
751
+
752
+ @trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
753
+ async searchPullRequests(
754
+ provider: Provider,
755
+ token: TokenWithInfo,
756
+ options?: { search?: string; user?: string; repos?: string[]; baseUrl?: string; avatarSize?: number },
757
+ cancellation?: AbortSignal,
758
+ ): Promise<PullRequest[]> {
759
+ const scope = getScopedLogger();
760
+ const search = options?.search;
761
+ if (!search) {
762
+ return [];
763
+ }
764
+
765
+ try {
766
+ const perPageLimit = 20; // with bigger amount we exceed the max GraphQL complexity in the next query
767
+ const restPRs = await this.request<GitLabMergeRequestREST[]>(
768
+ provider,
769
+ token,
770
+ options?.baseUrl,
771
+ `v4/search/?scope=merge_requests&search=${search}&per_page=${perPageLimit}`,
772
+ {
773
+ method: 'GET',
774
+ },
775
+ cancellation,
776
+ scope,
777
+ );
778
+ if (restPRs.length === 0) {
779
+ return [];
780
+ }
781
+
782
+ interface QueryResult {
783
+ data: Record<`mergeRequest_${number}`, GitLabMergeRequestFull>;
784
+ }
785
+
786
+ const queryArgs = restPRs.map((_, i) => `$id_${i}: MergeRequestID!`).join('\n');
787
+ const queryFields = restPRs
788
+ .map((_, i) => `mergeRequest_${i}: mergeRequest(id: $id_${i}) { ...mergeRequestFields }`)
789
+ .join('\n');
790
+ // Set of fields includes only additional fields that are not included in GitLabMergeRequestREST.
791
+ // It's limited as much as possible to reduce complexity of the query.
792
+ const queryMrFields = `fragment mergeRequestFields on MergeRequest {
793
+ diffRefs {
794
+ baseSha
795
+ headSha
796
+ }
797
+ project {
798
+ id
799
+ fullPath
800
+ webUrl
801
+ }
802
+ sourceProject {
803
+ id
804
+ fullPath
805
+ webUrl
806
+ }
807
+ }`;
808
+ const query = `query getMergeRequests (${queryArgs}) {${queryFields}} ${queryMrFields}`;
809
+
810
+ const params = restPRs.reduce<Record<`id_${number}`, string>>((ids, gitlabRestPr, i) => {
811
+ ids[`id_${i}`] = `${gitlabMergeRequestIdPrefix}${gitlabRestPr.id}`;
812
+ return ids;
813
+ }, {});
814
+ const rsp = await this.graphql<QueryResult>(
815
+ provider,
816
+ token,
817
+ options?.baseUrl,
818
+ query,
819
+ params,
820
+ cancellation,
821
+ scope,
822
+ );
823
+ if (rsp?.data != null) {
824
+ const resultPRs = restPRs.reduce<PullRequest[]>((accum, restPR, i) => {
825
+ const graphQlPR = rsp.data[`mergeRequest_${i}`];
826
+ if (graphQlPR == null) {
827
+ return accum;
828
+ }
829
+
830
+ const fullPr: GitLabMergeRequestFull = {
831
+ ...graphQlPR,
832
+ iid: String(restPR.iid),
833
+ id: String(restPR.id),
834
+ state: restPR.state,
835
+ author: {
836
+ id: buildGitLabUserId(restPR.author?.id) ?? '',
837
+ name: restPR.author?.name ?? 'Unknown',
838
+ avatarUrl: restPR.author?.avatar_url ?? '',
839
+ webUrl: restPR.author?.web_url ?? '',
840
+ },
841
+ title: restPR.title,
842
+ description: restPR.description,
843
+ webUrl: restPR.web_url,
844
+ createdAt: restPR.created_at,
845
+ updatedAt: restPR.updated_at,
846
+ mergedAt: restPR.merged_at,
847
+ sourceBranch: restPR.source_branch,
848
+ targetBranch: restPR.target_branch,
849
+ };
850
+ accum.push(fromGitLabMergeRequest(fullPr, provider));
851
+ return accum;
852
+ }, []);
853
+ return resultPRs;
854
+ }
855
+ return [];
856
+ } catch (ex) {
857
+ if (ex instanceof RequestNotFoundError) return [];
858
+
859
+ throw this.handleException(ex, provider, scope);
860
+ }
861
+ }
862
+
863
+ private async findUser(
864
+ provider: Provider,
865
+ token: TokenWithInfo,
866
+ search: string,
867
+ options?: {
868
+ baseUrl?: string;
869
+ avatarSize?: number;
870
+ },
871
+ cancellation?: AbortSignal,
872
+ ): Promise<GitLabUser[]> {
873
+ const scope = getScopedLogger();
874
+
875
+ interface QueryResult {
876
+ data: {
877
+ users: {
878
+ nodes: {
879
+ id: string;
880
+ name: string;
881
+ username: string;
882
+ publicEmail?: string;
883
+ state: string;
884
+ avatarUrl: string;
885
+ webUrl: string;
886
+ }[];
887
+ };
888
+ };
889
+ }
890
+
891
+ try {
892
+ const query = `query findUser(
893
+ $search: String!
894
+ ) {
895
+ users(search: $search) {
896
+ nodes {
897
+ id
898
+ name
899
+ username,
900
+ publicEmail,
901
+ state
902
+ avatarUrl
903
+ webUrl
904
+ }
905
+ }
906
+ }`;
907
+ const rsp = await this.graphql<QueryResult>(
908
+ provider,
909
+ token,
910
+ options?.baseUrl,
911
+ query,
912
+ {
913
+ search: search,
914
+ },
915
+ cancellation,
916
+ scope,
917
+ );
918
+
919
+ const matches = rsp?.data?.users?.nodes;
920
+ if (matches == null || matches.length === 0) return [];
921
+
922
+ const users: GitLabUser[] = [];
923
+
924
+ for (const user of matches) {
925
+ const match = /gid:\/\/gitlab\/User\/([0-9]+)\b/.exec(user.id);
926
+ if (match == null) continue;
927
+
928
+ users.push({
929
+ id: parseInt(match[1], 10),
930
+ name: user.name,
931
+ username: user.username,
932
+ publicEmail: user.publicEmail || undefined,
933
+ state: user.state,
934
+ avatarUrl: user.avatarUrl,
935
+ webUrl: user.webUrl,
936
+ });
937
+ }
938
+
939
+ return users;
940
+ } catch (ex) {
941
+ if (ex instanceof RequestNotFoundError) return [];
942
+
943
+ this.handleException(ex, provider, scope);
944
+ return [];
945
+ }
946
+ }
947
+
948
+ getProjectId(
949
+ provider: Provider,
950
+ token: TokenWithInfo,
951
+ group: string,
952
+ repo: string,
953
+ baseUrl: string | undefined,
954
+ cancellation: AbortSignal | undefined,
955
+ ): Promise<string | undefined> {
956
+ const { accessToken } = token;
957
+ const key = `${accessToken}|${group}/${repo}`;
958
+
959
+ let projectId = this._projectIds.get(key);
960
+ if (projectId == null) {
961
+ projectId = this.getProjectIdCore(provider, token, group, repo, baseUrl, cancellation);
962
+ this._projectIds.set(key, projectId);
963
+ }
964
+
965
+ return projectId;
966
+ }
967
+
968
+ private async getProjectIdCore(
969
+ provider: Provider,
970
+ token: TokenWithInfo,
971
+ group: string,
972
+ repo: string,
973
+ baseUrl: string | undefined,
974
+ cancellation: AbortSignal | undefined,
975
+ ): Promise<string | undefined> {
976
+ const scope = getScopedLogger();
977
+
978
+ interface QueryResult {
979
+ data: { project: { id: string } };
980
+ }
981
+
982
+ try {
983
+ const query = `query getProjectId(
984
+ $fullPath: ID!
985
+ ) {
986
+ project(fullPath: $fullPath) {
987
+ id
988
+ }
989
+ }`;
990
+ const rsp = await this.graphql<QueryResult>(
991
+ provider,
992
+ token,
993
+ baseUrl,
994
+ query,
995
+ {
996
+ fullPath: `${group}/${repo}`,
997
+ },
998
+ cancellation,
999
+ scope,
1000
+ );
1001
+
1002
+ const gid = rsp?.data?.project?.id;
1003
+ if (gid == null) return undefined;
1004
+
1005
+ const match = /gid:\/\/gitlab\/Project\/([0-9]+)\b/.exec(gid);
1006
+ if (match == null) return undefined;
1007
+
1008
+ const projectId = match[1];
1009
+
1010
+ scope?.addExitInfo(`projectId=${projectId}`);
1011
+ return projectId;
1012
+ } catch (ex) {
1013
+ if (ex instanceof RequestNotFoundError) return undefined;
1014
+
1015
+ this.handleException(ex, provider, scope);
1016
+ return undefined;
1017
+ }
1018
+ }
1019
+
1020
+ private async graphql<T extends object>(
1021
+ provider: Provider,
1022
+ token: TokenWithInfo,
1023
+ baseUrl: string | undefined,
1024
+ query: string,
1025
+ variables: Record<string, any>,
1026
+ cancellation: AbortSignal | undefined,
1027
+ scope: ScopedLogger | undefined,
1028
+ ): Promise<T | undefined> {
1029
+ const { accessToken } = token;
1030
+ let rsp: Response;
1031
+ try {
1032
+ const sw = maybeStopWatch(`[GITLAB] POST ${baseUrl}`, { log: { onlyExit: true } });
1033
+
1034
+ try {
1035
+ if (cancellation?.aborted) throw new CancellationError();
1036
+
1037
+ rsp = await this.ctx.http.wrapForForcedInsecureSSL(provider.getIgnoreSSLErrors(), () =>
1038
+ this.ctx.http.fetch(`${baseUrl ?? 'https://gitlab.com/api'}/graphql`, {
1039
+ method: 'POST',
1040
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
1041
+ signal: cancellation,
1042
+ body: JSON.stringify({ query: query, variables: variables }),
1043
+ }),
1044
+ );
1045
+
1046
+ if (rsp.ok) {
1047
+ const data = (await rsp.json()) as T | { errors: { message: string }[] };
1048
+
1049
+ if ('errors' in data) throw new ProviderFetchError('GitLab', rsp, data.errors);
1050
+ return data;
1051
+ }
1052
+
1053
+ throw new ProviderFetchError('GitLab', rsp);
1054
+ } finally {
1055
+ const match = /(^[^({\n]+)/.exec(query);
1056
+ const message = ` ${match?.[1].trim() ?? query}`;
1057
+
1058
+ sw?.stop({ message: message });
1059
+ }
1060
+ } catch (ex) {
1061
+ if (ex instanceof ProviderFetchError || ex.name === 'AbortError') {
1062
+ this.handleRequestError(provider, token, ex, scope);
1063
+ } else if (Logger.isDebugging) {
1064
+ this.ctx.hooks?.ui?.onError?.(`GitLab request failed: ${ex.message}`);
1065
+ }
1066
+
1067
+ throw ex;
1068
+ }
1069
+ }
1070
+
1071
+ private async request<T>(
1072
+ provider: Provider,
1073
+ token: TokenWithInfo,
1074
+ baseUrl: string | undefined,
1075
+ route: string,
1076
+ options: { method: RequestInit['method'] } & Record<string, unknown>,
1077
+ cancellation: AbortSignal | undefined,
1078
+ scope: ScopedLogger | undefined,
1079
+ ): Promise<T> {
1080
+ const { accessToken } = token;
1081
+ const url = `${baseUrl ?? 'https://gitlab.com/api'}/${route}`;
1082
+
1083
+ let rsp: Response;
1084
+ try {
1085
+ const sw = maybeStopWatch(`[GITLAB] ${options?.method ?? 'GET'} ${url}`, { log: { onlyExit: true } });
1086
+
1087
+ try {
1088
+ if (cancellation?.aborted) throw new CancellationError();
1089
+
1090
+ rsp = await this.ctx.http.wrapForForcedInsecureSSL(provider.getIgnoreSSLErrors(), () =>
1091
+ this.ctx.http.fetch(url, {
1092
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
1093
+ signal: cancellation,
1094
+ ...options,
1095
+ }),
1096
+ );
1097
+
1098
+ if (rsp.ok) {
1099
+ return (await rsp.json()) as T;
1100
+ }
1101
+
1102
+ throw new ProviderFetchError('GitLab', rsp);
1103
+ } finally {
1104
+ sw?.stop();
1105
+ }
1106
+ } catch (ex) {
1107
+ if (ex instanceof ProviderFetchError || ex.name === 'AbortError') {
1108
+ this.handleRequestError(provider, token, ex, scope);
1109
+ } else if (Logger.isDebugging) {
1110
+ this.ctx.hooks?.ui?.onError?.(`GitLab request failed: ${ex.message}`);
1111
+ }
1112
+
1113
+ throw ex;
1114
+ }
1115
+ }
1116
+
1117
+ private handleRequestError(
1118
+ provider: Provider | undefined,
1119
+ token: TokenWithInfo,
1120
+ ex: ProviderFetchError | (Error & { name: 'AbortError' }),
1121
+ scope: ScopedLogger | undefined,
1122
+ ): void {
1123
+ if (ex.name === 'AbortError' || !(ex instanceof ProviderFetchError)) throw new CancellationError(ex);
1124
+
1125
+ const { accessToken, ...tokenInfo } = token;
1126
+ switch (ex.status) {
1127
+ case 404: // Not found
1128
+ case 410: // Gone
1129
+ case 422: // Unprocessable Entity
1130
+ throw new RequestNotFoundError(ex);
1131
+ // case 429: //Too Many Requests
1132
+ case 401: // Unauthorized
1133
+ throw new AuthenticationError(tokenInfo, AuthenticationErrorReason.Unauthorized, ex);
1134
+ case 403: // Forbidden
1135
+ if (ex.message.includes('rate limit exceeded')) {
1136
+ let resetAt: number | undefined;
1137
+
1138
+ const reset = ex.response?.headers?.get('x-ratelimit-reset');
1139
+ if (reset != null) {
1140
+ resetAt = parseInt(reset, 10);
1141
+ if (Number.isNaN(resetAt)) {
1142
+ resetAt = undefined;
1143
+ }
1144
+ }
1145
+
1146
+ throw new RequestRateLimitError(ex, accessToken, resetAt);
1147
+ }
1148
+ throw new AuthenticationError(tokenInfo, AuthenticationErrorReason.Forbidden, ex);
1149
+ case 500: // Internal Server Error
1150
+ scope?.error(ex);
1151
+ if (ex.response != null) {
1152
+ provider?.trackRequestException();
1153
+ this.ctx.hooks?.ui?.onRequestFailed?.(
1154
+ `${provider?.name ?? 'GitLab'} failed to respond and might be experiencing issues.${
1155
+ provider == null || provider.id === 'gitlab'
1156
+ ? ' Please visit the [GitLab status page](https://status.gitlab.com) for more information.'
1157
+ : ''
1158
+ }`,
1159
+ );
1160
+ }
1161
+ return;
1162
+ case 502: // Bad Gateway
1163
+ scope?.error(ex);
1164
+ // GitHub seems to return this status code for timeouts
1165
+ if (ex.message.includes('timeout')) {
1166
+ provider?.trackRequestException();
1167
+ this.ctx.hooks?.ui?.onRequestTimedOut?.(provider?.name ?? 'GitLab');
1168
+ return;
1169
+ }
1170
+ break;
1171
+ default:
1172
+ if (ex.status >= 400 && ex.status < 500) throw new RequestClientError(ex);
1173
+ break;
1174
+ }
1175
+
1176
+ scope?.error(ex);
1177
+ if (Logger.isDebugging) {
1178
+ this.ctx.hooks?.ui?.onError?.(
1179
+ `GitLab request failed: ${(ex.response as any)?.errors?.[0]?.message ?? ex.message}`,
1180
+ );
1181
+ }
1182
+ }
1183
+
1184
+ private handleException(ex: Error, provider: Provider, scope: ScopedLogger | undefined): Error {
1185
+ scope?.error(ex);
1186
+ // debugger;
1187
+
1188
+ if (ex instanceof AuthenticationError) {
1189
+ void this.showAuthenticationErrorMessage(ex, provider);
1190
+ }
1191
+ return ex;
1192
+ }
1193
+
1194
+ private async showAuthenticationErrorMessage(ex: AuthenticationError, provider: Provider) {
1195
+ if (ex.reason === AuthenticationErrorReason.Unauthorized || ex.reason === AuthenticationErrorReason.Forbidden) {
1196
+ const reauthenticate = await this.ctx.hooks?.onReauthenticationRequired?.(
1197
+ `${ex.message}. Would you like to try reauthenticating${
1198
+ ex.reason === AuthenticationErrorReason.Forbidden ? ' to provide additional access' : ''
1199
+ }?`,
1200
+ );
1201
+
1202
+ if (reauthenticate) {
1203
+ await provider.reauthenticate();
1204
+ this.resetCaches();
1205
+ }
1206
+ } else {
1207
+ this.ctx.hooks?.ui?.onError?.(ex.message);
1208
+ }
1209
+ }
1210
+ }