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 const autoLinks = await integration . autolinks ( ) ;
@@ -194,8 +191,10 @@ export class Autolinks implements Disposable {
194191 }
195192 } ) ,
196193 ) ;
194+ }
197195
198- // Remote-specific autolinks and remote integration autolinks
196+ /** put remote-specific autolinks and remote integration autolinks to mutable refsets */
197+ private async collectRemoteAutolinks ( remote : GitRemote | undefined , refsets : RefSet [ ] ) {
199198 if ( remote ?. provider != null ) {
200199 const autoLinks = [ ] ;
201200 const integrationAutolinks = await ( await remote . getIntegration ( ) ) ?. autolinks ( ) ;
@@ -210,15 +209,136 @@ export class Autolinks implements Disposable {
210209 refsets . push ( [ remote . provider , autoLinks ] ) ;
211210 }
212211 }
212+ }
213213
214- // Custom-configured autolinks
215- if ( this . _references . length && ( remote ?. provider == null || ! options ?. excludeCustom ) ) {
214+ /** put custom-configured autolinks to mutable refsets */
215+ private collectCustomAutolinks ( remote : GitRemote | undefined , refsets : RefSet [ ] ) {
216+ if ( this . _references . length && remote ?. provider == null ) {
216217 refsets . push ( [ undefined , this . _references ] ) ;
217218 }
219+ }
220+
221+ /**
222+ * it should always return non-0 result that means a probability of the autolink `b` is more relevant of the autolink `a`
223+ */
224+ private static compareAutolinks ( a : ComparingAutolinkSet , b : ComparingAutolinkSet ) {
225+ // consider that if the number is in the start, it's the most relevant link
226+ if ( b . index === 0 ) {
227+ return 1 ;
228+ }
229+ if ( a . index === 0 ) {
230+ return - 1 ;
231+ }
232+ // maybe it worths to use some weight function instead.
233+ return (
234+ b . autolink . prefix . length - a . autolink . prefix . length ||
235+ - ( b . startIndex - a . startIndex ) ||
236+ - ( b . index - a . index )
237+ ) ;
238+ }
239+
240+ /**
241+ * returns sorted list of autolinks. the first is matched as the most relevant
242+ */
243+ async getBranchAutolinks (
244+ branchName : string ,
245+ remote ?: GitRemote ,
246+ options ?: { excludeCustom : boolean } ,
247+ ) : Promise < undefined | Autolink [ ] > {
248+ const refsets : RefSet [ ] = [ ] ;
249+ await this . collectIntegrationAutolinks ( refsets ) ;
250+ await this . collectRemoteAutolinks ( remote , refsets ) ;
251+ if ( ! options ?. excludeCustom ) {
252+ this . collectCustomAutolinks ( remote , refsets ) ;
253+ }
254+ if ( refsets . length === 0 ) return undefined ;
255+
256+ return Autolinks . _getBranchAutolinks ( branchName , refsets ) ;
257+ }
258+
259+ static _getBranchAutolinks ( branchName : string , refsets : Readonly < RefSet [ ] > ) {
260+ const autolinks = new Map < string , ComparingAutolinkSet > ( ) ;
261+
262+ let match ;
263+ let num ;
264+ for ( const [ provider , refs ] of refsets ) {
265+ for ( const ref of refs ) {
266+ if ( ! isCacheable ( ref ) ) {
267+ continue ;
268+ }
269+ if ( ref . type === 'pullrequest' || ( ref . referenceType && ref . referenceType !== 'branchName' ) ) {
270+ continue ;
271+ }
272+
273+ ensureCachedRegex ( ref , 'plaintext' ) ;
274+ const matches = branchName . matchAll ( ref . branchNameRegex ) ;
275+ do {
276+ match = matches . next ( ) ;
277+ if ( ! match . value ?. groups ) break ;
278+
279+ num = match ?. value ?. groups . issueKeyNumber ;
280+ let index = match . value . index ;
281+ const linkUrl = ref . url ?. replace ( numRegex , num ) ;
282+ // strange case (I would say synthetic), but if we parse the link twice, use the most relevant of them
283+ if ( autolinks . has ( linkUrl ) ) {
284+ index = Math . min ( index , autolinks . get ( linkUrl ) ! . index ) ;
285+ }
286+ autolinks . set ( linkUrl , {
287+ index : index ,
288+ // TODO: calc the distance from the nearest start-like symbol
289+ startIndex : 0 ,
290+ autolink : {
291+ ...ref ,
292+ provider : provider ,
293+ id : num ,
294+
295+ url : linkUrl ,
296+ title : ref . title ?. replace ( numRegex , num ) ,
297+ description : ref . description ?. replace ( numRegex , num ) ,
298+ descriptor : ref . descriptor ,
299+ } ,
300+ } ) ;
301+ } while ( ! match . done ) ;
302+ }
303+ }
304+
305+ return [ ...autolinks . values ( ) ]
306+ . flat ( )
307+ . sort ( this . compareAutolinks )
308+ . map ( x => x . autolink ) ;
309+ }
310+
311+ async getAutolinks ( message : string , remote ?: GitRemote ) : Promise < Map < string , Autolink > > ;
312+ async getAutolinks (
313+ message : string ,
314+ remote : GitRemote ,
315+ // eslint-disable-next-line @typescript-eslint/unified-signatures
316+ options ?: { excludeCustom ?: boolean } ,
317+ ) : Promise < Map < string , Autolink > > ;
318+ @debug < Autolinks [ 'getAutolinks' ] > ( {
319+ args : {
320+ 0 : '<message>' ,
321+ 1 : false ,
322+ } ,
323+ } )
324+ async getAutolinks (
325+ message : string ,
326+ remote ?: GitRemote ,
327+ options ?: { excludeCustom ?: boolean ; isBranchName ?: boolean } ,
328+ ) : Promise < Map < string , Autolink > > {
329+ const refsets : RefSet [ ] = [ ] ;
330+ await this . collectIntegrationAutolinks ( refsets ) ;
331+ await this . collectRemoteAutolinks ( remote , refsets ) ;
332+ if ( ! options ?. excludeCustom ) {
333+ this . collectCustomAutolinks ( remote , refsets ) ;
334+ }
218335 if ( refsets . length === 0 ) return emptyAutolinkMap ;
219336
220- const autolinks = new Map < string , Autolink > ( ) ;
337+ return Autolinks . _getAutolinks ( message , refsets ) ;
338+ }
221339
340+ static _getAutolinks ( message : string , refsets : Readonly < RefSet [ ] > ) {
341+ const autolinks = new Map < string , Autolink > ( ) ;
222342 let match ;
223343 let num ;
224344 for ( const [ provider , refs ] of refsets ) {
@@ -234,7 +354,7 @@ export class Autolinks implements Disposable {
234354
235355 do {
236356 match = ref . messageRegex . exec ( message ) ;
237- if ( match == null ) break ;
357+ if ( ! match ) break ;
238358
239359 [ , , , num ] = match ;
240360
@@ -623,7 +743,7 @@ function ensureCachedRegex(
623743function ensureCachedRegex (
624744 ref : CacheableAutolinkReference ,
625745 outputFormat : 'plaintext' ,
626- ) : asserts ref is RequireSome < CacheableAutolinkReference , 'messageRegex' > ;
746+ ) : asserts ref is RequireSome < CacheableAutolinkReference , 'messageRegex' | 'branchNameRegex' > ;
627747function ensureCachedRegex ( ref : CacheableAutolinkReference , outputFormat : 'html' | 'markdown' | 'plaintext' ) {
628748 // Regexes matches the ref prefix followed by a token (e.g. #1234)
629749 if ( outputFormat === 'markdown' && ref . messageMarkdownRegex == null ) {
@@ -644,6 +764,12 @@ function ensureCachedRegex(ref: CacheableAutolinkReference, outputFormat: 'html'
644764 `(^|\\s|\\(|\\[|\\{)(${ escapeRegex ( ref . prefix ) } (${ ref . alphanumeric ? '\\w' : '\\d' } +))\\b` ,
645765 ref . ignoreCase ? 'gi' : 'g' ,
646766 ) ;
767+ ref . branchNameRegex = new RegExp (
768+ `(^|\\-|_|\\.|\\/)(?<prefix>${ ref . prefix } )(?<issueKeyNumber>${
769+ ref . alphanumeric ? '\\w' : '\\d'
770+ } +)(?=$|\\-|_|\\.|\\/)`,
771+ 'gi' ,
772+ ) ;
647773 }
648774
649775 return true ;
0 commit comments