Skip to content

Adds AI powered rebase and explain feature of a branch #4522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds a new _upstream_ sub-command to the _branch_ Git Command Palette
- Adds updated AI model support for GitLens' AI features
- Adds GPT-5 family (GPT-5, GPT-5 Mini, GPT-5 Nano), and Claude 4.1 Opus models
- Add Azure DevOps Server integration support ([#4478](https://github.com/gitkraken/vscode-gitlens/issues/4478))
- Adds AI powered operations for a branch: "Recompose branch commits", "Recompose unpushed commits", "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 Azure DevOps Server integration support ([#4478](https://github.com/gitkraken/vscode-gitlens/issues/4478))

### Changed

Expand Down
78 changes: 78 additions & 0 deletions contributions.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,58 @@
]
}
},
"gitlens.ai.aiRebaseBranch:graph": {
"label": "AI Recompose Branch Commits (Preview)",
"icon": "$(sparkle)",
"menus": {
"webview/context": [
{
"when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+recomposable\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions",
"order": 6
}
]
}
},
"gitlens.ai.aiRebaseBranch:views": {
"label": "AI Recompose Branch Commits (Preview)",
"icon": "$(sparkle)",
"menus": {
"view/item/context": [
{
"when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+recomposable\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions",
"order": 6
}
]
}
},
"gitlens.ai.aiRebaseUnpushed:graph": {
"label": "AI Recompose Unpushed Commits (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_actions",
"order": 7
}
]
}
},
"gitlens.ai.aiRebaseUnpushed:views": {
"label": "AI Recompose Unpushed Commits (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_actions",
"order": 7
}
]
}
},
"gitlens.ai.explainBranch": {
"label": "Explain Branch Changes (Preview)...",
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
Expand Down Expand Up @@ -113,6 +165,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_actions_4",
"order": 102
}
]
}
},
"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": "3_gitlens_ai",
"order": 2
}
]
}
},
"gitlens.ai.explainWip": {
"label": "Explain Working Changes (Preview)...",
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
Expand Down
2 changes: 1 addition & 1 deletion docs/telemetry-events.md

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6147,6 +6147,26 @@
"category": "GitLens",
"icon": "$(person-add)"
},
{
"command": "gitlens.ai.aiRebaseBranch:graph",
"title": "AI Recompose Branch Commits (Preview)",
"icon": "$(sparkle)"
},
{
"command": "gitlens.ai.aiRebaseBranch:views",
"title": "AI Recompose Branch Commits (Preview)",
"icon": "$(sparkle)"
},
{
"command": "gitlens.ai.aiRebaseUnpushed:graph",
"title": "AI Recompose Unpushed Commits (Preview)",
"icon": "$(sparkle)"
},
{
"command": "gitlens.ai.aiRebaseUnpushed:views",
"title": "AI Recompose Unpushed Commits (Preview)",
"icon": "$(sparkle)"
},
{
"command": "gitlens.ai.explainBranch",
"title": "Explain Branch Changes (Preview)...",
Expand Down Expand Up @@ -6192,6 +6212,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)...",
Expand Down Expand Up @@ -10796,6 +10826,22 @@
"command": "gitlens.addAuthors",
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders"
},
{
"command": "gitlens.ai.aiRebaseBranch:graph",
"when": "false"
},
{
"command": "gitlens.ai.aiRebaseBranch:views",
"when": "false"
},
{
"command": "gitlens.ai.aiRebaseUnpushed:graph",
"when": "false"
},
{
"command": "gitlens.ai.aiRebaseUnpushed:views",
"when": "false"
},
{
"command": "gitlens.ai.explainBranch",
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
Expand Down Expand Up @@ -10832,6 +10878,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"
Expand Down Expand Up @@ -16990,6 +17044,16 @@
"when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(remote|tracking)\\b)(?!.*?\\b\\+closed\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && gitlens:repos:withRemotes",
"group": "1_gitlens_actions@3"
},
{
"command": "gitlens.ai.aiRebaseBranch:views",
"when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+recomposable\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions@6"
},
{
"command": "gitlens.ai.aiRebaseUnpushed:views",
"when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+ahead\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions@7"
},
{
"command": "gitlens.views.mergeBranchInto",
"when": "viewItem =~ /gitlens:branch\\b(?!.*?\\b\\+current\\b)(?!.*?\\b\\+closed\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
Expand Down Expand Up @@ -17080,6 +17144,11 @@
"when": "viewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "3_gitlens_ai@1"
},
{
"command": "gitlens.ai.explainUnpushed:views",
"when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+ahead\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "3_gitlens_ai@2"
},
{
"command": "gitlens.views.openChangedFileDiffsWithMergeBase",
"when": "viewItem =~ /gitlens:branch\\b(?!.*?\\b\\+current\\b)/ && !listMultiSelection",
Expand Down Expand Up @@ -22832,6 +22901,16 @@
"when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(remote|tracking)\\b)(?!.*?\\b\\+closed\\b)/ && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && gitlens:repos:withRemotes",
"group": "1_gitlens_actions@3"
},
{
"command": "gitlens.ai.aiRebaseBranch:graph",
"when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+recomposable\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions@6"
},
{
"command": "gitlens.ai.aiRebaseUnpushed:graph",
"when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+ahead\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions@7"
},
{
"command": "gitlens.graph.mergeBranchInto",
"when": "webviewItem =~ /gitlens:branch\\b(?!.*?\\b\\+current\\b)/ && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
Expand Down Expand Up @@ -22907,6 +22986,11 @@
"when": "webviewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
"group": "1_gitlens_actions_4@100"
},
{
"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_actions_4@102"
},
{
"command": "gitlens.graph.openBranchOnRemote",
"when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && gitlens:repos:withRemotes",
Expand Down
27 changes: 19 additions & 8 deletions src/commands/explainBranch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ExplainCommandBase } from './explainBase';

