Skip to content

Generates Bitbucket Data Center PR URL and retrieves repoId for cross-forks #4184

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 2 commits into from
Apr 14, 2025
Merged
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
4 changes: 2 additions & 2 deletions src/git/parsers/remoteParser.ts
Original file line number Diff line number Diff line change
@@ -52,15 +52,15 @@ export function parseGitRemotes(
scheme,
domain,
path,
remoteProviderMatcher(url, domain, path),
remoteProviderMatcher(url, domain, path, scheme),
[{ url: url, type: type as GitRemoteType }],
);
remotes.set(name, remote);
} else {
remote.urls.push({ url: url, type: type as GitRemoteType });
if (remote.provider != null && type !== 'push') continue;

const provider = remoteProviderMatcher(url, domain, path);
const provider = remoteProviderMatcher(url, domain, path, scheme);
if (provider == null) continue;

remote = new GitRemote(container, repoPath, name, scheme, domain, path, provider, remote.urls);
48 changes: 35 additions & 13 deletions src/git/remotes/bitbucket-server.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@ import type { Range, Uri } from 'vscode';
import type { AutolinkReference, DynamicAutolinkReference } from '../../autolinks/models/autolinks';
import type { Source } from '../../constants.telemetry';
import type { Container } from '../../container';
import { HostingIntegration } from '../../plus/integrations/integration';
import { remoteProviderIdToIntegrationId } from '../../plus/integrations/integrationService';
import type { Brand, Unbrand } from '../../system/brand';
import type { CreatePullRequestRemoteResource } from '../models/remoteResource';
import type { Repository } from '../models/repository';
@@ -62,18 +64,16 @@ export class BitbucketServerRemote extends RemoteProvider {
}

protected override get baseUrl(): string {
const [project, repo] = this.splitPath();
const [project, repo] = this.splitPath(this.path);
return `${this.protocol}://${this.domain}/projects/${project}/repos/${repo}`;
}

protected override splitPath(): [string, string] {
if (this.path.startsWith('scm/') && this.path.indexOf('/') !== this.path.lastIndexOf('/')) {
const path = this.path.replace('scm/', '');
const index = path.indexOf('/');
return [path.substring(0, index), path.substring(index + 1)];
protected override splitPath(path: string): [string, string] {
if (path.startsWith('scm/') && path.indexOf('/') !== path.lastIndexOf('/')) {
return super.splitPath(path.replace('scm/', ''));
}

return super.splitPath();
return super.splitPath(path);
}

override get icon(): string {
@@ -191,7 +191,13 @@ export class BitbucketServerRemote extends RemoteProvider {
}

protected override getUrlForComparison(base: string, head: string, _notation: GitRevisionRangeNotation): string {
return this.encodeUrl(`${this.baseUrl}/branches/compare/${base}%0D${head}`).replaceAll('%250D', '%0D');
return this.encodeUrl(`${this.baseUrl}/branches/compare/${head}\r${base}`);
}

override async isReadyForForCrossForkPullRequestUrls(): Promise<boolean> {
const integrationId = remoteProviderIdToIntegrationId(this.id);
const integration = integrationId && (await this.container.integrations.get(integrationId));
return integration?.maybeConnected ?? integration?.isConnected() ?? false;
}

protected override async getUrlForCreatePullRequest(
@@ -210,17 +216,33 @@ export class BitbucketServerRemote extends RemoteProvider {
}

const query = new URLSearchParams({ sourceBranch: head.branch, targetBranch: base.branch ?? '' });
// TODO: figure this out
// query.set('targetRepoId', base.repoId);

const [baseOwner, baseName] = this.splitPath(base.remote.path);
if (base.remote.url !== head.remote.url) {
const targetDesc = {
owner: baseOwner,
name: baseName,
};
const integrationId = remoteProviderIdToIntegrationId(this.id);
const integration = integrationId && (await this.container.integrations.get(integrationId));
let targetRepoId = undefined;
if (integration?.isConnected && integration instanceof HostingIntegration) {
targetRepoId = (await integration.getRepoInfo?.(targetDesc))?.id;
}
if (!targetRepoId) {
return undefined;
}
query.set('targetRepoId', targetRepoId);
}
if (details?.title) {
query.set('title', details.title);
}
if (details?.description) {
query.set('description', details.description);
}

return `${this.encodeUrl(`${this.baseUrl}/pull-requests?create`)}&${query.toString()}`;
const [headOwner, headName] = this.splitPath(head.remote.path);
return `${this.encodeUrl(
`${this.protocol}://${this.domain}/projects/${headOwner}/repos/${headName}/pull-requests?create`,
)}&${query.toString()}`;
}

protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
2 changes: 1 addition & 1 deletion src/git/remotes/custom.ts
Original file line number Diff line number Diff line change
@@ -128,7 +128,7 @@ export class CustomRemote extends RemoteProvider {
}

private getContext(additionalContext?: Record<string, string>) {
const [repoBase, repoPath] = this.splitPath();
const [repoBase, repoPath] = this.splitPath(this.path);
const context: Record<string, string> = {
repo: this.path,
repoBase: repoBase,
4 changes: 2 additions & 2 deletions src/git/remotes/github.ts
Original file line number Diff line number Diff line change
@@ -183,7 +183,7 @@ export class GitHubRemote extends RemoteProvider<GitHubRepositoryDescriptor> {
}

override get avatarUri(): Uri {
const [owner] = this.splitPath();
const [owner] = this.splitPath(this.path);
return Uri.parse(`https://avatars.githubusercontent.com/${owner}`);
}

@@ -207,7 +207,7 @@ export class GitHubRemote extends RemoteProvider<GitHubRepositoryDescriptor> {

@memoize()
override get repoDesc(): GitHubRepositoryDescriptor {
const [owner, repo] = this.splitPath();
const [owner, repo] = this.splitPath(this.path);
return { key: this.remoteKey, owner: owner, name: repo };
}

2 changes: 1 addition & 1 deletion src/git/remotes/gitlab.ts
Original file line number Diff line number Diff line change
@@ -305,7 +305,7 @@ export class GitLabRemote extends RemoteProvider<GitLabRepositoryDescriptor> {

@memoize()
override get repoDesc(): GitLabRepositoryDescriptor {
const [owner, repo] = this.splitPath();
const [owner, repo] = this.splitPath(this.path);
return { key: this.remoteKey, owner: owner, name: repo };
}

10 changes: 5 additions & 5 deletions src/git/remotes/remoteProvider.ts
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ export abstract class RemoteProvider<T extends ResourceDescriptor = ResourceDesc
}

get owner(): string | undefined {
return this.splitPath()[0];
return this.splitPath(this.path)[0];
}

@memoize()
@@ -108,7 +108,7 @@ export abstract class RemoteProvider<T extends ResourceDescriptor = ResourceDesc
}

get repoName(): string | undefined {
return this.splitPath()[1];
return this.splitPath(this.path)[1];
}

abstract get id(): RemoteProviderId;
@@ -186,9 +186,9 @@ export abstract class RemoteProvider<T extends ResourceDescriptor = ResourceDesc
return `${name}${this.custom ? ` (${this.domain})` : ''}`;
}

protected splitPath(): [string, string] {
const index = this.path.indexOf('/');
return [this.path.substring(0, index), this.path.substring(index + 1)];
protected splitPath(path: string): [string, string] {
const index = path.indexOf('/');
return [path.substring(0, index), path.substring(index + 1)];
}

protected abstract getUrlForBranch(branch: string): string;
35 changes: 25 additions & 10 deletions src/git/remotes/remoteProviders.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ import type { RemoteProvider } from './remoteProvider';
export type RemoteProviders = {
custom: boolean;
matcher: string | RegExp;
creator: (container: Container, domain: string, path: string) => RemoteProvider;
creator: (container: Container, domain: string, path: string, scheme?: string) => RemoteProvider;
}[];

const builtInProviders: RemoteProviders = [
@@ -80,16 +80,26 @@ const builtInProviders: RemoteProviders = [

const cloudProviderCreatorsMap: Record<
CloudSelfHostedIntegrationId,
(container: Container, domain: string, path: string) => RemoteProvider
(container: Container, domain: string, path: string, scheme: string | undefined) => RemoteProvider
> = {
[SelfHostedIntegrationId.CloudGitHubEnterprise]: (container: Container, domain: string, path: string) =>
new GitHubRemote(container, domain, path),
[SelfHostedIntegrationId.CloudGitLabSelfHosted]: (container: Container, domain: string, path: string) =>
new GitLabRemote(container, domain, path),
[SelfHostedIntegrationId.BitbucketServer]: (container: Container, domain: string, path: string) =>
new BitbucketServerRemote(container, domain, path),
[SelfHostedIntegrationId.BitbucketServer]: (
container: Container,
domain: string,
path: string,
scheme: string | undefined,
) => new BitbucketServerRemote(container, domain, path, cleanProtocol(scheme)),
};

const dirtyProtocolPattern = /(\w+)\W*/;
function cleanProtocol(scheme: string | undefined): string | undefined {
const match = scheme?.match(dirtyProtocolPattern);
return match?.[1] ?? undefined;
}

export function loadRemoteProviders(
cfg: RemotesConfig[] | null | undefined,
configuredIntegrations?: ConfiguredIntegrationDescriptor[],
@@ -181,16 +191,16 @@ function getCustomProviderCreator(cfg: RemotesConfig) {
export async function getRemoteProviderMatcher(
container: Container,
providers?: RemoteProviders,
): Promise<(url: string, domain: string, path: string) => RemoteProvider | undefined> {
): Promise<(url: string, domain: string, path: string, sheme: string | undefined) => RemoteProvider | undefined> {
if (providers == null) {
providers = loadRemoteProviders(
configuration.get('remotes', null),
await container.integrations.getConfigured(),
);
}

return (url: string, domain: string, path: string) =>
createBestRemoteProvider(container, providers, url, domain, path);
return (url: string, domain: string, path: string, scheme) =>
createBestRemoteProvider(container, providers, url, domain, path, scheme);
}

function createBestRemoteProvider(
@@ -199,22 +209,27 @@ function createBestRemoteProvider(
url: string,
domain: string,
path: string,
scheme: string | undefined,
): RemoteProvider | undefined {
try {
const key = domain.toLowerCase();
for (const { custom, matcher, creator } of providers) {
if (typeof matcher === 'string') {
if (matcher === key) return creator(container, domain, path);
if (matcher === key) {
return creator(container, domain, path, scheme);
}

continue;
}

if (matcher.test(key)) return creator(container, domain, path);
if (matcher.test(key)) {
return creator(container, domain, path, scheme);
}
if (!custom) continue;

const match = matcher.exec(url);
if (match != null) {
return creator(container, match[1], match[2]);
return creator(container, match[1], match[2], scheme);
}
}

2 changes: 1 addition & 1 deletion src/plus/drafts/draftsService.ts
Original file line number Diff line number Diff line change
@@ -725,7 +725,7 @@ export class DraftService implements Disposable {
name = data.provider.repoName;
} else if (data.remote?.url != null && data.remote?.domain != null && data.remote?.path != null) {
const matcher = await getRemoteProviderMatcher(this.container);
const provider = matcher(data.remote.url, data.remote.domain, data.remote.path);
const provider = matcher(data.remote.url, data.remote.domain, data.remote.path, undefined);
name = provider?.repoName ?? data.remote.path;
} else {
name =
9 changes: 9 additions & 0 deletions src/plus/integrations/providers/bitbucket-server.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import type { IntegrationAuthenticationService } from '../authentication/integra
import type { ProviderAuthenticationSession } from '../authentication/models';
import { HostingIntegration } from '../integration';
import type { BitbucketRepositoryDescriptor } from './bitbucket/models';
import type { ProviderRepository } from './models';
import { fromProviderPullRequest, providersMetadata } from './models';
import type { ProvidersApi } from './providersApi';

@@ -155,6 +156,14 @@ export class BitbucketServerIntegration extends HostingIntegration<
return Promise.resolve(undefined);
}

public override async getRepoInfo(repo: { owner: string; name: string }): Promise<ProviderRepository | undefined> {
const api = await this.getProvidersApi();
return api.getRepo(this.id, repo.owner, repo.name, undefined, {
accessToken: this._session?.accessToken,
baseUrl: this.apiBaseUrl,
});
}

protected override async getProviderRepositoryMetadata(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ export class RemotesGitSubProvider extends RemotesGitProviderBase {
const [, owner, repo] = uri.path.split('/', 3);

const url = `https://github.com/${owner}/${repo}.git`;
const protocol = 'https';
const domain = 'github.com';
const path = `${owner}/${repo}`;

@@ -27,10 +28,10 @@ export class RemotesGitSubProvider extends RemotesGitProviderBase {
this.container,
repoPath,
'origin',
'https',
protocol,
domain,
path,
(await getRemoteProviderMatcher(this.container, providers))(url, domain, path),
(await getRemoteProviderMatcher(this.container, providers))(url, domain, path, protocol),
[
{ type: 'fetch', url: url },
{ type: 'push', url: url },
1 change: 1 addition & 0 deletions src/plus/integrations/providers/providersApi.ts
Original file line number Diff line number Diff line change
@@ -221,6 +221,7 @@ export class ProvidersApi {
[SelfHostedIntegrationId.BitbucketServer]: {
...providersMetadata[SelfHostedIntegrationId.BitbucketServer],
provider: providerApis.bitbucketServer,
getRepoFn: providerApis.bitbucketServer.getRepo.bind(providerApis.bitbucketServer),
getCurrentUserFn: providerApis.bitbucketServer.getCurrentUser.bind(
providerApis.bitbucketServer,
) as GetCurrentUserFn,