Skip to content

Commit cc8dc40

Browse files
committed
Adds explain stash command
1 parent 03aad85 commit cc8dc40

File tree

6 files changed

+225
-0
lines changed

6 files changed

+225
-0
lines changed

contributions.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@
3636
]
3737
}
3838
},
39+
"gitlens.ai.explainStash": {
40+
"label": "Explain Stash (Preview)...",
41+
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
42+
"menus": {
43+
"view/item/context": [
44+
{
45+
"when": "viewItem =~ /gitlens:stash\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
46+
"group": "3_gitlens_ai",
47+
"order": 1
48+
}
49+
]
50+
}
51+
},
52+
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
53+
},
3954
"gitlens.ai.generateChangelog": {
4055
"label": "Generate Changelog (Preview)...",
4156
"commandPalette": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6032,6 +6032,11 @@
60326032
"title": "Explain Commit",
60336033
"category": "GitLens"
60346034
},
6035+
{
6036+
"command": "gitlens.ai.explainStash",
6037+
"title": "Explain Stash (Preview)...",
6038+
"category": "GitLens"
6039+
},
60356040
{
60366041
"command": "gitlens.ai.generateChangelog",
60376042
"title": "Generate Changelog (Preview)...",
@@ -10335,6 +10340,10 @@
1033510340
"command": "gitlens.addAuthors",
1033610341
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders"
1033710342
},
10343+
{
10344+
"command": "gitlens.ai.explainStash",
10345+
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
10346+
},
1033810347
{
1033910348
"command": "gitlens.ai.generateChangelog",
1034010349
"when": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
@@ -17603,6 +17612,11 @@
1760317612
"when": "viewItem == gitlens:stash && listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
1760417613
"group": "1_gitlens_actions@3"
1760517614
},
17615+
{
17616+
"command": "gitlens.ai.explainStash",
17617+
"when": "viewItem =~ /gitlens:stash\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
17618+
"group": "3_gitlens_ai@1"
17619+
},
1760617620
{
1760717621
"command": "gitlens.stashSave",
1760817622
"when": "viewItem =~ /^gitlens:(stashes|status:files)$/ && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",

src/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import './commands/diffWithRevisionFrom';
2222
import './commands/diffWithWorking';
2323
import './commands/externalDiff';
2424
import './commands/explainCommit';
25+
import './commands/explainStash';
2526
import './commands/generateChangelog';
2627
import './commands/generateCommitMessage';
2728
import './commands/ghpr/openOrCreateWorktree';

src/commands/explainStash.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { TextEditor, Uri } from 'vscode';
2+
import { ProgressLocation } from 'vscode';
3+
import type { Container } from '../container';
4+
import { GitUri } from '../git/gitUri';
5+
import type { GitStashCommit } from '../git/models/commit';
6+
import { showGenericErrorMessage } from '../messages';
7+
import type { AIExplainSource } from '../plus/ai/aiProviderService';
8+
import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker';
9+
import { showStashPicker } from '../quickpicks/stashPicker';
10+
import { command } from '../system/-webview/command';
11+
import { showMarkdownPreview } from '../system/-webview/markdown';
12+
import { Logger } from '../system/logger';
13+
import { GlCommandBase } from './commandBase';
14+
import { getCommandUri } from './commandBase.utils';
15+
import type { CommandContext } from './commandContext';
16+
import { isCommandContextViewNodeHasCommit } from './commandContext.utils';
17+
18+
export interface ExplainStashCommandArgs {
19+
repoPath?: string | Uri;
20+
ref?: string;
21+
source?: AIExplainSource;
22+
}
23+
24+
@command()
25+
export class ExplainStashCommand extends GlCommandBase {
26+
constructor(private readonly container: Container) {
27+
super('gitlens.ai.explainStash');
28+
}
29+
30+
protected override preExecute(context: CommandContext, args?: ExplainStashCommandArgs): Promise<void> {
31+
// Check if the command is being called from a CommitNode
32+
if (isCommandContextViewNodeHasCommit<GitStashCommit>(context)) {
33+
args = { ...args };
34+
args.repoPath = args.repoPath ?? context.node.commit.repoPath;
35+
args.ref = args.ref ?? context.node.commit.sha;
36+
args.source = args.source ?? { source: 'view', type: 'stash' };
37+
}
38+
39+
return this.execute(context.editor, context.uri, args);
40+
}
41+
42+
async execute(editor?: TextEditor, uri?: Uri, args?: ExplainStashCommandArgs): Promise<void> {
43+
args = { ...args };
44+
45+
let repository;
46+
if (args?.repoPath != null) {
47+
repository = this.container.git.getRepository(args.repoPath);
48+
}
49+
50+
if (repository == null) {
51+
uri = getCommandUri(uri, editor);
52+
const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined;
53+
repository = await getBestRepositoryOrShowPicker(
54+
gitUri,
55+
editor,
56+
'Explain Stash',
57+
'Choose which repository to explain a stash from',
58+
);
59+
}
60+
61+
if (repository == null) return;
62+
63+
try {
64+
// If no ref is provided, show a picker to select a stash
65+
if (args.ref == null) {
66+
const pick = await showStashPicker('Explain Stash', 'Choose a stash to explain', repository);
67+
if (pick?.ref == null) return;
68+
args.ref = pick.ref;
69+
}
70+
71+
// Get the stash commit
72+
const commit = await repository.git.commits().getCommit(args.ref);
73+
if (commit == null) {
74+
void showGenericErrorMessage('Unable to find the specified stash commit');
75+
return;
76+
}
77+
78+
// Call the AI service to explain the stash
79+
const result = await this.container.ai.explainCommit(
80+
commit,
81+
args.source ?? { source: 'commandPalette', type: 'stash' },
82+
{
83+
progress: { location: ProgressLocation.Notification, title: 'Explaining stash...' },
84+
},
85+
);
86+
87+
// Display the result
88+
let content = `# Stash Summary\n\n`;
89+
if (result != null) {
90+
content += `> Generated by ${result.model.name}\n\n## ${commit.message || commit.ref}}\n\n${result
91+
?.parsed.summary}\n\n${result?.parsed.body}`;
92+
} else {
93+
content += `> No changes found to explain.`;
94+
}
95+
void showMarkdownPreview(content);
96+
} catch (ex) {
97+
Logger.error(ex, 'ExplainStashCommand', 'execute');
98+
void showGenericErrorMessage('Unable to explain stash');
99+
}
100+
}
101+
}

src/constants.commands.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ export type ContributedCommands =
613613
export type ContributedPaletteCommands =
614614
| 'gitlens.addAuthors'
615615
| 'gitlens.ai.explainCommit'
616+
| 'gitlens.ai.explainStash'
616617
| 'gitlens.ai.generateChangelog'
617618
| 'gitlens.ai.generateCommitMessage'
618619
| 'gitlens.applyPatchFromClipboard'

src/quickpicks/stashPicker.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { Disposable } from 'vscode';
2+
import { window } from 'vscode';
3+
import { RevealInSideBarQuickInputButton, ShowDetailsViewQuickInputButton } from '../commands/quickCommand.buttons';
4+
import * as StashActions from '../git/actions/stash';
5+
import type { GitStashCommit } from '../git/models/commit';
6+
import { Repository } from '../git/models/repository';
7+
import { getQuickPickIgnoreFocusOut } from '../system/-webview/vscode';
8+
import type { CommitQuickPickItem } from './items/gitWizard';
9+
import { createStashQuickPickItem } from './items/gitWizard';
10+
11+
export async function showStashPicker(
12+
title: string | undefined,
13+
placeholder?: string,
14+
repository?: Repository | Repository[],
15+
options?: {
16+
filter?: (b: GitStashCommit) => boolean;
17+
},
18+
): Promise<GitStashCommit | undefined> {
19+
if (repository == null) {
20+
return undefined;
21+
}
22+
23+
if (repository instanceof Repository) {
24+
repository = [repository];
25+
}
26+
27+
let stashes: GitStashCommit[] = [];
28+
for (const repo of repository) {
29+
const stash = await repo.git.stash()?.getStash();
30+
if (stash == null || stash.stashes.size === 0) {
31+
continue;
32+
}
33+
34+
stashes.push(...stash.stashes.values());
35+
}
36+
37+
if (options?.filter != null) {
38+
stashes = stashes.filter(options.filter);
39+
}
40+
41+
if (stashes.length === 0) {
42+
return undefined;
43+
}
44+
45+
const items: CommitQuickPickItem<GitStashCommit>[] = stashes.map(stash =>
46+
createStashQuickPickItem(stash, false, {
47+
buttons: [ShowDetailsViewQuickInputButton, RevealInSideBarQuickInputButton],
48+
compact: true,
49+
icon: true,
50+
}),
51+
);
52+
53+
const quickpick = window.createQuickPick<CommitQuickPickItem<GitStashCommit>>();
54+
quickpick.ignoreFocusOut = getQuickPickIgnoreFocusOut();
55+
const disposables: Disposable[] = [];
56+
57+
try {
58+
const pick = await new Promise<CommitQuickPickItem<GitStashCommit> | undefined>(resolve => {
59+
disposables.push(
60+
quickpick.onDidHide(() => resolve(undefined)),
61+
quickpick.onDidAccept(() => {
62+
if (quickpick.activeItems.length !== 0) {
63+
resolve(quickpick.activeItems[0]);
64+
}
65+
}),
66+
quickpick.onDidTriggerItemButton(e => {
67+
if (e.button === ShowDetailsViewQuickInputButton) {
68+
void StashActions.showDetailsView(e.item.item, { pin: false, preserveFocus: true });
69+
} else if (e.button === RevealInSideBarQuickInputButton) {
70+
void StashActions.reveal(e.item.item, {
71+
select: true,
72+
focus: false,
73+
expand: true,
74+
});
75+
}
76+
}),
77+
);
78+
79+
quickpick.title = title;
80+
quickpick.placeholder = placeholder;
81+
quickpick.matchOnDescription = true;
82+
quickpick.matchOnDetail = true;
83+
quickpick.items = items;
84+
85+
quickpick.show();
86+
});
87+
88+
return pick?.item;
89+
} finally {
90+
quickpick.dispose();
91+
disposables.forEach(d => void d.dispose());
92+
}
93+
}

0 commit comments

Comments
 (0)