Skip to content

Commit f18105e

Browse files
Refines issue model and adds getIssue to supported integrations (#3865)
1 parent 8d00555 commit f18105e

File tree

12 files changed

+315
-56
lines changed

12 files changed

+315
-56
lines changed

src/cache.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Disposable } from './api/gitlens';
33
import type { Container } from './container';
44
import type { Account } from './git/models/author';
55
import type { DefaultBranch } from './git/models/defaultBranch';
6-
import type { IssueOrPullRequest } from './git/models/issue';
6+
import type { Issue, IssueOrPullRequest } from './git/models/issue';
77
import type { PullRequest } from './git/models/pullRequest';
88
import type { RepositoryMetadata } from './git/models/repositoryMetadata';
99
import type { HostingIntegration, IntegrationBase, ResourceDescriptor } from './plus/integrations/integration';
@@ -12,6 +12,8 @@ import { isPromise } from './system/promise';
1212
type Caches = {
1313
defaultBranch: { key: `repo:${string}`; value: DefaultBranch };
1414
// enrichedAutolinksBySha: { key: `sha:${string}:${string}`; value: Map<string, EnrichedAutolink> };
15+
issuesById: { key: `id:${string}:${string}`; value: Issue };
16+
issuesByIdAndResource: { key: `id:${string}:${string}:${string}`; value: Issue };
1517
issuesOrPrsById: { key: `id:${string}:${string}`; value: IssueOrPullRequest };
1618
issuesOrPrsByIdAndRepo: { key: `id:${string}:${string}:${string}`; value: IssueOrPullRequest };
1719
prByBranch: { key: `branch:${string}:${string}`; value: PullRequest };
@@ -127,6 +129,27 @@ export class CacheProvider implements Disposable {
127129
);
128130
}
129131

