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,6 +38,7 @@ 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}
@@ -105,6 +107,7 @@ export interface CacheableAutolinkReference extends AutolinkReference {
105107 messageHtmlRegex ?: RegExp ;
106108 messageMarkdownRegex ?: RegExp ;
107109 messageRegex ?: RegExp ;
110+ branchNameRegex ?: RegExp ;
108111}
109112
110113export interface DynamicAutolinkReference {
@@ -123,14 +126,27 @@ export interface DynamicAutolinkReference {
123126
124127export const supportedAutolinkIntegrations = [ IssueIntegrationId . Jira ] ;
125128
126- function isDynamic ( ref : AutolinkReference | DynamicAutolinkReference ) : ref is DynamicAutolinkReference {
129+ export function isDynamic ( ref : AutolinkReference | DynamicAutolinkReference ) : ref is DynamicAutolinkReference {
127130 return ! ( 'prefix' in ref ) && ! ( 'url' in ref ) ;
128131}
129132
130133function isCacheable ( ref : AutolinkReference | DynamicAutolinkReference ) : ref is CacheableAutolinkReference {
131134 return 'prefix' in ref && ref . prefix != null && 'url' in ref && ref . url != null ;
132135}
133136
137+ export type RefSet = [
138+ ProviderReference | undefined ,
139+ ( AutolinkReference | DynamicAutolinkReference ) [ ] | CacheableAutolinkReference [ ] ,
140+ ] ;
141+
142+ type ComparingAutolinkSet = {
143+ /** the place where the autolink is found from start-like symbol (/|_) */
144+ index : number ;
145+ /** the place where the autolink is found from start */
146+ startIndex : number ;
147+ autolink : Autolink ;
148+ } ;
149+
134150export class Autolinks implements Disposable {
135151 protected _disposable : Disposable | undefined ;
136152 private _references : CacheableAutolinkReference [ ] = [ ] ;
@@ -162,30 +178,11 @@ export class Autolinks implements Disposable {
162178 }
163179 }
164180
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 (
181+ /**
182+ * put connected integration autolinks to mutable refsets
183+ */
184+ private async collectIntegrationAutolinks ( refsets : RefSet [ ] ) {
185+ return Promise . allSettled (
189186 supportedAutolinkIntegrations . map ( async integrationId => {
190187 const integration = await this . container . integrations . get ( integrationId ) ;
191188 // Don't check for integration access, as we want to allow autolinks to always be generated
@@ -195,8 +192,10 @@ export class Autolinks implements Disposable {
195192 }
196193 } ) ,
197194 ) ;
195+ }
198196
199- // Remote-specific autolinks and remote integration autolinks
197+ /** put remote-specific autolinks and remote integration autolinks to mutable refsets */
198+ private async collectRemoteAutolinks ( remote : GitRemote | undefined , refsets : RefSet [ ] ) {
200199 if ( remote ?. provider != null ) {
201200 const autoLinks = [ ] ;
202201 // Don't check for integration access, as we want to allow autolinks to always be generated
@@ -212,15 +211,136 @@ export class Autolinks implements Disposable {
212211 refsets . push ( [ remote . provider , autoLinks ] ) ;
213212 }
214213 }
214+ }
215215
216- // Custom-configured autolinks
217- if ( this . _references . length && ( remote ?. provider == null || ! options ?. excludeCustom ) ) {
216+ /** put custom-configured autolinks to mutable refsets */
217+ private collectCustomAutolinks ( remote : GitRemote | undefined , refsets : RefSet [ ] ) {
218+ if ( this . _references . length && remote ?. provider == null ) {
218219 refsets . push ( [ undefined , this . _references ] ) ;
219220 }
221+ }
222+
223+ /**
224+ * it should always return non-0 result that means a probability of the autolink `b` is more relevant of the autolink `a`
225+ */
226+ private static compareAutolinks ( a : ComparingAutolinkSet , b : ComparingAutolinkSet ) {
227+ // consider that if the number is in the start, it's the most relevant link
228+ if ( b . index === 0 ) {
229+ return 1 ;
230+ }
231+ if ( a . index === 0 ) {
232+ return - 1 ;
233+ }
234+ // maybe it worths to use some weight function instead.
235+ return (
236+ b . autolink . prefix . length - a . autolink . prefix . length ||
237+ - ( b . startIndex - a . startIndex ) ||
238+ - ( b . index - a . index )
239+ ) ;
240+ }
241+
242+ /**
243+ * returns sorted list of autolinks. the first is matched as the most relevant
244+ */
245+ async getBranchAutolinks (
246+ branchName : string ,
247+ remote ?: GitRemote ,
248+ options ?: { excludeCustom : boolean } ,
249+ ) : Promise < undefined | Autolink [ ] > {
250+ const refsets : RefSet [ ] = [ ] ;
251+ await this . collectIntegrationAutolinks ( refsets ) ;
252+ await this . collectRemoteAutolinks ( remote , refsets ) ;
253+ if ( ! options ?. excludeCustom ) {
254+ this . collectCustomAutolinks ( remote , refsets ) ;
255+ }
256+ if ( refsets . length === 0 ) return undefined ;
257+
258+ return Autolinks . _getBranchAutolinks ( branchName , refsets ) ;
259+ }
260+
261+ static _getBranchAutolinks ( branchName : string , refsets : Readonly < RefSet [ ] > ) {
262+ const autolinks = new Map < string , ComparingAutolinkSet > ( ) ;
263+
264+ let match ;
265+ let num ;
266+ for ( const [ provider , refs ] of refsets ) {
267+ for ( const ref of refs ) {
268+ if ( ! isCacheable ( ref ) ) {
269+ continue ;
270+ }
271+ if ( ref . type === 'pullrequest' || ( ref . referenceType && ref . referenceType !== 'branchName' ) ) {
272+ continue ;
273+ }
274+
275+ ensureCachedRegex ( ref , 'plaintext' ) ;
276+ const matches = branchName . matchAll ( ref . branchNameRegex ) ;
277+ do {
278+ match = matches . next ( ) ;
279+ if ( ! match . value ?. groups ) break ;
280+
281+ num = match ?. value ?. groups . issueKeyNumber ;
282+ let index = match . value . index ;
283+ const linkUrl = ref . url ?. replace ( numRegex , num ) ;
284+ // strange case (I would say synthetic), but if we parse the link twice, use the most relevant of them
285+ if ( autolinks . has ( linkUrl ) ) {
286+ index = Math . min ( index , autolinks . get ( linkUrl ) ! . index ) ;
287+ }
288+ autolinks . set ( linkUrl , {
289+ index : index ,
290+ // TODO: calc the distance from the nearest start-like symbol
291+ startIndex : 0 ,
292+ autolink : {
293+ ...ref ,
294+ provider : provider ,
295+ id : num ,
296+
297+ url : linkUrl ,
298+ title : ref . title ?. replace ( numRegex , num ) ,
299+ description : ref . description ?. replace ( numRegex , num ) ,
300+ descriptor : ref . descriptor ,
301+ } ,
302+ } ) ;
303+ } while ( ! match . done ) ;
304+ }
305+ }
306+
307+ return [ ...autolinks . values ( ) ]
308+ . flat ( )
309+ . sort ( this . compareAutolinks )
310+ . map ( x => x . autolink ) ;
311+ }
312+
313+ async getAutolinks ( message : string , remote ?: GitRemote ) : Promise < Map < string , Autolink > > ;
314+ async getAutolinks (
315+ message : string ,
316+ remote : GitRemote ,
317+ // eslint-disable-next-line @typescript-eslint/unified-signatures
318+ options ?: { excludeCustom ?: boolean } ,
319+ ) : Promise < Map < string , Autolink > > ;
320+ @debug < Autolinks [ 'getAutolinks' ] > ( {
321+ args : {
322+ 0 : '<message>' ,
323+ 1 : false ,
324+ } ,
325+ } )
326+ async getAutolinks (
327+ message : string ,
328+ remote ?: GitRemote ,
329+ options ?: { excludeCustom ?: boolean ; isBranchName ?: boolean } ,
330+ ) : Promise < Map < string , Autolink > > {
331+ const refsets : RefSet [ ] = [ ] ;
332+ await this . collectIntegrationAutolinks ( refsets ) ;
333+ await this . collectRemoteAutolinks ( remote , refsets ) ;
334+ if ( ! options ?. excludeCustom ) {
335+ this . collectCustomAutolinks ( remote , refsets ) ;
336+ }
220337 if ( refsets . length === 0 ) return emptyAutolinkMap ;
221338
222- const autolinks = new Map < string , Autolink > ( ) ;
339+ return Autolinks . _getAutolinks ( message , refsets ) ;
340+ }
223341
342+ static _getAutolinks ( message : string , refsets : Readonly < RefSet [ ] > ) {
343+ const autolinks = new Map < string , Autolink > ( ) ;
224344 let match ;
225345 let num ;
226346 for ( const [ provider , refs ] of refsets ) {
@@ -236,7 +356,7 @@ export class Autolinks implements Disposable {
236356
237357 do {
238358 match = ref . messageRegex . exec ( message ) ;
239- if ( match == null ) break ;
359+ if ( ! match ) break ;
240360
241361 [ , , , num ] = match ;
242362
@@ -625,7 +745,7 @@ function ensureCachedRegex(
625745function ensureCachedRegex (
626746 ref : CacheableAutolinkReference ,
627747 outputFormat : 'plaintext' ,
628- ) : asserts ref is RequireSome < CacheableAutolinkReference , 'messageRegex' > ;
748+ ) : asserts ref is RequireSome < CacheableAutolinkReference , 'messageRegex' | 'branchNameRegex' > ;
629749function ensureCachedRegex ( ref : CacheableAutolinkReference , outputFormat : 'html' | 'markdown' | 'plaintext' ) {
630750 // Regexes matches the ref prefix followed by a token (e.g. #1234)
631751 if ( outputFormat === 'markdown' && ref . messageMarkdownRegex == null ) {
@@ -646,6 +766,12 @@ function ensureCachedRegex(ref: CacheableAutolinkReference, outputFormat: 'html'
646766 `(^|\\s|\\(|\\[|\\{)(${ escapeRegex ( ref . prefix ) } (${ ref . alphanumeric ? '\\w' : '\\d' } +))\\b` ,
647767 ref . ignoreCase ? 'gi' : 'g' ,
648768 ) ;
769+ ref . branchNameRegex = new RegExp (
770+ `(^|\\-|_|\\.|\\/)(?<prefix>${ ref . prefix } )(?<issueKeyNumber>${
771+ ref . alphanumeric ? '\\w' : '\\d'
772+ } +)(?=$|\\-|_|\\.|\\/)`,
773+ 'gi' ,
774+ ) ;
649775 }
650776
651777 return true ;
0 commit comments