Skip to content

Commit 60edf64

Browse files
Adds initial support for recomposing commits from a branch
1 parent e3e7c99 commit 60edf64

File tree

18 files changed

+817
-130
lines changed

18 files changed

+817
-130
lines changed

contributions.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4265,6 +4265,36 @@
42654265
]
42664266
}
42674267
},
4268+
"gitlens.recomposeBranch": {
4269+
"label": "Recompose Commits (Preview)...",
4270+
"commandPalette": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
4271+
},
4272+
"gitlens.recomposeBranch:graph": {
4273+
"label": "Recompose Commits (Preview)",
4274+
"icon": "$(sparkle)",
4275+
"menus": {
4276+
"webview/context": [
4277+
{
4278+
"when": "webviewItem =~ /gitlens:branch\\b(?!.*?\\b\\+remote\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
4279+
"group": "1_gitlens_ai",
4280+
"order": 15
4281+
}
4282+
]
4283+
}
4284+
},
4285+
"gitlens.recomposeBranch:views": {
4286+
"label": "Recompose Commits (Preview)",
4287+
"icon": "$(sparkle)",
4288+
"menus": {
4289+
"view/item/context": [
4290+
{
4291+
"when": "viewItem =~ /gitlens:branch\\b(?!.*?\\b\\+remote\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
4292+
"group": "1_gitlens_ai",
4293+
"order": 15
4294+
}
4295+
]
4296+
}
4297+
},
42684298
"gitlens.regenerateMarkdownDocument": {
42694299
"label": "Regenerate",
42704300
"icon": "$(refresh)",

package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7863,6 +7863,21 @@
78637863
"command": "gitlens.quickOpenFileHistory:graphDetails",
78647864
"title": "Quick Open File History"
78657865
},
7866+
{
7867+
"command": "gitlens.recomposeBranch",
7868+
"title": "Recompose Commits (Preview)...",
7869+
"category": "GitLens"
7870+
},
7871+
{
7872+
"command": "gitlens.recomposeBranch:graph",
7873+
"title": "Recompose Commits (Preview)",
7874+
"icon": "$(sparkle)"
7875+
},
7876+
{
7877+
"command": "gitlens.recomposeBranch:views",
7878+
"title": "Recompose Commits (Preview)",
7879+
"icon": "$(sparkle)"
7880+
},
78667881
{
78677882
"command": "gitlens.regenerateMarkdownDocument",
78687883
"title": "Regenerate",
@@ -12584,6 +12599,18 @@
1258412599
"command": "gitlens.quickOpenFileHistory:graphDetails",
1258512600
"when": "false"
1258612601
},
12602+
{
12603+
"command": "gitlens.recomposeBranch",
12604+
"when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
12605+
},
12606+
{
12607+
"command": "gitlens.recomposeBranch:graph",
12608+
"when": "false"
12609+
},
12610+
{
12611+
"command": "gitlens.recomposeBranch:views",
12612+
"when": "false"
12613+
},
1258712614
{
1258812615
"command": "gitlens.regenerateMarkdownDocument",
1258912616
"when": "false"
@@ -18164,6 +18191,11 @@
1816418191
"when": "viewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
1816518192
"group": "1_gitlens_ai@10"
1816618193
},
18194+
{
18195+
"command": "gitlens.recomposeBranch:views",
18196+
"when": "viewItem =~ /gitlens:branch\\b(?!.*?\\b\\+remote\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
18197+
"group": "1_gitlens_ai@15"
18198+
},
1816718199
{
1816818200
"command": "gitlens.views.openBranchOnRemote",
1816918201
"when": "viewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && !listMultiSelection",
@@ -24031,6 +24063,11 @@
2403124063
"when": "webviewItem =~ /gitlens:branch\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
2403224064
"group": "1_gitlens_ai@10"
2403324065
},
24066+
{
24067+
"command": "gitlens.recomposeBranch:graph",
24068+
"when": "webviewItem =~ /gitlens:branch\\b(?!.*?\\b\\+remote\\b)/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
24069+
"group": "1_gitlens_ai@15"
24070+
},
2403424071
{
2403524072
"command": "gitlens.graph.openBranchOnRemote",
2403624073
"when": "webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+(tracking|remote)\\b)/ && gitlens:repos:withRemotes",

src/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import './commands/openWorkingFile';
5454
import './commands/patches';
5555
import './commands/quickWizard';
5656
import './commands/rebaseEditor';
57+
import './commands/recomposeBranch';
5758
import './commands/refreshHover';
5859
import './commands/regenerateMarkdownDocument';
5960
import './commands/remoteProviders';

src/commands/recomposeBranch.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { window } from 'vscode';
2+
import type { Sources } from '../constants.telemetry';
3+
import type { Container } from '../container';
4+
import { CommandQuickPickItem } from '../quickpicks/items/common';
5+
import { ReferencesQuickPickIncludes, showReferencePicker } from '../quickpicks/referencePicker';
6+
import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker';
7+
import { command, executeCommand } from '../system/-webview/command';
8+
import { getNodeRepoPath } from '../views/nodes/abstract/viewNode';
9+
import type { WebviewPanelShowCommandArgs } from '../webviews/webviewsController';
10+
import { GlCommandBase } from './commandBase';
11+
import type { CommandContext } from './commandContext';
12+
import { isCommandContextViewNodeHasBranch } from './commandContext.utils';
13+
14+
export interface RecomposeBranchCommandArgs {
15+
repoPath?: string;
16+
branchName?: string;
17+
source?: Sources;
18+
}
19+
20+
@command()
21+
export class RecomposeBranchCommand extends GlCommandBase {
22+
constructor(private readonly container: Container) {
23+
super(['gitlens.recomposeBranch', 'gitlens.recomposeBranch:views']);
24+
}
25+
26+
protected override preExecute(context: CommandContext, args?: RecomposeBranchCommandArgs): Promise<void> {
27+
if (isCommandContextViewNodeHasBranch(context)) {
28+
args = { ...args };
29+
args.repoPath = args.repoPath ?? getNodeRepoPath(context.node);
30+
args.branchName = args.branchName ?? context.node.branch.name;
31+
args.source = args.source ?? 'view';
32+
}
33+
34+
return this.execute(args);
35+
}
36+
37+
async execute(args?: RecomposeBranchCommandArgs): Promise<void> {
38+
try {
39+
// Get repository path using picker fallback
40+
const repoPath =
41+
args?.repoPath ??
42+
(await getBestRepositoryOrShowPicker(this.container, undefined, undefined, 'Recompose Branch'))?.path;
43+
if (!repoPath) return;
44+
45+
args = { ...args };
46+
47+
// Get branch name using picker fallback
48+
let branchName = args.branchName;
49+
if (!branchName) {
50+
const pick = await showReferencePicker(repoPath, 'Recompose Branch', 'Choose a branch to recompose', {
51+
include: ReferencesQuickPickIncludes.Branches,
52+
sort: { branches: { current: true } },
53+
});
54+
if (pick == null || pick instanceof CommandQuickPickItem) return;
55+
56+
if (pick.refType === 'branch') {
57+
branchName = pick.name;
58+
} else {
59+
return;
60+
}
61+
}
62+
63+
// Validate that the repository exists
64+
const repo = this.container.git.getRepository(repoPath);
65+
if (!repo) {
66+
void window.showErrorMessage('Repository not found');
67+
return;
68+
}
69+
70+
// Validate that the branch exists
71+
const branch = await repo.git.branches.getBranch(branchName);
72+
if (!branch) {
73+
void window.showErrorMessage(`Branch '${branchName}' not found`);
74+
return;
75+
}
76+
77+
// Check if branch is remote-only
78+
if (branch.remote && !branch.upstream) {
79+
void window.showErrorMessage(`Cannot recompose remote-only branch '${branchName}'`);
80+
return;
81+
}
82+
83+
// Open the composer with branch mode
84+
await executeCommand<WebviewPanelShowCommandArgs>('gitlens.showComposerPage', undefined, {
85+
repoPath: repoPath,
86+
source: args?.source,
87+
mode: 'preview',
88+
branchName: branchName,
89+
});
90+
} catch (ex) {
91+
void window.showErrorMessage(`Failed to recompose branch: ${ex}`);
92+
}
93+
}
94+
}

src/constants.commands.generated.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ export type ContributedCommands =
238238
| 'gitlens.quickOpenFileHistory'
239239
| 'gitlens.quickOpenFileHistory:commitDetails'
240240
| 'gitlens.quickOpenFileHistory:graphDetails'
241+
| 'gitlens.recomposeBranch:graph'
242+
| 'gitlens.recomposeBranch:views'
241243
| 'gitlens.regenerateMarkdownDocument'
242244
| 'gitlens.restore.file:commitDetails'
243245
| 'gitlens.restore.file:graphDetails'
@@ -909,6 +911,7 @@ export type ContributedPaletteCommands =
909911
| 'gitlens.pullRepositories'
910912
| 'gitlens.pushRepositories'
911913
| 'gitlens.quickOpenFileHistory'
914+
| 'gitlens.recomposeBranch'
912915
| 'gitlens.reset'
913916
| 'gitlens.resetViewsLayout'
914917
| 'gitlens.revealCommitInView'

src/env/node/git/sub-providers/refs.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,21 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
178178
);
179179
return result.stdout.trim() || undefined;
180180
}
181+
182+
@log()
183+
async updateReference(
184+
repoPath: string,
185+
ref: string,
186+
newRef: string,
187+
cancellation?: CancellationToken,
188+
): Promise<void> {
189+
const scope = getLogScope();
190+
191+
try {
192+
await this.git.exec({ cwd: repoPath, cancellation: cancellation }, 'update-ref', ref, newRef);
193+
} catch (ex) {
194+
Logger.error(ex, scope);
195+
if (isCancellationError(ex)) throw ex;
196+
}
197+
}
181198
}

