Skip to content

Commit 48bde0a

Browse files
Copilotalexr00
andcommitted
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>
1 parent 239290f commit 48bde0a

File tree

5 files changed

+95
-3
lines changed

5 files changed

+95
-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/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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2450,6 +2450,57 @@ export class FolderRepositoryManager extends Disposable {
24502450
return await PullRequestGitHelper.getBranchNRemoteForPullRequest(this.repository, pullRequest);
24512451
}
24522452

2453+
async getWorktreeForBranch(branchName: string): Promise<string | undefined> {
2454+
try {
2455+
const { execFile } = await import('child_process');
2456+
const { promisify } = await import('util');
2457+
const execFileAsync = promisify(execFile);
2458+
const gitPath = vscode.workspace.getConfiguration('git').get<string>('path') || 'git';
2459+
const { stdout } = await execFileAsync(gitPath, ['worktree', 'list', '--porcelain'], {
2460+
cwd: this.repository.rootUri.fsPath,
2461+
});
2462+
2463+
const worktrees = stdout.split('\n\n');
2464+
for (const entry of worktrees) {
2465+
const lines = entry.trim().split('\n');
2466+
let worktreePath: string | undefined;
2467+
let branch: string | undefined;
2468+
for (const line of lines) {
2469+
if (line.startsWith('worktree ')) {
2470+
worktreePath = line.substring('worktree '.length);
2471+
} else if (line.startsWith('branch ')) {
2472+
branch = line.substring('branch '.length);
2473+
// branch line is like "branch refs/heads/branchName"
2474+
const prefix = 'refs/heads/';
2475+
if (branch.startsWith(prefix)) {
2476+
branch = branch.substring(prefix.length);
2477+
}
2478+
}
2479+
}
2480+
if (branch === branchName && worktreePath) {
2481+
// Don't return the main worktree (the repository root itself)
2482+
const repoRoot = this.repository.rootUri.fsPath;
2483+
if (nodePath.resolve(worktreePath) !== nodePath.resolve(repoRoot)) {
2484+
return worktreePath;
2485+
}
2486+
}
2487+
}
2488+
} catch (e) {
2489+
Logger.error(`Failed to get worktree for branch ${branchName}: ${e}`, this.id);
2490+
}
2491+
return undefined;
2492+
}
2493+
2494+
async removeWorktree(worktreePath: string): Promise<void> {
2495+
const { execFile } = await import('child_process');
2496+
const { promisify } = await import('util');
2497+
const execFileAsync = promisify(execFile);
2498+
const gitPath = vscode.workspace.getConfiguration('git').get<string>('path') || 'git';
2499+
await execFileAsync(gitPath, ['worktree', 'remove', worktreePath], {
2500+
cwd: this.repository.rootUri.fsPath,
2501+
});
2502+
}
2503+
24532504
async fetchAndCheckout(pullRequest: PullRequestModel, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<void> {
24542505
await PullRequestGitHelper.fetchAndCheckout(this.repository, this._allGitHubRemotes, pullRequest, progress);
24552506
}

src/github/pullRequestReviewCommon.ts

Lines changed: 37 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,7 +289,8 @@ export namespace PullRequestReviewCommon {
289289
}
290290

291291
interface SelectedAction {
292-
type: 'remoteHead' | 'local' | 'remote' | 'suspend'
292+
type: 'remoteHead' | 'local' | 'remote' | 'suspend' | 'worktree'
293+
worktreePath?: string;
293294
};
294295

295296
export async function deleteBranch(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<{ isReply: boolean, message: any }> {
@@ -333,6 +334,19 @@ export namespace PullRequestReviewCommon {
333334
picked: !!preferredRemoteDeletionMethod,
334335
});
335336
}
337+
338+
const worktreePath = await folderRepositoryManager.getWorktreeForBranch(branchInfo.branch);
339+
if (worktreePath) {
340+
const preferredWorktreeDeletion = vscode.workspace
341+
.getConfiguration(PR_SETTINGS_NAMESPACE)
342+
.get<boolean>(`${DEFAULT_DELETION_METHOD}.${SELECT_WORKTREE}`);
343+
actions.push({
344+
label: vscode.l10n.t('Remove worktree {0}', worktreePath),
345+
type: 'worktree',
346+
worktreePath,
347+
picked: !!preferredWorktreeDeletion,
348+
});
349+
}
336350
}
337351

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

387-
const promises = selectedActions.map(async action => {
401+
// Remove worktree first, before deleting the branch, since a branch checked out
402+
// in a worktree cannot be deleted.
403+
const worktreeAction = selectedActions.find(a => a.type === 'worktree');
404+
if (worktreeAction?.worktreePath) {
405+
await folderRepositoryManager.removeWorktree(worktreeAction.worktreePath);
406+
deletedBranchTypes.push(worktreeAction.type);
407+
}
408+
409+
const remainingActions = selectedActions.filter(a => a.type !== 'worktree');
410+
const promises = remainingActions.map(async action => {
388411
switch (action.type) {
389412
case 'remoteHead':
390413
await folderRepositoryManager.deleteBranch(item);
@@ -497,6 +520,17 @@ export namespace PullRequestReviewCommon {
497520
selectedActions.push({ type: 'remote' });
498521
}
499522

523+
// Remove worktree if preference is set
524+
const deleteWorktree = vscode.workspace
525+
.getConfiguration(PR_SETTINGS_NAMESPACE)
526+
.get<boolean>(`${DEFAULT_DELETION_METHOD}.${SELECT_WORKTREE}`, false);
527+
if (branchInfo && deleteWorktree) {
528+
const worktreePath = await folderRepositoryManager.getWorktreeForBranch(branchInfo.branch);
529+
if (worktreePath) {
530+
selectedActions.push({ type: 'worktree', worktreePath });
531+
}
532+
}
533+
500534
// Execute all deletions in parallel
501535
const deletedBranchTypes = await performBranchDeletion(folderRepositoryManager, item, defaultBranch, branchInfo!, selectedActions);
502536

0 commit comments

Comments
 (0)