Skip to content

Commit

Permalink
Refines issue model and adds getIssue to supported integrations (#3865)
Browse files Browse the repository at this point in the history
  • Loading branch information
axosoft-ramint authored Dec 16, 2024
1 parent 8d00555 commit f18105e
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 56 deletions.
37 changes: 36 additions & 1 deletion src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Disposable } from './api/gitlens';
import type { Container } from './container';
import type { Account } from './git/models/author';
import type { DefaultBranch } from './git/models/defaultBranch';
import type { IssueOrPullRequest } from './git/models/issue';
import type { Issue, IssueOrPullRequest } from './git/models/issue';
import type { PullRequest } from './git/models/pullRequest';
import type { RepositoryMetadata } from './git/models/repositoryMetadata';
import type { HostingIntegration, IntegrationBase, ResourceDescriptor } from './plus/integrations/integration';
Expand All @@ -12,6 +12,8 @@ import { isPromise } from './system/promise';
type Caches = {
defaultBranch: { key: `repo:${string}`; value: DefaultBranch };
// enrichedAutolinksBySha: { key: `sha:${string}:${string}`; value: Map<string, EnrichedAutolink> };
issuesById: { key: `id:${string}:${string}`; value: Issue };
issuesByIdAndResource: { key: `id:${string}:${string}:${string}`; value: Issue };
issuesOrPrsById: { key: `id:${string}:${string}`; value: IssueOrPullRequest };
issuesOrPrsByIdAndRepo: { key: `id:${string}:${string}:${string}`; value: IssueOrPullRequest };
prByBranch: { key: `branch:${string}:${string}`; value: PullRequest };
Expand Down Expand Up @@ -127,6 +129,27 @@ export class CacheProvider implements Disposable {
);
}

getIssue(
id: string,
resource: ResourceDescriptor,
integration: IntegrationBase | undefined,
cacheable: Cacheable<Issue>,
options?: { expiryOverride?: boolean | number },
): CacheResult<Issue> {
const { key, etag } = getResourceKeyAndEtag(resource, integration);

if (resource == null) {
return this.get('issuesById', `id:${id}:${key}`, etag, cacheable, options);
}
return this.get(
'issuesByIdAndResource',
`id:${id}:${key}:${JSON.stringify(resource)}}`,
etag,
cacheable,
options,
);
}

