Skip to content

Commit e30b8f8

Browse files
Copilotalexr00
andauthored
Add setting to automatically delete branches after merge (#8215)
* Initial plan * Initial plan for delete branch after merge feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add deleteBranchAfterMerge setting and implementation Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add autoDeleteBranchesAfterMerge function for silent branch deletion Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Address code review feedback: extract helper and improve error handling Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Refactor to reuse deleteBranch logic via shared performBranchDeletion helper Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Revert "Refactor to reuse deleteBranch logic via shared performBranchDeletion helper" This reverts commit 4cb17f1. * Revert "Address code review feedback: extract helper and improve error handling" This reverts commit f5d0f99. * Code reuse --------- 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 7c7f6bf commit e30b8f8

File tree

8 files changed

+127
-51
lines changed

8 files changed

+127
-51
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@
321321
"default": true,
322322
"description": "%githubPullRequests.defaultDeletionMethod.selectRemote.description%"
323323
},
324+
"githubPullRequests.deleteBranchAfterMerge": {
325+
"type": "boolean",
326+
"default": false,
327+
"description": "%githubPullRequests.deleteBranchAfterMerge.description%"
328+
},
324329
"githubPullRequests.terminalLinksHandler": {
325330
"type": "string",
326331
"enum": [

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"githubPullRequests.hideViewedFiles.description": "Hide files that have been marked as viewed in the pull request changes tree.",
4040
"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.",
4141
"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.",
42+
"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.",
4243
"githubPullRequests.terminalLinksHandler.description": "Default handler for terminal links.",
4344
"githubPullRequests.terminalLinksHandler.github": "Create the pull request on GitHub",
4445
"githubPullRequests.terminalLinksHandler.vscode": "Create the pull request in VS Code",

src/@types/vscode.proposed.chatParticipantAdditions.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ declare module 'vscode' {
105105
isComplete?: boolean;
106106
toolSpecificData?: ChatTerminalToolInvocationData;
107107
fromSubAgent?: boolean;
108+
presentation?: 'hidden' | 'hiddenAfterComplete' | undefined;
108109

109110
constructor(toolName: string, toolCallId: string, isError?: boolean);
110111
}

src/common/settingKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const DEFAULT_MERGE_METHOD = 'defaultMergeMethod';
3333
export const DEFAULT_DELETION_METHOD = 'defaultDeletionMethod';
3434
export const SELECT_LOCAL_BRANCH = 'selectLocalBranch';
3535
export const SELECT_REMOTE = 'selectRemote';
36+
export const DELETE_BRANCH_AFTER_MERGE = 'deleteBranchAfterMerge';
3637
export const REMOTES = 'remotes';
3738
export const PULL_PR_BRANCH_BEFORE_CHECKOUT = 'pullPullRequestBranchBeforeCheckout';
3839
export type PullPRBranchVariants = 'never' | 'pull' | 'pullAndMergeBase' | 'pullAndUpdateBase' | true | false;

src/github/activityBarViewProvider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MergeArguments, PullRequest, ReviewType } from './views';
1515
import { IComment } from '../common/comment';
1616
import { emojify, ensureEmojis } from '../common/emoji';
1717
import { disposeAll } from '../common/lifecycle';
18+
import { DELETE_BRANCH_AFTER_MERGE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
1819
import { ReviewEvent } from '../common/timelineEvent';
1920
import { formatError } from '../common/utils';
2021
import { generateUuid } from '../common/uuid';
@@ -409,6 +410,13 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W
409410

410411
if (!result.merged) {
411412
vscode.window.showErrorMessage(vscode.l10n.t('Merging pull request failed: {0}', result?.message ?? ''));
413+
} else {
414+
// Check if auto-delete branch setting is enabled
415+
const deleteBranchAfterMerge = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<boolean>(DELETE_BRANCH_AFTER_MERGE, false);
416+
if (deleteBranchAfterMerge) {
417+
// Automatically delete the branch after successful merge
418+
await PullRequestReviewCommon.autoDeleteBranchesAfterMerge(this._folderRepositoryManager, this._item);
419+
}
412420
}
413421

414422
this._replyMessage(message, {

src/github/pullRequestGitHelper.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ export interface BaseBranchMetadata {
3333
branch: string;
3434
}
3535

36+
export type BranchInfo = {
37+
branch: string;
38+
remote?: string;
39+
createdForPullRequest?: boolean;
40+
remoteInUse?: boolean;
41+
};
42+
3643
export class PullRequestGitHelper {
3744
static ID = 'PullRequestGitHelper';
3845
static async checkoutFromFork(
@@ -202,12 +209,7 @@ export class PullRequestGitHelper {
202209
static async getBranchNRemoteForPullRequest(
203210
repository: Repository,
204211
pullRequest: PullRequestModel,
205-
): Promise<{
206-
branch: string;
207-
remote?: string;
208-
createdForPullRequest?: boolean;
209-
remoteInUse?: boolean;
210-
} | null> {
212+
): Promise<BranchInfo | null> {
211213
let branchName: string | null = null;
212214
try {
213215
const key = PullRequestGitHelper.buildPullRequestMetadata(pullRequest);

src/github/pullRequestOverview.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCop
3131
import { commands, contexts } from '../common/executeCommands';
3232
import { disposeAll } from '../common/lifecycle';
3333
import Logger from '../common/logger';
34-
import { DEFAULT_MERGE_METHOD, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
34+
import { DEFAULT_MERGE_METHOD, DELETE_BRANCH_AFTER_MERGE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
3535
import { ITelemetry } from '../common/telemetry';
3636
import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../common/timelineEvent';
3737
import { asPromise, formatError } from '../common/utils';
@@ -626,6 +626,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
626626

627627
if (!result.merged) {
628628
vscode.window.showErrorMessage(`Merging pull request failed: ${result.message}`);
629+
} else {
630+
// Check if auto-delete branch setting is enabled
631+
const deleteBranchAfterMerge = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<boolean>(DELETE_BRANCH_AFTER_MERGE, false);
632+
if (deleteBranchAfterMerge) {
633+
// Automatically delete the branch after successful merge
634+
await PullRequestReviewCommon.autoDeleteBranchesAfterMerge(this._folderRepositoryManager, this._item);
635+
}
629636
}
630637

631638
const mergeResult: MergeResult = {

src/github/pullRequestReviewCommon.ts

Lines changed: 95 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as vscode from 'vscode';
88
import { FolderRepositoryManager } from './folderRepositoryManager';
99
import { IAccount, isITeam, ITeam, MergeMethod, PullRequestMergeability, reviewerId, ReviewState } from './interface';
10+
import { BranchInfo } from './pullRequestGitHelper';
1011
import { PullRequestModel } from './pullRequestModel';
1112
import { PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views';
1213
import { DEFAULT_DELETION_METHOD, PR_SETTINGS_NAMESPACE, SELECT_LOCAL_BRANCH, SELECT_REMOTE } from '../common/settingKeys';
@@ -274,9 +275,13 @@ export namespace PullRequestReviewCommon {
274275
}
275276
}
276277

278+
interface SelectedAction {
279+
type: 'remoteHead' | 'local' | 'remote' | 'suspend'
280+
};
281+
277282
export async function deleteBranch(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<{ isReply: boolean, message: any }> {
278283
const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item);
279-
const actions: (vscode.QuickPickItem & { type: 'remoteHead' | 'local' | 'remote' | 'suspend' })[] = [];
284+
const actions: (vscode.QuickPickItem & SelectedAction)[] = [];
280285
const defaultBranch = await folderRepositoryManager.getPullRequestRepositoryDefaultBranch(item);
281286

282287
if (item.isResolved()) {
@@ -341,51 +346,9 @@ export namespace PullRequestReviewCommon {
341346
ignoreFocusOut: true,
342347
});
343348

344-
const deletedBranchTypes: string[] = [];
345349

346350
if (selectedActions) {
347-
const isBranchActive = item.equals(folderRepositoryManager.activePullRequest) || (folderRepositoryManager.repository.state.HEAD?.name && folderRepositoryManager.repository.state.HEAD.name === branchInfo?.branch);
348-
349-
const promises = selectedActions.map(async action => {
350-
switch (action.type) {
351-
case 'remoteHead':
352-
await folderRepositoryManager.deleteBranch(item);
353-
deletedBranchTypes.push(action.type);
354-
await folderRepositoryManager.repository.fetch({ prune: true });
355-
// If we're in a remote repository, then we should checkout the default branch.
356-
if (folderRepositoryManager.repository.rootUri.scheme === Schemes.VscodeVfs) {
357-
await folderRepositoryManager.repository.checkout(defaultBranch);
358-
}
359-
return;
360-
case 'local':
361-
if (isBranchActive) {
362-
if (folderRepositoryManager.repository.state.workingTreeChanges.length) {
363-
const yes = vscode.l10n.t('Yes');
364-
const response = await vscode.window.showWarningMessage(
365-
vscode.l10n.t('Your local changes will be lost, do you want to continue?'),
366-
{ modal: true },
367-
yes,
368-
);
369-
if (response === yes) {
370-
await vscode.commands.executeCommand('git.cleanAll');
371-
} else {
372-
return;
373-
}
374-
}
375-
await folderRepositoryManager.checkoutDefaultBranch(defaultBranch);
376-
}
377-
await folderRepositoryManager.repository.deleteBranch(branchInfo!.branch, true);
378-
return deletedBranchTypes.push(action.type);
379-
case 'remote':
380-
deletedBranchTypes.push(action.type);
381-
return folderRepositoryManager.repository.removeRemote(branchInfo!.remote!);
382-
case 'suspend':
383-
deletedBranchTypes.push(action.type);
384-
return vscode.commands.executeCommand('github.codespaces.disconnectSuspend');
385-
}
386-
});
387-
388-
await Promise.all(promises);
351+
const deletedBranchTypes: string[] = await performBranchDeletion(folderRepositoryManager, item, defaultBranch, branchInfo!, selectedActions);
389352

390353
return {
391354
isReply: false,
@@ -403,4 +366,92 @@ export namespace PullRequestReviewCommon {
403366
};
404367
}
405368
}
369+
370+
async function performBranchDeletion(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel, defaultBranch: string, branchInfo: BranchInfo, selectedActions: SelectedAction[]): Promise<string[]> {
371+
const isBranchActive = item.equals(folderRepositoryManager.activePullRequest) || (folderRepositoryManager.repository.state.HEAD?.name && folderRepositoryManager.repository.state.HEAD.name === branchInfo?.branch);
372+
const deletedBranchTypes: string[] = [];
373+
374+
const promises = selectedActions.map(async action => {
375+
switch (action.type) {
376+
case 'remoteHead':
377+
await folderRepositoryManager.deleteBranch(item);
378+
deletedBranchTypes.push(action.type);
379+
await folderRepositoryManager.repository.fetch({ prune: true });
380+
// If we're in a remote repository, then we should checkout the default branch.
381+
if (folderRepositoryManager.repository.rootUri.scheme === Schemes.VscodeVfs) {
382+
await folderRepositoryManager.repository.checkout(defaultBranch);
383+
}
384+
return;
385+
case 'local':
386+
if (isBranchActive) {
387+
if (folderRepositoryManager.repository.state.workingTreeChanges.length) {
388+
const yes = vscode.l10n.t('Yes');
389+
const response = await vscode.window.showWarningMessage(
390+
vscode.l10n.t('Your local changes will be lost, do you want to continue?'),
391+
{ modal: true },
392+
yes,
393+
);
394+
if (response === yes) {
395+
await vscode.commands.executeCommand('git.cleanAll');
396+
} else {
397+
return;
398+
}
399+
}
400+
await folderRepositoryManager.checkoutDefaultBranch(defaultBranch);
401+
}
402+
await folderRepositoryManager.repository.deleteBranch(branchInfo!.branch, true);
403+
return deletedBranchTypes.push(action.type);
404+
case 'remote':
405+
deletedBranchTypes.push(action.type);
406+
return folderRepositoryManager.repository.removeRemote(branchInfo!.remote!);
407+
case 'suspend':
408+
deletedBranchTypes.push(action.type);
409+
return vscode.commands.executeCommand('github.codespaces.disconnectSuspend');
410+
}
411+
});
412+
413+
await Promise.all(promises);
414+
return deletedBranchTypes;
415+
}
416+
417+
/**
418+
* Automatically delete branches after merge based on user preferences.
419+
* This function does not show any prompts - it uses the default deletion method preferences.
420+
*/
421+
export async function autoDeleteBranchesAfterMerge(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<void> {
422+
const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item);
423+
const defaultBranch = await folderRepositoryManager.getPullRequestRepositoryDefaultBranch(item);
424+
425+
// Get user preferences for automatic deletion
426+
const deleteLocalBranch = vscode.workspace
427+
.getConfiguration(PR_SETTINGS_NAMESPACE)
428+
.get<boolean>(`${DEFAULT_DELETION_METHOD}.${SELECT_LOCAL_BRANCH}`, true);
429+
430+
const deleteRemote = vscode.workspace
431+
.getConfiguration(PR_SETTINGS_NAMESPACE)
432+
.get<boolean>(`${DEFAULT_DELETION_METHOD}.${SELECT_REMOTE}`, true);
433+
434+
const selectedActions: SelectedAction[] = [];
435+
436+
// Delete remote head branch if it's not the default branch
437+
if (item.isResolved()) {
438+
const isDefaultBranch = defaultBranch === item.head.ref;
439+
if (!isDefaultBranch && !item.isRemoteHeadDeleted) {
440+
selectedActions.push({ type: 'remoteHead' });
441+
}
442+
}
443+
444+
// Delete local branch if preference is set
445+
if (branchInfo && deleteLocalBranch) {
446+
selectedActions.push({ type: 'local' });
447+
}
448+
449+
// Delete remote if it's no longer used and preference is set
450+
if (branchInfo && branchInfo.remote && branchInfo.createdForPullRequest && !branchInfo.remoteInUse && deleteRemote) {
451+
selectedActions.push({ type: 'remote' });
452+
}
453+
454+
// Execute all deletions in parallel
455+
await performBranchDeletion(folderRepositoryManager, item, defaultBranch, branchInfo!, selectedActions);
456+
}
406457
}

0 commit comments

Comments
 (0)