Skip to content

Commit

Permalink
Retrieves and caches state-map for the current Azure project.
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeibbb committed Jan 30, 2025
1 parent 4653183 commit a97df88
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 5 deletions.
125 changes: 120 additions & 5 deletions src/plus/integrations/providers/azure/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import { Logger } from '../../../../system/logger';
import type { LogScope } from '../../../../system/logger.scope';
import { getLogScope } from '../../../../system/logger.scope';
import { maybeStopWatch } from '../../../../system/stopwatch';
import type { WorkItem } from './models';
import type { AzureWorkItemState, AzureWorkItemStateCategory, WorkItem } from './models';
import { azureWorkItemsStateCategoryToState, isClosedAzureWorkItemStateCategory } from './models';

export class AzureDevOpsApi implements Disposable {
private readonly _disposable: Disposable;
private _workItemStates: WorkItemStates = new WorkItemStates();

constructor(_container: Container) {
this._disposable = configuration.onDidChangeAny(e => {
Expand Down Expand Up @@ -54,8 +56,7 @@ export class AzureDevOpsApi implements Disposable {

private resetCaches(): void {
this._proxyAgent = null;
// this._defaults.clear();
// this._enterpriseVersions.clear();
this._workItemStates.clear();
}

@debug<AzureDevOpsApi['getIssueOrPullRequest']>({ args: { 0: p => p.name, 1: '<token>' } })
Expand Down Expand Up @@ -86,15 +87,27 @@ export class AzureDevOpsApi implements Disposable {
);

if (issueResult != null) {
const issueType = issueResult.fields['System.WorkItemType'];
const state = issueResult.fields['System.State'];
const stateCategory = await this.getWorkItemStateCategory(
issueType,
state,
provider,
token,
owner,
repo,
options,
);

return {
id: issueResult.id.toString(),
type: 'issue',
nodeId: issueResult.id.toString(),
provider: provider,
createdDate: new Date(issueResult.fields['System.CreatedDate']),
updatedDate: new Date(issueResult.fields['System.ChangedDate']),
state: issueResult.fields['System.State'] === 'Closed' ? 'closed' : 'opened',
closed: issueResult.fields['System.State'] === 'Closed',
state: azureWorkItemsStateCategoryToState(stateCategory),
closed: isClosedAzureWorkItemStateCategory(stateCategory),
title: issueResult.fields['System.Title'],
url: issueResult._links.html.href,
};
Expand All @@ -107,6 +120,60 @@ export class AzureDevOpsApi implements Disposable {
}
}

public async getWorkItemStateCategory(
issueType: string,
state: string,
provider: Provider,
token: string,
owner: string,
repo: string,
options: {
baseUrl: string;
},
): Promise<AzureWorkItemStateCategory | undefined> {
const [projectName] = repo.split('/');
const project = `${owner}/${projectName}`;
const category = this._workItemStates.getStateCategory(project, issueType, state);
if (category != null) return category;

const states = await this.retrieveWorkItemTypeStates(issueType, provider, token, owner, repo, options);
this._workItemStates.saveTypeStates(project, issueType, states);

return this._workItemStates.getStateCategory(project, issueType, state);
}

private async retrieveWorkItemTypeStates(
workItemType: string,
provider: Provider,
token: string,
owner: string,
repo: string,
options: {
baseUrl: string;
},
): Promise<AzureWorkItemState[]> {
const scope = getLogScope();
const [projectName] = repo.split('/');

try {
const issueResult = await this.request<{ value: AzureWorkItemState[]; count: number }>(
provider,
token,
options?.baseUrl,
`${owner}/${projectName}/_apis/wit/workItemTypes/${workItemType}/states`,
{
method: 'GET',
},
scope,
);

return issueResult?.value ?? [];
} catch (ex) {
Logger.error(ex, scope);
return [];
}
}

private async request<T>(
provider: Provider,
token: string,
Expand Down Expand Up @@ -227,3 +294,51 @@ export class AzureDevOpsApi implements Disposable {
}
}
}

class WorkItemStates {
private readonly _categories = new Map<string, AzureWorkItemStateCategory>();
private readonly _types = new Map<string, AzureWorkItemState[]>();

// TODO@sergeibbb: we might need some logic for invalidating
public getStateCategory(
project: string,
workItemType: string,
stateName: string,
): AzureWorkItemStateCategory | undefined {
return this._categories.get(this.getStateKey(project, workItemType, stateName));
}

public clear(): void {
this._categories.clear();
this._types.clear();
}

public saveTypeStates(project: string, workItemType: string, states: AzureWorkItemState[]): void {
this.clearTypeStates(project, workItemType);
this._types.set(this.getTypeKey(project, workItemType), states);
for (const state of states) {
this._categories.set(this.getStateKey(project, workItemType, state.name), state.category);
}
}

public hasTypeStates(project: string, workItemType: string): boolean {
return this._types.has(this.getTypeKey(project, workItemType));
}

private clearTypeStates(project: string, workItemType: string): void {
const states = this._types.get(this.getTypeKey(project, workItemType));
if (states == null) return;
for (const state of states) {
this._categories.delete(this.getStateKey(project, workItemType, state.name));
}
}

private getStateKey(project: string, workItemType: string, stateName: string): string {
// By stringifying the pair as JSON we make sure that all possible special characters are escaped
return JSON.stringify([project, workItemType, stateName]);
}

private getTypeKey(project: string, workItemType: string): string {
return JSON.stringify([project, workItemType]);
}
}
29 changes: 29 additions & 0 deletions src/plus/integrations/providers/azure/models.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
import type { IssueOrPullRequestState } from '../../../../git/models/issueOrPullRequest';

export type AzureWorkItemStateCategory = 'Proposed' | 'InProgress' | 'Resolved' | 'Completed' | 'Removed';

export function isClosedAzureWorkItemStateCategory(category: AzureWorkItemStateCategory | undefined): boolean {
return category === 'Completed' || category === 'Resolved' || category === 'Removed';
}

export function azureWorkItemsStateCategoryToState(
category: AzureWorkItemStateCategory | undefined,
): IssueOrPullRequestState {
switch (category) {
case 'Resolved':
case 'Completed':
case 'Removed':
return 'closed';
case 'Proposed':
case 'InProgress':
default:
return 'opened';
}
}

export interface AzureLink {
href: string;
}
Expand Down Expand Up @@ -46,3 +69,9 @@ export interface WorkItem {
rev: number;
url: string;
}

export interface AzureWorkItemState {
name: string;
color: string;
category: AzureWorkItemStateCategory;
}

0 comments on commit a97df88

Please sign in to comment.