diff --git a/extensions/git/package.json b/extensions/git/package.json index 560623ee3b2..d34fdb9f925 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -420,6 +420,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.deleteRemoteTag", + "title": "%command.deleteRemoteTag%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.fetch", "title": "%command.fetch%", @@ -986,6 +992,10 @@ "command": "git.deleteTag", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.deleteRemoteTag", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, { "command": "git.fetch", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -1929,6 +1939,10 @@ { "command": "git.deleteTag", "group": "tags@2" + }, + { + "command": "git.deleteRemoteTag", + "group": "tags@3" } ] }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index a50c35800c1..630cad75dfb 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -61,6 +61,7 @@ "command.rebase": "Rebase Branch...", "command.createTag": "Create Tag", "command.deleteTag": "Delete Tag", + "command.deleteRemoteTag": "Delete Remote Tag", "command.fetch": "Fetch", "command.fetchPrune": "Fetch (Prune)", "command.fetchAll": "Fetch From All Remotes", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index a5a1822f26a..6f2d99ee15e 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -312,7 +312,7 @@ function sanitizeRemoteName(name: string) { } class TagItem implements QuickPickItem { - get label(): string { return this.ref.name ?? ''; } + get label(): string { return `$(tag) ${this.ref.name ?? ''}`; } get description(): string { return this.ref.commit?.substr(0, 8) ?? ''; } constructor(readonly ref: Ref) { } } @@ -2380,6 +2380,42 @@ export class CommandCenter { await repository.deleteTag(choice.label); } + @command('git.deleteRemoteTag', { repository: true }) + async deleteRemoteTag(repository: Repository): Promise { + const remotePicks = repository.remotes + .filter(r => r.pushUrl !== undefined) + .map(r => new RemoteItem(repository, r)); + + if (remotePicks.length === 0) { + window.showErrorMessage(l10n.t("Your repository has no remotes configured to push to.")); + return; + } + + let remoteName = remotePicks[0].remoteName; + if (remotePicks.length > 1) { + const remotePickPlaceholder = l10n.t('Select a remote to delete a tag from'); + const remotePick = await window.showQuickPick(remotePicks, { placeHolder: remotePickPlaceholder }); + + if (!remotePick) { + return; + } + + remoteName = remotePick.remoteName; + } + + const remoteTagPicks = async (): Promise => { + const remoteTags = await repository.getRemoteRefs(remoteName, { tags: true }); + return remoteTags.length === 0 ? [{ label: l10n.t('Remote "{0}" has no tags.', remoteName) }] : remoteTags.map(ref => new TagItem(ref)); + }; + + const tagPickPlaceholder = l10n.t('Select a tag to delete'); + const remoteTagPick = await window.showQuickPick(remoteTagPicks(), { placeHolder: tagPickPlaceholder }); + + if (remoteTagPick && remoteTagPick instanceof TagItem && remoteTagPick.ref.name) { + await repository.deleteRemoteTag(remoteName, remoteTagPick.ref.name); + } + } + @command('git.fetch', { repository: true }) async fetch(repository: Repository): Promise { if (repository.remotes.length === 0) { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index ee89db10ed2..e27b18f1fc1 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1635,6 +1635,11 @@ export class Repository { await this.exec(args); } + async deleteRemoteTag(remoteName: string, tagName: string): Promise { + const args = ['push', '--delete', remoteName, tagName]; + await this.exec(args); + } + async clean(paths: string[]): Promise { const pathsByGroup = groupBy(paths.map(sanitizePath), p => path.dirname(p)); const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]); @@ -2207,6 +2212,43 @@ export class Repository { .filter(ref => !!ref) as Ref[]; } + async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean; cancellationToken?: CancellationToken }): Promise { + if (opts?.cancellationToken && opts?.cancellationToken.isCancellationRequested) { + throw new CancellationError(); + } + + const args = ['ls-remote']; + + if (opts?.heads) { + args.push('--heads'); + } + + if (opts?.tags) { + args.push('--tags'); + } + + args.push(remote); + + const result = await this.exec(args, { cancellationToken: opts?.cancellationToken }); + + const fn = (line: string): Ref | null => { + let match: RegExpExecArray | null; + + if (match = /^([0-9a-f]{40})\trefs\/heads\/([^ ]+)$/.exec(line)) { + return { name: match[1], commit: match[2], type: RefType.Head }; + } else if (match = /^([0-9a-f]{40})\trefs\/tags\/([^ ]+)$/.exec(line)) { + return { name: match[2], commit: match[1], type: RefType.Tag }; + } + + return null; + }; + + return result.stdout.split('\n') + .filter(line => !!line) + .map(fn) + .filter(ref => !!ref) as Ref[]; + } + async getStashes(): Promise { const result = await this.exec(['stash', 'list']); const regex = /^stash@{(\d+)}:(.+)$/; diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index e36e1bb3abd..e068f36ee6f 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -21,6 +21,7 @@ export const enum OperationKind { Config = 'Config', DeleteBranch = 'DeleteBranch', DeleteRef = 'DeleteRef', + DeleteRemoteTag = 'DeleteRemoteTag', DeleteTag = 'DeleteTag', Diff = 'Diff', Fetch = 'Fetch', @@ -30,6 +31,7 @@ export const enum OperationKind { GetBranches = 'GetBranches', GetCommitTemplate = 'GetCommitTemplate', GetObjectDetails = 'GetObjectDetails', + GetRemoteRefs = 'GetRemoteRefs', HashObject = 'HashObject', Ignore = 'Ignore', Log = 'Log', @@ -62,12 +64,12 @@ export const enum OperationKind { export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation | CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation | - DeleteRefOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | GetBranchOperation | - GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | HashObjectOperation | IgnoreOperation | LogOperation | - LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation | MoveOperation | PostCommitCommandOperation | - PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation | ResetOperation | RebaseOperation | - RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | SetBranchUpstreamOperation | ShowOperation | StageOperation | - StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation | TagOperation; + DeleteRefOperation | DeleteRemoteTagOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | + GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetRemoteRefsOperation | HashObjectOperation | + IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation | MoveOperation | + PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation | ResetOperation | + RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | SetBranchUpstreamOperation | ShowOperation | + StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation | TagOperation; type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean }; export type AddOperation = BaseOperation & { kind: OperationKind.Add }; @@ -83,6 +85,7 @@ export type CommitOperation = BaseOperation & { kind: OperationKind.Commit }; export type ConfigOperation = BaseOperation & { kind: OperationKind.Config }; export type DeleteBranchOperation = BaseOperation & { kind: OperationKind.DeleteBranch }; export type DeleteRefOperation = BaseOperation & { kind: OperationKind.DeleteRef }; +export type DeleteRemoteTagOperation = BaseOperation & { kind: OperationKind.DeleteRemoteTag }; export type DeleteTagOperation = BaseOperation & { kind: OperationKind.DeleteTag }; export type DiffOperation = BaseOperation & { kind: OperationKind.Diff }; export type FetchOperation = BaseOperation & { kind: OperationKind.Fetch }; @@ -91,6 +94,7 @@ export type GetBranchOperation = BaseOperation & { kind: OperationKind.GetBranch export type GetBranchesOperation = BaseOperation & { kind: OperationKind.GetBranches }; export type GetCommitTemplateOperation = BaseOperation & { kind: OperationKind.GetCommitTemplate }; export type GetObjectDetailsOperation = BaseOperation & { kind: OperationKind.GetObjectDetails }; +export type GetRemoteRefsOperation = BaseOperation & { kind: OperationKind.GetRemoteRefs }; export type HashObjectOperation = BaseOperation & { kind: OperationKind.HashObject }; export type IgnoreOperation = BaseOperation & { kind: OperationKind.Ignore }; export type LogOperation = BaseOperation & { kind: OperationKind.Log }; @@ -133,6 +137,7 @@ export const Operation = { Config: { kind: OperationKind.Config, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ConfigOperation, DeleteBranch: { kind: OperationKind.DeleteBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteBranchOperation, 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: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as DiffOperation, Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation), @@ -141,6 +146,7 @@ export const Operation = { GetBranches: { kind: OperationKind.GetBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchesOperation, GetCommitTemplate: { kind: OperationKind.GetCommitTemplate, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetCommitTemplateOperation, GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation, + GetRemoteRefs: { kind: OperationKind.GetRemoteRefs, blocking: false, readOnly: true, remote: true, retry: false, showProgress: false } as GetRemoteRefsOperation, HashObject: { kind: OperationKind.HashObject, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as HashObjectOperation, Ignore: { kind: OperationKind.Ignore, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as IgnoreOperation, Log: { kind: OperationKind.Log, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as LogOperation, diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4b706db9b4b..c8db25d35d6 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1378,6 +1378,10 @@ export class Repository implements Disposable { return await this.run(Operation.GetBranches, () => this.repository.getBranches(query)); } + async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean }): Promise { + return await this.run(Operation.GetRemoteRefs, () => this.repository.getRemoteRefs(remote, opts)); + } + async setBranchUpstream(name: string, upstream: string): Promise { await this.run(Operation.SetBranchUpstream, () => this.repository.setBranchUpstream(name, upstream)); } @@ -1402,6 +1406,10 @@ export class Repository implements Disposable { await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name)); } + async deleteRemoteTag(remoteName: string, tagName: string): Promise { + await this.run(Operation.DeleteRemoteTag, () => this.repository.deleteRemoteTag(remoteName, tagName)); + } + async checkout(treeish: string, opts?: { detached?: boolean; pullBeforeCheckout?: boolean }): Promise { const refLabel = opts?.detached ? treeish.substring(0, 8) : treeish;