Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 @@ -33,6 +33,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Fixes incorrect settings.json entry for Google Gemini 2.0 Flash Thinking causes linter warning ([#4168](https://github.com/gitkraken/vscode-gitlens/issues/4168))
- Fixes multiple autolinks in commit message are broken when enriched ([#4069](https://github.com/gitkraken/vscode-gitlens/issues/4069))
- Fixes `gitlens.hovers.autolinks.enhanced` setting is not respected ([#4174](https://github.com/gitkraken/vscode-gitlens/issues/4174))
- Fixes _Create Pull Request_ feature ([#4142](https://github.com/gitkraken/vscode-gitlens/issues/4142))

## [16.3.3] - 2025-03-13

Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3913,6 +3913,14 @@
"type": "string",
"markdownDescription": "Specifies the format of a commit URL for the custom remote service\n\nAvailable tokens\\\n`${repo}` — repository path\\\n`${id}` — commit SHA"
},
"comparison": {
"type": "string",
"markdownDescription": "Specifies the format of a comparison URL for the custom remote service\n\nAvailable tokens\\\n`${repo}` — repository path\\\n`${ref1}` — ref 1\\\n`${ref2}` — ref 2\\\n`${notation}` — notation"
},
"createPullRequest": {
"type": "string",
"markdownDescription": "Specifies the format of a create pull request URL for the custom remote service\n\nAvailable tokens\\\n`${repo}` — repository path\\\n`${base}` — base branch\\\n`${compare}` — compare branch"
},
"file": {
"type": "string",
"markdownDescription": "Specifies the format of a file URL for the custom remote service\n\nAvailable tokens\\\n`${repo}` — repository path\\\n`${file}` — file name\\\n`${line}` — formatted line information"
Expand Down
2 changes: 1 addition & 1 deletion src/commands/createPullRequestOnRemote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { GlCommandBase } from './commandBase';
import type { OpenOnRemoteCommandArgs } from './openOnRemote';

export interface CreatePullRequestOnRemoteCommandArgs {
base?: string;
base: string | undefined;
compare: string;
remote: string;
repoPath: string;
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ export interface RemotesUrlsConfig {
readonly branch: string;
readonly commit: string;
readonly comparison?: string;
readonly createPullRequest?: string;
readonly file: string;
readonly fileInBranch: string;
readonly fileInCommit: string;
Expand Down
2 changes: 1 addition & 1 deletion src/env/node/git/localGitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
case 'gitea':
case 'gerrit':
case 'google-source':
url = remote.provider.url({ type: RemoteResourceType.Repo });
url = await remote.provider.url({ type: RemoteResourceType.Repo });
if (url == null) return ['private', remote];

break;
Expand Down
7 changes: 7 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,10 @@ export class RequestsAreBlockedTemporarilyError extends Error {
Error.captureStackTrace?.(this, RequestsAreBlockedTemporarilyError);
}
}

export class RequiresIntegrationError extends Error {
constructor(message: string) {
super(message);
Error.captureStackTrace?.(this, RequiresIntegrationError);
}
}
2 changes: 1 addition & 1 deletion src/git/models/remoteResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type RemoteResource =
| {
type: RemoteResourceType.CreatePullRequest;
base: {
branch?: string;
branch: string | undefined;
remote: { path: string; url: string };
};
compare: {
Expand Down
72 changes: 69 additions & 3 deletions src/git/remotes/azure-devops.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Range, Uri } from 'vscode';
import type { AutolinkReference, DynamicAutolinkReference } from '../../autolinks/models/autolinks';
import type { Container } from '../../container';
import { HostingIntegration } from '../../plus/integrations/integration';
import { remoteProviderIdToIntegrationId } from '../../plus/integrations/integrationService';
import { parseAzureHttpsUrl } from '../../plus/integrations/providers/azure/models';
import type { Brand, Unbrand } from '../../system/brand';
import type { Repository } from '../models/repository';
import type { GkProviderId } from '../models/repositoryIdentities';
Expand All @@ -17,7 +21,14 @@ const rangeRegex = /line=(\d+)(?:&lineEnd=(\d+))?/;

export class AzureDevOpsRemote extends RemoteProvider {
private readonly project: string | undefined;
constructor(domain: string, path: string, protocol?: string, name?: string, legacy: boolean = false) {
constructor(
private readonly container: Container,
domain: string,
path: string,
protocol?: string,
name?: string,
legacy: boolean = false,
) {
let repoProject;
if (sshDomainRegex.test(domain)) {
path = path.replace(sshPathRegex, '');
Expand Down Expand Up @@ -182,8 +193,44 @@ export class AzureDevOpsRemote extends RemoteProvider {
return this.encodeUrl(`${this.baseUrl}/commit/${sha}`);
}

protected override getUrlForComparison(base: string, compare: string, _notation: '..' | '...'): string {
return this.encodeUrl(`${this.baseUrl}/branchCompare?baseVersion=GB${base}&targetVersion=GB${compare}`);
protected override getUrlForComparison(base: string, head: string, _notation: '..' | '...'): string {
return this.encodeUrl(`${this.baseUrl}/branchCompare?baseVersion=GB${base}&targetVersion=GB${head}`);
}

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(
base: { branch?: string; remote: { path: string; url: string } },
head: { branch: string; remote: { path: string; url: string } },
): Promise<string | undefined> {
const query = new URLSearchParams({ sourceRef: head.branch, targetRef: base.branch ?? '' });
if (base.remote.url !== head.remote.url) {
const parsedBaseUrl = parseAzureUrl(base.remote.url);
if (parsedBaseUrl == null) {
return undefined;
}
const { org: baseOrg, project: baseProject, repo: baseName } = parsedBaseUrl;
const targetDesc = { project: baseProject, name: baseName, owner: baseOrg };

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('targetRepositoryId', targetRepoId);
// query.set('sourceRepositoryId', compare.repoId); // ?? looks like not needed
}

return `${this.encodeUrl(`${this.getRepoBaseUrl(head.remote.path)}/pullrequestcreate`)}?${query.toString()}`;
}

protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
Expand All @@ -207,3 +254,22 @@ export class AzureDevOpsRemote extends RemoteProvider {
return this.encodeUrl(`${this.baseUrl}?path=/${fileName}${line}`);
}
}

const azureSshUrlRegex = /^(?:[^@]+@)?([^:]+):v\d\//;
function parseAzureUrl(url: string): { org: string; project: string; repo: string } | undefined {
if (azureSshUrlRegex.test(url)) {
// Examples of SSH urls:
// - old one: [email protected]:v3/bbbchiv/MyFirstProject/test
// - modern one: [email protected]:v3/bbbchiv2/MyFirstProject/test
url = url.replace(azureSshUrlRegex, '');
const match = orgAndProjectRegex.exec(url);
if (match != null) {
const [, org, project, rest] = match;
return { org: org, project: project, repo: rest };
}
} else {
const [org, project, rest] = parseAzureHttpsUrl(url);
return { org: org, project: project, repo: rest };
}
return undefined;
}
22 changes: 20 additions & 2 deletions src/git/remotes/bitbucket-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,26 @@
return this.encodeUrl(`${this.baseUrl}/commits/${sha}`);
}

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

protected override getUrlForCreatePullRequest(
base: { branch?: string; remote: { path: string; url: string } },
head: { branch: string; remote: { path: string; url: string } },
options?: { title?: string; description?: string },
): string | undefined {
const query = new URLSearchParams({ sourceBranch: head.branch, targetBranch: base.branch ?? '' });
// TODO: figure this out
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this just need to be removed or figured out still?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eamodio It's been waiting for bitbucket-server to be merged. But than I've switched to another task. So, yes, needs to be figured out.

// query.set('targetRepoId', base.repoId);
if (options?.title) {
query.set('title', options.title);
}
if (options?.description) {
query.set('description', options.description);
}

return `${this.encodeUrl(`${this.baseUrl}/pull-requests?create`)}&${query.toString()}`;
}

protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
Expand Down
17 changes: 14 additions & 3 deletions src/git/remotes/bitbucket.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Range, Uri } from 'vscode';
import type { AutolinkReference, DynamicAutolinkReference } from '../../autolinks/models/autolinks';
import type { RepositoryDescriptor } from '../../plus/integrations/integration';
import type { Brand, Unbrand } from '../../system/brand';
import type { Repository } from '../models/repository';
import type { GkProviderId } from '../models/repositoryIdentities';
Expand All @@ -10,7 +11,7 @@ import { RemoteProvider } from './remoteProvider';
const fileRegex = /^\/([^/]+)\/([^/]+?)\/src(.+)$/i;
const rangeRegex = /^lines-(\d+)(?::(\d+))?$/;

export class BitbucketRemote extends RemoteProvider {
export class BitbucketRemote extends RemoteProvider<RepositoryDescriptor> {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
super(domain, path, protocol, name, custom);
}
Expand Down Expand Up @@ -142,8 +143,18 @@ export class BitbucketRemote extends RemoteProvider {
return this.encodeUrl(`${this.baseUrl}/commits/${sha}`);
}

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

protected override getUrlForCreatePullRequest(
base: { branch?: string; remote: { path: string; url: string } },
head: { branch: string; remote: { path: string; url: string } },
_options?: { title?: string; description?: string },
): string | undefined {
const { owner, name } = this.repoDesc;
const query = new URLSearchParams({ source: head.branch, dest: `${owner}/${name}::${base.branch ?? ''}` });
return `${this.encodeUrl(`${this.getRepoBaseUrl(head.remote.path)}/pull-requests/new`)}?${query.toString()}`;
}

protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
Expand Down
17 changes: 15 additions & 2 deletions src/git/remotes/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,23 @@ export class CustomRemote extends RemoteProvider {
return this.getUrl(this.urls.commit, this.getContext({ id: sha }));
}

protected override getUrlForComparison(base: string, compare: string, notation: '..' | '...'): string | undefined {
protected override getUrlForComparison(base: string, head: string, notation: '..' | '...'): string | undefined {
if (this.urls.comparison == null) return undefined;

return this.getUrl(this.urls.comparison, this.getContext({ ref1: base, ref2: compare, notation: notation }));
return this.getUrl(this.urls.comparison, this.getContext({ ref1: base, ref2: head, notation: notation }));
}

protected override getUrlForCreatePullRequest(
base: { branch?: string; remote: { path: string; url: string } },
compare: { branch: string; remote: { path: string; url: string } },
_options?: { title?: string; description?: string },
): string | undefined {
if (this.urls.createPullRequest == null) return undefined;

return this.getUrl(
this.urls.createPullRequest,
this.getContext({ base: base.branch ?? '', head: compare.branch }),
);
}

protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
Expand Down
14 changes: 14 additions & 0 deletions src/git/remotes/gerrit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ export class GerritRemote extends RemoteProvider {
return this.encodeUrl(`${this.baseReviewUrl}/q/${sha}`);
}

protected override getUrlForComparison(base: string, head: string, notation: '..' | '...'): string | undefined {
return this.encodeUrl(`${this.baseReviewUrl}/q/${base}${notation}${head}`);
}

protected override getUrlForCreatePullRequest(
base: { branch?: string; remote: { path: string; url: string } },
head: { branch: string; remote: { path: string; url: string } },
_options?: { title?: string; description?: string },
): string | undefined {
const query = new URLSearchParams({ sourceBranch: head.branch, targetBranch: base.branch ?? '' });

return this.encodeUrl(`${this.baseReviewUrl}/createPullRequest?${query.toString()}`);
}

protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
const line = range != null ? `#${range.start.line}` : '';

Expand Down
19 changes: 17 additions & 2 deletions src/git/remotes/gitea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,23 @@ export class GiteaRemote extends RemoteProvider {
return this.encodeUrl(`${this.baseUrl}/commit/${sha}`);
}

protected override getUrlForComparison(ref1: string, ref2: string, _notation: '..' | '...'): string {
return this.encodeUrl(`${this.baseUrl}/compare/${ref1}...${ref2}`);
protected override getUrlForComparison(base: string, head: string, _notation: '..' | '...'): string {
return this.encodeUrl(`${this.baseUrl}/compare/${base}...${head}`);
}

protected override getUrlForCreatePullRequest(
base: { branch?: string; remote: { path: string; url: string } },
head: { branch: string; remote: { path: string; url: string } },
options?: { title?: string; description?: string },
): string | undefined {
const query = new URLSearchParams({ head: head.branch, base: base.branch ?? '' });
if (options?.title) {
query.set('title', options.title);
}
if (options?.description) {
query.set('body', options.description);
}
return `${this.encodeUrl(`${this.baseUrl}/pulls/new`)}?${query.toString()}`;
}

protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
Expand Down
27 changes: 20 additions & 7 deletions src/git/remotes/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,20 +276,33 @@ export class GitHubRemote extends RemoteProvider<GitHubRepositoryDescriptor> {
return this.encodeUrl(`${this.baseUrl}/commit/${sha}`);
}

protected override getUrlForComparison(base: string, compare: string, notation: '..' | '...'): string {
return this.encodeUrl(`${this.baseUrl}/compare/${base}${notation}${compare}`);
protected override getUrlForComparison(base: string, head: string, notation: '..' | '...'): string {
return this.encodeUrl(`${this.baseUrl}/compare/${base}${notation}${head}`);
}

protected override getUrlForCreatePullRequest(
base: { branch?: string; remote: { path: string; url: string } },
compare: { branch: string; remote: { path: string; url: string } },
head: { branch: string; remote: { path: string; url: string } },
options?: { title?: string; description?: string },
): string | undefined {
if (base.remote.url === compare.remote.url) {
return this.encodeUrl(`${this.baseUrl}/pull/new/${base.branch ?? 'HEAD'}...${compare.branch}`);
const query = new URLSearchParams();
if (options?.title) {
query.set('title', options.title);
}
if (options?.description) {
query.set('body', options.description);
}

if (base.remote.url === head.remote.url) {
return `${this.encodeUrl(
`${this.baseUrl}/pull/new/${base.branch ?? 'HEAD'}...${head.branch}`,
)}?${query.toString()}`;
}

const [owner] = compare.remote.path.split('/', 1);
return this.encodeUrl(`${this.baseUrl}/pull/new/${base.branch ?? 'HEAD'}...${owner}:${compare.branch}`);
const [owner] = head.remote.path.split('/', 1);
return `${this.encodeUrl(
`${this.baseUrl}/pull/new/${base.branch ?? 'HEAD'}...${owner}:${head.branch}`,
)}?${query.toString()}`;
}

protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
Expand Down
Loading