src/git/gitProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ export interface GitRefsSubProvider {
573573
pathOrUri?: string | Uri,
574574
cancellation?: CancellationToken,
575575
): Promise<boolean>;
576+
updateReference(repoPath: string, ref: string, newRef: string, cancellation?: CancellationToken): Promise<void>;
576577
}
577578

578579
export interface GitRemotesSubProvider {

src/plus/integrations/providers/github/sub-providers/refs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,14 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
133133
): Promise<boolean> {
134134
return Promise.resolve(true);
135135
}
136+
137+
@log()
138+
updateReference(
139+
_repoPath: string,
140+
_ref: string,
141+
_newRef: string,
142+
_cancellation?: CancellationToken,
143+
): Promise<void> {
144+
return Promise.resolve();
145+
}
136146
}

src/webviews/apps/plus/composer/components/app.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,23 +1176,35 @@ export class ComposerApp extends LitElement {
11761176
}
11771177

11781178
private get canFinishAndCommit(): boolean {
1179-
return this.state.commits.length > 0;
1179+
return !this.commitsLocked && this.state.commits.length > 0;
11801180
}
11811181

11821182
private get isPreviewMode(): boolean {
11831183
return this.state?.mode === 'preview';
11841184
}
11851185

1186+
private get commitsLocked(): boolean {
1187+
return this.state?.recompose?.enabled === true && this.state?.recompose?.locked === true;
1188+
}
1189+
11861190
private get canCombineCommits(): boolean {
1187-
return !this.isPreviewMode;
1191+
return !this.isPreviewMode && !this.commitsLocked;
11881192
}
11891193