getPullRequest(
id: string,
resource: ResourceDescriptor,
Expand Down Expand Up @@ -259,6 +282,18 @@ function getExpiresAt<T extends Cache>(cache: T, value: CacheValue<T> | undefine
case 'repoMetadata':
case 'currentAccount':
return 0; // Never expires
case 'issuesById':
case 'issuesByIdAndResource': {
if (value == null) return 0; // Never expires

// Open issues expire after 1 hour, but closed issues expire after 12 hours unless recently updated and then expire in 1 hour

const issue = value as CacheValue<'issuesById'>;
if (!issue.closed) return defaultExpiresAt;

const updatedAgo = now - (issue.closedDate ?? issue.updatedDate).getTime();
return now + (updatedAgo > 14 * 24 * 60 * 60 * 1000 ? 12 : 1) * 60 * 60 * 1000;
}
case 'issuesOrPrsById':
case 'issuesOrPrsByIdAndRepo': {
if (value == null) return 0; // Never expires
Expand Down
18 changes: 17 additions & 1 deletion src/git/models/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,19 @@ export interface IssueRepository {
url?: string;
}

export interface IssueProject {
id: string;
name: string;
resourceId: string;
}

export interface IssueShape extends IssueOrPullRequest {
author: IssueMember;
assignees: IssueMember[];
repository?: IssueRepository;
labels?: IssueLabel[];
body?: string;
project?: IssueProject;
}

export interface SearchedIssue {
Expand Down Expand Up @@ -118,6 +125,14 @@ export function serializeIssue(value: IssueShape): IssueShape {
repo: value.repository.repo,
url: value.repository.url,
},
project:
value.project == null
? undefined
: {
id: value.project.id,
name: value.project.name,
resourceId: value.project.resourceId,
},
assignees: value.assignees.map(assignee => ({
id: assignee.id,
name: assignee.name,
Expand Down Expand Up @@ -152,13 +167,14 @@ export class Issue implements IssueShape {
public readonly closed: boolean,
public readonly state: IssueOrPullRequestState,
public readonly author: IssueMember,
public readonly repository: IssueRepository,
public readonly assignees: IssueMember[],
public readonly repository?: IssueRepository,
public readonly closedDate?: Date,
public readonly labels?: IssueLabel[],
public readonly commentsCount?: number,
public readonly thumbsUpCount?: number,
public readonly body?: string,
public readonly project?: IssueProject,
) {}
}

Expand Down
71 changes: 70 additions & 1 deletion src/plus/integrations/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AuthenticationError, CancellationError, RequestClientError } from '../.
import type { PagedResult } from '../../git/gitProvider';
import type { Account, UnidentifiedAuthor } from '../../git/models/author';
import type { DefaultBranch } from '../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../git/models/issue';
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../git/models/issue';
import type {
PullRequest,
PullRequestMergeMethod,
Expand Down Expand Up @@ -70,6 +70,38 @@ export type IntegrationType = 'issues' | 'hosting';

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

export type IssueResourceDescriptor = ResourceDescriptor & {
id: string;
name: string;
};

export type RepositoryDescriptor = ResourceDescriptor & {
owner: string;
name: string;
};

export function isIssueResourceDescriptor(resource: ResourceDescriptor): resource is IssueResourceDescriptor {
return (
'key' in resource &&
resource.key != null &&
'id' in resource &&
resource.id != null &&
'name' in resource &&
resource.name != null
);
}

export function isRepositoryDescriptor(resource: ResourceDescriptor): resource is RepositoryDescriptor {
return (
'key' in resource &&
resource.key != null &&
'owner' in resource &&
resource.owner != null &&
'name' in resource &&
resource.name != null
);
}

export function isHostingIntegration(integration: Integration): integration is HostingIntegration {
return integration.type === 'hosting';
}
Expand Down Expand Up @@ -446,6 +478,43 @@ export abstract class IntegrationBase<
id: string,
): Promise<IssueOrPullRequest | undefined>;

@debug()
async getIssue(
resource: T,
id: string,
options?: { expiryOverride?: boolean | number },
): Promise<Issue | undefined> {
const scope = getLogScope();

const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;

const issue = this.container.cache.getIssue(
id,
resource,
this,
() => ({
value: (async () => {
try {
const result = await this.getProviderIssue(this._session!, resource, id);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<Issue | undefined>(ex, scope, undefined);
}
})(),
}),
options,
);
return issue;
}

protected abstract getProviderIssue(
session: ProviderAuthenticationSession,
resource: T,
id: string,
): Promise<Issue | undefined>;

async getCurrentAccount(options?: {
avatarSize?: number;
expiryOverride?: boolean | number;
Expand Down
10 changes: 9 additions & 1 deletion src/plus/integrations/providers/azureDevOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HostingIntegrationId } from '../../../constants.integrations';
import type { PagedResult } from '../../../git/gitProvider';
import type { Account } from '../../../git/models/author';
import type { DefaultBranch } from '../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type {
PullRequest,
PullRequestMergeMethod,
Expand Down Expand Up @@ -107,6 +107,14 @@ export class AzureDevOpsIntegration extends HostingIntegration<
return Promise.resolve(undefined);
}

protected override async getProviderIssue(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
_id: string,
): Promise<Issue | undefined> {
return Promise.resolve(undefined);
}

protected override async getProviderPullRequestForBranch(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
Expand Down
10 changes: 9 additions & 1 deletion src/plus/integrations/providers/bitbucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AuthenticationSession, CancellationToken } from 'vscode';
import { HostingIntegrationId } from '../../../constants.integrations';
import type { Account } from '../../../git/models/author';
import type { DefaultBranch } from '../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type {
PullRequest,
PullRequestMergeMethod,
Expand Down Expand Up @@ -86,6 +86,14 @@ export class BitbucketIntegration extends HostingIntegration<
return Promise.resolve(undefined);
}

protected override async getProviderIssue(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
_id: string,
): Promise<Issue | undefined> {
return Promise.resolve(undefined);
}

protected override async getProviderPullRequestForBranch(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
Expand Down
21 changes: 14 additions & 7 deletions src/plus/integrations/providers/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Sources } from '../../../constants.telemetry';
import type { Container } from '../../../container';
import type { Account, UnidentifiedAuthor } from '../../../git/models/author';
import type { DefaultBranch } from '../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type {
PullRequest,
PullRequestMergeMethod,
Expand All @@ -18,7 +18,7 @@ import type {
IntegrationAuthenticationProviderDescriptor,
IntegrationAuthenticationService,
} from '../authentication/integrationAuthentication';
import type { SupportedIntegrationIds } from '../integration';
import type { RepositoryDescriptor, SupportedIntegrationIds } from '../integration';
import { HostingIntegration } from '../integration';
import { providersMetadata } from './models';
import type { ProvidersApi } from './providersApi';
Expand All @@ -35,11 +35,7 @@ const enterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Obje
scopes: enterpriseMetadata.scopes,
});

export type GitHubRepositoryDescriptor = {
key: string;
owner: string;
name: string;
};
export type GitHubRepositoryDescriptor = RepositoryDescriptor;

abstract class GitHubIntegrationBase<ID extends SupportedIntegrationIds> extends HostingIntegration<
ID,
Expand Down Expand Up @@ -101,6 +97,17 @@ abstract class GitHubIntegrationBase<ID extends SupportedIntegrationIds> extends
);
}

