Skip to content

Commit f1680a0

Browse files
committed
Conects and syncs to Bitbucket Cloud
(#4044)
1 parent 75103e3 commit f1680a0

File tree

4 files changed

+23
-119
lines changed

4 files changed

+23
-119
lines changed

src/constants.integrations.ts

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const supportedOrderedCloudIntegrationIds = [
3030
HostingIntegrationId.GitLab,
3131
SelfHostedIntegrationId.CloudGitLabSelfHosted,
3232
HostingIntegrationId.AzureDevOps,
33+
HostingIntegrationId.Bitbucket,
3334
IssueIntegrationId.Jira,
3435
];
3536

@@ -84,6 +85,13 @@ export const supportedCloudIntegrationDescriptors: IntegrationDescriptor[] = [
8485
supports: ['prs', 'issues'],
8586
requiresPro: true,
8687
},
88+
{
89+
id: HostingIntegrationId.Bitbucket,
90+
name: 'Bitbucket',
91+
icon: 'gl-provider-bitbucket',
92+
supports: ['prs'],
93+
requiresPro: false,
94+
},
8795
{
8896
id: IssueIntegrationId.Jira,
8997
name: 'Jira',
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,8 @@
1-
import type { Disposable, QuickInputButton } from 'vscode';
2-
import { env, ThemeIcon, Uri, window } from 'vscode';
31
import { HostingIntegrationId } from '../../../constants.integrations';
4-
import { base64 } from '../../../system/string';
5-
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider';
6-
import { LocalIntegrationAuthenticationProvider } from './integrationAuthenticationProvider';
7-
import type { ProviderAuthenticationSession } from './models';
2+
import { CloudIntegrationAuthenticationProvider } from './integrationAuthenticationProvider';
83

9-
export class BitbucketAuthenticationProvider extends LocalIntegrationAuthenticationProvider<HostingIntegrationId.Bitbucket> {
4+
export class BitbucketAuthenticationProvider extends CloudIntegrationAuthenticationProvider<HostingIntegrationId.Bitbucket> {
105
protected override get authProviderId(): HostingIntegrationId.Bitbucket {
116
return HostingIntegrationId.Bitbucket;
127
}
13-
14-
override async createSession(
15-
descriptor: IntegrationAuthenticationSessionDescriptor,
16-
): Promise<ProviderAuthenticationSession | undefined> {
17-
let bitbucketUsername: string | undefined = descriptor.username as string | undefined;
18-
if (!bitbucketUsername) {
19-
const infoButton: QuickInputButton = {
20-
iconPath: new ThemeIcon(`link-external`),
21-
tooltip: 'Open the Bitbucket Settings Page',
22-
};
23-
24-
const usernameInput = window.createInputBox();
25-
usernameInput.ignoreFocusOut = true;
26-
const usernameInputDisposables: Disposable[] = [];
27-
try {
28-
bitbucketUsername = await new Promise<string | undefined>(resolve => {
29-
usernameInputDisposables.push(
30-
usernameInput.onDidHide(() => resolve(undefined)),
31-
usernameInput.onDidChangeValue(() => (usernameInput.validationMessage = undefined)),
32-
usernameInput.onDidAccept(() => {
33-
const value = usernameInput.value.trim();
34-
if (!value) {
35-
usernameInput.validationMessage = 'A Bitbucket username is required';
36-
return;
37-
}
38-
39-
resolve(value);
40-
}),
41-
usernameInput.onDidTriggerButton(e => {
42-
if (e === infoButton) {
43-
void env.openExternal(Uri.parse(`https://${descriptor.domain}/account/settings/`));
44-
}
45-
}),
46-
);
47-
48-
usernameInput.title = `Bitbucket Authentication \u2022 ${descriptor.domain}`;
49-
usernameInput.placeholder = 'Username';
50-
usernameInput.prompt = `Enter your [Bitbucket Username](https://${descriptor.domain}/account/settings/ "Get your Bitbucket App Password")`;
51-
usernameInput.show();
52-
});
53-
} finally {
54-
usernameInput.dispose();
55-
usernameInputDisposables.forEach(d => void d.dispose());
56-
}
57-
}
58-
59-
if (!bitbucketUsername) return undefined;
60-
61-
const appPasswordInput = window.createInputBox();
62-
appPasswordInput.ignoreFocusOut = true;
63-
64-
const disposables: Disposable[] = [];
65-
66-
let appPassword;
67-
try {
68-
const infoButton: QuickInputButton = {
69-
iconPath: new ThemeIcon(`link-external`),
70-
tooltip: 'Open the Bitbucket App Passwords Page',
71-
};
72-
73-
appPassword = await new Promise<string | undefined>(resolve => {
74-
disposables.push(
75-
appPasswordInput.onDidHide(() => resolve(undefined)),
76-
appPasswordInput.onDidChangeValue(() => (appPasswordInput.validationMessage = undefined)),
77-
appPasswordInput.onDidAccept(() => {
78-
const value = appPasswordInput.value.trim();
79-
if (!value) {
80-
appPasswordInput.validationMessage = 'An app password is required';
81-
return;
82-
}
83-
84-
resolve(value);
85-
}),
86-
appPasswordInput.onDidTriggerButton(e => {
87-
if (e === infoButton) {
88-
void env.openExternal(
89-
Uri.parse(`https://${descriptor.domain}/account/settings/app-passwords/`),
90-
);
91-
}
92-
}),
93-
);
94-
95-
appPasswordInput.password = true;
96-
appPasswordInput.title = `Bitbucket Authentication \u2022 ${descriptor.domain}`;
97-
appPasswordInput.placeholder = `Requires ${descriptor.scopes.join(', ')} scopes`;
98-
appPasswordInput.prompt = `Paste your [Bitbucket App Password](https://${descriptor.domain}/account/settings/app-passwords/ "Get your Bitbucket App Password")`;
99-
appPasswordInput.buttons = [infoButton];
100-
101-
appPasswordInput.show();
102-
});
103-
} finally {
104-
appPasswordInput.dispose();
105-
disposables.forEach(d => void d.dispose());
106-
}
107-
108-
if (!appPassword) return undefined;
109-
110-
return {
111-
id: this.configuredIntegrationService.getSessionId(descriptor),
112-
accessToken: base64(`${bitbucketUsername}:${appPassword}`),
113-
scopes: descriptor.scopes,
114-
account: {
115-
id: '',
116-
label: '',
117-
},
118-
cloud: false,
119-
domain: descriptor.domain,
120-
};
121-
}
1228
}

src/plus/integrations/integrationService.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import type {
5252
SupportedSelfHostedIntegrationIds,
5353
} from './integration';
5454
import { isAzureCloudDomain } from './providers/azureDevOps';
55+
import { isBitbucketCloudDomain } from './providers/bitbucket';
5556
import {
5657
isCloudSelfHostedIntegrationId,
5758
isGitHubDotCom,
@@ -680,14 +681,16 @@ export class IntegrationService implements Disposable {
680681
const get = getOrGetCached.bind(this);
681682

682683
switch (remote.provider.id) {
683-
// TODO: Uncomment when we support these integrations
684-
// case 'bitbucket':
685-
// return get(HostingIntegrationId.Bitbucket) as RT;
686684
case 'azure-devops':
687685
if (isAzureCloudDomain(remote.provider.domain)) {
688686
return get(HostingIntegrationId.AzureDevOps) as RT;
689687
}
690688
return (getOrGetCached === this.get ? Promise.resolve(undefined) : undefined) as RT;
689+
case 'bitbucket':
690+
if (isBitbucketCloudDomain(remote.provider.domain)) {
691+
return get(HostingIntegrationId.Bitbucket) as RT;
692+
}
693+
return (getOrGetCached === this.get ? Promise.resolve(undefined) : undefined) as RT;
691694
case 'github':
692695
if (remote.provider.domain != null && !isGitHubDotCom(remote.provider.domain)) {
693696
return get(
@@ -1049,6 +1052,8 @@ export function remoteProviderIdToIntegrationId(
10491052
switch (remoteProviderId) {
10501053
case 'azure-devops':
10511054
return HostingIntegrationId.AzureDevOps;
1055+
case 'bitbucket':
1056+
return HostingIntegrationId.Bitbucket;
10521057
case 'github':
10531058
return HostingIntegrationId.GitHub;
10541059
case 'gitlab':

src/plus/integrations/providers/bitbucket.ts

+5
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,8 @@ export class BitbucketIntegration extends HostingIntegration<
137137
return Promise.resolve(undefined);
138138
}
139139
}
140+
141+
const bitbucketCloudDomainRegex = /^bitbucket\.org$/i;
142+
export function isBitbucketCloudDomain(domain: string | undefined): boolean {
143+
return domain != null && bitbucketCloudDomainRegex.test(domain);
144+
}

0 commit comments

Comments
 (0)