1
1
import type { ConfigurationChangeEvent } from 'vscode' ;
2
2
import { 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' ;
21
21
22
22
const emptyAutolinkMap = Object . freeze ( new Map < string , Autolink > ( ) ) ;
23
23
24
24
const numRegex = / < n u m > / g;
25
25
26
26
export type AutolinkType = 'issue' | 'pullrequest' ;
27
+ export type AutolinkReferenceType = 'commitMessage' | 'branchName' ;
27
28
28
29
export interface AutolinkReference {
29
30
/** Short prefix to match to generate autolinks for the external resource */
@@ -37,13 +38,15 @@ export interface AutolinkReference {
37
38
readonly title : string | undefined ;
38
39
39
40
readonly type ?: AutolinkType ;
41
+ readonly referenceType ?: AutolinkReferenceType ;
40
42
readonly description ?: string ;
41
43
readonly descriptor ?: ResourceDescriptor ;
42
44
}
43
45
44
46
export interface Autolink extends AutolinkReference {
45
47
provider ?: ProviderReference ;
46
48
id : string ;
49
+ index ?: number ;
47
50
48
51
tokenize ?:
49
52
| ( (
@@ -78,6 +81,7 @@ export function serializeAutolink(value: Autolink): Autolink {
78
81
}
79
82
: undefined ,
80
83
id : value . id ,
84
+ index : value . index ,
81
85
prefix : value . prefix ,
82
86
url : value . url ,
83
87
alphanumeric : value . alphanumeric ,
@@ -105,6 +109,7 @@ export interface CacheableAutolinkReference extends AutolinkReference {
105
109
messageHtmlRegex ?: RegExp ;
106
110
messageMarkdownRegex ?: RegExp ;
107
111
messageRegex ?: RegExp ;
112
+ branchNameRegex ?: RegExp ;
108
113
}
109
114
110
115
export interface DynamicAutolinkReference {
@@ -131,6 +136,11 @@ function isCacheable(ref: AutolinkReference | DynamicAutolinkReference): ref is
131
136
return 'prefix' in ref && ref . prefix != null && 'url' in ref && ref . url != null ;
132
137
}
133
138
139
+ export type RefSet = [
140
+ ProviderReference | undefined ,
141
+ ( AutolinkReference | DynamicAutolinkReference ) [ ] | CacheableAutolinkReference [ ] ,
142
+ ] ;
143
+
134
144
export class Autolinks implements Disposable {
135
145
protected _disposable : Disposable | undefined ;
136
146
private _references : CacheableAutolinkReference [ ] = [ ] ;
@@ -162,30 +172,11 @@ export class Autolinks implements Disposable {
162
172
}
163
173
}
164
174
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 (
189
180
supportedAutolinkIntegrations . map ( async integrationId => {
190
181
const integration = await this . container . integrations . get ( integrationId ) ;
191
182
// 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 {
195
186
}
196
187
} ) ,
197
188
) ;
189
+ }
198
190
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 [ ] ) {
200
193
if ( remote ?. provider != null ) {
201
194
const autoLinks = [ ] ;
202
195
// 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 {
212
205
refsets . push ( [ remote . provider , autoLinks ] ) ;
213
206
}
214
207
}
208
+ }
215
209
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 ) {
218
213
refsets . push ( [ undefined , this . _references ] ) ;
219
214
}
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 ) ;
220
256
if ( refsets . length === 0 ) return emptyAutolinkMap ;
221
257
258
+ return Autolinks . _getBranchAutolinks ( branchName , refsets ) ;
259
+ }
260
+
261
+ static _getBranchAutolinks ( branchName : string , refsets : Readonly < RefSet [ ] > ) {
222
262
const autolinks = new Map < string , Autolink > ( ) ;
223
263
224
264
let match ;
225
265
let num ;
226
266
for ( const [ provider , refs ] of refsets ) {
227
267
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' ) ) {
229
338
if ( isDynamic ( ref ) ) {
230
339
ref . parse ( message , autolinks ) ;
231
340
}
@@ -236,13 +345,14 @@ export class Autolinks implements Disposable {
236
345
237
346
do {
238
347
match = ref . messageRegex . exec ( message ) ;
239
- if ( match == null ) break ;
348
+ if ( ! match ) break ;
240
349
241
350
[ , , , num ] = match ;
242
351
243
352
autolinks . set ( num , {
244
353
provider : provider ,
245
354
id : num ,
355
+ index : match . index ,
246
356
prefix : ref . prefix ,
247
357
url : ref . url ?. replace ( numRegex , num ) ,
248
358
alphanumeric : ref . alphanumeric ,
@@ -625,7 +735,7 @@ function ensureCachedRegex(
625
735
function ensureCachedRegex (
626
736
ref : CacheableAutolinkReference ,
627
737
outputFormat : 'plaintext' ,
628
- ) : asserts ref is RequireSome < CacheableAutolinkReference , 'messageRegex' > ;
738
+ ) : asserts ref is RequireSome < CacheableAutolinkReference , 'messageRegex' | 'branchNameRegex' > ;
629
739
function ensureCachedRegex ( ref : CacheableAutolinkReference , outputFormat : 'html' | 'markdown' | 'plaintext' ) {
630
740
// Regexes matches the ref prefix followed by a token (e.g. #1234)
631
741
if ( outputFormat === 'markdown' && ref . messageMarkdownRegex == null ) {
@@ -646,6 +756,12 @@ function ensureCachedRegex(ref: CacheableAutolinkReference, outputFormat: 'html'
646
756
`(^|\\s|\\(|\\[|\\{)(${ escapeRegex ( ref . prefix ) } (${ ref . alphanumeric ? '\\w' : '\\d' } +))\\b` ,
647
757
ref . ignoreCase ? 'gi' : 'g' ,
648
758
) ;
759
+ ref . branchNameRegex = new RegExp (
760
+ `(^|\\-|_|\\.|\\/)(?<prefix>${ ref . prefix } )(?<issueKeyNumber>${
761
+ ref . alphanumeric ? '\\w' : '\\d'
762
+ } +)(?=$|\\-|_|\\.|\\/)`,
763
+ 'gi' ,
764
+ ) ;
649
765
}
650
766
651
767
return true ;
0 commit comments