protected override async getProviderIssue(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
id: string,
): Promise<Issue | undefined> {
return (await this.container.github)?.getIssue(this, accessToken, repo.owner, repo.name, Number(id), {
baseUrl: this.apiBaseUrl,
includeBody: true,
});
}

protected override async getProviderPullRequest(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
Expand Down
69 changes: 68 additions & 1 deletion src/plus/integrations/providers/github/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import type { PagedResult, RepositoryVisibility } from '../../../../git/gitProvider';
import type { Account, UnidentifiedAuthor } from '../../../../git/models/author';
import type { DefaultBranch } from '../../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../../git/models/issue';
import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../../git/models/issue';
import type { PullRequest, SearchedPullRequest } from '../../../../git/models/pullRequest';
import { PullRequestMergeMethod } from '../../../../git/models/pullRequest';
import type { Provider } from '../../../../git/models/remoteProvider';
Expand Down Expand Up @@ -622,6 +622,73 @@ export class GitHubApi implements Disposable {
}
}

@debug<GitHubApi['getIssue']>({ args: { 0: p => p.name, 1: '<token>' } })
async getIssue(
provider: Provider,
token: string,
owner: string,
repo: string,
number: number,
options?: {
baseUrl?: string;
avatarSize?: number;
includeBody?: boolean;
},
): Promise<Issue | undefined> {
const scope = getLogScope();

interface QueryResult {
repository:
| {
issue: GitHubIssue | null | undefined;
}
| null
| undefined;
}

try {
const query = `query getIssue(
$owner: String!
$repo: String!
$number: Int!
$avatarSize: Int
) {
repository(name: $repo, owner: $owner) {
issue(number: $number) {
${gqIssueFragment}${
options?.includeBody
? `
body
`
: ''
}
}
}
}`;

const rsp = await this.graphql<QueryResult>(
provider,
token,
query,
{
...options,
owner: owner,
repo: repo,
number: number,
},
scope,
);

if (rsp?.repository?.issue == null) return undefined;

return fromGitHubIssue(rsp.repository.issue, provider);
} catch (ex) {
if (ex instanceof RequestNotFoundError) return undefined;

throw this.handleException(ex, provider, scope);
}
}

@debug<GitHubApi['getPullRequest']>({ args: { 0: p => p.name, 1: '<token>' } })
async getPullRequest(
provider: Provider,
Expand Down
Loading

0 comments on commit f18105e

Please sign in to comment.