export interface ExplainBranchCommandArgs extends ExplainBaseArgs {
ref?: string;
baseBranch?: string;
}

@command()
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/constants.commands.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
export type ContributedCommands =
| ContributedKeybindingCommands
| ContributedPaletteCommands
| 'gitlens.ai.aiRebaseBranch:graph'
| 'gitlens.ai.aiRebaseBranch:views'
| 'gitlens.ai.aiRebaseUnpushed:graph'
| 'gitlens.ai.aiRebaseUnpushed:views'
| 'gitlens.ai.explainBranch:graph'
| 'gitlens.ai.explainBranch:views'
| 'gitlens.ai.explainCommit:graph'
| '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'
Expand Down
2 changes: 2 additions & 0 deletions src/constants.commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type GlCommandsDeprecated =
| 'gitlens.showFileHistoryInView';

type InternalGraphWebviewCommands =
| 'gitlens.ai.aiRebaseBranch:graph'
| 'gitlens.ai.aiRebaseUnpushed:graph'
| 'gitlens.graph.abortPausedOperation'
| 'gitlens.graph.continuePausedOperation'
| 'gitlens.graph.openRebaseEditor'
Expand Down
16 changes: 16 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
17 changes: 15 additions & 2 deletions src/env/node/git/sub-providers/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
} from '../../../../git/parsers/logParser';
import type { GitGraphSearch, GitGraphSearchResultData, GitGraphSearchResults } from '../../../../git/search';
import { getSearchQueryComparisonKey, parseSearchQueryCommand } from '../../../../git/search';
import { isBranchStarred } from '../../../../git/utils/-webview/branch.utils';
import { getBranchMergeBaseAndCommonCommit, isBranchStarred } from '../../../../git/utils/-webview/branch.utils';
import { getRemoteIconUri } from '../../../../git/utils/-webview/icons';
import { groupWorktreesByBranch } from '../../../../git/utils/-webview/worktree.utils';
import {
Expand Down Expand Up @@ -347,6 +347,13 @@ export class GraphGitSubProvider implements GitGraphSubProvider {

branch = branchMap.get(tip);
branchId = branch?.id ?? getBranchId(repoPath, false, tip);

// Check if branch has commits that can be recomposed and get merge base
const mergeBaseResult =
branch && (await getBranchMergeBaseAndCommonCommit(this.container, branch));
const isRecomposable = Boolean(mergeBaseResult && mergeBaseResult.commit !== branch?.sha);
const mergeBase = isRecomposable ? mergeBaseResult : undefined;

context = {
webviewItem: `gitlens:branch${head ? '+current' : ''}${
branch?.upstream != null ? '+tracking' : ''
Expand All @@ -356,7 +363,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' : ''
}${mergeBase?.commit ? '+recomposable' : ''}`,
webviewItemValue: {
type: 'branch',
ref: createReference(tip, repoPath, {
Expand All @@ -366,6 +375,10 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
remote: false,
upstream: branch?.upstream,
}),
mergeBase: mergeBase && {
...mergeBase,
remote: branchMap.get(mergeBase?.branch)?.remote ?? false,
},
},
};

Expand Down
Loading