diff --git a/CHANGELOG.md b/CHANGELOG.md index a1f733514b2be..83316f96e528a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added - Adds an `Open in Commit Graph` action to the hovers and commit quick pick menus +- Adds support for configuring autolinks with regular expressions ### Changed @@ -23,6 +24,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Fixes Commit Details view showing incorrect actions for uncommitted changes - Fixes prioritization of multiple PRs associated with the same commit to choose a merged PR over others - Fixes Graph not showing account banners when access is not allowed and trial banners were previously dismissed +- Fixes [#2251] - Gerrit change-ids should be parsed in the whole commit message body ## [12.2.2] - 2022-09-06 diff --git a/package.json b/package.json index 9564531dbbe19..ae0219f8419ac 100644 --- a/package.json +++ b/package.json @@ -2383,15 +2383,45 @@ "default": null, "items": { "type": "object", - "required": [ - "prefix", - "url" + "oneOf": [ + { + "required": [ + "prefix", + "url" + ] + }, + { + "required": [ + "regex", + "url" + ], + "allOf": [ + { + "not": { + "required": [ + "alphanumeric" + ] + } + }, + { + "not": { + "required": [ + "ignoreCase" + ] + } + } + ] + } ], "properties": { "prefix": { "type": "string", "description": "Specifies the short prefix to use to generate autolinks for the external resource" }, + "regex": { + "type": "string", + "description": "Specifies the regular expression used to match autolinks for the external resource, where the first group matches the reference number" + }, "title": { "type": [ "string", @@ -2406,12 +2436,12 @@ }, "alphanumeric": { "type": "boolean", - "description": "Specifies whether alphanumeric characters should be allowed in ``", + "description": "When using a prefix, specifies whether alphanumeric characters should be allowed in ``", "default": false }, "ignoreCase": { "type": "boolean", - "description": "Specifies whether case should be ignored when matching the prefix", + "description": "When using a prefix, specifies whether case should be ignored when matching the prefix", "default": false } }, diff --git a/src/annotations/autolinks.ts b/src/annotations/autolinks.ts index 627b53508d123..0d50ef0e71109 100644 --- a/src/annotations/autolinks.ts +++ b/src/annotations/autolinks.ts @@ -23,7 +23,7 @@ const numRegex = //g; export interface Autolink { provider?: RemoteProviderReference; id: string; - prefix: string; + prefix?: string; title?: string; url: string; @@ -61,11 +61,15 @@ export interface DynamicAutolinkReference { } function isDynamic(ref: AutolinkReference | DynamicAutolinkReference): ref is DynamicAutolinkReference { - return !('prefix' in ref) && !('url' in ref); + return (!('prefix' in ref) || !('regex' in ref)) && !('url' in ref); } function isCacheable(ref: AutolinkReference | DynamicAutolinkReference): ref is CacheableAutolinkReference { - return 'prefix' in ref && ref.prefix != null && 'url' in ref && ref.url != null; + return ( + (('prefix' in ref && ref.prefix != null) || ('regex' in ref && ref.regex != null)) && + 'url' in ref && + ref.url != null + ); } export class Autolinks implements Disposable { @@ -88,12 +92,13 @@ export class Autolinks implements Disposable { // Since VS Code's configuration objects are live we need to copy them to avoid writing back to the configuration this._references = autolinks - ?.filter(a => a.prefix && a.url) + ?.filter(a => (a.prefix || a.regex) && a.url) /** * Only allow properties defined by {@link AutolinkReference} */ ?.map(a => ({ prefix: a.prefix, + regex: a.regex, url: a.url, title: a.title, alphanumeric: a.alphanumeric, @@ -268,7 +273,7 @@ export class Autolinks implements Disposable { ref: CacheableAutolinkReference | DynamicAutolinkReference, ): ref is CacheableAutolinkReference | DynamicAutolinkReference { if (isDynamic(ref)) return true; - if (!ref.prefix || !ref.url) return false; + if ((!ref.prefix && !ref.regex) || !ref.url) return false; if (ref.tokenize !== undefined || ref.tokenize === null) return true; try { @@ -422,22 +427,30 @@ function ensureCachedRegex(ref: CacheableAutolinkReference, outputFormat: 'html' // Regexes matches the ref prefix followed by a token (e.g. #1234) if (outputFormat === 'markdown' && ref.messageMarkdownRegex == null) { // Extra `\\\\` in `\\\\\\[` is because the markdown is escaped - ref.messageMarkdownRegex = new RegExp( - `(?<=^|\\s|\\(|\\\\\\[)(${escapeRegex(encodeHtmlWeak(escapeMarkdown(ref.prefix)))}(${ - ref.alphanumeric ? '\\w' : '\\d' - }+))\\b`, - ref.ignoreCase ? 'gi' : 'g', - ); + ref.messageMarkdownRegex = ref.regex + ? new RegExp(`((${ref.regex}))`, 'g') + : new RegExp( + `(?<=^|\\s|\\(|\\\\\\[)(${escapeRegex(encodeHtmlWeak(escapeMarkdown(ref.prefix!)))}(${ + ref.alphanumeric ? '\\w' : '\\d' + }+))\\b`, + ref.ignoreCase ? 'gi' : 'g', + ); } else if (outputFormat === 'html' && ref.messageHtmlRegex == null) { - ref.messageHtmlRegex = new RegExp( - `(?<=^|\\s|\\(|\\[)(${escapeRegex(encodeHtmlWeak(ref.prefix))}(${ref.alphanumeric ? '\\w' : '\\d'}+))\\b`, - ref.ignoreCase ? 'gi' : 'g', - ); + ref.messageHtmlRegex = ref.regex + ? new RegExp(`((${ref.regex}))`, 'g') + : new RegExp( + `(?<=^|\\s|\\(|\\[)(${escapeRegex(encodeHtmlWeak(ref.prefix!))}(${ + ref.alphanumeric ? '\\w' : '\\d' + }+))\\b`, + ref.ignoreCase ? 'gi' : 'g', + ); } else if (ref.messageRegex == null) { - ref.messageRegex = new RegExp( - `(?<=^|\\s|\\(|\\[)(${escapeRegex(ref.prefix)}(${ref.alphanumeric ? '\\w' : '\\d'}+))\\b`, - ref.ignoreCase ? 'gi' : 'g', - ); + ref.messageRegex = ref.regex + ? new RegExp(`((${ref.regex}))`, 'g') + : new RegExp( + `(?<=^|\\s|\\(|\\[)(${escapeRegex(ref.prefix!)}(${ref.alphanumeric ? '\\w' : '\\d'}+))\\b`, + ref.ignoreCase ? 'gi' : 'g', + ); } return true; diff --git a/src/config.ts b/src/config.ts index b331706dcb7b3..ab098c30390af 100644 --- a/src/config.ts +++ b/src/config.ts @@ -181,12 +181,15 @@ export const enum AutolinkType { } export interface AutolinkReference { - prefix: string; url: string; title?: string; + + prefix?: string; alphanumeric?: boolean; ignoreCase?: boolean; + regex?: string; + type?: AutolinkType; description?: string; } diff --git a/src/git/remotes/gerrit.ts b/src/git/remotes/gerrit.ts index e567cfdc9da6e..6e925fa8c322e 100644 --- a/src/git/remotes/gerrit.ts +++ b/src/git/remotes/gerrit.ts @@ -37,10 +37,9 @@ export class GerritRemote extends RemoteProvider { if (this._autolinks === undefined) { this._autolinks = [ { - prefix: 'Change-Id: ', + regex: '(I[a-z0-9]{40})', url: `${this.baseReviewUrl}/q/`, title: `Open Change # on ${this.name}`, - alphanumeric: true, description: `Change # on ${this.name}`, }, diff --git a/src/views/nodes/autolinkedItemNode.ts b/src/views/nodes/autolinkedItemNode.ts index 79ab33be1478c..5f6bddb11b8b3 100644 --- a/src/views/nodes/autolinkedItemNode.ts +++ b/src/views/nodes/autolinkedItemNode.ts @@ -33,7 +33,7 @@ export class AutolinkedItemNode extends ViewNode { if (!isIssueOrPullRequest(this.item)) { const { provider } = this.item; - const item = new TreeItem(`${this.item.prefix}${this.item.id}`, TreeItemCollapsibleState.None); + const item = new TreeItem(`${this.item.prefix ?? ''}${this.item.id}`, TreeItemCollapsibleState.None); item.description = provider?.name ?? 'Custom'; item.iconPath = new ThemeIcon( this.item.type == null @@ -53,7 +53,7 @@ export class AutolinkedItemNode extends ViewNode { : this.item.type === AutolinkType.PullRequest ? 'Autolinked Pull Request' : 'Autolinked Issue' - } ${this.item.prefix}${this.item.id}` + } ${this.item.prefix ?? ''}${this.item.id}` } \\\n[${this.item.url}](${this.item.url}${this.item.title != null ? ` "${this.item.title}"` : ''})`, ); return item;