From 8d1586f6534c8a889406895e6b1a7f93cbe8b124 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 13 Feb 2025 13:11:34 +0100 Subject: [PATCH] Conects and syncs to Bitbucket Cloud (#4044, #4052) --- src/constants.integrations.ts | 8 ++ .../integrations/authentication/bitbucket.ts | 118 +----------------- src/plus/integrations/integrationService.ts | 11 +- src/plus/integrations/providers/bitbucket.ts | 5 + 4 files changed, 23 insertions(+), 119 deletions(-) diff --git a/src/constants.integrations.ts b/src/constants.integrations.ts index 55e952c94a448..0ca539635410b 100644 --- a/src/constants.integrations.ts +++ b/src/constants.integrations.ts @@ -30,6 +30,7 @@ export const supportedOrderedCloudIntegrationIds = [ HostingIntegrationId.GitLab, SelfHostedIntegrationId.CloudGitLabSelfHosted, HostingIntegrationId.AzureDevOps, + HostingIntegrationId.Bitbucket, IssueIntegrationId.Jira, ]; @@ -84,6 +85,13 @@ export const supportedCloudIntegrationDescriptors: IntegrationDescriptor[] = [ supports: ['prs', 'issues'], requiresPro: true, }, + { + id: HostingIntegrationId.Bitbucket, + name: 'Bitbucket', + icon: 'gl-provider-bitbucket', + supports: ['prs'], + requiresPro: false, + }, { id: IssueIntegrationId.Jira, name: 'Jira', diff --git a/src/plus/integrations/authentication/bitbucket.ts b/src/plus/integrations/authentication/bitbucket.ts index bf652ff0ab1e4..68d1de4741a3d 100644 --- a/src/plus/integrations/authentication/bitbucket.ts +++ b/src/plus/integrations/authentication/bitbucket.ts @@ -1,122 +1,8 @@ -import type { Disposable, QuickInputButton } from 'vscode'; -import { env, ThemeIcon, Uri, window } from 'vscode'; import { HostingIntegrationId } from '../../../constants.integrations'; -import { base64 } from '../../../system/string'; -import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider'; -import { LocalIntegrationAuthenticationProvider } from './integrationAuthenticationProvider'; -import type { ProviderAuthenticationSession } from './models'; +import { CloudIntegrationAuthenticationProvider } from './integrationAuthenticationProvider'; -export class BitbucketAuthenticationProvider extends LocalIntegrationAuthenticationProvider { +export class BitbucketAuthenticationProvider extends CloudIntegrationAuthenticationProvider { protected override get authProviderId(): HostingIntegrationId.Bitbucket { return HostingIntegrationId.Bitbucket; } - - override async createSession( - descriptor: IntegrationAuthenticationSessionDescriptor, - ): Promise { - let bitbucketUsername: string | undefined = descriptor.username as string | undefined; - if (!bitbucketUsername) { - const infoButton: QuickInputButton = { - iconPath: new ThemeIcon(`link-external`), - tooltip: 'Open the Bitbucket Settings Page', - }; - - const usernameInput = window.createInputBox(); - usernameInput.ignoreFocusOut = true; - const usernameInputDisposables: Disposable[] = []; - try { - bitbucketUsername = await new Promise(resolve => { - usernameInputDisposables.push( - usernameInput.onDidHide(() => resolve(undefined)), - usernameInput.onDidChangeValue(() => (usernameInput.validationMessage = undefined)), - usernameInput.onDidAccept(() => { - const value = usernameInput.value.trim(); - if (!value) { - usernameInput.validationMessage = 'A Bitbucket username is required'; - return; - } - - resolve(value); - }), - usernameInput.onDidTriggerButton(e => { - if (e === infoButton) { - void env.openExternal(Uri.parse(`https://${descriptor.domain}/account/settings/`)); - } - }), - ); - - usernameInput.title = `Bitbucket Authentication \u2022 ${descriptor.domain}`; - usernameInput.placeholder = 'Username'; - usernameInput.prompt = `Enter your [Bitbucket Username](https://${descriptor.domain}/account/settings/ "Get your Bitbucket App Password")`; - usernameInput.show(); - }); - } finally { - usernameInput.dispose(); - usernameInputDisposables.forEach(d => void d.dispose()); - } - } - - if (!bitbucketUsername) return undefined; - - const appPasswordInput = window.createInputBox(); - appPasswordInput.ignoreFocusOut = true; - - const disposables: Disposable[] = []; - - let appPassword; - try { - const infoButton: QuickInputButton = { - iconPath: new ThemeIcon(`link-external`), - tooltip: 'Open the Bitbucket App Passwords Page', - }; - - appPassword = await new Promise(resolve => { - disposables.push( - appPasswordInput.onDidHide(() => resolve(undefined)), - appPasswordInput.onDidChangeValue(() => (appPasswordInput.validationMessage = undefined)), - appPasswordInput.onDidAccept(() => { - const value = appPasswordInput.value.trim(); - if (!value) { - appPasswordInput.validationMessage = 'An app password is required'; - return; - } - - resolve(value); - }), - appPasswordInput.onDidTriggerButton(e => { - if (e === infoButton) { - void env.openExternal( - Uri.parse(`https://${descriptor.domain}/account/settings/app-passwords/`), - ); - } - }), - ); - - appPasswordInput.password = true; - appPasswordInput.title = `Bitbucket Authentication \u2022 ${descriptor.domain}`; - appPasswordInput.placeholder = `Requires ${descriptor.scopes.join(', ')} scopes`; - appPasswordInput.prompt = `Paste your [Bitbucket App Password](https://${descriptor.domain}/account/settings/app-passwords/ "Get your Bitbucket App Password")`; - appPasswordInput.buttons = [infoButton]; - - appPasswordInput.show(); - }); - } finally { - appPasswordInput.dispose(); - disposables.forEach(d => void d.dispose()); - } - - if (!appPassword) return undefined; - - return { - id: this.configuredIntegrationService.getSessionId(descriptor), - accessToken: base64(`${bitbucketUsername}:${appPassword}`), - scopes: descriptor.scopes, - account: { - id: '', - label: '', - }, - cloud: false, - domain: descriptor.domain, - }; - } } diff --git a/src/plus/integrations/integrationService.ts b/src/plus/integrations/integrationService.ts index 5888333c39273..38d808b701614 100644 --- a/src/plus/integrations/integrationService.ts +++ b/src/plus/integrations/integrationService.ts @@ -52,6 +52,7 @@ import type { SupportedSelfHostedIntegrationIds, } from './integration'; import { isAzureCloudDomain } from './providers/azureDevOps'; +import { isBitbucketCloudDomain } from './providers/bitbucket'; import { isCloudSelfHostedIntegrationId, isGitHubDotCom, @@ -680,14 +681,16 @@ export class IntegrationService implements Disposable { const get = getOrGetCached.bind(this); switch (remote.provider.id) { - // TODO: Uncomment when we support these integrations - // case 'bitbucket': - // return get(HostingIntegrationId.Bitbucket) as RT; case 'azure-devops': if (isAzureCloudDomain(remote.provider.domain)) { return get(HostingIntegrationId.AzureDevOps) as RT; } return (getOrGetCached === this.get ? Promise.resolve(undefined) : undefined) as RT; + case 'bitbucket': + if (isBitbucketCloudDomain(remote.provider.domain)) { + return get(HostingIntegrationId.Bitbucket) as RT; + } + return (getOrGetCached === this.get ? Promise.resolve(undefined) : undefined) as RT; case 'github': if (remote.provider.domain != null && !isGitHubDotCom(remote.provider.domain)) { return get( @@ -1049,6 +1052,8 @@ export function remoteProviderIdToIntegrationId( switch (remoteProviderId) { case 'azure-devops': return HostingIntegrationId.AzureDevOps; + case 'bitbucket': + return HostingIntegrationId.Bitbucket; case 'github': return HostingIntegrationId.GitHub; case 'gitlab': diff --git a/src/plus/integrations/providers/bitbucket.ts b/src/plus/integrations/providers/bitbucket.ts index 18bdf3928cab3..fd1a5d25e6e0d 100644 --- a/src/plus/integrations/providers/bitbucket.ts +++ b/src/plus/integrations/providers/bitbucket.ts @@ -137,3 +137,8 @@ export class BitbucketIntegration extends HostingIntegration< return Promise.resolve(undefined); } } + +const bitbucketCloudDomainRegex = /^bitbucket\.org$/i; +export function isBitbucketCloudDomain(domain: string | undefined): boolean { + return domain != null && bitbucketCloudDomainRegex.test(domain); +}