From d142f2bd3c7c750331a665d4762923695bda3a93 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Wed, 30 Apr 2025 11:30:49 -0400 Subject: [PATCH 1/8] Add explain branch (wip) --- contributions.json | 4 + docs/telemetry-events.md | 4 +- package.json | 9 + src/commands.ts | 1 + src/commands/explainBranch.ts | 265 ++++++++++++++++++++++++++++ src/constants.commands.generated.ts | 1 + src/constants.telemetry.ts | 2 +- 7 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 src/commands/explainBranch.ts diff --git a/contributions.json b/contributions.json index 6c64aecfc6c7d..70705a93e21df 100644 --- a/contributions.json +++ b/contributions.json @@ -23,6 +23,10 @@ ] } }, + "gitlens.ai.explainBranch": { + "label": "Explain Branch (Preview)...", + "commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" + }, "gitlens.ai.explainCommit": { "label": "Explain Commit...", "commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled && config.gitlens.ai.enabled", diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index 5e7f33b77a4c1..a63d60f2f0ac7 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -114,7 +114,7 @@ ```typescript { - 'changeType': 'wip' | 'stash' | 'commit' | 'draft-stash' | 'draft-patch' | 'draft-suggested_pr_change', + 'changeType': 'wip' | 'stash' | 'commit' | 'branch' | 'draft-stash' | 'draft-patch' | 'draft-suggested_pr_change', 'config.largePromptThreshold': number, 'config.usedCustomInstructions': boolean, 'duration': number, @@ -2063,7 +2063,7 @@ or 'context.webview.type': string, 'period': 'all' | `${number}|D` | `${number}|M` | `${number}|Y`, 'showAllBranches': boolean, - 'sliceBy': 'author' | 'branch' + 'sliceBy': 'branch' | 'author' } ``` diff --git a/package.json b/package.json index 33337f77d6aca..f0a7d5884d91e 100644 --- a/package.json +++ b/package.json @@ -6074,6 +6074,11 @@ "category": "GitLens", "icon": "$(person-add)" }, + { + "command": "gitlens.ai.explainBranch", + "title": "Explain Branch (Preview)...", + "category": "GitLens" + }, { "command": "gitlens.ai.explainCommit", "title": "Explain Commit...", @@ -10409,6 +10414,10 @@ "command": "gitlens.addAuthors", "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders" }, + { + "command": "gitlens.ai.explainBranch", + "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" + }, { "command": "gitlens.ai.explainCommit", "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled && config.gitlens.ai.enabled" diff --git a/src/commands.ts b/src/commands.ts index 3bdb90d9413ce..9682333c5213f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -21,6 +21,7 @@ import './commands/diffWithRevision'; import './commands/diffWithRevisionFrom'; import './commands/diffWithWorking'; import './commands/externalDiff'; +import './commands/explainBranch'; import './commands/explainCommit'; import './commands/explainStash'; import './commands/explainWip'; diff --git a/src/commands/explainBranch.ts b/src/commands/explainBranch.ts new file mode 100644 index 0000000000000..46914542892ff --- /dev/null +++ b/src/commands/explainBranch.ts @@ -0,0 +1,265 @@ +import type { TextEditor, Uri } from 'vscode'; +import { ProgressLocation } from 'vscode'; +import type { Source } from '../constants.telemetry'; +import type { Container } from '../container'; +import { GitUri } from '../git/gitUri'; +import type { GitBranchReference } from '../git/models/reference'; +import { showGenericErrorMessage } from '../messages'; +import type { AIExplainSource } from '../plus/ai/aiProviderService'; +import { showComparisonPicker } from '../quickpicks/comparisonPicker'; +import { ReferencesQuickPickIncludes, showReferencePicker } from '../quickpicks/referencePicker'; +import { getBestRepositoryOrShowPicker, getRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; +import { command } from '../system/-webview/command'; +import { showMarkdownPreview } from '../system/-webview/markdown'; +import { Logger } from '../system/logger'; +import { getSettledValue, getSettledValues } from '../system/promise'; +import { GlCommandBase } from './commandBase'; +import { getCommandUri } from './commandBase.utils'; +import type { CommandContext } from './commandContext'; + +export interface ExplainBranchCommandArgs { + repoPath?: string | Uri; + ref?: string; + source?: AIExplainSource; +} + +@command() +export class ExplainBranchCommand extends GlCommandBase { + constructor(private readonly container: Container) { + super('gitlens.ai.explainBranch'); + } + + protected override preExecute(context: CommandContext, args?: ExplainBranchCommandArgs): Promise { + return this.execute(context.editor, context.uri, args); + } + + async execute(editor?: TextEditor, uri?: Uri, args?: ExplainBranchCommandArgs): Promise { + uri = getCommandUri(uri, editor); + + const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined; + + const repository = await getBestRepositoryOrShowPicker( + gitUri, + editor, + 'Explain Branch', + 'Choose which repository to explain a branch from', + ); + if (repository == null) return; + + args = { ...args }; + + try { + // If no ref is provided, show a picker to select a branch + if (args.ref == null) { + const pick = await showReferencePicker( + repository.path, + 'Explain Branch', + 'Choose a branch to explain', + { + include: ReferencesQuickPickIncludes.Branches, + sort: { branches: { current: true } }, + }, + ); + if (pick?.ref == null) return; + args.ref = pick.ref; + } + + // Get the branch + const branch = await repository.git.branches().getBranch(args.ref); + if (branch == null) { + void showGenericErrorMessage('Unable to find the specified branch'); + return; + } + + // Get the diff between the branch and its upstream or base + const diffService = repository.git.diff(); + if (diffService?.getDiff === undefined) { + void showGenericErrorMessage('Unable to get diff service'); + return; + } + + const diff = await diffService.getDiff(branch.ref); + if (!diff?.contents) { + void showGenericErrorMessage('No changes found to explain'); + return; + } + + // Call the AI service to explain the changes + const result = await this.container.ai.explainChanges( + { + diff: diff.contents, + message: `Changes in branch ${branch.name}`, + }, + args.source ?? { source: 'commandPalette', type: 'commit' }, + { + progress: { location: ProgressLocation.Notification, title: 'Explaining branch changes...' }, + }, + ); + + // Display the result + let content = `# Branch: ${branch.name}\n`; + if (result != null) { + content += `> Generated by ${result.model.name}\n\n----\n\n${result?.parsed.summary}\n\n${result?.parsed.body}`; + } else { + content += `> No changes found to explain.`; + } + void showMarkdownPreview(content); + } catch (ex) { + Logger.error(ex, 'ExplainBranchCommand', 'execute'); + void showGenericErrorMessage('Unable to explain branch'); + } + } +} + +export interface ExplainBranchCommandArgs2 { + repoPath: string; + branch: string; + source?: Source; +} + +@command() +export class ExplainBranchCommand2 extends GlCommandBase { + constructor(private readonly container: Container) { + super('gitlens.ai.explainBranch'); + } + + async execute(args?: ExplainBranchCommandArgs2): Promise { + let repo; + if (args?.repoPath != null) { + repo = this.container.git.getRepository(args.repoPath); + } + repo ??= await getRepositoryOrShowPicker( + 'Explain Branch', + 'Choose which repository to explain a branch from', + undefined, + ); + if (repo == null) return; + + try { + // If no ref is provided, show a picker to select a branch + if (args == null) { + const pick = await showReferencePicker(repo.path, 'Explain Branch', 'Choose a branch to explain', { + include: ReferencesQuickPickIncludes.Branches, + sort: { branches: { current: true } }, + }); + if (pick?.ref == null) return; + + args = { + repoPath: repo.path, + branch: pick.ref, + }; + } + + // Get the branch + const branch = await repo.git.branches().getBranch(args.branch); + if (branch == null) { + void showGenericErrorMessage('Unable to find the specified branch'); + return; + } + const headRef = branch.ref; + const baseRef = branch.upstream?.name; + + // Get the diff between the branch and its upstream or base + const [diffResult, logResult] = await Promise.allSettled([ + repo.git.diff().getDiff?.(headRef, baseRef, { notation: '...' }), + repo.git.commits().getLog(`${baseRef}..${headRef}`), + ]); + + const diff = getSettledValue(diffResult); + const log = getSettledValue(logResult); + + if (!diff?.contents || !log?.commits?.size) { + void showGenericErrorMessage('No changes found to explain'); + } + } catch (ex) { + Logger.error(ex, 'ExplainBranchCommand', 'execute'); + void showGenericErrorMessage('Unable to explain branch'); + } + } +} + +// export interface ExplainBranchCommandArgs { +// repoPath: string; +// branch: GitBranchReference; +// source?: Source; +// } + +// @command() +// export class ExplainBranchCommand extends GlCommandBase { +// constructor(private readonly container: Container) { +// super('gitlens.ai.explainBranch'); +// } + +// async execute(args?: ExplainBranchCommandArgs): Promise { +// try { +// const comparisonResult = await showComparisonPicker(this.container, args?.repoPath, { +// head: args?.branch, +// getTitleAndPlaceholder: step => { +// switch (step) { +// case 1: +// return { +// title: 'Explain Branch', +// placeholder: 'Choose a branch to explain', +// }; +// case 2: +// return { +// title: `Explain Branch \u2022 Select Base to Start From`, +// placeholder: 'Choose a base branch to explain from', +// }; +// } +// }, +// }); +// if (comparisonResult == null) return; + +// const repo = this.container.git.getRepository(comparisonResult.repoPath); +// if (repo == null) return; + +// const mergeBase = await repo.git.refs().getMergeBase(comparisonResult.head.ref, comparisonResult.base.ref); + +// const [diffResult, logResult] = await Promise.allSettled([ +// repo.git.diff().getDiff?.(comparisonResult.head.ref, mergeBase, { notation: '...' }), +// repo.git.commits().getLog(`${mergeBase}..${comparisonResult.head.ref}`), +// ]); + +// const diff = getSettledValue(diffResult); +// const log = getSettledValue(logResult); + +// if (!diff?.contents || !log?.commits?.size) { +// void showGenericErrorMessage('No changes found to explain'); +// return; +// } + +// const commitMessages: string[] = []; +// for (const commit of [...log.commits.values()].sort((a, b) => a.date.getTime() - b.date.getTime())) { +// commitMessages.push(commit.message ?? commit.summary); +// } + +// const result = await this.container.ai.explainChanges( +// { +// diff: diff.contents, +// message: commitMessages.join('\n\n'), +// }, +// { +// source: 'commandPalette', +// ...args?.source, +// type: 'branch', +// }, +// { +// progress: { location: ProgressLocation.Notification, title: 'Explaining branch changes...' }, +// }, +// ); + +// // Display the result +// let content = `# Branch: ${comparisonResult.head.name}\n`; +// if (result != null) { +// content += `> Generated by ${result.model.name}\n\n----\n\n${result?.parsed.summary}\n\n${result?.parsed.body}`; +// } else { +// content += `> No changes found to explain.`; +// } +// void showMarkdownPreview(content); +// } catch (ex) { +// Logger.error(ex, 'ExplainBranchCommand', 'execute'); +// void showGenericErrorMessage('Unable to explain branch'); +// } +// } +// } diff --git a/src/constants.commands.generated.ts b/src/constants.commands.generated.ts index cfb9a3396f57b..49e787d5976f1 100644 --- a/src/constants.commands.generated.ts +++ b/src/constants.commands.generated.ts @@ -614,6 +614,7 @@ export type ContributedCommands = export type ContributedPaletteCommands = | 'gitlens.addAuthors' + | 'gitlens.ai.explainBranch' | 'gitlens.ai.explainCommit' | 'gitlens.ai.explainStash' | 'gitlens.ai.explainWip' diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 6a6c37d32c877..94853632b7b8c 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -336,7 +336,7 @@ interface AIEventDataBase { interface AIExplainEvent extends AIEventDataBase { type: 'change'; - changeType: 'wip' | 'stash' | 'commit' | `draft-${'patch' | 'stash' | 'suggested_pr_change'}`; + changeType: 'wip' | 'stash' | 'commit' | 'branch' | `draft-${'patch' | 'stash' | 'suggested_pr_change'}`; } export interface AIGenerateCommitEventData extends AIEventDataBase { From 0a472256a70abb21079b8d91740308c08164c02b Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 2 May 2025 00:27:00 +0200 Subject: [PATCH 2/8] Explain branch comparing to its merge-target including all commits (wip) --- src/commands/explainBranch.ts | 99 +++++++++++++++++++--- src/env/node/git/sub-providers/branches.ts | 15 ++++ src/git/gitProvider.ts | 1 + 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/src/commands/explainBranch.ts b/src/commands/explainBranch.ts index 46914542892ff..0b96027477590 100644 --- a/src/commands/explainBranch.ts +++ b/src/commands/explainBranch.ts @@ -1,18 +1,17 @@ -import type { TextEditor, Uri } from 'vscode'; +import type { CancellationToken, TextEditor, Uri } from 'vscode'; import { ProgressLocation } from 'vscode'; import type { Source } from '../constants.telemetry'; import type { Container } from '../container'; import { GitUri } from '../git/gitUri'; -import type { GitBranchReference } from '../git/models/reference'; +import type { GitBranch } from '../git/models/branch'; import { showGenericErrorMessage } from '../messages'; import type { AIExplainSource } from '../plus/ai/aiProviderService'; -import { showComparisonPicker } from '../quickpicks/comparisonPicker'; import { ReferencesQuickPickIncludes, showReferencePicker } from '../quickpicks/referencePicker'; import { getBestRepositoryOrShowPicker, getRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; import { command } from '../system/-webview/command'; import { showMarkdownPreview } from '../system/-webview/markdown'; import { Logger } from '../system/logger'; -import { getSettledValue, getSettledValues } from '../system/promise'; +import { getSettledValue } from '../system/promise'; import { GlCommandBase } from './commandBase'; import { getCommandUri } from './commandBase.utils'; import type { CommandContext } from './commandContext'; @@ -34,10 +33,9 @@ export class ExplainBranchCommand extends GlCommandBase { } async execute(editor?: TextEditor, uri?: Uri, args?: ExplainBranchCommandArgs): Promise { + //// Clarifying the repository uri = getCommandUri(uri, editor); - const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined; - const repository = await getBestRepositoryOrShowPicker( gitUri, editor, @@ -49,8 +47,9 @@ export class ExplainBranchCommand extends GlCommandBase { args = { ...args }; try { - // If no ref is provided, show a picker to select a branch + //// Clarifying the head branch if (args.ref == null) { + // If no ref is provided, show a picker to select a branch const pick = await showReferencePicker( repository.path, 'Explain Branch', @@ -71,6 +70,16 @@ export class ExplainBranchCommand extends GlCommandBase { return; } + //// Clarifying the base branch + const baseBranchName = await getMergeTarget(this.container, branch); + const baseBranch = await repository.git.branches().getBranch(baseBranchName); + if (!baseBranch) { + void showGenericErrorMessage( + 'Unable to find the base branch for the specified branch. Probably it is undefined. Set it up and try again.', + ); + return; + } + // Get the diff between the branch and its upstream or base const diffService = repository.git.diff(); if (diffService?.getDiff === undefined) { @@ -78,18 +87,49 @@ export class ExplainBranchCommand extends GlCommandBase { return; } - const diff = await diffService.getDiff(branch.ref); - if (!diff?.contents) { + const commitsService = repository.git.commits(); + if (commitsService?.getLog === undefined) { + void showGenericErrorMessage('Unable to get commits service'); + return; + } + + const [diffResult, logResult] = await Promise.allSettled([ + diffService.getDiff?.(branch.ref, baseBranch.ref, { notation: '...' }), + commitsService.getLog(`${baseBranch.ref}..${branch.ref}`), + ]); + + const diff = getSettledValue(diffResult); + const log = getSettledValue(logResult); + if (!diff?.contents || !log?.commits?.size) { void showGenericErrorMessage('No changes found to explain'); return; } + const commitMessages: string[] = []; + for (const commit of [...log.commits.values()].sort((a, b) => a.date.getTime() - b.date.getTime())) { + const message = commit.message ?? commit.summary; + if (message) { + commitMessages.push( + `\n${ + commit.message ?? commit.summary + }\n`, + ); + } + } + + const changes = { + diff: diff.contents, + message: `Changes in branch ${branch.name} + that is ahead of its target by number of commits with the following messages:\n\n + + ${commitMessages.join('\n\n')} + + `, + }; + // Call the AI service to explain the changes const result = await this.container.ai.explainChanges( - { - diff: diff.contents, - message: `Changes in branch ${branch.name}`, - }, + changes, args.source ?? { source: 'commandPalette', type: 'commit' }, { progress: { location: ProgressLocation.Notification, title: 'Explaining branch changes...' }, @@ -103,7 +143,9 @@ export class ExplainBranchCommand extends GlCommandBase { } else { content += `> No changes found to explain.`; } - void showMarkdownPreview(content); + // Add changes temporarily for debug purposes, so it's easier to review what content has been explained + const changesMd = `${changes.message}\n\n${changes.diff}`; + void showMarkdownPreview(`${content}\n\n\`\`\`\n${changesMd.replaceAll('`', '')}\n\`\`\`\n`); } catch (ex) { Logger.error(ex, 'ExplainBranchCommand', 'execute'); void showGenericErrorMessage('Unable to explain branch'); @@ -192,6 +234,8 @@ export class ExplainBranchCommand2 extends GlCommandBase { // async execute(args?: ExplainBranchCommandArgs): Promise { // try { +// // I'm declining it for now, because it can be a behaviour for "explain comparison" command, +// // that can be called either from the command palette or from the compare view. // const comparisonResult = await showComparisonPicker(this.container, args?.repoPath, { // head: args?.branch, // getTitleAndPlaceholder: step => { @@ -263,3 +307,30 @@ export class ExplainBranchCommand2 extends GlCommandBase { // } // } // } + +async function getMergeTarget( + container: Container, + branch: GitBranch, + options?: { cancellation?: CancellationToken }, +): Promise { + const localValue = await container.git.branches(branch.repoPath).getMergeTargetBranchName?.(branch); + if (localValue) { + return localValue; + } + return getIntegrationDefaultBranchName(container, branch.repoPath, options); +} + +// This is similar to what we have in changeBranchMergeTarget.ts +// what is a proper utils files to put it to? +async function getIntegrationDefaultBranchName( + container: Container, + repoPath: string, + options?: { cancellation?: CancellationToken }, +): Promise { + const remote = await container.git.remotes(repoPath).getBestRemoteWithIntegration(); + if (remote == null) return undefined; + + const integration = await remote.getIntegration(); + const defaultBranch = await integration?.getDefaultBranch?.(remote.provider.repoDesc, options); + return defaultBranch && `${remote.name}/${defaultBranch?.name}`; +} diff --git a/src/env/node/git/sub-providers/branches.ts b/src/env/node/git/sub-providers/branches.ts index 85175944d953e..b6b56e771f50d 100644 --- a/src/env/node/git/sub-providers/branches.ts +++ b/src/env/node/git/sub-providers/branches.ts @@ -707,6 +707,21 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider { await this.provider.config.setConfig(repoPath, mergeTargetConfigKey, target); } + async getMergeTargetBranchName(repoPath: string, branch: GitBranch): Promise { + const [baseResult, defaultResult, targetResult, userTargetResult] = await Promise.allSettled([ + this.getBaseBranchName?.(repoPath, branch.name), + this.getDefaultBranchName(repoPath, branch.getRemoteName()), + this.getTargetBranchName?.(repoPath, branch.name), + this.getUserMergeTargetBranchName?.(repoPath, branch.name), + ]); + + const baseBranchName = getSettledValue(baseResult); + const defaultBranchName = getSettledValue(defaultResult); + const targetMaybeResult = getSettledValue(targetResult); + const userTargetBranchName = getSettledValue(userTargetResult); + return userTargetBranchName || targetMaybeResult || baseBranchName || defaultBranchName; + } + private async getBaseBranchFromReflog( repoPath: string, ref: string, diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 2cd7e5af5f207..9bf3d23467231 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -279,6 +279,7 @@ export interface GitBranchesSubProvider { setTargetBranchName?(repoPath: string, ref: string, target: string): Promise; getUserMergeTargetBranchName?(repoPath: string, ref: string): Promise; setUserMergeTargetBranchName?(repoPath: string, ref: string, target: string | undefined): Promise; + getMergeTargetBranchName?(repoPath: string, branch: GitBranch): Promise; renameBranch?(repoPath: string, oldName: string, newName: string): Promise; } From a63d57d62d8b4ef8c561af3cb841ca64d29c3775 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 2 May 2025 14:32:33 +0200 Subject: [PATCH 3/8] Isolates shared logic of collecting compare data for AI to a method --- src/commands/explainBranch.ts | 47 ++++++------------ src/plus/ai/aiProviderService.ts | 81 +++++++++++++++++++++++++------- 2 files changed, 78 insertions(+), 50 deletions(-) diff --git a/src/commands/explainBranch.ts b/src/commands/explainBranch.ts index 0b96027477590..f47b0e32fea47 100644 --- a/src/commands/explainBranch.ts +++ b/src/commands/explainBranch.ts @@ -81,48 +81,29 @@ export class ExplainBranchCommand extends GlCommandBase { } // Get the diff between the branch and its upstream or base - const diffService = repository.git.diff(); - if (diffService?.getDiff === undefined) { - void showGenericErrorMessage('Unable to get diff service'); - return; - } - - const commitsService = repository.git.commits(); - if (commitsService?.getLog === undefined) { - void showGenericErrorMessage('Unable to get commits service'); - return; - } - - const [diffResult, logResult] = await Promise.allSettled([ - diffService.getDiff?.(branch.ref, baseBranch.ref, { notation: '...' }), - commitsService.getLog(`${baseBranch.ref}..${branch.ref}`), - ]); + const compareData = await this.container.ai.prepareCompareDataForAIRequest( + repository, + branch.ref, + baseBranch.ref, + { + reportNoDiffService: () => void showGenericErrorMessage('Unable to get diff service'), + reportNoCommitsService: () => void showGenericErrorMessage('Unable to get commits service'), + reportNoChanges: () => void showGenericErrorMessage('No changes found to explain'), + }, + ); - const diff = getSettledValue(diffResult); - const log = getSettledValue(logResult); - if (!diff?.contents || !log?.commits?.size) { - void showGenericErrorMessage('No changes found to explain'); + if (compareData == null) { return; } - const commitMessages: string[] = []; - for (const commit of [...log.commits.values()].sort((a, b) => a.date.getTime() - b.date.getTime())) { - const message = commit.message ?? commit.summary; - if (message) { - commitMessages.push( - `\n${ - commit.message ?? commit.summary - }\n`, - ); - } - } + const { diff, logMessages } = compareData; const changes = { - diff: diff.contents, + diff: diff, message: `Changes in branch ${branch.name} that is ahead of its target by number of commits with the following messages:\n\n - ${commitMessages.join('\n\n')} + ${logMessages} `, }; diff --git a/src/plus/ai/aiProviderService.ts b/src/plus/ai/aiProviderService.ts index 20545efd21459..f4a8c4986b125 100644 --- a/src/plus/ai/aiProviderService.ts +++ b/src/plus/ai/aiProviderService.ts @@ -626,6 +626,63 @@ export class AIProviderService implements Disposable { return result != null ? { ...result, parsed: parseSummarizeResult(result.content) } : undefined; } + async prepareCompareDataForAIRequest( + repo: Repository, + headRef: string, + baseRef: string, + options?: { + cancellation?: CancellationToken; + reportNoDiffService?: () => void; + reportNoCommitsService?: () => void; + reportNoChanges?: () => void; + }, + ): Promise<{ diff: string; logMessages: string } | undefined> { + const { cancellation, reportNoDiffService, reportNoCommitsService, reportNoChanges } = options ?? {}; + const diffService = repo.git.diff(); + if (diffService?.getDiff === undefined) { + if (reportNoDiffService) { + reportNoDiffService(); + return; + } + } + + const commitsService = repo.git.commits(); + if (commitsService?.getLog === undefined) { + if (reportNoCommitsService) { + reportNoCommitsService(); + return; + } + } + + const [diffResult, logResult] = await Promise.allSettled([ + diffService.getDiff?.(headRef, baseRef, { notation: '...' }), + commitsService.getLog(`${baseRef}..${headRef}`), + ]); + const diff = getSettledValue(diffResult); + const log = getSettledValue(logResult); + + if (!diff?.contents || !log?.commits?.size) { + reportNoChanges?.(); + return undefined; + } + + if (cancellation?.isCancellationRequested) throw new CancellationError(); + + const commitMessages: string[] = []; + for (const commit of [...log.commits.values()].sort((a, b) => a.date.getTime() - b.date.getTime())) { + const message = commit.message ?? commit.summary; + if (message) { + commitMessages.push( + `\n${ + commit.message ?? commit.summary + }\n`, + ); + } + } + + return { diff: diff.contents, logMessages: commitMessages.join('\n\n') }; + } + async generateCreatePullRequest( repo: Repository, baseRef: string, @@ -641,31 +698,21 @@ export class AIProviderService implements Disposable { const result = await this.sendRequest( 'generate-create-pullRequest', async (model, reporting, cancellation, maxInputTokens, retries) => { - const [diffResult, logResult] = await Promise.allSettled([ - repo.git.diff().getDiff?.(headRef, baseRef, { notation: '...' }), - repo.git.commits().getLog(`${baseRef}..${headRef}`), - ]); - - const diff = getSettledValue(diffResult); - const log = getSettledValue(logResult); + const compareData = await this.prepareCompareDataForAIRequest(repo, headRef, baseRef, { + cancellation: cancellation, + }); - if (!diff?.contents || !log?.commits?.size) { + if (!compareData?.diff || !compareData?.logMessages) { throw new AINoRequestDataError('No changes to generate a pull request from.'); } - if (cancellation.isCancellationRequested) throw new CancellationError(); - - const commitMessages: string[] = []; - for (const commit of [...log.commits.values()].sort((a, b) => a.date.getTime() - b.date.getTime())) { - commitMessages.push(commit.message ?? commit.summary); - } - + const { diff, logMessages } = compareData; const { prompt } = await this.getPrompt( 'generate-create-pullRequest', model, { - diff: diff.contents, - data: commitMessages.join('\n'), + diff: diff, + data: logMessages, context: options?.context, instructions: configuration.get('ai.generateCreatePullRequest.customInstructions'), }, From fabe2a7b55e1e936c37b9ce4e454b016dac3d793 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 2 May 2025 14:36:38 +0200 Subject: [PATCH 4/8] Removes experimental code of the explain-branch command --- src/commands/explainBranch.ts | 159 +--------------------------------- 1 file changed, 1 insertion(+), 158 deletions(-) diff --git a/src/commands/explainBranch.ts b/src/commands/explainBranch.ts index f47b0e32fea47..6db881411cd79 100644 --- a/src/commands/explainBranch.ts +++ b/src/commands/explainBranch.ts @@ -1,17 +1,15 @@ import type { CancellationToken, TextEditor, Uri } from 'vscode'; import { ProgressLocation } from 'vscode'; -import type { Source } from '../constants.telemetry'; import type { Container } from '../container'; import { GitUri } from '../git/gitUri'; import type { GitBranch } from '../git/models/branch'; import { showGenericErrorMessage } from '../messages'; import type { AIExplainSource } from '../plus/ai/aiProviderService'; import { ReferencesQuickPickIncludes, showReferencePicker } from '../quickpicks/referencePicker'; -import { getBestRepositoryOrShowPicker, getRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; +import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; import { command } from '../system/-webview/command'; import { showMarkdownPreview } from '../system/-webview/markdown'; import { Logger } from '../system/logger'; -import { getSettledValue } from '../system/promise'; import { GlCommandBase } from './commandBase'; import { getCommandUri } from './commandBase.utils'; import type { CommandContext } from './commandContext'; @@ -134,161 +132,6 @@ export class ExplainBranchCommand extends GlCommandBase { } } -export interface ExplainBranchCommandArgs2 { - repoPath: string; - branch: string; - source?: Source; -} - -@command() -export class ExplainBranchCommand2 extends GlCommandBase { - constructor(private readonly container: Container) { - super('gitlens.ai.explainBranch'); - } - - async execute(args?: ExplainBranchCommandArgs2): Promise { - let repo; - if (args?.repoPath != null) { - repo = this.container.git.getRepository(args.repoPath); - } - repo ??= await getRepositoryOrShowPicker( - 'Explain Branch', - 'Choose which repository to explain a branch from', - undefined, - ); - if (repo == null) return; - - try { - // If no ref is provided, show a picker to select a branch - if (args == null) { - const pick = await showReferencePicker(repo.path, 'Explain Branch', 'Choose a branch to explain', { - include: ReferencesQuickPickIncludes.Branches, - sort: { branches: { current: true } }, - }); - if (pick?.ref == null) return; - - args = { - repoPath: repo.path, - branch: pick.ref, - }; - } - - // Get the branch - const branch = await repo.git.branches().getBranch(args.branch); - if (branch == null) { - void showGenericErrorMessage('Unable to find the specified branch'); - return; - } - const headRef = branch.ref; - const baseRef = branch.upstream?.name; - - // Get the diff between the branch and its upstream or base - const [diffResult, logResult] = await Promise.allSettled([ - repo.git.diff().getDiff?.(headRef, baseRef, { notation: '...' }), - repo.git.commits().getLog(`${baseRef}..${headRef}`), - ]); - - const diff = getSettledValue(diffResult); - const log = getSettledValue(logResult); - - if (!diff?.contents || !log?.commits?.size) { - void showGenericErrorMessage('No changes found to explain'); - } - } catch (ex) { - Logger.error(ex, 'ExplainBranchCommand', 'execute'); - void showGenericErrorMessage('Unable to explain branch'); - } - } -} - -// export interface ExplainBranchCommandArgs { -// repoPath: string; -// branch: GitBranchReference; -// source?: Source; -// } - -// @command() -// export class ExplainBranchCommand extends GlCommandBase { -// constructor(private readonly container: Container) { -// super('gitlens.ai.explainBranch'); -// } - -// async execute(args?: ExplainBranchCommandArgs): Promise { -// try { -// // I'm declining it for now, because it can be a behaviour for "explain comparison" command, -// // that can be called either from the command palette or from the compare view. -// const comparisonResult = await showComparisonPicker(this.container, args?.repoPath, { -// head: args?.branch, -// getTitleAndPlaceholder: step => { -// switch (step) { -// case 1: -// return { -// title: 'Explain Branch', -// placeholder: 'Choose a branch to explain', -// }; -// case 2: -// return { -// title: `Explain Branch \u2022 Select Base to Start From`, -// placeholder: 'Choose a base branch to explain from', -// }; -// } -// }, -// }); -// if (comparisonResult == null) return; - -// const repo = this.container.git.getRepository(comparisonResult.repoPath); -// if (repo == null) return; - -// const mergeBase = await repo.git.refs().getMergeBase(comparisonResult.head.ref, comparisonResult.base.ref); - -// const [diffResult, logResult] = await Promise.allSettled([ -// repo.git.diff().getDiff?.(comparisonResult.head.ref, mergeBase, { notation: '...' }), -// repo.git.commits().getLog(`${mergeBase}..${comparisonResult.head.ref}`), -// ]); - -// const diff = getSettledValue(diffResult); -// const log = getSettledValue(logResult); - -// if (!diff?.contents || !log?.commits?.size) { -// void showGenericErrorMessage('No changes found to explain'); -// return; -// } - -// const commitMessages: string[] = []; -// for (const commit of [...log.commits.values()].sort((a, b) => a.date.getTime() - b.date.getTime())) { -// commitMessages.push(commit.message ?? commit.summary); -// } - -// const result = await this.container.ai.explainChanges( -// { -// diff: diff.contents, -// message: commitMessages.join('\n\n'), -// }, -// { -// source: 'commandPalette', -// ...args?.source, -// type: 'branch', -// }, -// { -// progress: { location: ProgressLocation.Notification, title: 'Explaining branch changes...' }, -// }, -// ); - -// // Display the result -// let content = `# Branch: ${comparisonResult.head.name}\n`; -// if (result != null) { -// content += `> Generated by ${result.model.name}\n\n----\n\n${result?.parsed.summary}\n\n${result?.parsed.body}`; -// } else { -// content += `> No changes found to explain.`; -// } -// void showMarkdownPreview(content); -// } catch (ex) { -// Logger.error(ex, 'ExplainBranchCommand', 'execute'); -// void showGenericErrorMessage('Unable to explain branch'); -// } -// } -// } - async function getMergeTarget( container: Container, branch: GitBranch, From 7bbbefc5d2a29ea95a9ad915dd1dddabc17921c2 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 5 May 2025 14:54:03 -0400 Subject: [PATCH 5/8] Adds view support for explain branch --- contributions.json | 11 ++++++++++- package.json | 5 +++++ src/commands/explainBranch.ts | 36 ++++++++++++++++++++++++----------- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/contributions.json b/contributions.json index 70705a93e21df..5a2753ba99d5a 100644 --- a/contributions.json +++ b/contributions.json @@ -25,7 +25,16 @@ }, "gitlens.ai.explainBranch": { "label": "Explain Branch (Preview)...", - "commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" + "commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled", + "menus": { + "view/item/context": [ + { + "when": "viewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled && config.gitlens.ai.enabled", + "group": "3_gitlens_ai", + "order": 1 + } + ] + } }, "gitlens.ai.explainCommit": { "label": "Explain Commit...", diff --git a/package.json b/package.json index f0a7d5884d91e..11288d74eda2e 100644 --- a/package.json +++ b/package.json @@ -16397,6 +16397,11 @@ "when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && listMultiSelection", "group": "2_gitlens_quickopen@1" }, + { + "command": "gitlens.ai.explainBranch", + "when": "viewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled && config.gitlens.ai.enabled", + "group": "3_gitlens_ai@1" + }, { "command": "gitlens.views.openChangedFileDiffsWithMergeBase", "when": "viewItem =~ /gitlens:branch\\b(?!.*?\\b\\+current\\b)/ && !listMultiSelection", diff --git a/src/commands/explainBranch.ts b/src/commands/explainBranch.ts index 6db881411cd79..7b7a4c56e0587 100644 --- a/src/commands/explainBranch.ts +++ b/src/commands/explainBranch.ts @@ -10,9 +10,11 @@ import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; import { command } from '../system/-webview/command'; import { showMarkdownPreview } from '../system/-webview/markdown'; import { Logger } from '../system/logger'; +import { getNodeRepoPath } from '../views/nodes/abstract/viewNode'; import { GlCommandBase } from './commandBase'; import { getCommandUri } from './commandBase.utils'; import type { CommandContext } from './commandContext'; +import { isCommandContextViewNodeHasBranch } from './commandContext.utils'; export interface ExplainBranchCommandArgs { repoPath?: string | Uri; @@ -27,23 +29,35 @@ export class ExplainBranchCommand extends GlCommandBase { } protected override preExecute(context: CommandContext, args?: ExplainBranchCommandArgs): Promise { + if (isCommandContextViewNodeHasBranch(context)) { + args = { ...args }; + args.repoPath = args.repoPath ?? getNodeRepoPath(context.node); + args.ref = args.ref ?? context.node.branch.ref; + args.source = args.source ?? { source: 'view', type: 'branch' }; + } + return this.execute(context.editor, context.uri, args); } async execute(editor?: TextEditor, uri?: Uri, args?: ExplainBranchCommandArgs): Promise { - //// Clarifying the repository - uri = getCommandUri(uri, editor); - const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined; - const repository = await getBestRepositoryOrShowPicker( - gitUri, - editor, - 'Explain Branch', - 'Choose which repository to explain a branch from', - ); - if (repository == null) return; - args = { ...args }; + let repository; + if (args?.repoPath != null) { + repository = this.container.git.getRepository(args.repoPath); + } else { + uri = getCommandUri(uri, editor); + const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined; + repository = await getBestRepositoryOrShowPicker( + gitUri, + editor, + 'Explain Branch', + 'Choose which repository to explain a branch from', + ); + } + + if (repository == null) return; + try { //// Clarifying the head branch if (args.ref == null) { From dd3dd56ab32e25171a19041a8a3ff95f0b2d539f Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 5 May 2025 16:21:47 -0400 Subject: [PATCH 6/8] Adds explain branch to commit graph --- contributions.json | 12 ++++++++++++ package.json | 13 +++++++++++++ src/constants.commands.generated.ts | 1 + src/webviews/plus/graph/graphWebview.ts | 12 ++++++++++++ 4 files changed, 38 insertions(+) diff --git a/contributions.json b/contributions.json index 5a2753ba99d5a..bad9d551595c0 100644 --- a/contributions.json +++ b/contributions.json @@ -1387,6 +1387,18 @@ ] } }, + "gitlens.graph.ai.explainBranch": { + "label": "Explain Branch (Preview)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled && config.gitlens.ai.enabled", + "group": "1_gitlens_actions_4", + "order": 100 + } + ] + } + }, "gitlens.graph.ai.explainCommit": { "label": "Explain Commit", "menus": { diff --git a/package.json b/package.json index 11288d74eda2e..a544bda4bb63a 100644 --- a/package.json +++ b/package.json @@ -6629,6 +6629,10 @@ "title": "Add as Co-author", "icon": "$(person-add)" }, + { + "command": "gitlens.graph.ai.explainBranch", + "title": "Explain Branch (Preview)" + }, { "command": "gitlens.graph.ai.explainCommit", "title": "Explain Commit" @@ -10830,6 +10834,10 @@ "command": "gitlens.graph.addAuthor", "when": "false" }, + { + "command": "gitlens.graph.ai.explainBranch", + "when": "false" + }, { "command": "gitlens.graph.ai.explainCommit", "when": "false" @@ -20074,6 +20082,11 @@ "when": "webviewItem =~ /gitlens:(branch|tag)\\b/ && !gitlens:untrusted && gitlens:gk:organization:ai:enabled && config.gitlens.ai.enabled", "group": "1_gitlens_actions_3@100" }, + { + "command": "gitlens.graph.ai.explainBranch", + "when": "webviewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled && config.gitlens.ai.enabled", + "group": "1_gitlens_actions_4@100" + }, { "command": "gitlens.graph.openBranchOnRemote", "when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && gitlens:repos:withRemotes", diff --git a/src/constants.commands.generated.ts b/src/constants.commands.generated.ts index 49e787d5976f1..9ec376ffa99ae 100644 --- a/src/constants.commands.generated.ts +++ b/src/constants.commands.generated.ts @@ -23,6 +23,7 @@ export type ContributedCommands = | 'gitlens.copyRemoteRepositoryUrl' | 'gitlens.ghpr.views.openOrCreateWorktree' | 'gitlens.graph.addAuthor' + | 'gitlens.graph.ai.explainBranch' | 'gitlens.graph.ai.explainCommit' | 'gitlens.graph.ai.explainStash' | 'gitlens.graph.ai.explainWip' diff --git a/src/webviews/plus/graph/graphWebview.ts b/src/webviews/plus/graph/graphWebview.ts index 7dcfc62226373..f67efe4ac9786 100644 --- a/src/webviews/plus/graph/graphWebview.ts +++ b/src/webviews/plus/graph/graphWebview.ts @@ -685,6 +685,7 @@ export class GraphWebviewProvider implements WebviewProvider Date: Mon, 5 May 2025 17:15:44 -0400 Subject: [PATCH 7/8] Adds explain branch to home --- src/constants.commands.ts | 1 + .../apps/plus/home/components/branch-card.ts | 28 +++++++++++++++++-- src/webviews/home/homeWebview.ts | 1 + 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/constants.commands.ts b/src/constants.commands.ts index 6fe9520a72d54..202dc9191133f 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -55,6 +55,7 @@ type InternalHomeWebviewCommands = | 'gitlens.home.continuePausedOperation' | 'gitlens.home.abortPausedOperation' | 'gitlens.home.explainWip' + | 'gitlens.home.ai.explainBranch' | 'gitlens.home.openRebaseEditor'; type InternalHomeWebviewViewCommands = diff --git a/src/webviews/apps/plus/home/components/branch-card.ts b/src/webviews/apps/plus/home/components/branch-card.ts index bbb81b749298b..5f88d026f1913 100644 --- a/src/webviews/apps/plus/home/components/branch-card.ts +++ b/src/webviews/apps/plus/home/components/branch-card.ts @@ -1000,15 +1000,30 @@ export class GlBranchCard extends GlBranchCardBase { href=${this.createCommandLink('gitlens.home.openWorktree')} >`, ); - // add explain WIP - if (this.wip?.workingTreeState != null) { + + const hasWip = + this.wip?.workingTreeState != null + ? this.wip.workingTreeState.added + + this.wip.workingTreeState.changed + + this.wip.workingTreeState.deleted > + 0 + : false; + if (hasWip) { actions.push( html``, ); + } else { + actions.push( + html``, + ); } } else { actions.push( @@ -1018,6 +1033,13 @@ export class GlBranchCard extends GlBranchCardBase { href=${this.createCommandLink('gitlens.home.switchToBranch')} >`, ); + actions.push( + html``, + ); } // branch actions diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 0ec7aca2afdb2..96ac21ead3a1a 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -354,6 +354,7 @@ export class HomeWebviewProvider implements WebviewProvider Date: Wed, 7 May 2025 18:49:32 -0400 Subject: [PATCH 8/8] Updates explain branch - improves markdown format - moves prepareCompareDataForAIRequest out of AIProviderService --- src/commands/explainBranch.ts | 40 +++++------ src/plus/ai/aiProviderService.ts | 116 +++++++++++++++---------------- 2 files changed, 74 insertions(+), 82 deletions(-) diff --git a/src/commands/explainBranch.ts b/src/commands/explainBranch.ts index 7b7a4c56e0587..efcfdf9a30bce 100644 --- a/src/commands/explainBranch.ts +++ b/src/commands/explainBranch.ts @@ -5,6 +5,7 @@ import { GitUri } from '../git/gitUri'; import type { GitBranch } from '../git/models/branch'; import { showGenericErrorMessage } from '../messages'; import type { AIExplainSource } from '../plus/ai/aiProviderService'; +import { prepareCompareDataForAIRequest } from '../plus/ai/aiProviderService'; import { ReferencesQuickPickIncludes, showReferencePicker } from '../quickpicks/referencePicker'; import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; import { command } from '../system/-webview/command'; @@ -59,7 +60,7 @@ export class ExplainBranchCommand extends GlCommandBase { if (repository == null) return; try { - //// Clarifying the head branch + // Clarifying the head branch if (args.ref == null) { // If no ref is provided, show a picker to select a branch const pick = await showReferencePicker( @@ -82,27 +83,20 @@ export class ExplainBranchCommand extends GlCommandBase { return; } - //// Clarifying the base branch + // Clarifying the base branch const baseBranchName = await getMergeTarget(this.container, branch); const baseBranch = await repository.git.branches().getBranch(baseBranchName); if (!baseBranch) { - void showGenericErrorMessage( - 'Unable to find the base branch for the specified branch. Probably it is undefined. Set it up and try again.', - ); + void showGenericErrorMessage(`Unable to find the base branch for ${branch.name}.`); return; } // Get the diff between the branch and its upstream or base - const compareData = await this.container.ai.prepareCompareDataForAIRequest( - repository, - branch.ref, - baseBranch.ref, - { - reportNoDiffService: () => void showGenericErrorMessage('Unable to get diff service'), - reportNoCommitsService: () => void showGenericErrorMessage('Unable to get commits service'), - reportNoChanges: () => void showGenericErrorMessage('No changes found to explain'), - }, - ); + const compareData = await prepareCompareDataForAIRequest(repository, branch.ref, baseBranch.ref, { + reportNoDiffService: () => void showGenericErrorMessage('Unable to get diff service'), + reportNoCommitsService: () => void showGenericErrorMessage('Unable to get commits service'), + reportNoChanges: () => void showGenericErrorMessage('No changes found to explain'), + }); if (compareData == null) { return; @@ -129,16 +123,14 @@ export class ExplainBranchCommand extends GlCommandBase { }, ); - // Display the result - let content = `# Branch: ${branch.name}\n`; - if (result != null) { - content += `> Generated by ${result.model.name}\n\n----\n\n${result?.parsed.summary}\n\n${result?.parsed.body}`; - } else { - content += `> No changes found to explain.`; + if (result == null) { + void showGenericErrorMessage(`Unable to explain branch ${branch.name}`); + return; } - // Add changes temporarily for debug purposes, so it's easier to review what content has been explained - const changesMd = `${changes.message}\n\n${changes.diff}`; - void showMarkdownPreview(`${content}\n\n\`\`\`\n${changesMd.replaceAll('`', '')}\n\`\`\`\n`); + + const content = `# Branch Summary\n\n> Generated by ${result.model.name}\n\n## ${branch.name}\n\n${result?.parsed.summary}\n\n${result?.parsed.body}`; + + void showMarkdownPreview(content); } catch (ex) { Logger.error(ex, 'ExplainBranchCommand', 'execute'); void showGenericErrorMessage('Unable to explain branch'); diff --git a/src/plus/ai/aiProviderService.ts b/src/plus/ai/aiProviderService.ts index f4a8c4986b125..de2c76eb8c27d 100644 --- a/src/plus/ai/aiProviderService.ts +++ b/src/plus/ai/aiProviderService.ts @@ -626,63 +626,6 @@ export class AIProviderService implements Disposable { return result != null ? { ...result, parsed: parseSummarizeResult(result.content) } : undefined; } - async prepareCompareDataForAIRequest( - repo: Repository, - headRef: string, - baseRef: string, - options?: { - cancellation?: CancellationToken; - reportNoDiffService?: () => void; - reportNoCommitsService?: () => void; - reportNoChanges?: () => void; - }, - ): Promise<{ diff: string; logMessages: string } | undefined> { - const { cancellation, reportNoDiffService, reportNoCommitsService, reportNoChanges } = options ?? {}; - const diffService = repo.git.diff(); - if (diffService?.getDiff === undefined) { - if (reportNoDiffService) { - reportNoDiffService(); - return; - } - } - - const commitsService = repo.git.commits(); - if (commitsService?.getLog === undefined) { - if (reportNoCommitsService) { - reportNoCommitsService(); - return; - } - } - - const [diffResult, logResult] = await Promise.allSettled([ - diffService.getDiff?.(headRef, baseRef, { notation: '...' }), - commitsService.getLog(`${baseRef}..${headRef}`), - ]); - const diff = getSettledValue(diffResult); - const log = getSettledValue(logResult); - - if (!diff?.contents || !log?.commits?.size) { - reportNoChanges?.(); - return undefined; - } - - if (cancellation?.isCancellationRequested) throw new CancellationError(); - - const commitMessages: string[] = []; - for (const commit of [...log.commits.values()].sort((a, b) => a.date.getTime() - b.date.getTime())) { - const message = commit.message ?? commit.summary; - if (message) { - commitMessages.push( - `\n${ - commit.message ?? commit.summary - }\n`, - ); - } - } - - return { diff: diff.contents, logMessages: commitMessages.join('\n\n') }; - } - async generateCreatePullRequest( repo: Repository, baseRef: string, @@ -698,7 +641,7 @@ export class AIProviderService implements Disposable { const result = await this.sendRequest( 'generate-create-pullRequest', async (model, reporting, cancellation, maxInputTokens, retries) => { - const compareData = await this.prepareCompareDataForAIRequest(repo, headRef, baseRef, { + const compareData = await prepareCompareDataForAIRequest(repo, headRef, baseRef, { cancellation: cancellation, }); @@ -1452,3 +1395,60 @@ function isPrimaryAIProvider(provider: AIProviders): provider is AIPrimaryProvid function isPrimaryAIProviderModel(model: AIModel): model is AIModel { return isPrimaryAIProvider(model.provider.id); } + +export async function prepareCompareDataForAIRequest( + repo: Repository, + headRef: string, + baseRef: string, + options?: { + cancellation?: CancellationToken; + reportNoDiffService?: () => void; + reportNoCommitsService?: () => void; + reportNoChanges?: () => void; + }, +): Promise<{ diff: string; logMessages: string } | undefined> { + const { cancellation, reportNoDiffService, reportNoCommitsService, reportNoChanges } = options ?? {}; + const diffService = repo.git.diff(); + if (diffService?.getDiff === undefined) { + if (reportNoDiffService) { + reportNoDiffService(); + return; + } + } + + const commitsService = repo.git.commits(); + if (commitsService?.getLog === undefined) { + if (reportNoCommitsService) { + reportNoCommitsService(); + return; + } + } + + const [diffResult, logResult] = await Promise.allSettled([ + diffService.getDiff?.(headRef, baseRef, { notation: '...' }), + commitsService.getLog(`${baseRef}..${headRef}`), + ]); + const diff = getSettledValue(diffResult); + const log = getSettledValue(logResult); + + if (!diff?.contents || !log?.commits?.size) { + reportNoChanges?.(); + return undefined; + } + + if (cancellation?.isCancellationRequested) throw new CancellationError(); + + const commitMessages: string[] = []; + for (const commit of [...log.commits.values()].sort((a, b) => a.date.getTime() - b.date.getTime())) { + const message = commit.message ?? commit.summary; + if (message) { + commitMessages.push( + `\n${ + commit.message ?? commit.summary + }\n`, + ); + } + } + + return { diff: diff.contents, logMessages: commitMessages.join('\n\n') }; +}