11901194
private get showHistoryButtons(): boolean {
11911195
return true; // Show history buttons in both interactive and AI preview modes
11921196
}
11931197

11941198
private get canMoveHunks(): boolean {
1195-
return !this.isPreviewMode;
1199+
return !this.isPreviewMode && !this.commitsLocked;
1200+
}
1201+
1202+
private get canEditCommitMessages(): boolean {
1203+
return !this.commitsLocked;
1204+
}
1205+
1206+
private get canReorderCommits(): boolean {
1207+
return !this.isPreviewMode && !this.commitsLocked;
11961208
}
11971209

11981210
private get isReadyToFinishAndCommit(): boolean {
@@ -1225,10 +1237,6 @@ export class ComposerApp extends LitElement {
12251237
return availableHunks;
12261238
}
12271239

1228-
private get canEditCommitMessages(): boolean {
1229-
return true; // Always allowed
1230-
}
1231-
12321240
private get canGenerateCommitMessages(): boolean {
12331241
return this.aiEnabled; // Allowed in both modes if AI is enabled
12341242
}
@@ -1580,6 +1588,7 @@ export class ComposerApp extends LitElement {
15801588
.canCombineCommits=${this.canCombineCommits}
15811589
.canMoveHunks=${this.canMoveHunks}
15821590
.canGenerateCommitsWithAI=${this.canGenerateCommitsWithAI}
1591+
.canReorderCommits=${this.canReorderCommits}
15831592
.isPreviewMode=${this.isPreviewMode}
15841593
.baseCommit=${this.state.baseCommit}
15851594
.repoName=${this.state.baseCommit?.repoName ?? this.state.repositoryState?.current.name}
@@ -1591,6 +1600,7 @@ export class ComposerApp extends LitElement {
15911600
.compositionFeedback=${this.compositionFeedback}
15921601
.compositionSessionId=${this.compositionSessionId}
15931602
.isReadyToCommit=${this.isReadyToFinishAndCommit}
1603+
.recompose=${this.state.recompose}
15941604
@commit-select=${(e: CustomEvent) => this.selectCommit(e.detail.commitId, e.detail.multiSelect)}
15951605
@unassigned-select=${(e: CustomEvent) => this.selectUnassignedSection(e.detail.section)}
15961606
@combine-commits=${this.combineSelectedCommits}

src/webviews/apps/plus/composer/components/commit-item.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ export class CommitItem extends LitElement {
8383
@property({ type: Boolean })
8484
isPreviewMode = false;
8585

86+
@property({ type: Boolean })
87+
isRecomposeLocked = false;
88+
8689
@property({ type: Boolean })
8790
first = false;
8891

@@ -123,7 +126,9 @@ export class CommitItem extends LitElement {
123126
<div
124127
class="composer-item commit-item ${this.selected ? ' is-selected' : ''}${this.multiSelected
125128
? ' multi-selected'
126-
: ''}${this.first ? ' is-first' : ''}${this.last ? ' is-last' : ''}"
129+
: ''}${this.first ? ' is-first' : ''}${this.last ? ' is-last' : ''}${this.isRecomposeLocked
130+
? ' is-recompose-locked'
131+
: ''}"
127132
tabindex="0"
128133
@click=${this.handleClick}
129134
@keydown=${this.handleClick}

0 commit comments

Comments
 (0)