Skip to content

Commit d39d22b

Browse files
Copilotalexr00
andauthored
Add worktree removal option when deleting PR branches (#8559)
* Initial plan * Initial plan for worktree deletion support Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add worktree deletion support when deleting PR branches - Add SELECT_WORKTREE constant in settingKeys.ts - Add githubPullRequests.defaultDeletionMethod.selectWorktree setting in package.json - Add NLS description in package.nls.json - Add getWorktreeForBranch() and removeWorktree() methods to FolderRepositoryManager - Add worktree option to SelectedAction type and quick pick dropdown - Add worktree deletion to auto-delete flow (autoDeleteBranchesAfterMerge) - Ensure worktree removal happens before branch deletion in performBranchDeletion Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Address code review: static imports, error handling, documentation Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix webworker build: move child_process usage to env/node module Move git worktree operations (child_process/util) into dedicated env/node/gitWorktree.ts and env/browser/gitWorktree.ts modules, following the existing env pattern. The webpack alias ensures the browser/no-op version is used for the webworker target. Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Update git API * refactor: use git API for worktree operations instead of child_process - Use repository.state.worktrees to find worktrees for a branch - Use repository.deleteWorktree() to remove worktrees - Add Worktree interface to api.d.ts - Remove env/node/gitWorktree.ts and env/browser/gitWorktree.ts - Remove webpack alias for gitWorktree Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Skip worktree deletion when worktree is a workspace folder Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Hide worktree delete option when worktree is a workspace folder Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use isDescendant for cross-platform path comparison in worktree workspace check Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Remove unused top-level isWorktreeInWorkspace function to fix lint error Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use case-insensitive path comparison for isWorktreeInWorkspace on Windows Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix worktree/workspace path compare * Fix error --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 2957a71 commit d39d22b

File tree

6 files changed

+86
-3
lines changed

6 files changed

+86
-3
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,11 @@
309309
"default": true,
310310
"description": "%githubPullRequests.defaultDeletionMethod.selectRemote.description%"
311311
},
312+
"githubPullRequests.defaultDeletionMethod.selectWorktree": {
313+
"type": "boolean",
314+
"default": false,
315+
"description": "%githubPullRequests.defaultDeletionMethod.selectWorktree.description%"
316+
},
312317
"githubPullRequests.deleteBranchAfterMerge": {
313318
"type": "boolean",
314319
"default": false,

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"githubPullRequests.fileAutoReveal.description": "Automatically reveal open files in the pull request changes tree.",
4545
"githubPullRequests.defaultDeletionMethod.selectLocalBranch.description": "When true, the option to delete the local branch will be selected by default when deleting a branch from a pull request.",
4646
"githubPullRequests.defaultDeletionMethod.selectRemote.description": "When true, the option to delete the remote will be selected by default when deleting a branch from a pull request.",
47+
"githubPullRequests.defaultDeletionMethod.selectWorktree.description": "When true, the option to remove the associated worktree will be selected by default when deleting a branch from a pull request.",
4748
"githubPullRequests.deleteBranchAfterMerge.description": "Automatically delete the branch after merging a pull request. This setting only applies when the pull request is merged through this extension. When using merge queues, this will only delete the local branch.",
4849
"githubPullRequests.terminalLinksHandler.description": "Default handler for terminal links.",
4950
"githubPullRequests.terminalLinksHandler.github": "Create the pull request on GitHub.",

src/api/api.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ export interface Repository {
218218
add(paths: string[]): Promise<void>;
219219
merge(ref: string): Promise<void>;
220220
mergeAbort(): Promise<void>;
221+
222+
createWorktree?(options?: { path?: string; commitish?: string; branch?: string }): Promise<string>;
223+
deleteWorktree?(path: string, options?: { force?: boolean }): Promise<void>;
221224
}
222225

223226
/**

src/common/settingKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const DEFAULT_MERGE_METHOD = 'defaultMergeMethod';
3636
export const DEFAULT_DELETION_METHOD = 'defaultDeletionMethod';
3737
export const SELECT_LOCAL_BRANCH = 'selectLocalBranch';
3838
export const SELECT_REMOTE = 'selectRemote';
39+
export const SELECT_WORKTREE = 'selectWorktree';
3940
export const DELETE_BRANCH_AFTER_MERGE = 'deleteBranchAfterMerge';
4041
export const REMOTES = 'remotes';
4142
export const PULL_PR_BRANCH_BEFORE_CHECKOUT = 'pullPullRequestBranchBeforeCheckout';

src/github/folderRepositoryManager.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2450,6 +2450,35 @@ export class FolderRepositoryManager extends Disposable {
24502450
return await PullRequestGitHelper.getBranchNRemoteForPullRequest(this.repository, pullRequest);
24512451
}
24522452

2453+
getWorktreeForBranch(branchName: string): vscode.Uri | undefined {
2454+
const worktrees = this.repository.state.worktrees;
2455+
if (!worktrees) {
2456+
return undefined;
2457+
}
2458+
const refsHeadsPrefix = 'refs/heads/';
2459+
const worktree = worktrees.find(wt => {
2460+
if (wt.main) {
2461+
return false;
2462+
}
2463+
const ref = wt.ref.startsWith(refsHeadsPrefix) ? wt.ref.substring(refsHeadsPrefix.length) : wt.ref;
2464+
return ref === branchName;
2465+
});
2466+
return worktree ? vscode.Uri.file(worktree.path) : undefined;
2467+
}
2468+
2469+
async removeWorktree(worktreePath: string): Promise<void> {
2470+
if (!this.repository.deleteWorktree) {
2471+
Logger.error(`deleteWorktree is not available on this repository`, this.id);
2472+
return;
2473+
}
2474+
try {
2475+
await this.repository.deleteWorktree(worktreePath);
2476+
} catch (e) {
2477+
Logger.error(`Failed to remove worktree ${worktreePath}: ${e}`, this.id);
2478+
throw e;
2479+
}
2480+
}
2481+
24532482
async fetchAndCheckout(pullRequest: PullRequestModel, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<void> {
24542483
await PullRequestGitHelper.fetchAndCheckout(this.repository, this._allGitHubRemotes, pullRequest, progress);
24552484
}

src/github/pullRequestReviewCommon.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { IAccount, isITeam, ITeam, MergeMethod, PullRequestMergeability, reviewe
1010
import { BranchInfo } from './pullRequestGitHelper';
1111
import { PullRequestModel } from './pullRequestModel';
1212
import { ConvertToDraftReply, PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views';
13-
import { DEFAULT_DELETION_METHOD, PR_SETTINGS_NAMESPACE, SELECT_LOCAL_BRANCH, SELECT_REMOTE } from '../common/settingKeys';
13+
import { DEFAULT_DELETION_METHOD, PR_SETTINGS_NAMESPACE, SELECT_LOCAL_BRANCH, SELECT_REMOTE, SELECT_WORKTREE } from '../common/settingKeys';
1414
import { ReviewEvent, TimelineEvent } from '../common/timelineEvent';
1515
import { Schemes } from '../common/uri';
1616
import { formatError } from '../common/utils';
@@ -289,9 +289,20 @@ export namespace PullRequestReviewCommon {
289289
}
290290

291291
interface SelectedAction {
292-
type: 'remoteHead' | 'local' | 'remote' | 'suspend'
292+
type: 'remoteHead' | 'local' | 'remote' | 'suspend' | 'worktree'
293+
/** Path to the worktree directory to remove. Only used when type is 'worktree'. */
294+
worktreePath?: string;
293295
};
294296

