From b235339fb5adaaba6081dbc29d93acd953c95307 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Thu, 31 Jul 2025 20:13:12 +0200 Subject: [PATCH 1/4] Adds AI-powered explanation for unpushed branch changes on Graph View Also adds more item context for new AI-assisted commands to explain unpushed changes (#4443, #4522) --- contributions.json | 13 ++++++++++++ package.json | 14 +++++++++++++ src/commands/explainBranch.ts | 27 +++++++++++++++++-------- src/constants.commands.generated.ts | 1 + src/env/node/git/sub-providers/graph.ts | 4 +++- src/webviews/plus/graph/graphWebview.ts | 20 ++++++++++++++++++ 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/contributions.json b/contributions.json index 98f2a653ee66e..5029e383e8b59 100644 --- a/contributions.json +++ b/contributions.json @@ -113,6 +113,19 @@ ] } }, + "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.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..364d5ded3d223 100644 --- a/package.json +++ b/package.json @@ -6251,6 +6251,11 @@ "title": "Explain Changes (Preview)", "icon": "$(sparkle)" }, + { + "command": "gitlens.ai.explainUnpushed:graph", + "title": "Explain Unpushed Changes (Preview)", + "icon": "$(sparkle)" + }, { "command": "gitlens.ai.explainWip", "title": "Explain Working Changes (Preview)...", @@ -11371,6 +11376,10 @@ "command": "gitlens.ai.explainStash:views", "when": "false" }, + { + "command": "gitlens.ai.explainUnpushed:graph", + "when": "false" + }, { "command": "gitlens.ai.explainWip", "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" @@ -24162,6 +24171,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..3547219e15745 100644 --- a/src/constants.commands.generated.ts +++ b/src/constants.commands.generated.ts @@ -12,6 +12,7 @@ export type ContributedCommands = | 'gitlens.ai.explainCommit:views' | 'gitlens.ai.explainStash:graph' | 'gitlens.ai.explainStash:views' + | 'gitlens.ai.explainUnpushed:graph' | 'gitlens.ai.explainWip:graph' | 'gitlens.ai.explainWip:views' | 'gitlens.ai.feedback.helpful' 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/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'); From 95742ccd230b4d9c4b4adad21c3624492f09425b Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 8 Aug 2025 00:35:54 +0200 Subject: [PATCH 2/4] Add AI explain and recompose branch actions to Branch Views Introduces AI-powered branch actions ("Recompose" and "Explain") directly into the branches view, enabling context menu options when branches are recomposable or have unpushed commits. Refactors and centralizes branch recomposability detection to ensure consistent logic across graph and views, improving maintainability and user experience. Enhances discoverability and workflow integration for AI-assisted Git operations. (#4443, #4522) --- contributions.json | 13 +++++++++++++ package.json | 14 ++++++++++++++ src/constants.commands.generated.ts | 1 + src/views/viewCommands.ts | 16 ++++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/contributions.json b/contributions.json index 5029e383e8b59..1f828d41554ee 100644 --- a/contributions.json +++ b/contributions.json @@ -126,6 +126,19 @@ ] } }, + "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 364d5ded3d223..883ec8c830c5d 100644 --- a/package.json +++ b/package.json @@ -6256,6 +6256,11 @@ "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)...", @@ -11380,6 +11385,10 @@ "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" @@ -18299,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", diff --git a/src/constants.commands.generated.ts b/src/constants.commands.generated.ts index 3547219e15745..01766eb63712d 100644 --- a/src/constants.commands.generated.ts +++ b/src/constants.commands.generated.ts @@ -13,6 +13,7 @@ export type ContributedCommands = | '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/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) { From 84fed4fc4fe4cd9ee0704b1a6e5ccb14c3ba0a89 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 8 Aug 2025 16:10:09 +0200 Subject: [PATCH 3/4] Clarifies Git merge base and target config key semantics (#4443, #4522) --- src/constants.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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` From d532af2f8d2e1320759b774b46fd31b9a5c1f74d Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 8 Aug 2025 16:13:59 +0200 Subject: [PATCH 4/4] Updates CHANGELOG by describing new AI powered actions on a branch (#4443, #4522) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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