Skip to content
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 @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Added

- Adds AI model status and model switcher to the _Home_ view ([#4064](https://github.com/gitkraken/vscode-gitlens/issues/4064))
- Adds an optional "Create with AI" button to generate pull requests using AI assistance for GitHub and GitLab.
- Adds Anthropic Claude 3.7 Sonnet model for GitLens' AI features ([#4101](https://github.com/gitkraken/vscode-gitlens/issues/4101))
- Adds Google Gemini 2.5 Pro (Experimental) and Gemini 2.0 Flash-Lite model for GitLens' AI features ([#4104](https://github.com/gitkraken/vscode-gitlens/issues/4104))
- Adds integration with Bitbucket Cloud and Data Center ([#3916](https://github.com/gitkraken/vscode-gitlens/issues/3916))
Expand Down
23 changes: 23 additions & 0 deletions docs/telemetry-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,29 @@ or

or

```typescript
{
'duration': number,
'failed.error': string,
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
'input.length': number,
'model.id': string,
'model.provider.id': 'anthropic' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'openai' | 'vscode' | 'xai',
'model.provider.name': string,
'output.length': number,
'retry.count': number,
'type': 'createPullRequest',
'usage.completionTokens': number,
'usage.limits.limit': number,
'usage.limits.resetsOn': string,
'usage.limits.used': number,
'usage.promptTokens': number,
'usage.totalTokens': number
}
```

or

```typescript
{
'duration': number,
Expand Down
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4152,7 +4152,7 @@
"preview"
]
},
"gitlens.ai.generateCloudPatchMessage.customInstructions": {
"gitlens.ai.generateCreateCloudPatch.customInstructions": {
"type": "string",
"default": null,
"markdownDescription": "Specifies custom instructions to provide to the AI provider when generating a cloud patch title and description",
Expand All @@ -4162,7 +4162,7 @@
"preview"
]
},
"gitlens.ai.generateCodeSuggestMessage.customInstructions": {
"gitlens.ai.generateCreateCodeSuggest.customInstructions": {
"type": "string",
"default": null,
"markdownDescription": "Specifies custom instructions to provide to the AI provider when generating a code suggest title and description",
Expand All @@ -4171,6 +4171,16 @@
"tags": [
"preview"
]
},
"gitlens.ai.generateCreatePullRequest.customInstructions": {
"type": "string",
"default": null,
"markdownDescription": "Specifies custom instructions to provide to the AI provider when generating a pull request title and description",
"scope": "window",
"order": 500,
"tags": [
"preview"
]
}
}
},
Expand Down
3 changes: 3 additions & 0 deletions src/api/gitlens.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Disposable } from 'vscode';
import type { Sources } from '../constants.telemetry';

export type { Disposable } from 'vscode';

Expand All @@ -24,6 +25,8 @@ export interface CreatePullRequestActionContext {
readonly url?: string;
}
| undefined;
readonly source?: Sources;
readonly useAI?: boolean;
}

export interface OpenPullRequestActionContext {
Expand Down
30 changes: 29 additions & 1 deletion src/commands/createPullRequestOnRemote.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { window } from 'vscode';
import type { Sources } from '../constants.telemetry';
import type { Container } from '../container';
import type { GitRemote } from '../git/models/remote';
import type { RemoteResource } from '../git/models/remoteResource';
Expand All @@ -17,6 +18,8 @@ export interface CreatePullRequestOnRemoteCommandArgs {
repoPath: string;

clipboard?: boolean;
source?: Sources;
useAI?: boolean;
}

@command()
Expand Down Expand Up @@ -72,8 +75,33 @@ export class CreatePullRequestOnRemoteCommand extends GlCommandBase {
},
compare: {
branch: args.compare,
remote: { path: compareRemote.path, url: compareRemote.url },
remote: { path: compareRemote.path, url: compareRemote.url, name: compareRemote.name },
},
describePullRequest: !args.useAI
? undefined
: async (completedResource: RemoteResource & { type: RemoteResourceType.CreatePullRequest }) => {
const base = completedResource.base;
const compare = completedResource.compare;
if (!base?.remote || !compare?.remote || !base?.branch || !compare?.branch) {
return undefined;
}
const baseRef = `${base.remote.name}/${base.branch}`;
const compareRef = `${compare.remote.name}/${compare.branch}`;
try {
const result = await this.container.ai.generatePullRequestMessage(
repo,
baseRef,
compareRef,
{
source: args.source ?? 'scm-input',
},
);
return result?.parsed;
} catch (e) {
void window.showErrorMessage(`Unable to generate pull request details: ${e}`);
return undefined;
}
},
};

void (await executeCommand<OpenOnRemoteCommandArgs>('gitlens.openOnRemote', {
Expand Down
11 changes: 9 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,24 @@ interface AIConfig {
readonly generateChangelog: {
readonly customInstructions: string;
};
readonly generatePullRequestMessage: {
readonly customInstructions: string;
readonly enabled: boolean;
};
readonly generateCommitMessage: {
readonly customInstructions: string;
readonly enabled: boolean;
};
readonly generateStashMessage: {
readonly customInstructions: string;
};
readonly generateCloudPatchMessage: {
readonly generateCreateCloudPatch: {
readonly customInstructions: string;
};
readonly generateCreateCodeSuggest: {
readonly customInstructions: string;
};
readonly generateCodeSuggestMessage: {
readonly generateCreatePullRequest: {
readonly customInstructions: string;
};
readonly gitkraken: {
Expand Down
5 changes: 5 additions & 0 deletions src/constants.telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,15 @@ export interface AIGenerateChangelogEventData extends AIEventDataBase {
type: 'changelog';
}

export interface AIGenerateCreatePullRequestEventData extends AIEventDataBase {
type: 'createPullRequest';
}

type AIGenerateEvent =
| AIGenerateCommitEventData
| AIGenerateDraftEventData
| AIGenerateStashEventData
| AIGenerateCreatePullRequestEventData
| AIGenerateChangelogEventData;

export type AISwitchModelEvent =
Expand Down
8 changes: 5 additions & 3 deletions src/env/node/git/sub-providers/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
repoPath: string,
to: string,
from?: string,
options?: { context?: number; includeUntracked: boolean; uris?: Uri[] },
options?: { context?: number; includeUntracked: boolean; uris?: Uri[]; notation?: '..' | '...' },
): Promise<GitDiff | undefined> {
const scope = getLogScope();
const args = [`-U${options?.context ?? 3}`];

from = prepareToFromDiffArgs(to, from, args);
from = prepareToFromDiffArgs(to, from, args, options?.notation);

let paths: Set<string> | undefined;
let untrackedPaths: string[] | undefined;
Expand Down Expand Up @@ -630,7 +630,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
}
}
}
function prepareToFromDiffArgs(to: string, from: string | undefined, args: string[]) {
function prepareToFromDiffArgs(to: string, from: string | undefined, args: string[], notation?: '..' | '...'): string {
if (to === uncommitted) {
if (from != null) {
args.push(from);
Expand All @@ -656,6 +656,8 @@ function prepareToFromDiffArgs(to: string, from: string | undefined, args: strin
}
} else if (to === '') {
args.push(from);
} else if (notation != null) {
args.push(`${from}${notation}${to}`);
} else {
args.push(from, to);
}
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ function registerBuiltInActionRunners(container: Container): void {
: ctx.branch.name,
remote: ctx.remote?.name ?? '',
repoPath: ctx.repoPath,
source: ctx.source,
useAI: ctx.useAI,
}));
},
}),
Expand Down
5 changes: 3 additions & 2 deletions src/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ export type ProFeatures =
| 'startWork'
| 'associateIssueWithBranch'
| ProAIFeatures;
export type ProAIFeatures = 'generateStashMessage' | 'explainCommit' | 'cloudPatchGenerateTitleAndDescription';
export type ProAIFeatures = 'explainCommit' | 'generateCreateDraft' | 'generateStashMessage';

export type AdvancedFeatures = AdvancedAIFeatures;
export type AdvancedAIFeatures = 'generateChangelog';
export type AdvancedAIFeatures = 'generateChangelog' | 'generateCreatePullRequest';

export type AIFeatures = ProAIFeatures | AdvancedAIFeatures;

export function isAdvancedFeature(feature: PlusFeatures): feature is AdvancedFeatures {
switch (feature) {
case 'generateChangelog':
case 'generateCreatePullRequest':
return true;
default:
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/git/gitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ export interface GitDiffSubProvider {
repoPath: string | Uri,
to: string,
from?: string,
options?: { context?: number; includeUntracked?: boolean; uris?: Uri[] },
options?: { context?: number; includeUntracked?: boolean; uris?: Uri[]; notation?: '..' | '...' },
): Promise<GitDiff | undefined>;
getDiffFiles?(repoPath: string | Uri, contents: string): Promise<GitDiffFiles | undefined>;
getDiffStatus(
Expand Down
7 changes: 4 additions & 3 deletions src/git/gitProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,10 +779,11 @@ export class GitProviderService implements Disposable {
feature === 'launchpad' ||
feature === 'startWork' ||
feature === 'associateIssueWithBranch' ||
feature === 'generateStashMessage' ||
feature === 'explainCommit' ||
feature === 'cloudPatchGenerateTitleAndDescription' ||
feature === 'generateChangelog'
feature === 'generateChangelog' ||
feature === 'generateCreateDraft' ||
feature === 'generateCreatePullRequest' ||
feature === 'generateStashMessage'
) {
return { allowed: false, subscription: { current: subscription, required: SubscriptionPlanId.Pro } };
}
Expand Down
7 changes: 5 additions & 2 deletions src/git/models/remoteResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ export type RemoteResource =
type: RemoteResourceType.CreatePullRequest;
base: {
branch: string | undefined;
remote: { path: string; url: string };
remote: { path: string; url: string; name: string };
};
compare: {
branch: string;
remote: { path: string; url: string };
remote: { path: string; url: string; name: string };
};
describePullRequest?: (
completedResource: RemoteResource & { type: RemoteResourceType.CreatePullRequest },
) => Promise<{ summary: string; body: string } | undefined>;
}
| {
type: RemoteResourceType.File;
Expand Down
30 changes: 22 additions & 8 deletions src/git/remotes/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,28 +280,42 @@ export class GitHubRemote extends RemoteProvider<GitHubRepositoryDescriptor> {
return this.encodeUrl(`${this.baseUrl}/compare/${base}${notation}${head}`);
}

protected override getUrlForCreatePullRequest(
protected override async 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();
options?: {
title?: string;
description?: string;
describePullRequest?: () => Promise<{ summary: string; body: string } | undefined>;
},
): Promise<string | undefined> {
const query = new URLSearchParams({ expand: '1' });
if (options?.title) {
query.set('title', options.title);
}
if (options?.description) {
query.set('body', options.description);
}

if ((!options?.title || !options?.description) && options?.describePullRequest) {
const result = await options.describePullRequest();
if (result?.summary) {
query.set('title', result.summary);
}
if (result?.body) {
query.set('body', result.body);
}
}

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

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

Expand Down
15 changes: 14 additions & 1 deletion src/git/remotes/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,11 @@ export class GitLabRemote extends RemoteProvider<GitLabRepositoryDescriptor> {
protected override async getUrlForCreatePullRequest(
base: { branch?: string; remote: { path: string; url: string } },
head: { branch: string; remote: { path: string; url: string } },
options?: { title?: string; description?: string },
options?: {
title?: string;
description?: string;
describePullRequest?: () => Promise<{ summary: string; body: string } | undefined>;
},
): Promise<string | undefined> {
const query = new URLSearchParams({
utf8: '✓',
Expand Down Expand Up @@ -425,6 +429,15 @@ export class GitLabRemote extends RemoteProvider<GitLabRepositoryDescriptor> {
if (options?.description) {
query.set('merge_request[description]', options.description);
}
if ((!options?.title || !options?.description) && options?.describePullRequest) {
const result = await options.describePullRequest();
if (result?.summary) {
query.set('merge_request[title]', result.summary);
}
if (result?.body) {
query.set('merge_request[description]', result.body);
}
}

return `${this.encodeUrl(`${this.getRepoBaseUrl(head.remote.path)}/-/merge_requests/new`)}?${query.toString()}`;
}
Expand Down
Loading