diff --git a/CHANGELOG.md b/CHANGELOG.md index 702296f10dcff..75cabe30940d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added +- Adds AI powered operations for a branch: "Explain Unpushed Changed". They are added to the _Commit Graph_ and views context menu for branches ([#4443](https://github.com/gitkraken/vscode-gitlens/issues/4443)) - Adds a new _Safe Hard Reset_ (`--keep`) option to Git _reset_ command - Adds support for reference or range commit searches on the _Commit Graph_, _Search & Compare_ view, and in the _Search Commits_ command ([#4723](https://github.com/gitkraken/vscode-gitlens/issues/4723)) - Adds natural language support to allow for more powerful queries diff --git a/contributions.json b/contributions.json index 98f2a653ee66e..1f828d41554ee 100644 --- a/contributions.json +++ b/contributions.json @@ -113,6 +113,32 @@ ] } }, + "gitlens.ai.explainUnpushed:graph": { + "label": "Explain Unpushed Changes (Preview)", + "icon": "$(sparkle)", + "menus": { + "webview/context": [ + { + "when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+ahead\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled", + "group": "1_gitlens_ai", + "order": 20 + } + ] + } + }, + "gitlens.ai.explainUnpushed:views": { + "label": "Explain Unpushed Changes (Preview)", + "icon": "$(sparkle)", + "menus": { + "view/item/context": [ + { + "when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+ahead\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled", + "group": "1_gitlens_ai", + "order": 20 + } + ] + } + }, "gitlens.ai.explainWip": { "label": "Explain Working Changes (Preview)...", "commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" diff --git a/package.json b/package.json index dfc652126d3d7..883ec8c830c5d 100644 --- a/package.json +++ b/package.json @@ -6251,6 +6251,16 @@ "title": "Explain Changes (Preview)", "icon": "$(sparkle)" }, + { + "command": "gitlens.ai.explainUnpushed:graph", + "title": "Explain Unpushed Changes (Preview)", + "icon": "$(sparkle)" + }, + { + "command": "gitlens.ai.explainUnpushed:views", + "title": "Explain Unpushed Changes (Preview)", + "icon": "$(sparkle)" + }, { "command": "gitlens.ai.explainWip", "title": "Explain Working Changes (Preview)...", @@ -11371,6 +11381,14 @@ "command": "gitlens.ai.explainStash:views", "when": "false" }, + { + "command": "gitlens.ai.explainUnpushed:graph", + "when": "false" + }, + { + "command": "gitlens.ai.explainUnpushed:views", + "when": "false" + }, { "command": "gitlens.ai.explainWip", "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" @@ -18290,6 +18308,11 @@ "when": "viewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled", "group": "1_gitlens_ai@10" }, + { + "command": "gitlens.ai.explainUnpushed:views", + "when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+ahead\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled", + "group": "1_gitlens_ai@20" + }, { "command": "gitlens.views.openBranchOnRemote", "when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && !listMultiSelection", @@ -24162,6 +24185,11 @@ "when": "webviewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled", "group": "1_gitlens_ai@10" }, + { + "command": "gitlens.ai.explainUnpushed:graph", + "when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+ahead\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled", + "group": "1_gitlens_ai@20" + }, { "command": "gitlens.graph.openBranchOnRemote", "when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && gitlens:repos:withRemotes", diff --git a/src/commands/explainBranch.ts b/src/commands/explainBranch.ts index a3e1269205597..e9a32f1e5968b 100644 --- a/src/commands/explainBranch.ts +++ b/src/commands/explainBranch.ts @@ -16,6 +16,7 @@ import { ExplainCommandBase } from './explainBase'; export interface ExplainBranchCommandArgs extends ExplainBaseArgs { ref?: string; + baseBranch?: string; } @command() @@ -67,15 +68,25 @@ export class ExplainBranchCommand extends ExplainCommandBase { } // Clarifying the base branch - const baseBranchNameResult = await getBranchMergeTargetName(this.container, branch); let baseBranch; - if (!baseBranchNameResult.paused) { - baseBranch = await svc.branches.getBranch(baseBranchNameResult.value); - } - - if (!baseBranch) { - void showGenericErrorMessage(`Unable to find the base branch for branch ${branch.name}.`); - return; + if (args.baseBranch) { + // Use the provided base branch + baseBranch = await svc.branches.getBranch(args.baseBranch); + if (!baseBranch) { + void showGenericErrorMessage(`Unable to find the specified base branch: ${args.baseBranch}`); + return; + } + } else { + // Fall back to automatic merge target detection + const baseBranchNameResult = await getBranchMergeTargetName(this.container, branch); + if (!baseBranchNameResult.paused) { + baseBranch = await svc.branches.getBranch(baseBranchNameResult.value); + } + + if (!baseBranch) { + void showGenericErrorMessage(`Unable to find the base branch for branch ${branch.name}.`); + return; + } } // Get the diff between the branch and its upstream or base diff --git a/src/constants.commands.generated.ts b/src/constants.commands.generated.ts index 6c19fa7c1f77f..01766eb63712d 100644 --- a/src/constants.commands.generated.ts +++ b/src/constants.commands.generated.ts @@ -12,6 +12,8 @@ export type ContributedCommands = | 'gitlens.ai.explainCommit:views' | 'gitlens.ai.explainStash:graph' | 'gitlens.ai.explainStash:views' + | 'gitlens.ai.explainUnpushed:graph' + | 'gitlens.ai.explainUnpushed:views' | 'gitlens.ai.explainWip:graph' | 'gitlens.ai.explainWip:views' | 'gitlens.ai.feedback.helpful' diff --git a/src/constants.ts b/src/constants.ts index 1a893ee8bb044..5bbf613ed61bc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -47,6 +47,22 @@ export const enum CharCode { z = 122, } +/** + * `gk-merge-target` means the branch that the current branch is most likely to be merged into, e.g. + * - branch to compare with by default + * - default target for creating a PR + * - etc. + * + * `gk-merge-target-user` — merge target branch explicitly defined by user, + * if it's defined we use this value instead of `gk-merge-target`, but we keep storing `gk-merge-target` value that was determined automatically. + * + * `gk-merge-base` means the branch that the current branch originates from, e.g. what was the base in the moment of creation. + * This value is used for: ... (TODO describe use cases). + * + * `vscode-merge-base` — value determined by VS Code that is used to determine the merge base for the current branch. + * once `gk-merge-base` is determined, we stop using `vscode-merge-base` + * + */ export type GitConfigKeys = | `branch.${string}.vscode-merge-base` | `branch.${string}.gk-merge-base` diff --git a/src/env/node/git/sub-providers/graph.ts b/src/env/node/git/sub-providers/graph.ts index 4dabb3035cc55..1c7e01a8f0e6c 100644 --- a/src/env/node/git/sub-providers/graph.ts +++ b/src/env/node/git/sub-providers/graph.ts @@ -359,7 +359,9 @@ export class GraphGitSubProvider implements GitGraphSubProvider { : branchIdOfMainWorktree === branchId ? '+checkedout' : '' - }${branch?.starred ? '+starred' : ''}`, + }${branch?.starred ? '+starred' : ''}${branch?.upstream?.state.ahead ? '+ahead' : ''}${ + branch?.upstream?.state.behind ? '+behind' : '' + }`, webviewItemValue: { type: 'branch', ref: createReference(tip, repoPath, { diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index c1b5e968e276d..684aff64b6ef6 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -5,6 +5,7 @@ import type { CreatePullRequestActionContext, OpenPullRequestActionContext } fro import type { DiffWithCommandArgs } from '../commands/diffWith'; import type { DiffWithPreviousCommandArgs } from '../commands/diffWithPrevious'; import type { DiffWithWorkingCommandArgs } from '../commands/diffWithWorking'; +import type { ExplainBranchCommandArgs } from '../commands/explainBranch'; import type { GenerateChangelogCommandArgs } from '../commands/generateChangelog'; import { generateChangelogAndOpenMarkdownDocument } from '../commands/generateChangelog'; import type { GenerateRebaseCommandArgs } from '../commands/generateRebase'; @@ -927,6 +928,21 @@ export class ViewCommands implements Disposable { }); } + @command('gitlens.ai.explainUnpushed:views') + @log() + private async explainUnpushed(node: BranchNode) { + if (!node.is('branch') || !node.branch.upstream) { + return Promise.resolve(); + } + + await executeCommand('gitlens.ai.explainBranch', { + repoPath: node.repoPath, + ref: node.branch.ref, + baseBranch: node.branch.upstream.name, + source: { source: 'view', context: { type: 'branch' } }, + }); + } + @command('gitlens.views.rebaseOntoUpstream') @log() private rebaseToRemote(node: BranchNode | BranchTrackingStatusNode) { diff --git a/src/webviews/plus/graph/graphWebview.ts b/src/webviews/plus/graph/graphWebview.ts index 8b77eaf12e04c..9f181967c13c4 100644 --- a/src/webviews/plus/graph/graphWebview.ts +++ b/src/webviews/plus/graph/graphWebview.ts @@ -719,6 +719,7 @@ export class GraphWebviewProvider implements WebviewProvider('gitlens.ai.explainBranch', { + repoPath: ref.repoPath, + ref: ref.ref, + baseBranch: ref.upstream.name, + source: { source: 'graph', context: { type: 'branch' } }, + }); + } + + return Promise.resolve(); + } @log() private explainBranch(item?: GraphItemContext) { const ref = this.getGraphItemRef(item, 'branch');