diff --git a/extensions/git/package.json b/extensions/git/package.json index f000da7f3f228..4d5a504517b3a 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -455,6 +455,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.fetchPruneAndDelete", + "title": "%command.fetchPruneAndDelete%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.renameBranch", "title": "%command.renameBranch%", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 6eba0f44d8f71..b0f00c568901a 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -63,6 +63,7 @@ "command.branch": "Create Branch...", "command.branchFrom": "Create Branch From...", "command.deleteBranch": "Delete Branch...", + "command.fetchPruneAndDelete": "Fetch (Prune) & Delete...", "command.renameBranch": "Rename Branch...", "command.cherryPick": "Cherry Pick...", "command.merge": "Merge...", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 01a8434c448ea..8a6d224993963 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2790,6 +2790,15 @@ export class CommandCenter { } } + @command('git.fetchPruneAndDelete', { repository: true }) + async fetchPruneAndDelete(repository: Repository): Promise { + try { + repository.fetchPruneAndDelete(); + } catch (error) { + throw error; + } + } + @command('git.renameBranch', { repository: true }) async renameBranch(repository: Repository): Promise { const currentBranchName = repository.HEAD && repository.HEAD.name; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 710d7a4d11076..f221f1e3b5980 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -13,7 +13,7 @@ import { EventEmitter } from 'events'; import * as iconv from '@vscode/iconv-lite-umd'; import * as filetype from 'file-type'; import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant } from './util'; -import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; +import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, l10n, Progress, Uri, workspace, window } from 'vscode'; import { detectEncoding } from './encoding'; import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions } from './api/git'; import * as byline from 'byline'; @@ -1730,6 +1730,64 @@ export class Repository { await this.exec(args); } + async fetchPruneAndDelete(): Promise { + const fetchPruneOutput = await this.exec(['fetch', '--prune']); + const { exitCode, stderr } = fetchPruneOutput; + + if (exitCode) { + throw new GitError({ + message: 'Could not fetch.', + exitCode + }); + } + + // The outputs of the fetch prune is at stderr + if (!stderr) { + throw new GitError({ + message: 'Could not find pruned branches to delete.', + }); + } + + const branchRegex = /->\s*(\S+)/; + // [ + // 'From github.com:username/some-repo', + // ' - [deleted] (none) -> origin/foo', + // ' - [deleted] (none) -> origin/bar', + // '' + // ] + const branches: string[] = stderr.split('\n') + // [ + // 'origin/foo', + // 'origin/bar', + // ] + // TODO: origin is hardcoded + .map((el) => el.match(branchRegex)?.[1]?.replace('origin/', '') ?? '') + // The origin branch name to run `git branch -d ` + // [ + // 'foo', + // 'bar', + // ] + .filter(Boolean); + + for (const branch of branches) { + try { + await this.deleteBranch(branch); + } catch (error) { + if (error.gitErrorCode !== GitErrorCodes.BranchNotFullyMerged) { + throw error; + } + + const message = l10n.t(`The branch "${branch}" is not fully merged. Delete anyway?`); + const yes = l10n.t('Delete Branch'); + + const pick = await window.showWarningMessage(message, { modal: true }, yes); + if (pick === yes) { + await this.deleteBranch(branch, true); + } + } + } + } + async renameBranch(name: string): Promise { const args = ['branch', '-m', name]; await this.exec(args); diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index 223f1945b0214..ba401558fad0b 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -18,6 +18,7 @@ export const enum OperationKind { Commit = 'Commit', Config = 'Config', DeleteBranch = 'DeleteBranch', + FetchPruneAndDelete = 'FetchPruneAndDelete', DeleteRef = 'DeleteRef', DeleteRemoteTag = 'DeleteRemoteTag', DeleteTag = 'DeleteTag', @@ -64,7 +65,7 @@ export const enum OperationKind { } export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation | - CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation | + CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation | FetchPruneAndDeleteOperation | DeleteRefOperation | DeleteRemoteTagOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation | GetRemoteRefsOperation | HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | @@ -86,6 +87,7 @@ export type CleanOperation = BaseOperation & { kind: OperationKind.Clean }; export type CommitOperation = BaseOperation & { kind: OperationKind.Commit }; export type ConfigOperation = BaseOperation & { kind: OperationKind.Config }; export type DeleteBranchOperation = BaseOperation & { kind: OperationKind.DeleteBranch }; +export type FetchPruneAndDeleteOperation = BaseOperation & { kind: OperationKind.FetchPruneAndDelete }; export type DeleteRefOperation = BaseOperation & { kind: OperationKind.DeleteRef }; export type DeleteRemoteTagOperation = BaseOperation & { kind: OperationKind.DeleteRemoteTag }; export type DeleteTagOperation = BaseOperation & { kind: OperationKind.DeleteTag }; @@ -143,6 +145,7 @@ export const Operation = { Commit: { kind: OperationKind.Commit, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true } as CommitOperation, Config: (readOnly: boolean) => ({ kind: OperationKind.Config, blocking: false, readOnly, remote: false, retry: false, showProgress: true } as ConfigOperation), DeleteBranch: { kind: OperationKind.DeleteBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteBranchOperation, + FetchPruneAndDelete: { kind: OperationKind.FetchPruneAndDelete, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as FetchPruneAndDeleteOperation, DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation, DeleteRemoteTag: { kind: OperationKind.DeleteRemoteTag, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteTagOperation, DeleteTag: { kind: OperationKind.DeleteTag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteTagOperation, diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index edd250797e098..ea1c72a2fd01c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1385,6 +1385,10 @@ export class Repository implements Disposable { await this.run(Operation.DeleteBranch, () => this.repository.deleteBranch(name, force)); } + async fetchPruneAndDelete(): Promise { + await this.run(Operation.FetchPruneAndDelete, () => this.repository.fetchPruneAndDelete()); + } + async renameBranch(name: string): Promise { await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name)); }