Skip to content

Commit a9f4270

Browse files
committed
Adds ability to copy changes between worktrees
Changes "Explain WIP" to only show on worktrees if there are changes
1 parent d9ab101 commit a9f4270

File tree

9 files changed

+98
-59
lines changed

9 files changed

+98
-59
lines changed

contributions.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136
"menus": {
137137
"view/item/context": [
138138
{
139-
"when": "viewItem =~ /gitlens:(worktree|uncommitted)\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
139+
"when": "viewItem =~ /gitlens:(worktree\\b(?=.*?\\b\\+working\\b)|uncommitted)\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
140140
"group": "3_gitlens_ai",
141141
"order": 1
142142
}
@@ -895,6 +895,18 @@
895895
"label": "Copy Working Changes to Worktree...",
896896
"commandPalette": "gitlens:enabled && !gitlens:untrusted && !gitlens:hasVirtualFolders"
897897
},
898+
"gitlens.copyWorkingChangesToWorktree:views": {
899+
"label": "Copy Working Changes to Worktree...",
900+
"menus": {
901+
"view/item/context": [
902+
{
903+
"when": "viewItem =~ /gitlens:(worktree\\b(?=.*?\\b\\+working\\b)|uncommitted)\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
904+
"group": "3_gitlens",
905+
"order": 99
906+
}
907+
]
908+
}
909+
},
898910
"gitlens.createCloudPatch": {
899911
"label": "Create Patch...",
900912
"commandPalette": "gitlens:enabled && gitlens:gk:organization:drafts:enabled && config.gitlens.cloudPatches.enabled",

package.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6394,6 +6394,10 @@
63946394
"title": "Copy Working Changes to Worktree...",
63956395
"category": "GitLens"
63966396
},
6397+
{
6398+
"command": "gitlens.copyWorkingChangesToWorktree:views",
6399+
"title": "Copy Working Changes to Worktree..."
6400+
},
63976401
{
63986402
"command": "gitlens.createCloudPatch",
63996403
"title": "Create Patch...",
@@ -10702,6 +10706,10 @@
1070210706
"command": "gitlens.copyWorkingChangesToWorktree",
1070310707
"when": "gitlens:enabled && !gitlens:untrusted && !gitlens:hasVirtualFolders"
1070410708
},
10709+
{
10710+
"command": "gitlens.copyWorkingChangesToWorktree:views",
10711+
"when": "false"
10712+
},
1070510713
{
1070610714
"command": "gitlens.createCloudPatch",
1070710715
"when": "gitlens:enabled && gitlens:gk:organization:drafts:enabled && config.gitlens.cloudPatches.enabled"
@@ -17965,9 +17973,14 @@
1796517973
"when": "viewItem =~ /gitlens:tags\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
1796617974
"group": "1_gitlens_actions@1"
1796717975
},
17976+
{
17977+
"command": "gitlens.copyWorkingChangesToWorktree:views",
17978+
"when": "viewItem =~ /gitlens:(worktree\\b(?=.*?\\b\\+working\\b)|uncommitted)\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders",
17979+
"group": "3_gitlens@99"
17980+
},
1796817981
{
1796917982
"command": "gitlens.ai.explainWip:views",
17970-
"when": "viewItem =~ /gitlens:(worktree|uncommitted)\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
17983+
"when": "viewItem =~ /gitlens:(worktree\\b(?=.*?\\b\\+working\\b)|uncommitted)\\b/ && !listMultiSelection && !gitlens:readonly && !gitlens:untrusted && gitlens:gk:organization:ai:enabled",
1797117984
"group": "3_gitlens_ai@1"
1797217985
},
1797317986
{

src/commands/explainWip.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { command } from '../system/-webview/command';
1111
import { showMarkdownPreview } from '../system/-webview/markdown';
1212
import { createMarkdownCommandLink } from '../system/commands';
1313
import { Logger } from '../system/logger';
14+
import { capitalize } from '../system/string';
1415
import { GlCommandBase } from './commandBase';
1516
import { getCommandUri } from './commandBase.utils';
1617
import type { CommandContext } from './commandContext';
@@ -67,23 +68,22 @@ export class ExplainWipCommand extends GlCommandBase {
6768
}
6869

6970
try {
70-
// If args?.staged is undefined, get all changes (staged and unstaged)?
71-
let stagedLabel;
71+
let label;
7272
let to;
7373
if (args?.staged === true) {
74-
stagedLabel = 'Staged';
74+
label = 'staged';
7575
to = uncommittedStaged;
7676
} else if (args?.staged === false) {
77-
stagedLabel = 'Unstaged';
77+
label = 'unstaged';
7878
to = uncommitted;
7979
} else {
80-
stagedLabel = 'Uncommitted';
80+
label = 'working';
8181
to = '';
8282
}
8383

8484
const diff = await svc.diff.getDiff(to, undefined);
8585
if (!diff?.contents) {
86-
void showGenericErrorMessage('No working changes found to explain');
86+
void showGenericErrorMessage(`No ${label} changes found to explain`);
8787
return;
8888
}
8989

@@ -109,7 +109,7 @@ export class ExplainWipCommand extends GlCommandBase {
109109
const result = await this.container.ai.explainChanges(
110110
{
111111
diff: diff.contents,
112-
message: `${stagedLabel} working changes${worktreeInfo}`,
112+
message: `${capitalize(label)} changes${worktreeInfo}`,
113113
},
114114
{
115115
...args.source,
@@ -119,18 +119,18 @@ export class ExplainWipCommand extends GlCommandBase {
119119
{
120120
progress: {
121121
location: ProgressLocation.Notification,
122-
title: `Explaining working changes${worktreeInfo}...`,
122+
title: `Explaining ${label} changes${worktreeInfo}...`,
123123
},
124124
},
125125
);
126126

127127
if (result == null) {
128-
void showGenericErrorMessage('Unable to explain working changes');
128+
void showGenericErrorMessage(`Unable to explain ${label} changes`);
129129
return;
130130
}
131131

132-
const title = `Working Changes Summary${worktreeDisplayName}`;
133-
const content = `# ${title}\n\n> Generated by ${result.model.name}\n\n## ${stagedLabel} Changes\n\n${result.parsed.summary}\n\n${result.parsed.body}`;
132+
const title = `${capitalize(label)} Changes Summary${worktreeDisplayName}`;
133+
const content = `# ${title}\n\n> Generated by ${result.model.name}\n\n## ${label} Changes\n\n${result.parsed.summary}\n\n${result.parsed.body}`;
134134

135135
void showMarkdownPreview(content);
136136
} catch (ex) {

src/commands/git/worktree.ts

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,9 @@ interface OpenState {
147147
interface CopyChangesState {
148148
subcommand: 'copy-changes';
149149
repo: string | Repository;
150-
worktree: GitWorktree;
150+
/** Optional source worktree, defaults to the current worktree if not provided */
151+
source?: GitWorktree;
152+
target: GitWorktree;
151153
changes:
152154
| { baseSha?: string; contents?: string; type: 'index' | 'working-tree' }
153155
| { baseSha: string; contents: string; type?: 'index' | 'working-tree' };
@@ -228,7 +230,7 @@ export class WorktreeGitCommand extends QuickCommand<State> {
228230

229231
break;
230232
case 'copy-changes':
231-
if (args.state.worktree != null) {
233+
if (args.state.target != null) {
232234
counter++;
233235
}
234236

@@ -1153,39 +1155,46 @@ export class WorktreeGitCommand extends QuickCommand<State> {
11531155
while (this.canStepsContinue(state)) {
11541156
context.title = state?.overrides?.title ?? getTitle(state.subcommand);
11551157

1156-
if (state.counter < 3 || state.worktree == null) {
1158+
if (state.counter < 3 || state.target == null) {
11571159
context.worktrees ??= (await state.repo.git.worktrees?.getWorktrees()) ?? [];
11581160

11591161
let placeholder;
11601162
switch (state.changes.type) {
11611163
case 'index':
1162-
context.title = state?.overrides?.title ?? 'Copy Staged Changes to Worktree';
1163-
placeholder = 'Choose a worktree to copy your staged changes to';
1164+
context.title =
1165+
state?.overrides?.title ??
1166+
`Copy Staged${state.source?.name ? ' Worktree' : ''} Changes to Worktree`;
1167+
placeholder = `Choose a worktree to copy your staged${
1168+
state.source?.name ? ' Worktree' : ''
1169+
} changes to`;
11641170
break;
11651171
case 'working-tree':
1166-
context.title = state?.overrides?.title ?? 'Copy Working Changes to Worktree';
1167-
placeholder = 'Choose a worktree to copy your working changes to';
1168-
break;
11691172
default:
1170-
context.title = state?.overrides?.title ?? 'Copy Changes to Worktree';
1171-
placeholder = 'Choose a worktree to copy changes to';
1173+
context.title =
1174+
state?.overrides?.title ??
1175+
`Copy Working${state.source?.name ? ' Worktree' : ''} Changes to Worktree`;
1176+
placeholder = `Choose a worktree to copy your working${
1177+
state.source?.name ? ' worktree' : ''
1178+
} changes to`;
11721179
break;
11731180
}
11741181

11751182
const result = yield* pickWorktreeStep(state, context, {
11761183
excludeOpened: true,
11771184
includeStatus: true,
1178-
picked: state.worktree?.uri?.toString(),
1185+
picked: state.target?.uri?.toString(),
11791186
placeholder: placeholder,
11801187
});
11811188
// Always break on the first step (so we will go back)
11821189
if (result === StepResultBreak) break;
11831190

1184-
state.worktree = result;
1191+
state.target = result;
11851192
}
11861193

1194+
const sourceSvc = this.container.git.getRepositoryService(state.source?.uri ?? state.repo.uri);
1195+
11871196
if (!state.changes.contents || !state.changes.baseSha) {
1188-
const diff = await state.repo.git.diff.getDiff?.(
1197+
const diff = await sourceSvc.diff.getDiff?.(
11891198
state.changes.type === 'index' ? uncommittedStaged : uncommitted,
11901199
'HEAD',
11911200
{
@@ -1204,7 +1213,7 @@ export class WorktreeGitCommand extends QuickCommand<State> {
12041213
}
12051214

12061215
if (!isSha(state.changes.baseSha)) {
1207-
const sha = (await state.repo.git.revision.resolveRevision(state.changes.baseSha)).sha;
1216+
const sha = (await sourceSvc.revision.resolveRevision(state.changes.baseSha)).sha;
12081217
if (sha != null) {
12091218
state.changes.baseSha = sha;
12101219
}
@@ -1218,15 +1227,15 @@ export class WorktreeGitCommand extends QuickCommand<State> {
12181227
endSteps(state);
12191228

12201229
try {
1221-
const svc = this.container.git.getRepositoryService(state.worktree.uri);
1222-
const commit = await svc.patch?.createUnreachableCommitForPatch(
1230+
const commit = await sourceSvc.patch?.createUnreachableCommitForPatch(
12231231
state.changes.baseSha,
12241232
'Copied Changes',
12251233
state.changes.contents,
12261234
);
12271235
if (commit == null) return;
12281236

1229-
await svc.patch?.applyUnreachableCommitForPatch(commit.sha, { stash: false });
1237+
const targetSvc = this.container.git.getRepositoryService(state.target.uri);
1238+
await targetSvc.patch?.applyUnreachableCommitForPatch(commit.sha, { stash: false });
12301239
void window.showInformationMessage(`Changes copied successfully`);
12311240
} catch (ex) {
12321241
if (ex instanceof CancellationError) return;
@@ -1253,7 +1262,7 @@ export class WorktreeGitCommand extends QuickCommand<State> {
12531262
{
12541263
subcommand: 'open',
12551264
repo: state.repo,
1256-
worktree: state.worktree,
1265+
worktree: state.target,
12571266
flags: [],
12581267
counter: 3,
12591268
confirm: true,
@@ -1277,37 +1286,23 @@ export class WorktreeGitCommand extends QuickCommand<State> {
12771286
case 'index':
12781287
confirmations.push({
12791288
label: 'Copy Staged Changes to Worktree',
1280-
detail: `Will copy the staged changes${
1281-
count > 0 ? ` (${pluralize('file', count)})` : ''
1282-
} to worktree '${state.worktree.name}'`,
1289+
detail: `Will copy the staged changes${count > 0 ? ` (${pluralize('file', count)})` : ''}${
1290+
state.source ? ` from worktree '${state.source.name}'` : ''
1291+
} to worktree '${state.target.name}'`,
12831292
});
12841293
break;
12851294
case 'working-tree':
1295+
default:
12861296
confirmations.push({
12871297
label: 'Copy Working Changes to Worktree',
1288-
detail: `Will copy the working changes${
1289-
count > 0 ? ` (${pluralize('file', count)})` : ''
1290-
} to worktree '${state.worktree.name}'`,
1298+
detail: `Will copy the working changes${count > 0 ? ` (${pluralize('file', count)})` : ''}${
1299+
state.source ? ` from worktree '${state.source.name}'` : ''
1300+
} to worktree '${state.target.name}'`,
12911301
});
12921302
break;
1293-
1294-
default:
1295-
confirmations.push(
1296-
createFlagsQuickPickItem([], [], {
1297-
label: 'Copy Changes to Worktree',
1298-
detail: `Will copy the changes${
1299-
count > 0 ? ` (${pluralize('file', count)})` : ''
1300-
} to worktree '${state.worktree.name}'`,
1301-
}),
1302-
);
1303-
break;
13041303
}
13051304

1306-
const step = createConfirmStep(
1307-
`Confirm ${context.title} \u2022 ${state.worktree.name}`,
1308-
confirmations,
1309-
context,
1310-
);
1305+
const step = createConfirmStep(`Confirm ${context.title} \u2022 ${state.target.name}`, confirmations, context);
13111306

13121307
const selection: StepSelection<typeof step> = yield step;
13131308
return canPickStepContinue(step, state, selection) ? undefined : StepResultBreak;

src/constants.commands.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type ContributedCommands =
3434
| 'gitlens.copyRemoteFileUrlWithoutRange'
3535
| 'gitlens.copyRemotePullRequestUrl'
3636
| 'gitlens.copyRemoteRepositoryUrl'
37+
| 'gitlens.copyWorkingChangesToWorktree:views'
3738
| 'gitlens.ghpr.views.openOrCreateWorktree'
3839
| 'gitlens.graph.addAuthor'
3940
| 'gitlens.graph.associateIssueWithBranch'

src/git/actions/worktree.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,17 @@ export async function create(
4242
export function copyChangesToWorktree(
4343
type: 'working-tree' | 'index',
4444
repo?: string | Repository,
45-
worktree?: GitWorktree,
45+
target?: GitWorktree,
46+
source?: GitWorktree,
4647
): Promise<void> {
4748
return executeGitCommand({
4849
command: 'worktree',
4950
state: {
5051
subcommand: 'copy-changes',
5152
repo: repo,
52-
worktree: worktree,
53-
changes: {
54-
type: type,
55-
},
53+
source: source,
54+
target: target,
55+
changes: { type: type },
5656
},
5757
});
5858
}

src/views/nodes/utils/-webview/node.utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { StashNode } from '../../stashNode';
2727
import type { StatusFileNode } from '../../statusFileNode';
2828
import type { TagNode } from '../../tagNode';
2929
import type { UncommittedFileNode } from '../../UncommittedFileNode';
30+
import type { WorktreeNode } from '../../worktreeNode';
3031

3132
// prettier-ignore
3233
export type TreeViewNodesByType = {
@@ -78,6 +79,8 @@ export type TreeViewNodesByType = {
7879
? BranchTrackingStatusFilesNode
7980
: T extends 'uncommitted-file'
8081
? UncommittedFileNode
82+
: T extends 'worktree'
83+
? WorktreeNode
8184
: ViewNode<T>;
8285
};
8386

src/views/nodes/worktreeNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ export class WorktreeNode extends CacheableChildrenViewNode<'worktree', ViewsWit
400400
item.description = description;
401401
item.contextValue = `${ContextValues.Worktree}${this.worktree.isDefault ? '+default' : ''}${
402402
this.worktree.opened ? '+active' : ''
403-
}`;
403+
}${hasChanges ? '+working' : ''}`;
404404
item.iconPath =
405405
pendingPullRequest != null
406406
? new ThemeIcon('loading~spin')

src/views/viewCommands.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import type { StashNode } from './nodes/stashNode';
105105
import type { StatusFileNode } from './nodes/statusFileNode';
106106
import type { TagNode } from './nodes/tagNode';
107107
import type { TagsNode } from './nodes/tagsNode';
108+
import type { UncommittedFilesNode } from './nodes/UncommittedFilesNode';
108109
import type { WorktreeNode } from './nodes/worktreeNode';
109110
import type { WorktreesNode } from './nodes/worktreesNode';
110111

@@ -1769,6 +1770,20 @@ export class ViewCommands implements Disposable {
17691770
source: { source: 'view', detail: node.is('branch') ? 'branch' : 'tag' },
17701771
});
17711772
}
1773+
1774+
@command('gitlens.copyWorkingChangesToWorktree:views')
1775+
@log()
1776+
private async copyWorkingChangesToWorktree(node: WorktreeNode | UncommittedFilesNode) {
1777+
if (node.is('uncommitted-files')) {
1778+
const parent = node.getParent()!;
1779+
if (parent?.is('worktree')) {
1780+
node = parent;
1781+
}
1782+
}
1783+
if (!node.is('worktree')) return;
1784+
1785+
return WorktreeActions.copyChangesToWorktree('working-tree', node.worktree.repoPath, undefined, node.worktree);
1786+
}
17721787
}
17731788

17741789
interface CompareSelectedInfo {

0 commit comments

Comments
 (0)