Skip to content

Commit 342e5fd

Browse files
committed
Adds support of Bitbucket issues in autolinks
(#4045, #4070)
1 parent 18b00d3 commit 342e5fd

File tree

6 files changed

+156
-53
lines changed

6 files changed

+156
-53
lines changed

src/autolinks/autolinksProvider.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,12 @@ export class AutolinksProvider implements Disposable {
248248
? integration.getIssueOrPullRequest(
249249
link.descriptor ?? remote.provider.repoDesc,
250250
this.getAutolinkEnrichableId(link),
251+
{ type: link.type },
251252
)
252253
: link.descriptor != null
253-
? linkIntegration?.getIssueOrPullRequest(link.descriptor, this.getAutolinkEnrichableId(link))
254+
? linkIntegration?.getIssueOrPullRequest(link.descriptor, this.getAutolinkEnrichableId(link), {
255+
type: link.type,
256+
})
254257
: undefined;
255258
enrichedAutolinks.set(id, [issueOrPullRequestPromise, link]);
256259
}

src/plus/integrations/integration.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { PagedResult } from '../../git/gitProvider';
1515
import type { Account, UnidentifiedAuthor } from '../../git/models/author';
1616
import type { DefaultBranch } from '../../git/models/defaultBranch';
1717
import type { Issue, IssueShape } from '../../git/models/issue';
18-
import type { IssueOrPullRequest } from '../../git/models/issueOrPullRequest';
18+
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../git/models/issueOrPullRequest';
1919
import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../git/models/pullRequest';
2020
import type { RepositoryMetadata } from '../../git/models/repositoryMetadata';
2121
import type { PullRequestUrlIdentity } from '../../git/utils/pullRequest.utils';
@@ -450,7 +450,7 @@ export abstract class IntegrationBase<
450450
async getIssueOrPullRequest(
451451
resource: T,
452452
id: string,
453-
options?: { expiryOverride?: boolean | number },
453+
options?: { expiryOverride?: boolean | number; type?: IssueOrPullRequestType },
454454
): Promise<IssueOrPullRequest | undefined> {
455455
const scope = getLogScope();
456456

@@ -464,7 +464,12 @@ export abstract class IntegrationBase<
464464
() => ({
465465
value: (async () => {
466466
try {
467-
const result = await this.getProviderIssueOrPullRequest(this._session!, resource, id);
467+
const result = await this.getProviderIssueOrPullRequest(
468+
this._session!,
469+
resource,
470+
id,
471+
options?.type,
472+
);
468473
this.resetRequestExceptionCount();
469474
return result;
470475
} catch (ex) {
@@ -481,6 +486,7 @@ export abstract class IntegrationBase<
481486
session: ProviderAuthenticationSession,
482487
resource: T,
483488
id: string,
489+
type: undefined | IssueOrPullRequestType,
484490
): Promise<IssueOrPullRequest | undefined>;
485491

486492
@debug()

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

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -376,22 +376,22 @@ export class AzureDevOpsApi implements Disposable {
376376
throw new RequestNotFoundError(ex);
377377
case 401: // Unauthorized
378378
throw new AuthenticationError('azureDevOps', AuthenticationErrorReason.Unauthorized, ex);
379-
// TODO: Learn the Azure API docs and put it in order:
380-
// case 403: // Forbidden
381-
// if (ex.message.includes('rate limit')) {
382-
// let resetAt: number | undefined;
383-
384-
// const reset = ex.response?.headers?.get('x-ratelimit-reset');
385-
// if (reset != null) {
386-
// resetAt = parseInt(reset, 10);
387-
// if (Number.isNaN(resetAt)) {
388-
// resetAt = undefined;
389-
// }
390-
// }
391-
392-
// throw new RequestRateLimitError(ex, token, resetAt);
393-
// }
394-
// throw new AuthenticationError('azure', AuthenticationErrorReason.Forbidden, ex);
379+
case 403: // Forbidden
380+
// TODO: Learn the Azure API docs and put it in order:
381+
// if (ex.message.includes('rate limit')) {
382+
// let resetAt: number | undefined;
383+
384+
// const reset = ex.response?.headers?.get('x-ratelimit-reset');
385+
// if (reset != null) {
386+
// resetAt = parseInt(reset, 10);
387+
// if (Number.isNaN(resetAt)) {
388+
// resetAt = undefined;
389+
// }
390+
// }
391+
392+
// throw new RequestRateLimitError(ex, token, resetAt);
393+
// }
394+
throw new AuthenticationError('azure', AuthenticationErrorReason.Forbidden, ex);
395395
case 500: // Internal Server Error
396396
Logger.error(ex, scope);
397397
if (ex.response != null) {

src/plus/integrations/providers/bitbucket.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HostingIntegrationId } from '../../../constants.integrations';
33
import type { Account } from '../../../git/models/author';
44
import type { DefaultBranch } from '../../../git/models/defaultBranch';
55
import type { Issue, IssueShape } from '../../../git/models/issue';
6-
import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest';
6+
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest';
77
import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../../git/models/pullRequest';
88
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata';
99
import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider';
@@ -78,9 +78,11 @@ export class BitbucketIntegration extends HostingIntegration<
7878
{ accessToken }: AuthenticationSession,
7979
repo: BitbucketRepositoryDescriptor,
8080
id: string,
81+
type: undefined | IssueOrPullRequestType,
8182
): Promise<IssueOrPullRequest | undefined> {
8283
return (await this.container.bitbucket)?.getIssueOrPullRequest(this, accessToken, repo.owner, repo.name, id, {
8384
baseUrl: this.apiBaseUrl,
85+
type: type,
8486
});
8587
}
8688

src/plus/integrations/providers/bitbucket/bitbucket.ts

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
RequestClientError,
1414
RequestNotFoundError,
1515
} from '../../../../errors';
16-
import type { IssueOrPullRequest } from '../../../../git/models/issueOrPullRequest';
16+
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../../git/models/issueOrPullRequest';
1717
import type { PullRequest } from '../../../../git/models/pullRequest';
1818
import type { Provider } from '../../../../git/models/remoteProvider';
1919
import { showIntegrationRequestFailed500WarningMessage } from '../../../../messages';
@@ -23,8 +23,8 @@ import { Logger } from '../../../../system/logger';
2323
import type { LogScope } from '../../../../system/logger.scope';
2424
import { getLogScope } from '../../../../system/logger.scope';
2525
import { maybeStopWatch } from '../../../../system/stopwatch';
26-
import type { BitbucketPullRequest } from './models';
27-
import { fromBitbucketPullRequest } from './models';
26+
import type { BitbucketIssue, BitbucketPullRequest } from './models';
27+
import { bitbucketIssueStateToState, fromBitbucketPullRequest } from './models';
2828

2929
export class BitbucketApi implements Disposable {
3030
private readonly _disposable: Disposable;
@@ -102,25 +102,69 @@ export class BitbucketApi implements Disposable {
102102
id: string,
103103
options: {
104104
baseUrl: string;
105+
type?: IssueOrPullRequestType;
105106
},
106107
): Promise<IssueOrPullRequest | undefined> {
107108
const scope = getLogScope();
108109

109-
const response = await this.request<BitbucketPullRequest>(
110-
provider,
111-
token,
112-
options.baseUrl,
113-
`repositories/${owner}/${repo}/pullrequests/${id}?fields=*`,
114-
{
115-
method: 'GET',
116-
},
117-
scope,
118-
);
110+
if (options?.type !== 'issue') {
111+
try {
112+
const prResponse = await this.request<BitbucketPullRequest>(
113+
provider,
114+
token,
115+
options.baseUrl,
116+
`repositories/${owner}/${repo}/pullrequests/${id}?fields=%2Bvalues.reviewers,%2Bvalues.participants`,
117+
{
118+
method: 'GET',
119+
},
120+
scope,
121+
);
119122

120-
if (!response) {
121-
return undefined;
123+
if (prResponse) {
124+
return fromBitbucketPullRequest(prResponse, provider);
125+
}
126+
} catch (ex) {
127+
if (ex.original?.status !== 404) {
128+
Logger.error(ex, scope);
129+
return undefined;
130+
}
131+
}
122132
}
123-
return fromBitbucketPullRequest(response, provider);
133+
134+
if (options?.type !== 'pullrequest') {
135+
try {
136+
const issueResponse = await this.request<BitbucketIssue>(
137+
provider,
138+
token,
139+
options.baseUrl,
140+
`repositories/${owner}/${repo}/issues/${id}`,
141+
{
142+
method: 'GET',
143+
},
144+
scope,
145+
);
146+
147+
if (issueResponse) {
148+
return {
149+
id: issueResponse.id.toString(),
150+
type: 'issue',
151+
nodeId: issueResponse.id.toString(),
152+
provider: provider,
153+
createdDate: new Date(issueResponse.created_on),
154+
updatedDate: new Date(issueResponse.updated_on),
155+
state: bitbucketIssueStateToState(issueResponse.state),
156+
closed: issueResponse.state === 'closed',
157+
title: issueResponse.title,
158+
url: issueResponse.links.html.href,
159+
};
160+
}
161+
} catch (ex) {
162+
Logger.error(ex, scope);
163+
return undefined;
164+
}
165+
}
166+
167+
return undefined;
124168
}
125169

126170
private async request<T>(
@@ -192,22 +236,22 @@ export class BitbucketApi implements Disposable {
192236
throw new RequestNotFoundError(ex);
193237
case 401: // Unauthorized
194238
throw new AuthenticationError('bitbucket', AuthenticationErrorReason.Unauthorized, ex);
195-
// TODO: Learn the Bitbucket API docs and put it in order:
196-
// case 403: // Forbidden
197-
// if (ex.message.includes('rate limit')) {
198-
// let resetAt: number | undefined;
199-
200-
// const reset = ex.response?.headers?.get('x-ratelimit-reset');
201-
// if (reset != null) {
202-
// resetAt = parseInt(reset, 10);
203-
// if (Number.isNaN(resetAt)) {
204-
// resetAt = undefined;
205-
// }
206-
// }
207-
208-
// throw new RequestRateLimitError(ex, token, resetAt);
209-
// }
210-
// throw new AuthenticationError('bitbucket', AuthenticationErrorReason.Forbidden, ex);
239+
case 403: // Forbidden
240+
// TODO: Learn the Bitbucket API docs and put it in order:
241+
// if (ex.message.includes('rate limit')) {
242+
// let resetAt: number | undefined;
243+
244+
// const reset = ex.response?.headers?.get('x-ratelimit-reset');
245+
// if (reset != null) {
246+
// resetAt = parseInt(reset, 10);
247+
// if (Number.isNaN(resetAt)) {
248+
// resetAt = undefined;
249+
// }
250+
// }
251+
252+
// throw new RequestRateLimitError(ex, token, resetAt);
253+
// }
254+
throw new AuthenticationError('bitbucket', AuthenticationErrorReason.Forbidden, ex);
211255
case 500: // Internal Server Error
212256
Logger.error(ex, scope);
213257
if (ex.response != null) {

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ interface BitbucketPullRequestCommit {
7272
};
7373
}
7474

75+
export type BitbucketIssueState =
76+
| 'submitted'
77+
| 'new'
78+
| 'open'
79+
| 'resolved'
80+
| 'on hold'
81+
| 'invalid'
82+
| 'duplicate'
83+
| 'wontfix'
84+
| 'closed';
85+
7586
export interface BitbucketPullRequest {
7687
type: 'pullrequest';
7788
id: number;
@@ -121,6 +132,26 @@ export interface BitbucketPullRequest {
121132
};
122133
}
123134

135+
export interface BitbucketIssue {
136+
type: string;
137+
id: number;
138+
title: string;
139+
reporter: BitbucketUser;
140+
assignee?: BitbucketUser;
141+
state: BitbucketIssueState;
142+
created_on: string;
143+
updated_on: string;
144+
repository: BitbucketRepository;
145+
links: {
146+
self: BitbucketLink;
147+
html: BitbucketLink;
148+
comments: BitbucketLink;
149+
attachments: BitbucketLink;
150+
watch: BitbucketLink;
151+
vote: BitbucketLink;
152+
};
153+
}
154+
124155
export function bitbucketPullRequestStateToState(state: BitbucketPullRequestState): IssueOrPullRequestState {
125156
switch (state) {
126157
case 'DECLINED':
@@ -134,6 +165,23 @@ export function bitbucketPullRequestStateToState(state: BitbucketPullRequestStat
134165
}
135166
}
136167

168+
export function bitbucketIssueStateToState(state: BitbucketIssueState): IssueOrPullRequestState {
169+
switch (state) {
170+
case 'resolved':
171+
case 'invalid':
172+
case 'duplicate':
173+
case 'wontfix':
174+
case 'closed':
175+
return 'closed';
176+
case 'submitted':
177+
case 'new':
178+
case 'open':
179+
case 'on hold':
180+
default:
181+
return 'opened';
182+
}
183+
}
184+
137185
export function isClosedBitbucketPullRequestState(state: BitbucketPullRequestState): boolean {
138186
return bitbucketPullRequestStateToState(state) !== 'opened';
139187
}

0 commit comments

Comments
 (0)