Skip to content

Adds Bitbucket PR Support: remotes, autolinks, home, etc. #4070

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds AI model status and model switcher to the _Home_ view ([#4064](https://github.com/gitkraken/vscode-gitlens/issues/4064))
- Adds Anthropic Claude 3.7 Sonnet model for GitLens' AI features ([#4101](https://github.com/gitkraken/vscode-gitlens/issues/4101))
- Adds Google Gemini 2.0 Flash-Lite model for GitLens' AI features ([#4104](https://github.com/gitkraken/vscode-gitlens/issues/4104))
- Adds integration with Bitbucket Cloud by showing enriched links to PRs and issues [#4045](https://github.com/gitkraken/vscode-gitlens/issues/4045)

### Changed

Expand Down
5 changes: 4 additions & 1 deletion src/autolinks/autolinksProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,12 @@ export class AutolinksProvider implements Disposable {
? integration.getIssueOrPullRequest(
link.descriptor ?? remote.provider.repoDesc,
this.getAutolinkEnrichableId(link),
{ type: link.type },
)
: link.descriptor != null
? linkIntegration?.getIssueOrPullRequest(link.descriptor, this.getAutolinkEnrichableId(link))
? linkIntegration?.getIssueOrPullRequest(link.descriptor, this.getAutolinkEnrichableId(link), {
type: link.type,
})
: undefined;
enrichedAutolinks.set(id, [issueOrPullRequestPromise, link]);
}
Expand Down
25 changes: 25 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ConfiguredIntegrationService } from './plus/integrations/authentication
import { IntegrationAuthenticationService } from './plus/integrations/authentication/integrationAuthenticationService';
import { IntegrationService } from './plus/integrations/integrationService';
import type { AzureDevOpsApi } from './plus/integrations/providers/azure/azure';
import type { BitbucketApi } from './plus/integrations/providers/bitbucket/bitbucket';
import type { GitHubApi } from './plus/integrations/providers/github/github';
import type { GitLabApi } from './plus/integrations/providers/gitlab/gitlab';
import { EnrichmentService } from './plus/launchpad/enrichmentService';
Expand Down Expand Up @@ -487,6 +488,30 @@ export class Container {
return this._azure;
}

private _bitbucket: Promise<BitbucketApi | undefined> | undefined;
get bitbucket(): Promise<BitbucketApi | undefined> {
if (this._bitbucket == null) {
async function load(this: Container) {
try {
const bitbucket = new (
await import(
/* webpackChunkName: "integrations" */ './plus/integrations/providers/bitbucket/bitbucket'
)
).BitbucketApi(this);
this._disposables.push(bitbucket);
return bitbucket;
} catch (ex) {
Logger.error(ex);
return undefined;
}
}

this._bitbucket = load.call(this);
}

return this._bitbucket;
}

private _github: Promise<GitHubApi | undefined> | undefined;
get github(): Promise<GitHubApi | undefined> {
if (this._github == null) {
Expand Down
12 changes: 9 additions & 3 deletions src/plus/integrations/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { PagedResult } from '../../git/gitProvider';
import type { Account, UnidentifiedAuthor } from '../../git/models/author';
import type { DefaultBranch } from '../../git/models/defaultBranch';
import type { Issue, IssueShape } from '../../git/models/issue';
import type { IssueOrPullRequest } from '../../git/models/issueOrPullRequest';
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../git/models/issueOrPullRequest';
import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../git/models/repositoryMetadata';
import type { PullRequestUrlIdentity } from '../../git/utils/pullRequest.utils';
Expand Down Expand Up @@ -450,7 +450,7 @@ export abstract class IntegrationBase<
async getIssueOrPullRequest(
resource: T,
id: string,
options?: { expiryOverride?: boolean | number },
options?: { expiryOverride?: boolean | number; type?: IssueOrPullRequestType },
): Promise<IssueOrPullRequest | undefined> {
const scope = getLogScope();

Expand All @@ -464,7 +464,12 @@ export abstract class IntegrationBase<
() => ({
value: (async () => {
try {
const result = await this.getProviderIssueOrPullRequest(this._session!, resource, id);
const result = await this.getProviderIssueOrPullRequest(
this._session!,
resource,
id,
options?.type,
);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
Expand All @@ -481,6 +486,7 @@ export abstract class IntegrationBase<
session: ProviderAuthenticationSession,
resource: T,
id: string,
type: undefined | IssueOrPullRequestType,
): Promise<IssueOrPullRequest | undefined>;

@debug()
Expand Down
32 changes: 16 additions & 16 deletions src/plus/integrations/providers/azure/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,22 +376,22 @@ export class AzureDevOpsApi implements Disposable {
throw new RequestNotFoundError(ex);
case 401: // Unauthorized
throw new AuthenticationError('azureDevOps', AuthenticationErrorReason.Unauthorized, ex);
// TODO: Learn the Azure API docs and put it in order:
// case 403: // Forbidden
// if (ex.message.includes('rate limit')) {
// let resetAt: number | undefined;

// const reset = ex.response?.headers?.get('x-ratelimit-reset');
// if (reset != null) {
// resetAt = parseInt(reset, 10);
// if (Number.isNaN(resetAt)) {
// resetAt = undefined;
// }
// }

// throw new RequestRateLimitError(ex, token, resetAt);
// }
// throw new AuthenticationError('azure', AuthenticationErrorReason.Forbidden, ex);
case 403: // Forbidden
// TODO: Learn the Azure API docs and put it in order:
// if (ex.message.includes('rate limit')) {
// let resetAt: number | undefined;

// const reset = ex.response?.headers?.get('x-ratelimit-reset');
// if (reset != null) {
// resetAt = parseInt(reset, 10);
// if (Number.isNaN(resetAt)) {
// resetAt = undefined;
// }
// }

// throw new RequestRateLimitError(ex, token, resetAt);
// }
throw new AuthenticationError('azure', AuthenticationErrorReason.Forbidden, ex);
case 500: // Internal Server Error
Logger.error(ex, scope);
if (ex.response != null) {
Expand Down
36 changes: 27 additions & 9 deletions src/plus/integrations/providers/bitbucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HostingIntegrationId } from '../../../constants.integrations';
import type { Account } from '../../../git/models/author';
import type { DefaultBranch } from '../../../git/models/defaultBranch';
import type { Issue, IssueShape } from '../../../git/models/issue';
import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest';
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest';
import type { PullRequest, PullRequestMergeMethod, PullRequestState } from '../../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata';
import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider';
Expand Down Expand Up @@ -75,11 +75,22 @@ export class BitbucketIntegration extends HostingIntegration<
}

protected override async getProviderIssueOrPullRequest(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
_id: string,
{ accessToken }: AuthenticationSession,
repo: BitbucketRepositoryDescriptor,
id: string,
type: undefined | IssueOrPullRequestType,
): Promise<IssueOrPullRequest | undefined> {
return Promise.resolve(undefined);
return (await this.container.bitbucket)?.getIssueOrPullRequest(
this,
accessToken,
repo.owner,
repo.name,
id,
this.apiBaseUrl,
{
type: type,
},
);
}

protected override async getProviderIssue(
Expand All @@ -91,15 +102,22 @@ export class BitbucketIntegration extends HostingIntegration<
}

protected override async getProviderPullRequestForBranch(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
_branch: string,
{ accessToken }: AuthenticationSession,
repo: BitbucketRepositoryDescriptor,
branch: string,
_options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
return Promise.resolve(undefined);
return (await this.container.bitbucket)?.getPullRequestForBranch(
this,
accessToken,
repo.owner,
repo.name,
branch,
this.apiBaseUrl,
);
}

protected override async getProviderPullRequestForCommit(
Expand Down
Loading