@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,691 @@
1
+ import type { UnidentifiedAuthor } from '../../../../git/models/author.js';
2
+ import type { Issue } from '../../../../git/models/issue.js';
3
+ import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../../git/models/issueOrPullRequest.js';
4
+ import type { PullRequest } from '../../../../git/models/pullRequest.js';
5
+ import type { Provider } from '../../../../git/models/remoteProvider.js';
6
+ import { base64 } from '../../../../utils/base64.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 type { TokenInfo, TokenWithInfo } from '../../authentication/models.js';
15
+ import type { IntegrationServiceContext } from '../../context.js';
16
+ import {
17
+ AuthenticationError,
18
+ AuthenticationErrorReason,
19
+ ProviderFetchError,
20
+ RequestClientError,
21
+ RequestNotFoundError,
22
+ } from '../../errors.js';
23
+ import type {
24
+ AzureGitCommit,
25
+ AzureProjectDescriptor,
26
+ AzurePullRequest,
27
+ AzurePullRequestWithLinks,
28
+ AzureWorkItemState,
29
+ AzureWorkItemStateCategory,
30
+ WorkItem,
31
+ } from './models.js';
32
+ import {
33
+ azurePullRequestStatusToState,
34
+ azureWorkItemsStateCategoryToState,
35
+ fromAzurePullRequest,
36
+ fromAzureWorkItem,
37
+ getAzurePullRequestWebUrl,
38
+ isClosedAzurePullRequestStatus,
39
+ isClosedAzureWorkItemStateCategory,
40
+ } from './models.js';
41
+
42
+ class WorkItemStates {
43
+ private readonly _categories = new Map<string, AzureWorkItemStateCategory>();
44
+ private readonly _types = new Map<string, AzureWorkItemState[]>();
45
+
46
+ // TODO@sergeibbb: we might need some logic for invalidating
47
+ public getStateCategory(
48
+ project: string,
49
+ workItemType: string,
50
+ stateName: string,
51
+ ): AzureWorkItemStateCategory | undefined {
52
+ return this._categories.get(this.getStateKey(project, workItemType, stateName));
53
+ }
54
+
55
+ public clear(): void {
56
+ this._categories.clear();
57
+ this._types.clear();
58
+ }
59
+
60
+ public saveTypeStates(project: string, workItemType: string, states: AzureWorkItemState[]): void {
61
+ this.clearTypeStates(project, workItemType);
62
+ this._types.set(this.getTypeKey(project, workItemType), states);
63
+ for (const state of states) {
64
+ this._categories.set(this.getStateKey(project, workItemType, state.name), state.category);
65
+ }
66
+ }
67
+
68
+ public hasTypeStates(project: string, workItemType: string): boolean {
69
+ return this._types.has(this.getTypeKey(project, workItemType));
70
+ }
71
+
72
+ private clearTypeStates(project: string, workItemType: string): void {
73
+ const states = this._types.get(this.getTypeKey(project, workItemType));
74
+ if (states == null) return;
75
+
76
+ for (const state of states) {
77
+ this._categories.delete(this.getStateKey(project, workItemType, state.name));
78
+ }
79
+ }
80
+
81
+ private getStateKey(project: string, workItemType: string, stateName: string): string {
82
+ // By stringifying the pair as JSON we make sure that all possible special characters are escaped
83
+ return JSON.stringify([project, workItemType, stateName]);
84
+ }
85
+
86
+ private getTypeKey(project: string, workItemType: string): string {
87
+ return JSON.stringify([project, workItemType]);
88
+ }
89
+ }
90
+
91
+ export class AzureDevOpsApi implements Disposable {
92
+ private readonly _disposable: Disposable;
93
+ private _workItemStates: WorkItemStates = new WorkItemStates();
94
+
95
+ constructor(private readonly ctx: IntegrationServiceContext) {
96
+ this._disposable = ctx.config.onDidChange(e => {
97
+ if (e.httpProxy) {
98
+ this.resetCaches();
99
+ }
100
+ });
101
+ }
102
+
103
+ dispose(): void {
104
+ this._disposable.dispose();
105
+ }
106
+
107
+ private resetCaches(): void {
108
+ this._workItemStates.clear();
109
+ }
110
+
111
+ @trace({
112
+ args: (provider, token, owner, repo, branch) => ({
113
+ provider: provider.name,
114
+ token: `<token:${token.microHash}>`,
115
+ owner: owner,
116
+ repo: repo,
117
+ branch: branch,
118
+ }),
119
+ })
120
+ public async getPullRequestForBranch(
121
+ provider: Provider,
122
+ token: TokenWithInfo,
123
+ owner: string,
124
+ repo: string,
125
+ branch: string,
126
+ options: {
127
+ baseUrl: string;
128
+ },
129
+ ): Promise<PullRequest | undefined> {
130
+ const scope = getScopedLogger();
131
+ const [projectName, _, repoName] = repo.split('/');
132
+
133
+ try {
134
+ const prResult = await this.request<{ value: AzurePullRequest[] }>(
135
+ provider,
136
+ token,
137
+ options?.baseUrl,
138
+ `${owner}/${projectName}/_apis/git/repositories/${repoName}/pullRequests?searchCriteria.status=all&searchCriteria.sourceRefName=refs/heads/${branch}`,
139
+ {
140
+ method: 'GET',
141
+ },
142
+ scope,
143
+ );
144
+
145
+ // Sort PRs: open PRs first, then by most recent activity (creation or closure date)
146
+ const sortedPRs = prResult?.value.sort((a, b) => {
147
+ // First, prioritize open PRs (active/notSet) over closed ones (abandoned/completed)
148
+ const aIsOpen = a.status === 'active' || a.status === 'notSet';
149
+ const bIsOpen = b.status === 'active' || b.status === 'notSet';
150
+
151
+ if (aIsOpen !== bIsOpen) {
152
+ return aIsOpen ? -1 : 1; // Open PRs come first
153
+ }
154
+
155
+ // Among PRs with the same status, sort by most recent activity
156
+ // Use closedDate if available, otherwise use creationDate
157
+ const aDate = new Date(a.closedDate || a.creationDate);
158
+ const bDate = new Date(b.closedDate || b.creationDate);
159
+
160
+ return bDate.getTime() - aDate.getTime(); // Most recent first
161
+ });
162
+
163
+ const pr = sortedPRs?.[0];
164
+ if (pr == null) return undefined;
165
+
166
+ return fromAzurePullRequest(pr, provider, owner);
167
+ } catch (ex) {
168
+ scope?.error(ex);
169
+ return undefined;
170
+ }
171
+ }
172
+
173
+ @trace({
174
+ args: (provider, token, owner, repo, rev, baseUrl) => ({
175
+ provider: provider.name,
176
+ token: `<token:${token.microHash}>`,
177
+ owner: owner,
178
+ repo: repo,
179
+ rev: rev,
180
+ baseUrl: baseUrl,
181
+ }),
182
+ })
183
+ async getPullRequestForCommit(
184
+ provider: Provider,
185
+ token: TokenWithInfo,
186
+ owner: string,
187
+ repo: string,
188
+ rev: string,
189
+ baseUrl: string,
190
+ _options?: {
191
+ avatarSize?: number;
192
+ },
193
+ cancellation?: AbortSignal,
194
+ ): Promise<PullRequest | undefined> {
195
+ const scope = getScopedLogger();
196
+ const [projectName, _, repoName] = repo.split('/');
197
+ try {
198
+ const prResult = await this.request<{ results: Record<string, AzurePullRequest[]>[] }>(
199
+ provider,
200
+ token,
201
+ baseUrl,
202
+ `${owner}/${projectName}/_apis/git/repositories/${repoName}/pullrequestquery?api-version=4.1`,
203
+ {
204
+ method: 'POST',
205
+ body: JSON.stringify({
206
+ queries: [
207
+ {
208
+ items: [rev],
209
+ type: 'commit',
210
+ },
211
+ ],
212
+ }),
213
+ },
214
+ scope,
215
+ cancellation,
216
+ );
217
+
218
+ const pr = prResult?.results[0]?.[rev]?.[0];
219
+ if (pr == null) return undefined;
220
+
221
+ const pullRequest = await this.request<AzurePullRequestWithLinks>(
222
+ provider,
223
+ token,
224
+ undefined,
225
+ pr.url,
226
+ { method: 'GET' },
227
+ scope,
228
+ cancellation,
229
+ );
230
+ if (pullRequest == null) return undefined;
231
+
232
+ return fromAzurePullRequest(pullRequest, provider, owner);
233
+ } catch (ex) {
234
+ scope?.error(ex);
235
+ return undefined;
236
+ }
237
+ }
238
+
239
+ @trace({
240
+ args: (provider, token, owner, repo, id) => ({
241
+ provider: provider.name,
242
+ token: `<token:${token.microHash}>`,
243
+ owner: owner,
244
+ repo: repo,
245
+ id: id,
246
+ }),
247
+ })
248
+ public async getIssueOrPullRequest(
249
+ provider: Provider,
250
+ token: TokenWithInfo,
251
+ owner: string,
252
+ repo: string,
253
+ id: string,
254
+ options: {
255
+ baseUrl: string;
256
+ type?: IssueOrPullRequestType;
257
+ },
258
+ ): Promise<IssueOrPullRequest | undefined> {
259
+ const scope = getScopedLogger();
260
+ const [projectName, _, repoName] = repo.split('/');
261
+
262
+ if (options?.type === undefined || options?.type === 'issue') {
263
+ try {
264
+ // Try to get the Work item (wit) first with specific fields
265
+ const issueResult = await this.request<WorkItem>(
266
+ provider,
267
+ token,
268
+ options?.baseUrl,
269
+ `${owner}/${projectName}/_apis/wit/workItems/${id}`,
270
+ {
271
+ method: 'GET',
272
+ },
273
+ scope,
274
+ );
275
+
276
+ if (issueResult != null) {
277
+ const issueType = issueResult.fields['System.WorkItemType'];
278
+ const state = issueResult.fields['System.State'];
279
+ const stateCategory = await this.getWorkItemStateCategory(
280
+ issueType,
281
+ state,
282
+ provider,
283
+ token,
284
+ owner,
285
+ projectName,
286
+ options,
287
+ );
288
+
289
+ return {
290
+ id: issueResult.id.toString(),
291
+ type: 'issue',
292
+ nodeId: issueResult.id.toString(),
293
+ provider: provider,
294
+ createdDate: new Date(issueResult.fields['System.CreatedDate']),
295
+ updatedDate: new Date(issueResult.fields['System.ChangedDate']),
296
+ state: azureWorkItemsStateCategoryToState(stateCategory),
297
+ closed: isClosedAzureWorkItemStateCategory(stateCategory),
298
+ title: issueResult.fields['System.Title'],
299
+ url: issueResult._links.html.href,
300
+ };
301
+ }
302
+ } catch (ex) {
303
+ if (ex.original?.status !== 404) {
304
+ scope?.error(ex);
305
+ return undefined;
306
+ }
307
+ }
308
+ }
309
+
310
+ if (options?.type === undefined || options?.type === 'pullrequest') {
311
+ try {
312
+ const prResult = await this.request<AzurePullRequestWithLinks>(
313
+ provider,
314
+ token,
315
+ options?.baseUrl,
316
+ `${owner}/${projectName}/_apis/git/repositories/${repoName}/pullRequests/${id}`,
317
+ {
318
+ method: 'GET',
319
+ },
320
+ scope,
321
+ );
322
+
323
+ if (prResult != null) {
324
+ return {
325
+ id: prResult.pullRequestId.toString(),
326
+ type: 'pullrequest',
327
+ nodeId: prResult.pullRequestId.toString(), // prResult.artifactId maybe?
328
+ provider: provider,
329
+ createdDate: new Date(prResult.creationDate),
330
+ updatedDate: new Date(prResult.creationDate),
331
+ state: azurePullRequestStatusToState(prResult.status),
332
+ closed: isClosedAzurePullRequestStatus(prResult.status),
333
+ title: prResult.title,
334
+ url: getAzurePullRequestWebUrl(prResult),
335
+ };
336
+ }
337
+
338
+ return undefined;
339
+ } catch (ex) {
340
+ if (ex.original?.status !== 404) {
341
+ scope?.error(ex);
342
+ return undefined;
343
+ }
344
+ }
345
+ }
346
+ return undefined;
347
+ }
348
+
349
+ @trace({
350
+ args: (provider, token, project, id) => ({
351
+ provider: provider.name,
352
+ token: `<token:${token.microHash}>`,
353
+ project: project,
354
+ id: id,
355
+ }),
356
+ })
357
+ public async getIssue(
358
+ provider: Provider,
359
+ token: TokenWithInfo,
360
+ project: AzureProjectDescriptor,
361
+ id: string,
362
+ options: {
363
+ baseUrl: string;
364
+ },
365
+ ): Promise<Issue | undefined> {
366
+ const scope = getScopedLogger();
367
+
368
+ try {
369
+ // Try to get the Work item (wit) first with specific fields
370
+ const issueResult = await this.request<WorkItem>(
371
+ provider,
372
+ token,
373
+ options?.baseUrl,
374
+ `${project.resourceName}/${project.name}/_apis/wit/workItems/${id}`,
375
+ {
376
+ method: 'GET',
377
+ },
378
+ scope,
379
+ );
380
+
381
+ if (issueResult != null) {
382
+ const issueType = issueResult.fields['System.WorkItemType'];
383
+ const state = issueResult.fields['System.State'];
384
+ const stateCategory = await this.getWorkItemStateCategory(
385
+ issueType,
386
+ state,
387
+ provider,
388
+ token,
389
+ project.resourceName,
390
+ project.name,
391
+ options,
392
+ );
393
+ return fromAzureWorkItem(issueResult, provider, project, stateCategory);
394
+ }
395
+ } catch (ex) {
396
+ if (ex.original?.status !== 404) {
397
+ scope?.error(ex);
398
+ return undefined;
399
+ }
400
+ }
401
+
402
+ return undefined;
403
+ }
404
+
405
+ @trace({
406
+ args: (provider, token, owner, repo, rev, baseUrl) => ({
407
+ provider: provider.name,
408
+ token: `<token:${token.microHash}>`,
409
+ owner: owner,
410
+ repo: repo,
411
+ rev: rev,
412
+ baseUrl: baseUrl,
413
+ }),
414
+ })
415
+ async getAccountForCommit(
416
+ provider: Provider,
417
+ token: TokenWithInfo,
418
+ owner: string,
419
+ repo: string,
420
+ rev: string,
421
+ baseUrl: string,
422
+ _options?: {
423
+ avatarSize?: number;
424
+ },
425
+ ): Promise<UnidentifiedAuthor | undefined> {
426
+ const scope = getScopedLogger();
427
+ const [projectName, _, repoName] = repo.split('/');
428
+
429
+ try {
430
+ // Try to get the Work item (wit) first with specific fields
431
+ const commit = await this.request<AzureGitCommit>(
432
+ provider,
433
+ token,
434
+ baseUrl,
435
+ `${owner}/${projectName}/_apis/git/repositories/${repoName}/commits/${rev}`,
436
+ {
437
+ method: 'GET',
438
+ },
439
+ scope,
440
+ );
441
+ const author = commit?.author;
442
+ if (!author) {
443
+ return undefined;
444
+ }
445
+ // Azure API never gives us an id/username we can use, therefore we always return UnidentifiedAuthor
446
+ return {
447
+ provider: provider,
448
+ id: undefined,
449
+ username: undefined,
450
+ name: author?.name,
451
+ email: author?.email,
452
+ avatarUrl: undefined,
453
+ } satisfies UnidentifiedAuthor;
454
+ } catch (ex) {
455
+ if (ex.original?.status !== 404) {
456
+ scope?.error(ex);
457
+ return undefined;
458
+ }
459
+ }
460
+
461
+ return undefined;
462
+ }
463
+
464
+ @trace({
465
+ args: (provider, token, baseUrl) => ({
466
+ provider: provider.name,
467
+ token: `<token:${token.microHash}>`,
468
+ baseUrl: baseUrl,
469
+ }),
470
+ })
471
+ async getCurrentUserOnServer(
472
+ provider: Provider,
473
+ token: TokenWithInfo,
474
+ baseUrl: string,
475
+ ): Promise<{ id: string; name?: string; email?: string; username?: string; avatarUrl?: string } | undefined> {
476
+ const scope = getScopedLogger();
477
+
478
+ try {
479
+ const connectionData = await this.request<{
480
+ authenticatedUser?: {
481
+ id: string;
482
+ descriptor: string;
483
+ isActive: boolean;
484
+ metTypeId: number;
485
+ providerDisplayName?: string;
486
+ emailAddress?: string;
487
+ resourceVersion: 2;
488
+ subjectDescriptor: string;
489
+ properties?: {
490
+ Account?: {
491
+ $type: string;
492
+ $value: string;
493
+ };
494
+ };
495
+ };
496
+ }>(
497
+ provider,
498
+ token,
499
+ baseUrl,
500
+ '_apis/connectionData',
501
+ {
502
+ method: 'GET',
503
+ },
504
+ scope,
505
+ );
506
+
507
+ const user = connectionData?.authenticatedUser;
508
+ const username = user?.properties?.Account?.$value;
509
+ if (!username) {
510
+ return undefined;
511
+ }
512
+
513
+ return {
514
+ id: user.id,
515
+ name: user.providerDisplayName,
516
+ email: user.emailAddress,
517
+ username: username,
518
+ };
519
+ } catch (ex) {
520
+ scope?.error(ex, `Failed to get current user from ${baseUrl}`);
521
+ return undefined;
522
+ }
523
+ }
524
+
525
+ async getWorkItemStateCategory(
526
+ issueType: string,
527
+ state: string,
528
+ provider: Provider,
529
+ token: TokenWithInfo,
530
+ owner: string,
531
+ projectName: string,
532
+ options: {
533
+ baseUrl: string;
534
+ },
535
+ ): Promise<AzureWorkItemStateCategory | undefined> {
536
+ const project = `${owner}/${projectName}`;
537
+ const category = this._workItemStates.getStateCategory(project, issueType, state);
538
+ if (category != null) return category;
539
+
540
+ const states = await this.retrieveWorkItemTypeStates(issueType, provider, token, owner, projectName, options);
541
+ this._workItemStates.saveTypeStates(project, issueType, states);
542
+
543
+ return this._workItemStates.getStateCategory(project, issueType, state);
544
+ }
545
+
546
+ private async retrieveWorkItemTypeStates(
547
+ workItemType: string,
548
+ provider: Provider,
549
+ token: TokenWithInfo,
550
+ owner: string,
551
+ projectName: string,
552
+ options: {
553
+ baseUrl: string;
554
+ },
555
+ ): Promise<AzureWorkItemState[]> {
556
+ const scope = getScopedLogger();
557
+
558
+ try {
559
+ const issueResult = await this.request<{ value: AzureWorkItemState[]; count: number }>(
560
+ provider,
561
+ token,
562
+ options?.baseUrl,
563
+ `${owner}/${projectName}/_apis/wit/workItemTypes/${workItemType}/states`,
564
+ {
565
+ method: 'GET',
566
+ },
567
+ scope,
568
+ );
569
+
570
+ return issueResult?.value ?? [];
571
+ } catch (ex) {
572
+ scope?.error(ex);
573
+ return [];
574
+ }
575
+ }
576
+
577
+ private async request<T>(
578
+ provider: Provider,
579
+ token: TokenWithInfo,
580
+ baseUrl: string | undefined,
581
+ route: string,
582
+ options: { method: RequestInit['method'] } & Record<string, unknown>,
583
+ scope: ScopedLogger | undefined,
584
+ cancellation?: AbortSignal | undefined,
585
+ ): Promise<T | undefined> {
586
+ const { accessToken, ...tokenInfo } = token;
587
+ const url = baseUrl ? `${baseUrl}/${route}` : route;
588
+
589
+ let rsp: Response;
590
+ try {
591
+ const sw = maybeStopWatch(`[AZURE] ${options?.method ?? 'GET'} ${url}`, { log: { onlyExit: true } });
592
+
593
+ try {
594
+ if (cancellation?.aborted) throw new CancellationError();
595
+
596
+ rsp = await this.ctx.http.wrapForForcedInsecureSSL(provider.getIgnoreSSLErrors(), () =>
597
+ this.ctx.http.fetch(url, {
598
+ headers: {
599
+ Authorization: `Basic ${base64(`PAT:${accessToken}`)}`,
600
+ 'Content-Type': 'application/json',
601
+ },
602
+ signal: cancellation,
603
+ ...options,
604
+ }),
605
+ );
606
+
607
+ if (rsp.ok) {
608
+ return (await rsp.json()) as T;
609
+ }
610
+
611
+ throw new ProviderFetchError('AzureDevOps', rsp);
612
+ } finally {
613
+ sw?.stop();
614
+ }
615
+ } catch (ex) {
616
+ if (ex instanceof ProviderFetchError || ex.name === 'AbortError') {
617
+ this.handleRequestError(provider, tokenInfo, ex, scope);
618
+ } else if (Logger.isDebugging) {
619
+ this.ctx.hooks?.ui?.onError?.(`AzureDevOps request failed: ${ex.message}`);
620
+ }
621
+
622
+ throw ex;
623
+ }
624
+ }
625
+
626
+ private handleRequestError(
627
+ provider: Provider | undefined,
628
+ tokenInfo: TokenInfo,
629
+ ex: ProviderFetchError | (Error & { name: 'AbortError' }),
630
+ scope: ScopedLogger | undefined,
631
+ ): void {
632
+ if (ex.name === 'AbortError' || !(ex instanceof ProviderFetchError)) throw new CancellationError(ex);
633
+
634
+ switch (ex.status) {
635
+ case 404: // Not found
636
+ case 410: // Gone
637
+ case 422: // Unprocessable Entity
638
+ throw new RequestNotFoundError(ex);
639
+ case 401: // Unauthorized
640
+ throw new AuthenticationError(tokenInfo, AuthenticationErrorReason.Unauthorized, ex);
641
+ case 403: // Forbidden
642
+ // TODO: Learn the Azure API docs and put it in order:
643
+ // if (ex.message.includes('rate limit')) {
644
+ // let resetAt: number | undefined;
645
+
646
+ // const reset = ex.response?.headers?.get('x-ratelimit-reset');
647
+ // if (reset != null) {
648
+ // resetAt = parseInt(reset, 10);
649
+ // if (Number.isNaN(resetAt)) {
650
+ // resetAt = undefined;
651
+ // }
652
+ // }
653
+
654
+ // throw new RequestRateLimitError(ex, token, resetAt);
655
+ // }
656
+ throw new AuthenticationError(tokenInfo, AuthenticationErrorReason.Forbidden, ex);
657
+ case 500: // Internal Server Error
658
+ scope?.error(ex);
659
+ if (ex.response != null) {
660
+ provider?.trackRequestException();
661
+ this.ctx.hooks?.ui?.onRequestFailed?.(
662
+ `${provider?.name ?? 'AzureDevOps'} failed to respond and might be experiencing issues.${
663
+ provider == null || provider.id === 'azure'
664
+ ? ' Please visit the [AzureDevOps status page](https://status.dev.azure.com) for more information.'
665
+ : ''
666
+ }`,
667
+ );
668
+ }
669
+ return;
670
+ case 502: // Bad Gateway
671
+ scope?.error(ex);
672
+ // TODO: Learn the Azure API docs and put it in order:
673
+ // if (ex.message.includes('timeout')) {
674
+ // provider?.trackRequestException();
675
+ // void showIntegrationRequestTimedOutWarningMessage(provider?.name ?? 'Azure');
676
+ // return;
677
+ // }
678
+ break;
679
+ default:
680
+ if (ex.status >= 400 && ex.status < 500) throw new RequestClientError(ex);
681
+ break;
682
+ }
683
+
684
+ scope?.error(ex);
685
+ if (Logger.isDebugging) {
686
+ this.ctx.hooks?.ui?.onError?.(
687
+ `AzureDevOps request failed: ${(ex.response as any)?.errors?.[0]?.message ?? ex.message}`,
688
+ );
689
+ }
690
+ }
691
+ }