Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d667451

Browse files
committedFeb 21, 2025
Adds support of Bitbucket issues in autolinks
(#4045, #4070)
1 parent b61c6a3 commit d667451

File tree

6 files changed

+156
-53
lines changed

6 files changed

+156
-53
lines changed
 

Diff for: ‎src/autolinks/autolinksProvider.ts

+4-1
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
}

Diff for: ‎src/plus/integrations/integration.ts

+9-3
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, SearchedIssue } from '../../git/models/issue';
18-
import type { IssueOrPullRequest } from '../../git/models/issueOrPullRequest';
18+
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../git/models/issueOrPullRequest';
1919
import type {
2020
PullRequest,
2121
PullRequestMergeMethod,
@@ -455,7 +455,7 @@ export abstract class IntegrationBase<
455455
async getIssueOrPullRequest(
456456
resource: T,
457457
id: string,
458-
options?: { expiryOverride?: boolean | number },
458+
options?: { expiryOverride?: boolean | number; type?: IssueOrPullRequestType },
459459
): Promise<IssueOrPullRequest | undefined> {
460460
const scope = getLogScope();
461461

@@ -469,7 +469,12 @@ export abstract class IntegrationBase<
469469
() => ({
470470
value: (async () => {
471471
try {
472-
const result = await this.getProviderIssueOrPullRequest(this._session!, resource, id);
472+
const result = await this.getProviderIssueOrPullRequest(
473+
this._session!,
474+
resource,
475+
id,
476+
options?.type,
477+
);
473478
this.resetRequestExceptionCount();
474479
return result;
475480
} catch (ex) {
@@ -486,6 +491,7 @@ export abstract class IntegrationBase<
486491
session: ProviderAuthenticationSession,
487492
resource: T,
488493
id: string,
494+
type: undefined | IssueOrPullRequestType,
489495
): Promise<IssueOrPullRequest | undefined>;
490496

491497
@debug()

Diff for: ‎src/plus/integrations/providers/azure/azure.ts

+16-16
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) {

Diff for: ‎src/plus/integrations/providers/bitbucket.ts

+3-1
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, SearchedIssue } from '../../../git/models/issue';
6-
import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest';
6+
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest';
77
import type {
88
PullRequest,
99
PullRequestMergeMethod,
@@ -83,9 +83,11 @@ export class BitbucketIntegration extends HostingIntegration<
8383
{ accessToken }: AuthenticationSession,
8484
repo: BitbucketRepositoryDescriptor,
8585
id: string,
86+
type: undefined | IssueOrPullRequestType,
8687
): Promise<IssueOrPullRequest | undefined> {
8788
return (await this.container.bitbucket)?.getIssueOrPullRequest(this, accessToken, repo.owner, repo.name, id, {
8889
baseUrl: this.apiBaseUrl,
90+
type: type,
8991
});
9092
}
9193

Diff for: ‎src/plus/integrations/providers/bitbucket/bitbucket-api.ts

+76-32
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=*`,
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) {

Diff for: ‎src/plus/integrations/providers/bitbucket/models.ts

+48
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)
Please sign in to comment.