11import type { ConfigurationChangeEvent } from 'vscode' ;
22import { Disposable } from 'vscode' ;
3- import { GlyphChars } from './constants' ;
4- import type { IntegrationId } from './constants.integrations' ;
5- import { IssueIntegrationId } from './constants.integrations' ;
6- import type { Container } from './container' ;
7- import type { IssueOrPullRequest } from './git/models/issue' ;
8- import { getIssueOrPullRequestHtmlIcon , getIssueOrPullRequestMarkdownIcon } from './git/models/issue' ;
9- import type { GitRemote } from './git/models/remote' ;
10- import type { ProviderReference } from './git/models/remoteProvider' ;
11- import type { ResourceDescriptor } from './plus/integrations/integration' ;
12- import { fromNow } from './system/date' ;
13- import { debug } from './system/decorators/log' ;
14- import { encodeUrl } from './system/encoding' ;
15- import { join , map } from './system/iterable' ;
16- import { Logger } from './system/logger' ;
17- import { escapeMarkdown } from './system/markdown' ;
18- import type { MaybePausedResult } from './system/promise' ;
19- import { capitalize , encodeHtmlWeak , escapeRegex , getSuperscript } from './system/string' ;
20- import { configuration } from './system/vscode/configuration' ;
3+ import { GlyphChars } from '.. /constants' ;
4+ import type { IntegrationId } from '.. /constants.integrations' ;
5+ import { IssueIntegrationId } from '.. /constants.integrations' ;
6+ import type { Container } from '.. /container' ;
7+ import type { IssueOrPullRequest } from '.. /git/models/issue' ;
8+ import { getIssueOrPullRequestHtmlIcon , getIssueOrPullRequestMarkdownIcon } from '.. /git/models/issue' ;
9+ import type { GitRemote } from '.. /git/models/remote' ;
10+ import type { ProviderReference } from '.. /git/models/remoteProvider' ;
11+ import type { ResourceDescriptor } from '.. /plus/integrations/integration' ;
12+ import { fromNow } from '.. /system/date' ;
13+ import { debug } from '.. /system/decorators/log' ;
14+ import { encodeUrl } from '.. /system/encoding' ;
15+ import { join , map } from '.. /system/iterable' ;
16+ import { Logger } from '.. /system/logger' ;
17+ import { escapeMarkdown } from '.. /system/markdown' ;
18+ import type { MaybePausedResult } from '.. /system/promise' ;
19+ import { capitalize , encodeHtmlWeak , escapeRegex , getSuperscript } from '.. /system/string' ;
20+ import { configuration } from '.. /system/vscode/configuration' ;
2121
2222const emptyAutolinkMap = Object . freeze ( new Map < string , Autolink > ( ) ) ;
2323
2424const numRegex = / < n u m > / g;
2525
2626export type AutolinkType = 'issue' | 'pullrequest' ;
27+ export type AutolinkReferenceType = 'commitMessage' | 'branchName' ;
2728
2829export interface AutolinkReference {
2930 /** Short prefix to match to generate autolinks for the external resource */
@@ -37,13 +38,15 @@ export interface AutolinkReference {
3738 readonly title : string | undefined ;
3839
3940 readonly type ?: AutolinkType ;
41+ readonly referenceType ?: AutolinkReferenceType ;
4042 readonly description ?: string ;
4143 readonly descriptor ?: ResourceDescriptor ;
4244}
4345
4446export interface Autolink extends AutolinkReference {
4547 provider ?: ProviderReference ;
4648 id : string ;
49+ index ?: number ;
4750
4851 tokenize ?:
4952 | ( (
@@ -78,6 +81,7 @@ export function serializeAutolink(value: Autolink): Autolink {
7881 }
7982 : undefined ,
8083 id : value . id ,
84+ index : value . index ,
8185 prefix : value . prefix ,
8286 url : value . url ,
8387 alphanumeric : value . alphanumeric ,
@@ -105,6 +109,7 @@ export interface CacheableAutolinkReference extends AutolinkReference {
105109 messageHtmlRegex ?: RegExp ;
106110 messageMarkdownRegex ?: RegExp ;
107111 messageRegex ?: RegExp ;
112+ branchNameRegex ?: RegExp ;
108113}
109114
110115export interface DynamicAutolinkReference {
@@ -131,6 +136,11 @@ function isCacheable(ref: AutolinkReference | DynamicAutolinkReference): ref is
131136 return 'prefix' in ref && ref . prefix != null && 'url' in ref && ref . url != null ;
132137}
133138
139+ export type RefSet = [
140+ ProviderReference | undefined ,
141+ ( AutolinkReference | DynamicAutolinkReference ) [ ] | CacheableAutolinkReference [ ] ,
142+ ] ;
143+
134144export class Autolinks implements Disposable {
135145 protected _disposable : Disposable | undefined ;
136146 private _references : CacheableAutolinkReference [ ] = [ ] ;
@@ -162,30 +172,11 @@ export class Autolinks implements Disposable {
162172 }
163173 }
164174
165- async getAutolinks ( message : string , remote ?: GitRemote ) : Promise < Map < string , Autolink > > ;
166- async getAutolinks (
167- message : string ,
168- remote : GitRemote ,
169- // eslint-disable-next-line @typescript-eslint/unified-signatures
170- options ?: { excludeCustom ?: boolean } ,
171- ) : Promise < Map < string , Autolink > > ;
172- @debug < Autolinks [ 'getAutolinks' ] > ( {
173- args : {
174- 0 : '<message>' ,
175- 1 : false ,
176- } ,
177- } )
178- async getAutolinks (
179- message : string ,
180- remote ?: GitRemote ,
181- options ?: { excludeCustom ?: boolean } ,
182- ) : Promise < Map < string , Autolink > > {
183- const refsets : [
184- ProviderReference | undefined ,
185- ( AutolinkReference | DynamicAutolinkReference ) [ ] | CacheableAutolinkReference [ ] ,
186- ] [ ] = [ ] ;
187- // Connected integration autolinks
188- await Promise . allSettled (
175+ /**
176+ * put connected integration autolinks to mutable refsets
177+ */
178+ private async collectIntegrationAutolinks ( refsets : RefSet [ ] ) {
179+ return Promise . allSettled (
189180 supportedAutolinkIntegrations . map ( async integrationId => {
190181 const integration = await this . container . integrations . get ( integrationId ) ;
191182 // Don't check for integration access, as we want to allow autolinks to always be generated
@@ -195,8 +186,10 @@ export class Autolinks implements Disposable {
195186 }
196187 } ) ,
197188 ) ;
189+ }
198190
199- // Remote-specific autolinks and remote integration autolinks
191+ /** put remote-specific autolinks and remote integration autolinks to mutable refsets */
192+ private async collectRemoteAutolinks ( remote : GitRemote | undefined , refsets : RefSet [ ] ) {
200193 if ( remote ?. provider != null ) {
201194 const autoLinks = [ ] ;
202195 // Don't check for integration access, as we want to allow autolinks to always be generated
@@ -212,20 +205,136 @@ export class Autolinks implements Disposable {
212205 refsets . push ( [ remote . provider , autoLinks ] ) ;
213206 }
214207 }
208+ }
215209
216- // Custom-configured autolinks
217- if ( this . _references . length && ( remote ?. provider == null || ! options ?. excludeCustom ) ) {
210+ /** put custom-configured autolinks to mutable refsets */
211+ private collectCustomAutolinks ( remote : GitRemote | undefined , refsets : RefSet [ ] ) {
212+ if ( this . _references . length && remote ?. provider == null ) {
218213 refsets . push ( [ undefined , this . _references ] ) ;
219214 }
215+ }
216+
217+ /**
218+ * it should always return non-0 result that means a probability of the autolink `b` is more relevant of the autolink `a`
219+ */
220+ private static compareAutolinks ( a : Autolink , b : Autolink ) {
221+ // consider that if the number is in the start, it's the most relevant link
222+ if ( b . index === 0 ) {
223+ return 1 ;
224+ }
225+ if ( a . index === 0 ) {
226+ return - 1 ;
227+ }
228+
229+ // maybe it worths to use some weight function instead.
230+ return (
231+ b . prefix . length - a . prefix . length ||
232+ b . id . length - a . id . length ||
233+ ( b . index != null && a . index != null ? - ( b . index - a . index ) : 0 )
234+ ) ;
235+ }
236+
237+ private async getRefsets ( remote ?: GitRemote , options ?: { excludeCustom ?: boolean } ) {
238+ const refsets : RefSet [ ] = [ ] ;
239+ await this . collectIntegrationAutolinks ( refsets ) ;
240+ await this . collectRemoteAutolinks ( remote , refsets ) ;
241+ if ( ! options ?. excludeCustom ) {
242+ this . collectCustomAutolinks ( remote , refsets ) ;
243+ }
244+ return refsets ;
245+ }
246+
247+ /**
248+ * returns sorted list of autolinks. the first is matched as the most relevant
249+ */
250+ async getBranchAutolinks (
251+ branchName : string ,
252+ remote ?: GitRemote ,
253+ options ?: { excludeCustom ?: boolean } ,
254+ ) : Promise < Map < string , Autolink > > {
255+ const refsets = await this . getRefsets ( remote , options ) ;
220256 if ( refsets . length === 0 ) return emptyAutolinkMap ;
221257
258+ return Autolinks . _getBranchAutolinks ( branchName , refsets ) ;
259+ }
260+
261+ static _getBranchAutolinks ( branchName : string , refsets : Readonly < RefSet [ ] > ) {
222262 const autolinks = new Map < string , Autolink > ( ) ;
223263
224264 let match ;
225265 let num ;
226266 for ( const [ provider , refs ] of refsets ) {
227267 for ( const ref of refs ) {
228- if ( ! isCacheable ( ref ) ) {
268+ if (
269+ ! isCacheable ( ref ) ||
270+ ref . type === 'pullrequest' ||
271+ ( ref . referenceType && ref . referenceType !== 'branchName' )
272+ ) {
273+ continue ;
274+ }
275+
276+ ensureCachedRegex ( ref , 'plaintext' ) ;
277+ const matches = branchName . matchAll ( ref . branchNameRegex ) ;
278+ do {
279+ match = matches . next ( ) ;
280+ if ( ! match . value ?. groups ) break ;
281+
282+ num = match ?. value ?. groups . issueKeyNumber ;
283+ let index = match . value . index ;
284+ const linkUrl = ref . url ?. replace ( numRegex , num ) ;
285+ // strange case (I would say synthetic), but if we parse the link twice, use the most relevant of them
286+ const existingIndex = autolinks . get ( linkUrl ) ?. index ;
287+ if ( existingIndex != null ) {
288+ index = Math . min ( index , existingIndex ) ;
289+ }
290+ autolinks . set ( linkUrl , {
291+ ...ref ,
292+ provider : provider ,
293+ id : num ,
294+ index : index ,
295+ url : linkUrl ,
296+ title : ref . title ?. replace ( numRegex , num ) ,
297+ description : ref . description ?. replace ( numRegex , num ) ,
298+ descriptor : ref . descriptor ,
299+ } ) ;
300+ } while ( ! match . done ) ;
301+ }
302+ }
303+
304+ return new Map ( [ ...autolinks . entries ( ) ] . sort ( ( a , b ) => this . compareAutolinks ( a [ 1 ] , b [ 1 ] ) ) ) ;
305+ }
306+
307+ async getAutolinks ( message : string , remote ?: GitRemote ) : Promise < Map < string , Autolink > > ;
308+ async getAutolinks (
309+ message : string ,
310+ remote : GitRemote ,
311+ // eslint-disable-next-line @typescript-eslint/unified-signatures
312+ options ?: { excludeCustom ?: boolean } ,
313+ ) : Promise < Map < string , Autolink > > ;
314+ @debug < Autolinks [ 'getAutolinks' ] > ( {
315+ args : {
316+ 0 : '<message>' ,
317+ 1 : false ,
318+ } ,
319+ } )
320+ async getAutolinks (
321+ message : string ,
322+ remote ?: GitRemote ,
323+ options ?: { excludeCustom ?: boolean } ,
324+ ) : Promise < Map < string , Autolink > > {
325+ const refsets = await this . getRefsets ( remote , options ) ;
326+ if ( refsets . length === 0 ) return emptyAutolinkMap ;
327+
328+ return Autolinks . _getAutolinks ( message , refsets ) ;
329+ }
330+
331+ static _getAutolinks ( message : string , refsets : Readonly < RefSet [ ] > ) {
332+ const autolinks = new Map < string , Autolink > ( ) ;
333+ let match ;
334+ let num ;
335+ for ( const [ provider , refs ] of refsets ) {
336+ for ( const ref of refs ) {
337+ if ( ! isCacheable ( ref ) || ( ref . referenceType && ref . referenceType !== 'commitMessage' ) ) {
229338 if ( isDynamic ( ref ) ) {
230339 ref . parse ( message , autolinks ) ;
231340 }
@@ -236,13 +345,14 @@ export class Autolinks implements Disposable {
236345
237346 do {
238347 match = ref . messageRegex . exec ( message ) ;
239- if ( match == null ) break ;
348+ if ( ! match ) break ;
240349
241350 [ , , , num ] = match ;
242351
243352 autolinks . set ( num , {
244353 provider : provider ,
245354 id : num ,
355+ index : match . index ,
246356 prefix : ref . prefix ,
247357 url : ref . url ?. replace ( numRegex , num ) ,
248358 alphanumeric : ref . alphanumeric ,
@@ -625,7 +735,7 @@ function ensureCachedRegex(
625735function ensureCachedRegex (
626736 ref : CacheableAutolinkReference ,
627737 outputFormat : 'plaintext' ,
628- ) : asserts ref is RequireSome < CacheableAutolinkReference , 'messageRegex' > ;
738+ ) : asserts ref is RequireSome < CacheableAutolinkReference , 'messageRegex' | 'branchNameRegex' > ;
629739function ensureCachedRegex ( ref : CacheableAutolinkReference , outputFormat : 'html' | 'markdown' | 'plaintext' ) {
630740 // Regexes matches the ref prefix followed by a token (e.g. #1234)
631741 if ( outputFormat === 'markdown' && ref . messageMarkdownRegex == null ) {
@@ -646,6 +756,12 @@ function ensureCachedRegex(ref: CacheableAutolinkReference, outputFormat: 'html'
646756 `(^|\\s|\\(|\\[|\\{)(${ escapeRegex ( ref . prefix ) } (${ ref . alphanumeric ? '\\w' : '\\d' } +))\\b` ,
647757 ref . ignoreCase ? 'gi' : 'g' ,
648758 ) ;
759+ ref . branchNameRegex = new RegExp (
760+ `(^|\\-|_|\\.|\\/)(?<prefix>${ ref . prefix } )(?<issueKeyNumber>${
761+ ref . alphanumeric ? '\\w' : '\\d'
762+ } +)(?=$|\\-|_|\\.|\\/)`,
763+ 'gi' ,
764+ ) ;
649765 }
650766
651767 return true ;
0 commit comments