diff --git a/package.json b/package.json index 121adffe67..40a0862981 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/package.nls.json b/package.nls.json index 73c02b7684..a352820de6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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", diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index 71520fa1ec..aa7001a3d2 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -105,6 +105,7 @@ declare module 'vscode' { isComplete?: boolean; toolSpecificData?: ChatTerminalToolInvocationData; fromSubAgent?: boolean; + presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; constructor(toolName: string, toolCallId: string, isError?: boolean); } diff --git a/src/common/settingKeys.ts b/src/common/settingKeys.ts index 1376c4c7bc..b3d358bf31 100644 --- a/src/common/settingKeys.ts +++ b/src/common/settingKeys.ts @@ -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; diff --git a/src/github/activityBarViewProvider.ts b/src/github/activityBarViewProvider.ts index 00f623fa1f..345582cd17 100644 --- a/src/github/activityBarViewProvider.ts +++ b/src/github/activityBarViewProvider.ts @@ -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'; @@ -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(DELETE_BRANCH_AFTER_MERGE, false); + if (deleteBranchAfterMerge) { + // Automatically delete the branch after successful merge + await PullRequestReviewCommon.autoDeleteBranchesAfterMerge(this._folderRepositoryManager, this._item); + } } this._replyMessage(message, { diff --git a/src/github/pullRequestGitHelper.ts b/src/github/pullRequestGitHelper.ts index 599a3eda6d..74239cf974 100644 --- a/src/github/pullRequestGitHelper.ts +++ b/src/github/pullRequestGitHelper.ts @@ -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( @@ -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 { let branchName: string | null = null; try { const key = PullRequestGitHelper.buildPullRequestMetadata(pullRequest); diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 50aed1e41f..816f754880 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -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'; @@ -626,6 +626,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel(DELETE_BRANCH_AFTER_MERGE, false); + if (deleteBranchAfterMerge) { + // Automatically delete the branch after successful merge + await PullRequestReviewCommon.autoDeleteBranchesAfterMerge(this._folderRepositoryManager, this._item); + } } const mergeResult: MergeResult = { diff --git a/src/github/pullRequestReviewCommon.ts b/src/github/pullRequestReviewCommon.ts index f6b76eacd1..4ccab16546 100644 --- a/src/github/pullRequestReviewCommon.ts +++ b/src/github/pullRequestReviewCommon.ts @@ -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'; @@ -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()) { @@ -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, @@ -403,4 +366,92 @@ export namespace PullRequestReviewCommon { }; } } + + async function performBranchDeletion(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel, defaultBranch: string, branchInfo: BranchInfo, selectedActions: SelectedAction[]): Promise { + 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 { + 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(`${DEFAULT_DELETION_METHOD}.${SELECT_LOCAL_BRANCH}`, true); + + const deleteRemote = vscode.workspace + .getConfiguration(PR_SETTINGS_NAMESPACE) + .get(`${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); + } }