297+
function isWorktreeInWorkspace(worktreePath: vscode.Uri): boolean {
298+
const worktreeFsPath = worktreePath.fsPath;
299+
return !!vscode.workspace.workspaceFolders?.some(folder => {
300+
const folderPath = folder.uri.fsPath;
301+
return folderPath === worktreeFsPath ||
302+
(process.platform === 'win32' && folderPath.toLowerCase() === worktreeFsPath.toLowerCase());
303+
});
304+
}
305+
295306
export async function deleteBranch(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<{ isReply: boolean, message: any }> {
296307
const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item);
297308
const actions: (vscode.QuickPickItem & SelectedAction)[] = [];
@@ -333,6 +344,19 @@ export namespace PullRequestReviewCommon {
333344
picked: !!preferredRemoteDeletionMethod,
334345
});
335346
}
347+
348+
const worktreePath = folderRepositoryManager.getWorktreeForBranch(branchInfo.branch);
349+
if (worktreePath && !isWorktreeInWorkspace(worktreePath)) {
350+
const preferredWorktreeDeletion = vscode.workspace
351+
.getConfiguration(PR_SETTINGS_NAMESPACE)
352+
.get<boolean>(`${DEFAULT_DELETION_METHOD}.${SELECT_WORKTREE}`);
353+
actions.push({
354+
label: vscode.l10n.t('Remove worktree {0}', worktreePath.fsPath),
355+
type: 'worktree',
356+
worktreePath: worktreePath.fsPath,
357+
picked: !!preferredWorktreeDeletion,
358+
});
359+
}
336360
}
337361

338362
if (vscode.env.remoteName === 'codespaces') {
@@ -384,7 +408,16 @@ export namespace PullRequestReviewCommon {
384408
const isBranchActive = item.equals(folderRepositoryManager.activePullRequest) || (folderRepositoryManager.repository.state.HEAD?.name && folderRepositoryManager.repository.state.HEAD.name === branchInfo?.branch);
385409
const deletedBranchTypes: string[] = [];
386410

387-
const promises = selectedActions.map(async action => {
411+
// Remove worktree first, before deleting the branch, since a branch checked out
412+
// in a worktree cannot be deleted.
413+
const worktreeAction = selectedActions.find(a => a.type === 'worktree');
414+
if (worktreeAction?.worktreePath) {
415+
await folderRepositoryManager.removeWorktree(worktreeAction.worktreePath);
416+
deletedBranchTypes.push(worktreeAction.type);
417+
}
418+
419+
const remainingActions = selectedActions.filter(a => a.type !== 'worktree');
420+
const promises = remainingActions.map(async action => {
388421
switch (action.type) {
389422
case 'remoteHead':
390423
await folderRepositoryManager.deleteBranch(item);
@@ -497,6 +530,17 @@ export namespace PullRequestReviewCommon {
497530
selectedActions.push({ type: 'remote' });
498531
}
499532

533+
// Remove worktree if preference is set
534+
const deleteWorktree = vscode.workspace
535+
.getConfiguration(PR_SETTINGS_NAMESPACE)
536+
.get<boolean>(`${DEFAULT_DELETION_METHOD}.${SELECT_WORKTREE}`, false);
537+
if (branchInfo && deleteWorktree) {
538+
const worktreePath = folderRepositoryManager.getWorktreeForBranch(branchInfo.branch);
539+
if (worktreePath && !isWorktreeInWorkspace(worktreePath)) {
540+
selectedActions.push({ type: 'worktree', worktreePath: worktreePath.fsPath });
541+
}
542+
}
543+
500544
// Execute all deletions in parallel
501545
const deletedBranchTypes = await performBranchDeletion(folderRepositoryManager, item, defaultBranch, branchInfo!, selectedActions);
502546

0 commit comments

Comments
 (0)