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
50 changes: 50 additions & 0 deletions contributions.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,32 @@
]
}
},
"gitlens.ai.explainCommit": {
"label": "Explain Commit...",
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"menus": {
"view/item/context": [
{
"when": "viewItem =~ /gitlens:commit\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "3_gitlens_ai",
"order": 1
}
]
}
},
"gitlens.ai.explainStash": {
"label": "Explain Stash (Preview)...",
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"menus": {
"view/item/context": [
{
"when": "viewItem =~ /gitlens:stash\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "3_gitlens_ai",
"order": 1
}
]
}
},
"gitlens.ai.generateChangelog": {
"label": "Generate Changelog (Preview)...",
"commandPalette": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
Expand Down Expand Up @@ -1962,6 +1988,30 @@
]
}
},
"gitlens.graph.explainCommit": {
"label": "Explain Commit",
"menus": {
"webview/context": [
{
"when": "webviewItem =~ /gitlens:commit\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions_3",
"order": 1
}
]
}
},
"gitlens.graph.explainStash": {
"label": "Explain Stash (Preview)",
"menus": {
"webview/context": [
{
"when": "webviewItem =~ /gitlens:stash\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions_3",
"order": 1
}
]
}
},
"gitlens.graph.fetch": {
"label": "Fetch",
"icon": "$(repo-fetch)",
Expand Down
54 changes: 54 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6054,6 +6054,16 @@
"category": "GitLens",
"icon": "$(person-add)"
},
{
"command": "gitlens.ai.explainCommit",
"title": "Explain Commit...",
"category": "GitLens"
},
{
"command": "gitlens.ai.explainStash",
"title": "Explain Stash (Preview)...",
"category": "GitLens"
},
{
"command": "gitlens.ai.generateChangelog",
"title": "Generate Changelog (Preview)...",
Expand Down Expand Up @@ -6817,6 +6827,14 @@
"icon": "$(trash)",
"enablement": "!operationInProgress"
},
{
"command": "gitlens.graph.explainCommit",
"title": "Explain Commit"
},
{
"command": "gitlens.graph.explainStash",
"title": "Explain Stash (Preview)"
},
{
"command": "gitlens.graph.fetch",
"title": "Fetch",
Expand Down Expand Up @@ -10362,6 +10380,14 @@
"command": "gitlens.addAuthors",
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders"
},
{
"command": "gitlens.ai.explainCommit",
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
},
{
"command": "gitlens.ai.explainStash",
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
},
{
"command": "gitlens.ai.generateChangelog",
"when": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
Expand Down Expand Up @@ -10954,6 +10980,14 @@
"command": "gitlens.graph.deleteTag",
"when": "false"
},
{
"command": "gitlens.graph.explainCommit",
"when": "false"
},
{
"command": "gitlens.graph.explainStash",
"when": "false"
},
{
"command": "gitlens.graph.fetch",
"when": "false"
Expand Down Expand Up @@ -16578,6 +16612,11 @@
"when": "viewItem =~ /gitlens:(compare:results(?!:)\\b(?!.*?\\b\\+filtered\\b)|commit|stash|results:files|status-branch:files|status:upstream:(ahead|behind))\\b/ && !listMultiSelection",
"group": "2_gitlens_quickopen@1"
},
{
"command": "gitlens.ai.explainCommit",
"when": "viewItem =~ /gitlens:commit\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "3_gitlens_ai@1"
},
{
"command": "gitlens.showInDetailsView",
"when": "viewItem =~ /gitlens:(commit|stash)\\b/ && !listMultiSelection",
Expand Down Expand Up @@ -17634,6 +17673,11 @@
"when": "viewItem == gitlens:stash && listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
"group": "1_gitlens_actions@3"
},
{
"command": "gitlens.ai.explainStash",
"when": "viewItem =~ /gitlens:stash\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "3_gitlens_ai@1"
},
{
"command": "gitlens.stashSave",
"when": "viewItem =~ /^gitlens:(stashes|status:files)$/ && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
Expand Down Expand Up @@ -20099,6 +20143,11 @@
"when": "webviewItem =~ /gitlens:commit\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
"group": "1_gitlens_actions_1@4"
},
{
"command": "gitlens.graph.explainCommit",
"when": "webviewItem =~ /gitlens:commit\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions_3@1"
},
{
"submenu": "gitlens/graph/commit/changes",
"when": "webviewItem =~ /gitlens:(commit|stash|wip)\\b/ && !listMultiSelection",
Expand Down Expand Up @@ -20259,6 +20308,11 @@
"when": "webviewItem == gitlens:stash && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
"group": "1_gitlens_actions@3"
},
{
"command": "gitlens.graph.explainStash",
"when": "webviewItem =~ /gitlens:stash\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions_3@1"
},
{
"command": "gitlens.graph.switchToTag",
"when": "webviewItem =~ /gitlens:tag\\b/ && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
Expand Down
2 changes: 2 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import './commands/diffWithRevision';
import './commands/diffWithRevisionFrom';
import './commands/diffWithWorking';
import './commands/externalDiff';
import './commands/explainCommit';
import './commands/explainStash';
import './commands/generateChangelog';
import './commands/generateCommitMessage';
import './commands/ghpr/openOrCreateWorktree';
Expand Down
102 changes: 102 additions & 0 deletions src/commands/explainCommit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { TextEditor, Uri } from 'vscode';
import { ProgressLocation } from 'vscode';
import type { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { showGenericErrorMessage } from '../messages';
import type { AIExplainSource } from '../plus/ai/aiProviderService';
import { showCommitPicker } from '../quickpicks/commitPicker';
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 { isCommandContextViewNodeHasCommit } from './commandContext.utils';

export interface ExplainCommitCommandArgs {
repoPath?: string | Uri;
ref?: string;
source?: AIExplainSource;
}

@command()
export class ExplainCommitCommand extends GlCommandBase {
constructor(private readonly container: Container) {
super('gitlens.ai.explainCommit');
}

protected override preExecute(context: CommandContext, args?: ExplainCommitCommandArgs): Promise<void> {
// Check if the command is being called from a CommitNode
if (isCommandContextViewNodeHasCommit(context)) {
args = { ...args };
args.repoPath = args.repoPath ?? getNodeRepoPath(context.node);
args.ref = args.ref ?? context.node.commit.sha;
args.source = args.source ?? { source: 'view', type: 'commit' };
}

return this.execute(context.editor, context.uri, args);
}

async execute(editor?: TextEditor, uri?: Uri, args?: ExplainCommitCommandArgs): Promise<void> {
args = { ...args };

let repository;
if (args?.repoPath != null) {
repository = this.container.git.getRepository(args.repoPath);
}

if (repository == null) {
uri = getCommandUri(uri, editor);
const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined;
repository = await getBestRepositoryOrShowPicker(
gitUri,
editor,
'Explain Commit',
'Choose which repository to explain a commit from',
);
}

if (repository == null) return;

try {
// If no ref is provided, show a picker to select a commit
if (args.ref == null) {
const commitsProvider = repository.git.commits();
const log = await commitsProvider.getLog();
const pick = await showCommitPicker(log, 'Explain Commit', 'Choose a commit to explain');
if (pick?.sha == null) return;
args.ref = pick.sha;
}

// Get the commit
const commit = await repository.git.commits().getCommit(args.ref);
if (commit == null) {
void showGenericErrorMessage('Unable to find the specified commit');
return;
}

// Call the AI service to explain the commit
const result = await this.container.ai.explainCommit(
commit,
args.source ?? { source: 'commandPalette', type: 'commit' },
{
progress: { location: ProgressLocation.Notification, title: 'Explaining commit...' },
},
);

// Display the result
let content = `# Commit Summary\n\n`;
if (result != null) {
content += `> Generated by ${result.model.name}\n\n## ${commit.summary} (${commit.shortSha})\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, 'ExplainCommitCommand', 'execute');
void showGenericErrorMessage('Unable to explain commit');
}
}
}
101 changes: 101 additions & 0 deletions src/commands/explainStash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { TextEditor, Uri } from 'vscode';
import { ProgressLocation } from 'vscode';
import type { Container } from '../container';
import { GitUri } from '../git/gitUri';
import type { GitStashCommit } from '../git/models/commit';
import { showGenericErrorMessage } from '../messages';
import type { AIExplainSource } from '../plus/ai/aiProviderService';
import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker';
import { showStashPicker } from '../quickpicks/stashPicker';
import { command } from '../system/-webview/command';
import { showMarkdownPreview } from '../system/-webview/markdown';
import { Logger } from '../system/logger';
import { GlCommandBase } from './commandBase';
import { getCommandUri } from './commandBase.utils';
import type { CommandContext } from './commandContext';
import { isCommandContextViewNodeHasCommit } from './commandContext.utils';

export interface ExplainStashCommandArgs {
repoPath?: string | Uri;
ref?: string;
source?: AIExplainSource;
}

@command()
export class ExplainStashCommand extends GlCommandBase {
constructor(private readonly container: Container) {
super('gitlens.ai.explainStash');
}

protected override preExecute(context: CommandContext, args?: ExplainStashCommandArgs): Promise<void> {
// Check if the command is being called from a CommitNode
if (isCommandContextViewNodeHasCommit<GitStashCommit>(context)) {
args = { ...args };
args.repoPath = args.repoPath ?? context.node.commit.repoPath;
args.ref = args.ref ?? context.node.commit.sha;
args.source = args.source ?? { source: 'view', type: 'stash' };
}

return this.execute(context.editor, context.uri, args);
}

async execute(editor?: TextEditor, uri?: Uri, args?: ExplainStashCommandArgs): Promise<void> {
args = { ...args };

let repository;
if (args?.repoPath != null) {
repository = this.container.git.getRepository(args.repoPath);
}

if (repository == null) {
uri = getCommandUri(uri, editor);
const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined;
repository = await getBestRepositoryOrShowPicker(
gitUri,
editor,
'Explain Stash',
'Choose which repository to explain a stash from',
);
}

if (repository == null) return;

try {
// If no ref is provided, show a picker to select a stash
if (args.ref == null) {
const pick = await showStashPicker('Explain Stash', 'Choose a stash to explain', repository);
if (pick?.ref == null) return;
args.ref = pick.ref;
}

// Get the stash commit
const commit = await repository.git.commits().getCommit(args.ref);
if (commit == null) {
void showGenericErrorMessage('Unable to find the specified stash commit');
return;
}

// Call the AI service to explain the stash
const result = await this.container.ai.explainCommit(
commit,
args.source ?? { source: 'commandPalette', type: 'stash' },
{
progress: { location: ProgressLocation.Notification, title: 'Explaining stash...' },
},
);

// Display the result
let content = `# Stash Summary\n\n`;
if (result != null) {
content += `> Generated by ${result.model.name}\n\n## ${commit.message || commit.ref}}\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, 'ExplainStashCommand', 'execute');
void showGenericErrorMessage('Unable to explain stash');
}
}
}
Loading