diff --git a/contributions.json b/contributions.json index 4f5738a34a7cc..f3cd204c95828 100644 --- a/contributions.json +++ b/contributions.json @@ -629,6 +629,11 @@ ] } }, + "gitlens.copyRemoteTagUrl": { + "label": "Copy Remote Tag URL", + "icon": "$(copy)", + "commandPalette": "gitlens:repos:withRemotes" + }, "gitlens.copyShaToClipboard": { "label": "Copy SHA", "icon": "$(copy)", @@ -3315,6 +3320,11 @@ ] } }, + "gitlens.openTagOnRemote": { + "label": "Open Tag on Remote", + "icon": "$(globe)", + "commandPalette": "gitlens:repos:withRemotes" + }, "gitlens.openWorkingFile": { "label": "Open File", "icon": "$(go-to-file)", @@ -6206,6 +6216,10 @@ ] } }, + "gitlens.views.copyRemoteTagUrl": { + "label": "Copy Remote Tag URL", + "icon": "$(copy)" + }, "gitlens.views.copyUrl": { "label": "Copy URL", "icon": "$(copy)", @@ -7762,6 +7776,38 @@ ] } }, + "gitlens.views.openTagOnRemote": { + "label": "Open Tag on Remote", + "icon": "$(globe)", + "menus": { + "view/item/context": [ + { + "when": "viewItem =~ /gitlens:tag\\b/ && gitlens:repos:withRemotes", + "group": "inline", + "order": 100, + "alt": "gitlens.views.copyRemoteTagUrl" + }, + { + "when": "viewItem =~ /gitlens:tag\\b/ && !listMultiSelection && gitlens:repos:withRemotes", + "group": "3_gitlens_explore", + "order": 3 + } + ] + } + }, + "gitlens.views.openTagOnRemote.multi": { + "label": "Open Tags on Remote", + "icon": "$(globe)", + "menus": { + "view/item/context": [ + { + "when": "viewItem =~ /gitlens:tag\\b/ && listMultiSelection && gitlens:repos:withRemotes", + "group": "3_gitlens_explore", + "order": 3 + } + ] + } + }, "gitlens.views.openUrl": { "label": "Open URL", "icon": "$(globe)", diff --git a/package.json b/package.json index 60b6a016e45be..15d8eb1090fec 100644 --- a/package.json +++ b/package.json @@ -3733,7 +3733,8 @@ "fileInCommit", "fileInBranch", "fileLine", - "fileRange" + "fileRange", + "tag" ], "properties": { "repository": { @@ -3771,6 +3772,10 @@ "fileRange": { "type": "string", "markdownDescription": "Specifies the format of a range in a file URL for the custom remote service\n\nAvailable tokens\\\n`${start}` — starting line\\\n`${end}` — ending line" + }, + "tag": { + "type": "string", + "markdownDescription": "Specifies the format of a tag URL for the custom remote service\n\nAvailable tokens\\\n`${repo}` — repository path\\\n`${tagName}` — name of the tag" } }, "additionalProperties": false @@ -4754,6 +4759,7 @@ "default": { "suppressCommitHasNoPreviousCommitWarning": false, "suppressCommitNotFoundWarning": false, + "suppressTagNotFoundWarning": false, "suppressCreatePullRequestPrompt": false, "suppressDebugLoggingWarning": false, "suppressFileNotUnderSourceControlWarning": false, @@ -4780,6 +4786,11 @@ "default": false, "description": "Commit Not Found Warning" }, + "suppressTagNotFoundWarning": { + "type": "boolean", + "default": false, + "description": "Tag Not Found Warning" + }, "suppressCreatePullRequestPrompt": { "type": "boolean", "default": false, @@ -5986,6 +5997,12 @@ "title": "Copy Remote Repository URL", "icon": "$(copy)" }, + { + "command": "gitlens.copyRemoteTagUrl", + "title": "Copy Remote Tag URL", + "category": "GitLens", + "icon": "$(copy)" + }, { "command": "gitlens.copyShaToClipboard", "title": "Copy SHA", @@ -7019,6 +7036,12 @@ "icon": "$(gitlens-open-revision)", "enablement": "gitlens:enabled && resourceScheme =~ /^(gitlens|pr)$/ " }, + { + "command": "gitlens.openTagOnRemote", + "title": "Open Tag on Remote", + "category": "GitLens", + "icon": "$(globe)" + }, { "command": "gitlens.openWorkingFile", "title": "Open File", @@ -7960,6 +7983,11 @@ "title": "Copy Remote Commit URLs", "icon": "$(copy)" }, + { + "command": "gitlens.views.copyRemoteTagUrl", + "title": "Copy Remote Tag URL", + "icon": "$(copy)" + }, { "command": "gitlens.views.copyUrl", "title": "Copy URL", @@ -8435,6 +8463,16 @@ "title": "Compare Pull Request", "icon": "$(compare-changes)" }, + { + "command": "gitlens.views.openTagOnRemote", + "title": "Open Tag on Remote", + "icon": "$(globe)" + }, + { + "command": "gitlens.views.openTagOnRemote.multi", + "title": "Open Tags on Remote", + "icon": "$(globe)" + }, { "command": "gitlens.views.openUrl", "title": "Open URL", @@ -10081,6 +10119,10 @@ "command": "gitlens.copyRemoteRepositoryUrl", "when": "false" }, + { + "command": "gitlens.copyRemoteTagUrl", + "when": "gitlens:repos:withRemotes" + }, { "command": "gitlens.copyShaToClipboard", "when": "resource in gitlens:tabs:blameable" @@ -10893,6 +10935,10 @@ "command": "gitlens.openRevisionFileInDiffRight", "when": "false" }, + { + "command": "gitlens.openTagOnRemote", + "when": "gitlens:repos:withRemotes" + }, { "command": "gitlens.openWorkingFile", "when": "gitlens:enabled && resourceScheme =~ /^(gitlens|git|pr)$/" @@ -11621,6 +11667,10 @@ "command": "gitlens.views.copyRemoteCommitUrl.multi", "when": "false" }, + { + "command": "gitlens.views.copyRemoteTagUrl", + "when": "false" + }, { "command": "gitlens.views.copyUrl", "when": "false" @@ -12017,6 +12067,14 @@ "command": "gitlens.views.openPullRequestComparison", "when": "false" }, + { + "command": "gitlens.views.openTagOnRemote", + "when": "false" + }, + { + "command": "gitlens.views.openTagOnRemote.multi", + "when": "false" + }, { "command": "gitlens.views.openUrl", "when": "false" @@ -16884,6 +16942,12 @@ "when": "viewItem =~ /gitlens:tag\\b/ && !gitlens:hasVirtualFolders && !gitlens:readonly && !gitlens:untrusted", "group": "inline@10" }, + { + "command": "gitlens.views.openTagOnRemote", + "when": "viewItem =~ /gitlens:tag\\b/ && gitlens:repos:withRemotes", + "group": "inline@100", + "alt": "gitlens.views.copyRemoteTagUrl" + }, { "command": "gitlens.views.switchToTag", "when": "viewItem =~ /gitlens:tag\\b/ && !listMultiSelection && !gitlens:hasVirtualFolders && !gitlens:readonly && !gitlens:untrusted", @@ -16904,6 +16968,16 @@ "when": "viewItem =~ /gitlens:tag\\b/ && !listMultiSelection && !gitlens:hasVirtualFolders && !gitlens:readonly && !gitlens:untrusted", "group": "1_gitlens_actions@3" }, + { + "command": "gitlens.views.openTagOnRemote", + "when": "viewItem =~ /gitlens:tag\\b/ && !listMultiSelection && gitlens:repos:withRemotes", + "group": "3_gitlens_explore@3" + }, + { + "command": "gitlens.views.openTagOnRemote.multi", + "when": "viewItem =~ /gitlens:tag\\b/ && listMultiSelection && gitlens:repos:withRemotes", + "group": "3_gitlens_explore@3" + }, { "command": "gitlens.views.createTag", "when": "viewItem =~ /gitlens:tags\\b/ && !gitlens:hasVirtualFolders && !gitlens:readonly && !gitlens:untrusted", diff --git a/src/commands.ts b/src/commands.ts index dfe74c2b285b2..6374c0ec49086 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -33,6 +33,7 @@ import './commands/openBranchOnRemote'; import './commands/openCurrentBranchOnRemote'; import './commands/openChangedFiles'; import './commands/openCommitOnRemote'; +import './commands/openTagOnRemote'; import './commands/openComparisonOnRemote'; import './commands/openFileFromRemote'; import './commands/openFileOnRemote'; diff --git a/src/commands/openOnRemote.ts b/src/commands/openOnRemote.ts index d056d019fb456..fb69ea10343da 100644 --- a/src/commands/openOnRemote.ts +++ b/src/commands/openOnRemote.ts @@ -196,13 +196,13 @@ export class OpenOnRemoteCommand extends GlCommandBase { break; } - // case RemoteResourceType.Tag: { - // title = getTitlePrefix('Tag'); - // if (resources.length === 1) { - // title += `${pad(GlyphChars.Dot, 2, 2)}${args.resource.tag}`; - // } - // break; - // } + case RemoteResourceType.Tag: { + title = getTitlePrefix('Tag'); + if (resources.length === 1) { + title += `${pad(GlyphChars.Dot, 2, 2)}${resource.tag}`; + } + break; + } } const pick = await showRemoteProviderPicker(title, placeholder, resources, remotes, options); diff --git a/src/commands/openTagOnRemote.ts b/src/commands/openTagOnRemote.ts new file mode 100644 index 0000000000000..6d9b0acdd26c5 --- /dev/null +++ b/src/commands/openTagOnRemote.ts @@ -0,0 +1,98 @@ +import type { TextEditor, Uri } from 'vscode'; +import { GlCommand } from '../constants.commands'; +import type { Container } from '../container'; +import { GitUri } from '../git/gitUri'; +import { RemoteResourceType } from '../git/models/remoteResource'; +import { showGenericErrorMessage } from '../messages'; +import { CommandQuickPickItem } from '../quickpicks/items/common'; +import { ReferencesQuickPickIncludes, showReferencePicker } from '../quickpicks/referencePicker'; +import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; +import { command, executeCommand } from '../system/-webview/command'; +import { Logger } from '../system/logger'; +import { ActiveEditorCommand } from './commandBase'; +import { getCommandUri } from './commandBase.utils'; +import type { CommandContext } from './commandContext'; +import { isCommandContextViewNodeHasTag } from './commandContext.utils'; +import type { OpenOnRemoteCommandArgs } from './openOnRemote'; + +export interface OpenTagOnRemoteCommandArgs { + tag?: string; + clipboard?: boolean; + remote?: string; +} + +@command() +export class OpenTagOnRemoteCommand extends ActiveEditorCommand { + constructor(private readonly container: Container) { + super([GlCommand.OpenTagOnRemote, GlCommand.CopyRemoteTagUrl]); + } + + protected override preExecute(context: CommandContext, args?: OpenTagOnRemoteCommandArgs): Promise { + if (isCommandContextViewNodeHasTag(context)) { + args = { + ...args, + tag: context.node.tag.name, + remote: context.node.tag.name, + }; + } + + if (context.command === GlCommand.CopyRemoteTagUrl) { + args = { ...args, clipboard: true }; + } + + return this.execute(context.editor, context.uri, args); + } + + async execute(editor?: TextEditor, uri?: Uri, args?: OpenTagOnRemoteCommandArgs): Promise { + uri = getCommandUri(uri, editor); + + const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined; + + const repoPath = ( + await getBestRepositoryOrShowPicker( + gitUri, + editor, + args?.clipboard ? 'Copy Remote Tag URL' : 'Open Tag On Remote', + ) + )?.path; + if (!repoPath) return; + + args = { ...args }; + + try { + if (args.tag == null) { + const pick = await showReferencePicker( + repoPath, + args.clipboard ? 'Copy Remote Tag URL' : 'Open Tag On Remote', + args.clipboard ? 'Choose a Tag to copy the URL from' : 'Choose a Tag to open', + { + autoPick: true, + filter: { tags: () => true, branches: () => false }, + include: ReferencesQuickPickIncludes.Tags, + sort: { tags: { current: true } }, + }, + ); + if (pick == null || pick instanceof CommandQuickPickItem) return; + + if (pick.refType === 'tag') { + args.tag = pick.name; + } else { + args.tag = pick.ref; + } + } + + void (await executeCommand(GlCommand.OpenOnRemote, { + resource: { + type: RemoteResourceType.Tag, + tag: args.tag, + }, + repoPath: repoPath, + remote: args.remote, + clipboard: args.clipboard, + })); + } catch (ex) { + Logger.error(ex, 'OpenTagOnRemoteCommand'); + void showGenericErrorMessage('Unable to open Tag on remote provider'); + } + } +} diff --git a/src/config.ts b/src/config.ts index 9073aea23eb82..541263c1a4228 100644 --- a/src/config.ts +++ b/src/config.ts @@ -155,6 +155,7 @@ export const enum StatusBarCommand { export type SuppressedMessages = | 'suppressCommitHasNoPreviousCommitWarning' | 'suppressCommitNotFoundWarning' + | 'suppressTagNotFoundWarning' | 'suppressCreatePullRequestPrompt' | 'suppressDebugLoggingWarning' | 'suppressFileNotUnderSourceControlWarning' @@ -626,6 +627,7 @@ export interface RemotesUrlsConfig { readonly fileInCommit: string; readonly fileLine: string; readonly fileRange: string; + readonly tag: string; } interface StatusBarConfig { diff --git a/src/constants.commands.ts b/src/constants.commands.ts index 79bc6912724b5..4ba376aeb22d9 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -31,6 +31,7 @@ export const enum GlCommand { CopyRemoteBranchesUrl = 'gitlens.copyRemoteBranchesUrl', CopyRemoteBranchUrl = 'gitlens.copyRemoteBranchUrl', CopyRemoteCommitUrl = 'gitlens.copyRemoteCommitUrl', + CopyRemoteTagUrl = 'gitlens.copyRemoteTagUrl', CopyRemoteComparisonUrl = 'gitlens.copyRemoteComparisonUrl', CopyRemoteFileUrl = 'gitlens.copyRemoteFileUrlToClipboard', CopyRemoteFileUrlWithoutRange = 'gitlens.copyRemoteFileUrlWithoutRange', @@ -83,6 +84,7 @@ export const enum GlCommand { OpenCurrentBranchOnRemote = 'gitlens.openCurrentBranchOnRemote', OpenChangedFiles = 'gitlens.openChangedFiles', OpenCommitOnRemote = 'gitlens.openCommitOnRemote', + OpenTagOnRemote = 'gitlens.openTagOnRemote', OpenComparisonOnRemote = 'gitlens.openComparisonOnRemote', OpenFileHistory = 'gitlens.openFileHistory', OpenFileFromRemote = 'gitlens.openFileFromRemote', @@ -581,6 +583,10 @@ export type TreeViewCommands = `gitlens.views.${ | 'copyRemoteCommitUrl.multi' | 'openCommitOnRemote' | 'openCommitOnRemote.multi' + | 'openTagOnRemote' + | 'openTagOnRemote.multi' + | 'copyRemoteTagUrl' + | 'copyRemoteTagUrl.multi' | 'openChanges' | 'openChangesWithWorking' | 'openPreviousChangesWithWorking' @@ -736,6 +742,8 @@ type GraphWebviewCommands = `graph.${ | 'cherryPick' | 'copyRemoteCommitUrl' | 'copyRemoteCommitUrl.multi' + | 'copyRemoteTagUrl' + | 'copyRemoteTagUrl.multi' | 'openCommitOnRemote' | 'openCommitOnRemote.multi' | 'commitViaSCM' diff --git a/src/git/models/remoteResource.ts b/src/git/models/remoteResource.ts index 560926dffdc74..e0013a6662637 100644 --- a/src/git/models/remoteResource.ts +++ b/src/git/models/remoteResource.ts @@ -10,7 +10,7 @@ export const enum RemoteResourceType { File = 'file', Repo = 'repo', Revision = 'revision', - // Tag = 'tag', + Tag = 'tag', } export type RemoteResource = @@ -58,4 +58,8 @@ export type RemoteResource = fileName: string; range?: Range; sha?: string; + } + | { + type: RemoteResourceType.Tag; + tag: string; }; diff --git a/src/git/remotes/azure-devops.ts b/src/git/remotes/azure-devops.ts index b93f18578a8b2..350ed8e58e69c 100644 --- a/src/git/remotes/azure-devops.ts +++ b/src/git/remotes/azure-devops.ts @@ -206,4 +206,8 @@ export class AzureDevOpsRemote extends RemoteProvider { if (branch) return this.encodeUrl(`${this.baseUrl}/?path=/${fileName}&version=GB${branch}&_a=contents${line}`); return this.encodeUrl(`${this.baseUrl}?path=/${fileName}${line}`); } + + protected override getUrlForTag(tag: string): string { + return this.encodeUrl(`${this.baseUrl}?version=GT${tag}`); + } } diff --git a/src/git/remotes/bitbucket-server.ts b/src/git/remotes/bitbucket-server.ts index a6f5171649f1f..fdf23fc335790 100644 --- a/src/git/remotes/bitbucket-server.ts +++ b/src/git/remotes/bitbucket-server.ts @@ -176,4 +176,8 @@ export class BitbucketServerRemote extends RemoteProvider { if (branch) return `${this.encodeUrl(`${this.baseUrl}/browse/${fileName}?at=${branch}`)}${line}`; return `${this.encodeUrl(`${this.baseUrl}/browse/${fileName}`)}${line}`; } + + protected override getUrlForTag(tag: string): string { + return this.encodeUrl(`${this.baseUrl}/commits/tag/${tag}`); + } } diff --git a/src/git/remotes/bitbucket.ts b/src/git/remotes/bitbucket.ts index f2a85d8e6e1c8..5ebc6561450df 100644 --- a/src/git/remotes/bitbucket.ts +++ b/src/git/remotes/bitbucket.ts @@ -162,4 +162,8 @@ export class BitbucketRemote extends RemoteProvider { if (branch) return `${this.encodeUrl(`${this.baseUrl}/src/${branch}/${fileName}`)}${line}`; return `${this.encodeUrl(`${this.baseUrl}?path=${fileName}`)}${line}`; } + + protected override getUrlForTag(tag: string): string { + return this.encodeUrl(`${this.baseUrl}/commits/tag/${tag}`); + } } diff --git a/src/git/remotes/custom.ts b/src/git/remotes/custom.ts index 7f770eb14fc81..9aae10e2e3169 100644 --- a/src/git/remotes/custom.ts +++ b/src/git/remotes/custom.ts @@ -130,4 +130,8 @@ export class CustomRemote extends RemoteProvider { return context; } + + protected override getUrlForTag(tag: string): string { + return this.getUrl(this.urls.tag, this.getContext({ tag: tag })); + } } diff --git a/src/git/remotes/gerrit.ts b/src/git/remotes/gerrit.ts index b65559f16fc45..db7e4120ff353 100644 --- a/src/git/remotes/gerrit.ts +++ b/src/git/remotes/gerrit.ts @@ -196,4 +196,8 @@ export class GerritRemote extends RemoteProvider { if (branch) return `${this.encodeUrl(`${this.getUrlForBranch(branch)}/${fileName}`)}${line}`; return `${this.encodeUrl(`${this.baseUrl}/+/HEAD/${fileName}`)}${line}`; } + + protected override getUrlForTag(): string | undefined { + return undefined; + } } diff --git a/src/git/remotes/gitea.ts b/src/git/remotes/gitea.ts index 4f85fbf27391e..0fb08794bac58 100644 --- a/src/git/remotes/gitea.ts +++ b/src/git/remotes/gitea.ts @@ -160,4 +160,8 @@ export class GiteaRemote extends RemoteProvider { // this route is deprecated but there is no alternative return `${this.encodeUrl(`${this.baseUrl}/src/${fileName}`)}${line}`; } + + protected getUrlForTag(tag: string): string { + return this.encodeUrl(`${this.baseUrl}/releases/tag/${tag}`); + } } diff --git a/src/git/remotes/github.ts b/src/git/remotes/github.ts index 4a0a62cdfbd05..30b2888efdaa1 100644 --- a/src/git/remotes/github.ts +++ b/src/git/remotes/github.ts @@ -303,6 +303,10 @@ export class GitHubRemote extends RemoteProvider { if (branch) return `${this.encodeUrl(`${this.baseUrl}/blob/${branch}/${fileName}`)}${line}`; return `${this.encodeUrl(`${this.baseUrl}?path=${fileName}`)}${line}`; } + + protected override getUrlForTag(tag: string): string { + return this.encodeUrl(`${this.baseUrl}/releases/tag/${tag}`); + } } const gitHubNoReplyAddressRegex = /^(?:(\d+)\+)?([a-zA-Z\d-]{1,39})@users\.noreply\.(.*)$/i; diff --git a/src/git/remotes/gitlab.ts b/src/git/remotes/gitlab.ts index 09f8914cf04c4..49a1eceb1f413 100644 --- a/src/git/remotes/gitlab.ts +++ b/src/git/remotes/gitlab.ts @@ -387,4 +387,8 @@ export class GitLabRemote extends RemoteProvider { if (branch) return `${this.encodeUrl(`${this.baseUrl}/-/blob/${branch}/${fileName}`)}${line}`; return `${this.encodeUrl(`${this.baseUrl}?path=${fileName}`)}${line}`; } + + protected override getUrlForTag(tag: string): string { + return this.encodeUrl(`${this.baseUrl}/-/tags/${tag}`); + } } diff --git a/src/git/remotes/google-source.ts b/src/git/remotes/google-source.ts index 2587db9970e60..a3f7e14bb13ba 100644 --- a/src/git/remotes/google-source.ts +++ b/src/git/remotes/google-source.ts @@ -40,4 +40,8 @@ export class GoogleSourceRemote extends GerritRemote { protected override get baseReviewUrl(): string { return `${this.protocol}://${this.reviewDomain}`; } + + protected override getUrlForTag(): string | undefined { + return undefined; + } } diff --git a/src/git/remotes/remoteProvider.ts b/src/git/remotes/remoteProvider.ts index e8687ba7155e8..1ae39896b3556 100644 --- a/src/git/remotes/remoteProvider.ts +++ b/src/git/remotes/remoteProvider.ts @@ -150,9 +150,8 @@ export abstract class RemoteProvider { + return showMessage('warn', `${message}. The tag could not be found.`, 'suppressTagNotFoundWarning'); +} + export async function showCreatePullRequestPrompt(branch: string): Promise { const create = { title: 'Create Pull Request...' }; const result = await showMessage( diff --git a/src/views/nodes/tagNode.ts b/src/views/nodes/tagNode.ts index 39d3c7be39a40..3efdbce21a273 100644 --- a/src/views/nodes/tagNode.ts +++ b/src/views/nodes/tagNode.ts @@ -26,6 +26,7 @@ export class TagNode extends ViewRefNode<'tag', ViewsWithTags, GitTagReference> view: ViewsWithTags, public override parent: ViewNode, public readonly tag: GitTag, + public readonly remoteUrl: string | undefined, ) { super('tag', uri, view, parent); @@ -81,7 +82,11 @@ export class TagNode extends ViewRefNode<'tag', ViewsWithTags, GitTagReference> getTreeItem(): TreeItem { const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed); item.id = this.id; - item.contextValue = ContextValues.Tag; + let contextValue: string = ContextValues.Tag; + if (this.remoteUrl) { + contextValue += '+remote'; + } + item.contextValue = contextValue; item.description = emojify(this.tag.message); item.tooltip = `${this.tag.name}${pad(GlyphChars.Dash, 2, 2)}${shortenRevision(this.tag.sha, { force: true, diff --git a/src/views/nodes/tagsNode.ts b/src/views/nodes/tagsNode.ts index dca0a908e1bbd..a1ce5a57fe7f5 100644 --- a/src/views/nodes/tagsNode.ts +++ b/src/views/nodes/tagsNode.ts @@ -1,5 +1,6 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GitUri } from '../../git/gitUri'; +import { RemoteResourceType } from '../../git/models/remoteResource'; import type { Repository } from '../../git/models/repository'; import { makeHierarchical } from '../../system/array'; import { debug } from '../../system/decorators/log'; @@ -36,10 +37,17 @@ export class TagsNode extends CacheableChildrenViewNode<'tags', ViewsWithTagsNod if (this.children == null) { const tags = await this.repo.git.tags().getTags({ sort: true }); if (tags.values.length === 0) return [new MessageNode(this.view, this, 'No tags could be found.')]; - + const remote = await this.repo.git.remotes().getBestRemoteWithProvider(); // TODO@eamodio handle paging const tagNodes = tags.values.map( - t => new TagNode(GitUri.fromRepoPath(this.uri.repoPath!, t.ref), this.view, this, t), + t => + new TagNode( + GitUri.fromRepoPath(this.uri.repoPath!, t.ref), + this.view, + this, + t, + remote?.provider?.url({ type: RemoteResourceType.Tag, tag: t.name }), + ), ); if (this.view.config.branches.layout === 'list') return tagNodes; diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 390371aa433c8..b26f0493fe029 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -270,6 +270,22 @@ export class ViewCommands implements Disposable { (n, nodes) => this.openCommitOnRemote(n, nodes), this, ), + registerViewCommand('gitlens.views.openTagOnRemote', (n, nodes) => this.openTagOnRemote(n, nodes), this), + registerViewCommand( + 'gitlens.views.openTagOnRemote.multi', + (n, nodes) => this.openTagOnRemote(n, nodes), + this, + ), + registerViewCommand( + 'gitlens.views.copyRemoteTagUrl', + (n, nodes) => this.openTagOnRemote(n, nodes, true), + this, + ), + registerViewCommand( + 'gitlens.views.copyRemoteTagUrl.multi', + (n, nodes) => this.openTagOnRemote(n, nodes, true), + this, + ), registerViewCommand('gitlens.views.openChanges', this.openChanges, this), registerViewCommand('gitlens.views.openChangesWithWorking', this.openChangesWithWorking, this), @@ -1441,6 +1457,17 @@ export class ViewCommands implements Disposable { }); } + @log() + private openTagOnRemote(node: ViewRefNode, nodes?: ViewRefNode[], clipboard?: boolean) { + const refs = nodes?.length ? nodes.map(n => n.ref) : [node.ref]; + + return executeCommand(GlCommand.OpenOnRemote, { + repoPath: refs[0].repoPath, + resource: refs.map(r => ({ type: RemoteResourceType.Tag, tag: r.name })), + clipboard: clipboard, + }); + } + @log() private openChanges(node: ViewRefFileNode | MergeConflictFileNode | StatusFileNode) { if (node.is('conflict-file')) {