Skip to content

Commit 8570f4c

Browse files
Adds Start Work & associated issue support for Azure DevOps
1 parent bcd75f9 commit 8570f4c

File tree

10 files changed

+311
-69
lines changed

10 files changed

+311
-69
lines changed

src/plus/integrations/integrationService.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -724,12 +724,15 @@ export class IntegrationService implements Disposable {
724724
id => id in HostingIntegrationId || id in SelfHostedIntegrationId,
725725
) as SupportedHostingIntegrationIds[];
726726
const openRemotesByIntegrationId = new Map<IntegrationId, ResourceDescriptor[]>();
727+
let hasOpenAzureRepository = false;
727728
for (const repository of this.container.git.openRepositories) {
728729
const remotes = await repository.git.remotes().getRemotes();
729-
if (remotes.length === 0) continue;
730730
for (const remote of remotes) {
731731
const remoteIntegration = await remote.getIntegration();
732732
if (remoteIntegration == null) continue;
733+
if (remoteIntegration.id === HostingIntegrationId.AzureDevOps) {
734+
hasOpenAzureRepository = true;
735+
}
733736
for (const integrationId of hostingIntegrationIds?.length
734737
? hostingIntegrationIds
735738
: [...Object.values(HostingIntegrationId), ...Object.values(SelfHostedIntegrationId)]) {
@@ -760,20 +763,19 @@ export class IntegrationService implements Disposable {
760763
...Object.values(SelfHostedIntegrationId),
761764
]) {
762765
const integration = await this.get(integrationId);
763-
if (
764-
integration == null ||
766+
const isInvalidIntegration =
765767
(options?.openRepositoriesOnly &&
768+
integrationId !== HostingIntegrationId.AzureDevOps &&
766769
(isHostingIntegrationId(integrationId) || isSelfHostedIntegrationId(integrationId)) &&
767-
!openRemotesByIntegrationId.has(integrationId))
768-
) {
770+
!openRemotesByIntegrationId.has(integrationId)) ||
771+
(integrationId === HostingIntegrationId.AzureDevOps && !hasOpenAzureRepository);
772+
if (integration == null || isInvalidIntegration) {
769773
continue;
770774
}
771775

772776
integrations.set(
773777
integration,
774-
options?.openRepositoriesOnly &&
775-
(isHostingIntegrationId(integrationId) || isSelfHostedIntegrationId(integrationId)) &&
776-
openRemotesByIntegrationId.has(integrationId)
778+
options?.openRepositoriesOnly && !isInvalidIntegration
777779
? openRemotesByIntegrationId.get(integrationId)
778780
: undefined,
779781
);

src/plus/integrations/providers/azure/azure.ts

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
RequestClientError,
1414
RequestNotFoundError,
1515
} from '../../../../errors';
16+
import type { Issue } from '../../../../git/models/issue';
1617
import type { IssueOrPullRequest } from '../../../../git/models/issueOrPullRequest';
1718
import type { PullRequest } from '../../../../git/models/pullRequest';
1819
import type { Provider } from '../../../../git/models/remoteProvider';
@@ -24,6 +25,7 @@ import type { LogScope } from '../../../../system/logger.scope';
2425
import { getLogScope } from '../../../../system/logger.scope';
2526
import { maybeStopWatch } from '../../../../system/stopwatch';
2627
import type {
28+
AzureProjectDescriptor,
2729
AzurePullRequest,
2830
AzurePullRequestWithLinks,
2931
AzureWorkItemState,
@@ -34,6 +36,7 @@ import {
3436
azurePullRequestStatusToState,
3537
azureWorkItemsStateCategoryToState,
3638
fromAzurePullRequest,
39+
fromAzureWorkItem,
3740
getAzurePullRequestWebUrl,
3841
isClosedAzurePullRequestStatus,
3942
isClosedAzureWorkItemStateCategory,
@@ -145,7 +148,7 @@ export class AzureDevOpsApi implements Disposable {
145148
provider,
146149
token,
147150
owner,
148-
repo,
151+
projectName,
149152
options,
150153
);
151154

@@ -203,23 +206,71 @@ export class AzureDevOpsApi implements Disposable {
203206
}
204207
}
205208

206-
public async getWorkItemStateCategory(
209+
@debug<AzureDevOpsApi['getIssue']>({ args: { 0: p => p.name, 1: '<token>' } })
210+
public async getIssue(
211+
provider: Provider,
212+
token: string,
213+
project: AzureProjectDescriptor,
214+
id: string,
215+
options: {
216+
baseUrl: string;
217+
},
218+
): Promise<Issue | undefined> {
219+
const scope = getLogScope();
220+
221+
try {
222+
// Try to get the Work item (wit) first with specific fields
223+
const issueResult = await this.request<WorkItem>(
224+
provider,
225+
token,
226+
options?.baseUrl,
227+
`${project.resourceName}/${project.name}/_apis/wit/workItems/${id}`,
228+
{
229+
method: 'GET',
230+
},
231+
scope,
232+
);
233+
234+
if (issueResult != null) {
235+
const issueType = issueResult.fields['System.WorkItemType'];
236+
const state = issueResult.fields['System.State'];
237+
const stateCategory = await this.getWorkItemStateCategory(
238+
issueType,
239+
state,
240+
provider,
241+
token,
242+
project.resourceName,
243+
project.name,
244+
options,
245+
);
246+
return fromAzureWorkItem(issueResult, provider, project, stateCategory);
247+
}
248+
} catch (ex) {
249+
if (ex.original?.status !== 404) {
250+
Logger.error(ex, scope);
251+
return undefined;
252+
}
253+
}
254+
255+
return undefined;
256+
}
257+
258+
async getWorkItemStateCategory(
207259
issueType: string,
208260
state: string,
209261
provider: Provider,
210262
token: string,
211263
owner: string,
212-
repo: string,
264+
projectName: string,
213265
options: {
214266
baseUrl: string;
215267
},
216268
): Promise<AzureWorkItemStateCategory | undefined> {
217-
const [projectName] = repo.split('/');
218269
const project = `${owner}/${projectName}`;
219270
const category = this._workItemStates.getStateCategory(project, issueType, state);
220271
if (category != null) return category;
221272

222-
const states = await this.retrieveWorkItemTypeStates(issueType, provider, token, owner, repo, options);
273+
const states = await this.retrieveWorkItemTypeStates(issueType, provider, token, owner, projectName, options);
223274
this._workItemStates.saveTypeStates(project, issueType, states);
224275

225276
return this._workItemStates.getStateCategory(project, issueType, state);
@@ -230,13 +281,12 @@ export class AzureDevOpsApi implements Disposable {
230281
provider: Provider,
231282
token: string,
232283
owner: string,
233-
repo: string,
284+
projectName: string,
234285
options: {
235286
baseUrl: string;
236287
},
237288
): Promise<AzureWorkItemState[]> {
238289
const scope = getLogScope();
239-
const [projectName] = repo.split('/');
240290

241291
try {
242292
const issueResult = await this.request<{ value: AzureWorkItemState[]; count: number }>(

src/plus/integrations/providers/azure/models.ts

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { RepositoryAccessLevel } from '../../../../git/models/issue';
1+
import type { IssueMember } from '../../../../git/models/issue';
2+
import { Issue, RepositoryAccessLevel } from '../../../../git/models/issue';
23
import type { IssueOrPullRequestState } from '../../../../git/models/issueOrPullRequest';
34
import type { PullRequestMember, PullRequestReviewer } from '../../../../git/models/pullRequest';
45
import {
@@ -8,9 +9,43 @@ import {
89
PullRequestReviewState,
910
} from '../../../../git/models/pullRequest';
1011
import type { Provider } from '../../../../git/models/remoteProvider';
12+
import type { ResourceDescriptor } from '../../integration';
1113

1214
const vstsHostnameRegex = /\.visualstudio\.com$/;
1315

16+
export interface AzureRepositoryDescriptor extends ResourceDescriptor {
17+
owner: string;
18+
name: string;
19+
}
20+
21+
export interface AzureOrganizationDescriptor extends ResourceDescriptor {
22+
id: string;
23+
name: string;
24+
}
25+
26+
export interface AzureProjectDescriptor extends ResourceDescriptor {
27+
id: string;
28+
name: string;
29+
resourceId: string;
30+
resourceName: string;
31+
}
32+
33+
export interface AzureRemoteRepositoryDescriptor extends ResourceDescriptor {
34+
id: string;
35+
nodeId?: string;
36+
resourceName: string;
37+
name: string;
38+
projectName?: string;
39+
url?: string;
40+
cloneUrlHttps?: string;
41+
cloneUrlSsh?: string;
42+
}
43+
44+
export interface AzureProjectInputDescriptor extends ResourceDescriptor {
45+
owner: string;
46+
name: string;
47+
}
48+
1449
export type AzureWorkItemStateCategory = 'Proposed' | 'InProgress' | 'Resolved' | 'Completed' | 'Removed';
1550

1651
export function isClosedAzureWorkItemStateCategory(category: AzureWorkItemStateCategory | undefined): boolean {
@@ -91,18 +126,21 @@ export interface WorkItem {
91126
workItemUpdates: AzureLink;
92127
};
93128
fields: {
94-
// 'System.AreaPath': string;
95-
// 'System.TeamProject': string;
129+
//'System.AreaPath': string;
130+
'System.TeamProject': string;
96131
// 'System.IterationPath': string;
97132
'System.WorkItemType': string;
98133
'System.State': string;
99134
// 'System.Reason': string;
135+
'System.AssignedTo': AzureUser;
100136
'System.CreatedDate': string;
101-
// 'System.CreatedBy': AzureUser;
137+
'System.CreatedBy': AzureUser;
102138
'System.ChangedDate': string;
103-
// 'System.ChangedBy': AzureUser;
104-
// 'System.CommentCount': number;
139+
'System.ChangedBy': AzureUser;
140+
'System.CommentCount': number;
141+
'System.Description': string;
105142
'System.Title': string;
143+
'Microsoft.VSTS.Common.ClosedDate': string;
106144
// 'Microsoft.VSTS.Common.StateChangeDate': string;
107145
// 'Microsoft.VSTS.Common.Priority': number;
108146
// 'Microsoft.VSTS.Common.Severity': string;
@@ -390,6 +428,17 @@ function normalizeAzureBranchName(branchName: string): string {
390428
return branchName.startsWith('refs/heads/') ? branchName.replace('refs/heads/', '') : branchName;
391429
}
392430

431+
function fromAzureUserToMember(user: AzureUser, type: 'issue'): IssueMember;
432+
function fromAzureUserToMember(user: AzureUser, type: 'pullRequest'): PullRequestMember;
433+
function fromAzureUserToMember(user: AzureUser, _type: 'issue' | 'pullRequest'): PullRequestMember | IssueMember {
434+
return {
435+
avatarUrl: user.imageUrl,
436+
id: user.id,
437+
name: user.displayName,
438+
url: user.url,
439+
};
440+
}
441+
393442
export function fromAzurePullRequest(
394443
pr: AzurePullRequest,
395444
provider: Provider,
@@ -399,12 +448,7 @@ export function fromAzurePullRequest(
399448
const url = new URL(pr.url);
400449
return new PullRequest(
401450
provider,
402-
{
403-
id: pr.createdBy.id,
404-
name: pr.createdBy.displayName,
405-
avatarUrl: pr.createdBy.imageUrl,
406-
url: pr.createdBy.url,
407-
},
451+
fromAzureUserToMember(pr.createdBy, 'pullRequest'),
408452
pr.pullRequestId.toString(),
409453
pr.pullRequestId.toString(),
410454
pr.title,
@@ -460,3 +504,33 @@ export function fromAzurePullRequest(
460504
},
461505
);
462506
}
507+
508+
export function fromAzureWorkItem(
509+
workItem: WorkItem,
510+
provider: Provider,
511+
project: AzureProjectDescriptor,
512+
stateCategory?: AzureWorkItemStateCategory,
513+
): Issue {
514+
return new Issue(
515+
provider,
516+
workItem.id.toString(),
517+
workItem.id.toString(),
518+
workItem.fields['System.Title'],
519+
workItem._links.html.href,
520+
new Date(workItem.fields['System.CreatedDate']),
521+
new Date(workItem.fields['System.ChangedDate']),
522+
isClosedAzureWorkItemStateCategory(stateCategory),
523+
azureWorkItemsStateCategoryToState(stateCategory),
524+
fromAzureUserToMember(workItem.fields['System.CreatedBy'], 'issue'),
525+
[fromAzureUserToMember(workItem.fields['System.AssignedTo'], 'issue')],
526+
undefined,
527+
workItem.fields['Microsoft.VSTS.Common.ClosedDate']
528+
? new Date(workItem.fields['Microsoft.VSTS.Common.ClosedDate'])
529+
: undefined,
530+
undefined,
531+
workItem.fields['System.CommentCount'],
532+
undefined,
533+
workItem.fields['System.Description'],
534+
project,
535+
);
536+
}

0 commit comments

Comments
 (0)