Skip to content
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@
"default": true,
"description": "%githubPullRequests.defaultDeletionMethod.selectRemote.description%"
},
"githubPullRequests.deleteBranchAfterMerge": {
"type": "boolean",
"default": false,
"description": "%githubPullRequests.deleteBranchAfterMerge.description%"
},
"githubPullRequests.terminalLinksHandler": {
"type": "string",
"enum": [
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"githubPullRequests.hideViewedFiles.description": "Hide files that have been marked as viewed in the pull request changes tree.",
"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.",
"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.",
"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.",
"githubPullRequests.terminalLinksHandler.description": "Default handler for terminal links.",
"githubPullRequests.terminalLinksHandler.github": "Create the pull request on GitHub",
"githubPullRequests.terminalLinksHandler.vscode": "Create the pull request in VS Code",
Expand Down
1 change: 1 addition & 0 deletions src/@types/vscode.proposed.chatParticipantAdditions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ declare module 'vscode' {
isComplete?: boolean;
toolSpecificData?: ChatTerminalToolInvocationData;
fromSubAgent?: boolean;
presentation?: 'hidden' | 'hiddenAfterComplete' | undefined;

constructor(toolName: string, toolCallId: string, isError?: boolean);
}
Expand Down
1 change: 1 addition & 0 deletions src/common/settingKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const DEFAULT_MERGE_METHOD = 'defaultMergeMethod';
export const DEFAULT_DELETION_METHOD = 'defaultDeletionMethod';
export const SELECT_LOCAL_BRANCH = 'selectLocalBranch';
export const SELECT_REMOTE = 'selectRemote';
export const DELETE_BRANCH_AFTER_MERGE = 'deleteBranchAfterMerge';
export const REMOTES = 'remotes';
export const PULL_PR_BRANCH_BEFORE_CHECKOUT = 'pullPullRequestBranchBeforeCheckout';
export type PullPRBranchVariants = 'never' | 'pull' | 'pullAndMergeBase' | 'pullAndUpdateBase' | true | false;
Expand Down
8 changes: 8 additions & 0 deletions src/github/activityBarViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { MergeArguments, PullRequest, ReviewType } from './views';
import { IComment } from '../common/comment';
import { emojify, ensureEmojis } from '../common/emoji';
import { disposeAll } from '../common/lifecycle';
import { DELETE_BRANCH_AFTER_MERGE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
import { ReviewEvent } from '../common/timelineEvent';
import { formatError } from '../common/utils';
import { generateUuid } from '../common/uuid';
Expand Down Expand Up @@ -409,6 +410,13 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W

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

this._replyMessage(message, {
Expand Down
14 changes: 8 additions & 6 deletions src/github/pullRequestGitHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export interface BaseBranchMetadata {
branch: string;
}

export type BranchInfo = {
branch: string;
remote?: string;
createdForPullRequest?: boolean;
remoteInUse?: boolean;
};

export class PullRequestGitHelper {
static ID = 'PullRequestGitHelper';
static async checkoutFromFork(
Expand Down Expand Up @@ -202,12 +209,7 @@ export class PullRequestGitHelper {
static async getBranchNRemoteForPullRequest(
repository: Repository,
pullRequest: PullRequestModel,
): Promise<{
branch: string;
remote?: string;
createdForPullRequest?: boolean;
remoteInUse?: boolean;
} | null> {
): Promise<BranchInfo | null> {
let branchName: string | null = null;
try {
const key = PullRequestGitHelper.buildPullRequestMetadata(pullRequest);
Expand Down
9 changes: 8 additions & 1 deletion src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCop
import { commands, contexts } from '../common/executeCommands';
import { disposeAll } from '../common/lifecycle';
import Logger from '../common/logger';
import { DEFAULT_MERGE_METHOD, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
import { DEFAULT_MERGE_METHOD, DELETE_BRANCH_AFTER_MERGE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
import { ITelemetry } from '../common/telemetry';
import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../common/timelineEvent';
import { asPromise, formatError } from '../common/utils';
Expand Down Expand Up @@ -626,6 +626,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode

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

const mergeResult: MergeResult = {
Expand Down
139 changes: 95 additions & 44 deletions src/github/pullRequestReviewCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import * as vscode from 'vscode';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { IAccount, isITeam, ITeam, MergeMethod, PullRequestMergeability, reviewerId, ReviewState } from './interface';
import { BranchInfo } from './pullRequestGitHelper';
import { PullRequestModel } from './pullRequestModel';
import { PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views';
import { DEFAULT_DELETION_METHOD, PR_SETTINGS_NAMESPACE, SELECT_LOCAL_BRANCH, SELECT_REMOTE } from '../common/settingKeys';
Expand Down Expand Up @@ -274,9 +275,13 @@ export namespace PullRequestReviewCommon {
}
}

interface SelectedAction {
type: 'remoteHead' | 'local' | 'remote' | 'suspend'
};

export async function deleteBranch(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<{ isReply: boolean, message: any }> {
const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item);
const actions: (vscode.QuickPickItem & { type: 'remoteHead' | 'local' | 'remote' | 'suspend' })[] = [];
const actions: (vscode.QuickPickItem & SelectedAction)[] = [];
const defaultBranch = await folderRepositoryManager.getPullRequestRepositoryDefaultBranch(item);

if (item.isResolved()) {
Expand Down Expand Up @@ -341,51 +346,9 @@ export namespace PullRequestReviewCommon {
ignoreFocusOut: true,
});

const deletedBranchTypes: string[] = [];

if (selectedActions) {
const isBranchActive = item.equals(folderRepositoryManager.activePullRequest) || (folderRepositoryManager.repository.state.HEAD?.name && folderRepositoryManager.repository.state.HEAD.name === branchInfo?.branch);

const promises = selectedActions.map(async action => {
switch (action.type) {
case 'remoteHead':
await folderRepositoryManager.deleteBranch(item);
deletedBranchTypes.push(action.type);
await folderRepositoryManager.repository.fetch({ prune: true });
// If we're in a remote repository, then we should checkout the default branch.
if (folderRepositoryManager.repository.rootUri.scheme === Schemes.VscodeVfs) {
await folderRepositoryManager.repository.checkout(defaultBranch);
}
return;
case 'local':
if (isBranchActive) {
if (folderRepositoryManager.repository.state.workingTreeChanges.length) {
const yes = vscode.l10n.t('Yes');
const response = await vscode.window.showWarningMessage(
vscode.l10n.t('Your local changes will be lost, do you want to continue?'),
{ modal: true },
yes,
);
if (response === yes) {
await vscode.commands.executeCommand('git.cleanAll');
} else {
return;
}
}
await folderRepositoryManager.checkoutDefaultBranch(defaultBranch);
}
await folderRepositoryManager.repository.deleteBranch(branchInfo!.branch, true);
return deletedBranchTypes.push(action.type);
case 'remote':
deletedBranchTypes.push(action.type);
return folderRepositoryManager.repository.removeRemote(branchInfo!.remote!);
case 'suspend':
deletedBranchTypes.push(action.type);
return vscode.commands.executeCommand('github.codespaces.disconnectSuspend');
}
});

await Promise.all(promises);
const deletedBranchTypes: string[] = await performBranchDeletion(folderRepositoryManager, item, defaultBranch, branchInfo!, selectedActions);

return {
isReply: false,
Expand All @@ -403,4 +366,92 @@ export namespace PullRequestReviewCommon {
};
}
}

async function performBranchDeletion(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel, defaultBranch: string, branchInfo: BranchInfo, selectedActions: SelectedAction[]): Promise<string[]> {
const isBranchActive = item.equals(folderRepositoryManager.activePullRequest) || (folderRepositoryManager.repository.state.HEAD?.name && folderRepositoryManager.repository.state.HEAD.name === branchInfo?.branch);
const deletedBranchTypes: string[] = [];

const promises = selectedActions.map(async action => {
switch (action.type) {
case 'remoteHead':
await folderRepositoryManager.deleteBranch(item);
deletedBranchTypes.push(action.type);
await folderRepositoryManager.repository.fetch({ prune: true });
// If we're in a remote repository, then we should checkout the default branch.
if (folderRepositoryManager.repository.rootUri.scheme === Schemes.VscodeVfs) {
await folderRepositoryManager.repository.checkout(defaultBranch);
}
return;
case 'local':
if (isBranchActive) {
if (folderRepositoryManager.repository.state.workingTreeChanges.length) {
const yes = vscode.l10n.t('Yes');
const response = await vscode.window.showWarningMessage(
vscode.l10n.t('Your local changes will be lost, do you want to continue?'),
{ modal: true },
yes,
);
if (response === yes) {
await vscode.commands.executeCommand('git.cleanAll');
} else {
return;
}
}
await folderRepositoryManager.checkoutDefaultBranch(defaultBranch);
}
await folderRepositoryManager.repository.deleteBranch(branchInfo!.branch, true);
return deletedBranchTypes.push(action.type);
case 'remote':
deletedBranchTypes.push(action.type);
return folderRepositoryManager.repository.removeRemote(branchInfo!.remote!);
case 'suspend':
deletedBranchTypes.push(action.type);
return vscode.commands.executeCommand('github.codespaces.disconnectSuspend');
}
});

await Promise.all(promises);
return deletedBranchTypes;
}

/**
* Automatically delete branches after merge based on user preferences.
* This function does not show any prompts - it uses the default deletion method preferences.
*/
export async function autoDeleteBranchesAfterMerge(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<void> {
const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item);
const defaultBranch = await folderRepositoryManager.getPullRequestRepositoryDefaultBranch(item);

// Get user preferences for automatic deletion
const deleteLocalBranch = vscode.workspace
.getConfiguration(PR_SETTINGS_NAMESPACE)
.get<boolean>(`${DEFAULT_DELETION_METHOD}.${SELECT_LOCAL_BRANCH}`, true);

const deleteRemote = vscode.workspace
.getConfiguration(PR_SETTINGS_NAMESPACE)
.get<boolean>(`${DEFAULT_DELETION_METHOD}.${SELECT_REMOTE}`, true);

const selectedActions: SelectedAction[] = [];

// Delete remote head branch if it's not the default branch
if (item.isResolved()) {
const isDefaultBranch = defaultBranch === item.head.ref;
if (!isDefaultBranch && !item.isRemoteHeadDeleted) {
selectedActions.push({ type: 'remoteHead' });
}
}

// Delete local branch if preference is set
if (branchInfo && deleteLocalBranch) {
selectedActions.push({ type: 'local' });
}

// Delete remote if it's no longer used and preference is set
if (branchInfo && branchInfo.remote && branchInfo.createdForPullRequest && !branchInfo.remoteInUse && deleteRemote) {
selectedActions.push({ type: 'remote' });
}

// Execute all deletions in parallel
await performBranchDeletion(folderRepositoryManager, item, defaultBranch, branchInfo!, selectedActions);
}
}