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
25 changes: 25 additions & 0 deletions contributions.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@
]
}
},
"gitlens.ai.explainBranch": {
"label": "Explain Branch (Preview)...",
"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...",
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled && config.gitlens.ai.enabled",
Expand Down Expand Up @@ -1374,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": {
Expand Down
4 changes: 2 additions & 2 deletions docs/telemetry-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'
}
```

Expand Down
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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...",
Expand Down Expand Up @@ -6624,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"
Expand Down Expand Up @@ -10409,6 +10418,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"
Expand Down Expand Up @@ -10821,6 +10834,10 @@
"command": "gitlens.graph.addAuthor",
"when": "false"
},
{
"command": "gitlens.graph.ai.explainBranch",
"when": "false"
},
{
"command": "gitlens.graph.ai.explainCommit",
"when": "false"
Expand Down Expand Up @@ -16388,6 +16405,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",
Expand Down Expand Up @@ -20060,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",
Expand Down
1 change: 1 addition & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
166 changes: 166 additions & 0 deletions src/commands/explainBranch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { CancellationToken, TextEditor, Uri } from 'vscode';
import { ProgressLocation } from 'vscode';
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 { prepareCompareDataForAIRequest } from '../plus/ai/aiProviderService';
import { ReferencesQuickPickIncludes, showReferencePicker } from '../quickpicks/referencePicker';
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;
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<void> {
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<void> {
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) {
// If no ref is provided, show a picker to select a branch
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;
}

// 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 ${branch.name}.`);
return;
}

// Get the diff between the branch and its upstream or base
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;
}

const { diff, logMessages } = compareData;

const changes = {
diff: diff,
message: `Changes in branch ${branch.name}
that is ahead of its target by number of commits with the following messages:\n\n
<commits>
${logMessages}
<end-of-commits>
`,
};

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

if (result == null) {
void showGenericErrorMessage(`Unable to explain branch ${branch.name}`);
return;
}

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');
}
}
}

async function getMergeTarget(
container: Container,
branch: GitBranch,
options?: { cancellation?: CancellationToken },
): Promise<string | undefined> {
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<string | undefined> {
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}`;
}
2 changes: 2 additions & 0 deletions src/constants.commands.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -614,6 +615,7 @@ export type ContributedCommands =

export type ContributedPaletteCommands =
| 'gitlens.addAuthors'
| 'gitlens.ai.explainBranch'
| 'gitlens.ai.explainCommit'
| 'gitlens.ai.explainStash'
| 'gitlens.ai.explainWip'
Expand Down
1 change: 1 addition & 0 deletions src/constants.commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type InternalHomeWebviewCommands =
| 'gitlens.home.continuePausedOperation'
| 'gitlens.home.abortPausedOperation'
| 'gitlens.home.explainWip'
| 'gitlens.home.ai.explainBranch'
| 'gitlens.home.openRebaseEditor';

type InternalHomeWebviewViewCommands =
Expand Down
2 changes: 1 addition & 1 deletion src/constants.telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions src/env/node/git/sub-providers/branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,21 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
await this.provider.config.setConfig(repoPath, mergeTargetConfigKey, target);
}

async getMergeTargetBranchName(repoPath: string, branch: GitBranch): Promise<string | undefined> {
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,
Expand Down
1 change: 1 addition & 0 deletions src/git/gitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export interface GitBranchesSubProvider {
setTargetBranchName?(repoPath: string, ref: string, target: string): Promise<void>;
getUserMergeTargetBranchName?(repoPath: string, ref: string): Promise<string | undefined>;
setUserMergeTargetBranchName?(repoPath: string, ref: string, target: string | undefined): Promise<void>;
getMergeTargetBranchName?(repoPath: string, branch: GitBranch): Promise<string | undefined>;
renameBranch?(repoPath: string, oldName: string, newName: string): Promise<void>;
}

Expand Down
Loading