132+
getIssue(
133+
id: string,
134+
resource: ResourceDescriptor,
135+
integration: IntegrationBase | undefined,
136+
cacheable: Cacheable<Issue>,
137+
options?: { expiryOverride?: boolean | number },
138+
): CacheResult<Issue> {
139+
const { key, etag } = getResourceKeyAndEtag(resource, integration);
140+
141+
if (resource == null) {
142+
return this.get('issuesById', `id:${id}:${key}`, etag, cacheable, options);
143+
}
144+
return this.get(
145+
'issuesByIdAndResource',
146+
`id:${id}:${key}:${JSON.stringify(resource)}}`,
147+
etag,
148+
cacheable,
149+
options,
150+
);
151+
}
152+
130153
getPullRequest(
131154
id: string,
132155
resource: ResourceDescriptor,
@@ -259,6 +282,18 @@ function getExpiresAt<T extends Cache>(cache: T, value: CacheValue<T> | undefine
259282
case 'repoMetadata':
260283
case 'currentAccount':
261284
return 0; // Never expires
285+
case 'issuesById':
286+
case 'issuesByIdAndResource': {
287+
if (value == null) return 0; // Never expires
288+
289+
// Open issues expire after 1 hour, but closed issues expire after 12 hours unless recently updated and then expire in 1 hour
290+
291+
const issue = value as CacheValue<'issuesById'>;
292+
if (!issue.closed) return defaultExpiresAt;
293+
294+
const updatedAgo = now - (issue.closedDate ?? issue.updatedDate).getTime();
295+
return now + (updatedAgo > 14 * 24 * 60 * 60 * 1000 ? 12 : 1) * 60 * 60 * 1000;
296+
}
262297
case 'issuesOrPrsById':
263298
case 'issuesOrPrsByIdAndRepo': {
264299
if (value == null) return 0; // Never expires

src/git/models/issue.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,19 @@ export interface IssueRepository {
5151
url?: string;
5252
}
5353

54+
export interface IssueProject {
55+
id: string;
56+
name: string;
57+
resourceId: string;
58+
}
59+
5460
export interface IssueShape extends IssueOrPullRequest {
5561
author: IssueMember;
5662
assignees: IssueMember[];
5763
repository?: IssueRepository;
5864
labels?: IssueLabel[];
5965
body?: string;
66+
project?: IssueProject;
6067
}
6168

6269
export interface SearchedIssue {
@@ -118,6 +125,14 @@ export function serializeIssue(value: IssueShape): IssueShape {
118125
repo: value.repository.repo,
119126
url: value.repository.url,
120127
},
128+
project:
129+
value.project == null
130+
? undefined
131+
: {
132+
id: value.project.id,
133+
name: value.project.name,
134+
resourceId: value.project.resourceId,
135+
},
121136
assignees: value.assignees.map(assignee => ({
122137
id: assignee.id,
123138
name: assignee.name,
@@ -152,13 +167,14 @@ export class Issue implements IssueShape {
152167
public readonly closed: boolean,
153168
public readonly state: IssueOrPullRequestState,
154169
public readonly author: IssueMember,
155-
public readonly repository: IssueRepository,
156170
public readonly assignees: IssueMember[],
171+
public readonly repository?: IssueRepository,
157172
public readonly closedDate?: Date,
158173
public readonly labels?: IssueLabel[],
159174
public readonly commentsCount?: number,
160175
public readonly thumbsUpCount?: number,
161176
public readonly body?: string,
177+
public readonly project?: IssueProject,
162178
) {}
163179
}
164180

src/plus/integrations/integration.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { AuthenticationError, CancellationError, RequestClientError } from '../.
99
import type { PagedResult } from '../../git/gitProvider';
1010
import type { Account, UnidentifiedAuthor } from '../../git/models/author';
1111
import type { DefaultBranch } from '../../git/models/defaultBranch';
12-
import type { IssueOrPullRequest, SearchedIssue } from '../../git/models/issue';
12+
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../git/models/issue';
1313
import type {
1414
PullRequest,
1515
PullRequestMergeMethod,
@@ -70,6 +70,38 @@ export type IntegrationType = 'issues' | 'hosting';
7070

7171
export type ResourceDescriptor = { key: string } & Record<string, unknown>;
7272

73+
export type IssueResourceDescriptor = ResourceDescriptor & {
74+
id: string;
75+
name: string;
76+
};
77+
78+
export type RepositoryDescriptor = ResourceDescriptor & {
79+
owner: string;
80+
name: string;
81+
};
82+
83+
export function isIssueResourceDescriptor(resource: ResourceDescriptor): resource is IssueResourceDescriptor {
84+
return (
85+
'key' in resource &&
86+
resource.key != null &&
87+
'id' in resource &&
88+
resource.id != null &&
89+
'name' in resource &&
90+
resource.name != null
91+
);
92+
}
93+
94+
export function isRepositoryDescriptor(resource: ResourceDescriptor): resource is RepositoryDescriptor {
95+
return (
96+
'key' in resource &&
97+
resource.key != null &&
98+
'owner' in resource &&
99+
resource.owner != null &&
100+
'name' in resource &&
101+
resource.name != null
102+
);
103+
}
104+
73105
export function isHostingIntegration(integration: Integration): integration is HostingIntegration {
74106
return integration.type === 'hosting';
75107
}
@@ -446,6 +478,43 @@ export abstract class IntegrationBase<
446478
id: string,
447479
): Promise<IssueOrPullRequest | undefined>;
448480

481+
@debug()
482+
async getIssue(
483+
resource: T,
484+
id: string,
485+
options?: { expiryOverride?: boolean | number },
486+
): Promise<Issue | undefined> {
487+
const scope = getLogScope();
488+
489+
const connected = this.maybeConnected ?? (await this.isConnected());
490+
if (!connected) return undefined;
491+
492+
const issue = this.container.cache.getIssue(
493+
id,
494+
resource,
495+
this,
496+
() => ({
497+
value: (async () => {
498+
try {
499+
const result = await this.getProviderIssue(this._session!, resource, id);
500+
this.resetRequestExceptionCount();
501+
return result;
502+
} catch (ex) {
503+
return this.handleProviderException<Issue | undefined>(ex, scope, undefined);
504+
}
505+
})(),
506+
}),
507+
options,
508+
);
509+
return issue;
510+
}
511+
512+
protected abstract getProviderIssue(
513+
session: ProviderAuthenticationSession,
514+
resource: T,
515+
id: string,
516+
): Promise<Issue | undefined>;
517+
449518
async getCurrentAccount(options?: {
450519
avatarSize?: number;
451520
expiryOverride?: boolean | number;

src/plus/integrations/providers/azureDevOps.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HostingIntegrationId } from '../../../constants.integrations';
33
import type { PagedResult } from '../../../git/gitProvider';
44
import type { Account } from '../../../git/models/author';
55
import type { DefaultBranch } from '../../../git/models/defaultBranch';
6-
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
6+
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
77
import type {
88
PullRequest,
99
PullRequestMergeMethod,
@@ -107,6 +107,14 @@ export class AzureDevOpsIntegration extends HostingIntegration<
107107
return Promise.resolve(undefined);
108108
}
109109

110+
protected override async getProviderIssue(
111+
_session: AuthenticationSession,
112+
_repo: AzureRepositoryDescriptor,
113+
_id: string,
114+
): Promise<Issue | undefined> {
115+
return Promise.resolve(undefined);
116+
}
117+
110118
protected override async getProviderPullRequestForBranch(
111119
_session: AuthenticationSession,
112120
_repo: AzureRepositoryDescriptor,

src/plus/integrations/providers/bitbucket.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { AuthenticationSession, CancellationToken } from 'vscode';
22
import { HostingIntegrationId } from '../../../constants.integrations';
33
import type { Account } from '../../../git/models/author';
44
import type { DefaultBranch } from '../../../git/models/defaultBranch';
5-
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
5+
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
66
import type {
77
PullRequest,
88
PullRequestMergeMethod,
@@ -86,6 +86,14 @@ export class BitbucketIntegration extends HostingIntegration<
8686
return Promise.resolve(undefined);
8787
}
8888

89+
protected override async getProviderIssue(
90+
_session: AuthenticationSession,
91+
_repo: BitbucketRepositoryDescriptor,
92+
_id: string,
93+
): Promise<Issue | undefined> {
94+
return Promise.resolve(undefined);
95+
}
96+
8997
protected override async getProviderPullRequestForBranch(
9098
_session: AuthenticationSession,
9199
_repo: BitbucketRepositoryDescriptor,

src/plus/integrations/providers/github.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Sources } from '../../../constants.telemetry';
44
import type { Container } from '../../../container';
55
import type { Account, UnidentifiedAuthor } from '../../../git/models/author';
66
import type { DefaultBranch } from '../../../git/models/defaultBranch';
7-
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
7+
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
88
import type {
99
PullRequest,
1010
PullRequestMergeMethod,
@@ -18,7 +18,7 @@ import type {
1818
IntegrationAuthenticationProviderDescriptor,
1919
IntegrationAuthenticationService,
2020
} from '../authentication/integrationAuthentication';
21-
import type { SupportedIntegrationIds } from '../integration';
21+
import type { RepositoryDescriptor, SupportedIntegrationIds } from '../integration';
2222
import { HostingIntegration } from '../integration';
2323
import { providersMetadata } from './models';
2424
import type { ProvidersApi } from './providersApi';
@@ -35,11 +35,7 @@ const enterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Obje
3535
scopes: enterpriseMetadata.scopes,
3636
});
3737

38-
export type GitHubRepositoryDescriptor = {
39-
key: string;
40-
owner: string;
41-
name: string;
42-
};
38+
export type GitHubRepositoryDescriptor = RepositoryDescriptor;
4339

4440
abstract class GitHubIntegrationBase<ID extends SupportedIntegrationIds> extends HostingIntegration<
4541
ID,
@@ -101,6 +97,17 @@ abstract class GitHubIntegrationBase<ID extends SupportedIntegrationIds> extends
10197
);
10298
}
10399

100+
protected override async getProviderIssue(
101+
{ accessToken }: AuthenticationSession,
102+
repo: GitHubRepositoryDescriptor,
103+
id: string,
104+
): Promise<Issue | undefined> {
105+
return (await this.container.github)?.getIssue(this, accessToken, repo.owner, repo.name, Number(id), {
106+
baseUrl: this.apiBaseUrl,
107+
includeBody: true,
108+
});
109+
}
110+
104111
protected override async getProviderPullRequest(
105112
{ accessToken }: AuthenticationSession,
106113
repo: GitHubRepositoryDescriptor,

src/plus/integrations/providers/github/github.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import type { PagedResult, RepositoryVisibility } from '../../../../git/gitProvider';
2020
import type { Account, UnidentifiedAuthor } from '../../../../git/models/author';
2121
import type { DefaultBranch } from '../../../../git/models/defaultBranch';
22-
import type { IssueOrPullRequest, SearchedIssue } from '../../../../git/models/issue';
22+
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../../git/models/issue';
2323
import type { PullRequest, SearchedPullRequest } from '../../../../git/models/pullRequest';
2424
import { PullRequestMergeMethod } from '../../../../git/models/pullRequest';
2525
import type { Provider } from '../../../../git/models/remoteProvider';
@@ -622,6 +622,73 @@ export class GitHubApi implements Disposable {
622622
}
623623
}
624624

625+
@debug<GitHubApi['getIssue']>({ args: { 0: p => p.name, 1: '<token>' } })
626+
async getIssue(
627+
provider: Provider,
628+
token: string,
629+
owner: string,
630+
repo: string,
631+
number: number,
632+
options?: {
633+
baseUrl?: string;
634+
avatarSize?: number;
635+
includeBody?: boolean;
636+
},
637+
): Promise<Issue | undefined> {
638+
const scope = getLogScope();
639+
640+
interface QueryResult {
641+
repository:
642+
| {
643+
issue: GitHubIssue | null | undefined;
644+
}
645+
| null
646+
| undefined;
647+
}
648+
649+
try {
650+
const query = `query getIssue(
651+
$owner: String!
652+
$repo: String!
653+
$number: Int!
654+
$avatarSize: Int
655+
) {
656+
repository(name: $repo, owner: $owner) {
657+
issue(number: $number) {
658+
${gqIssueFragment}${
659+
options?.includeBody
660+
? `
661+
body
662+
`
663+
: ''
664+
}
665+
}
666+
}
667+
}`;
668+
669+
const rsp = await this.graphql<QueryResult>(
670+
provider,
671+
token,
672+
query,
673+
{
674+
...options,
675+
owner: owner,
676+
repo: repo,
677+
number: number,
678+
},
679+
scope,
680+
);
681+
682+
if (rsp?.repository?.issue == null) return undefined;
683+
684+
return fromGitHubIssue(rsp.repository.issue, provider);
685+
} catch (ex) {
686+
if (ex instanceof RequestNotFoundError) return undefined;
687+
688+
throw this.handleException(ex, provider, scope);
689+
}
690+
}
691+
625692
@debug<GitHubApi['getPullRequest']>({ args: { 0: p => p.name, 1: '<token>' } })
626693
async getPullRequest(
627694
provider: Provider,

0 commit comments

Comments
 (0)