From 6daf1778c37f00432b8f2c79b8dd5beb9d77f7bf Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Fri, 20 Dec 2024 12:06:58 +0700 Subject: [PATCH 1/3] Improves branch autolinks experiments --- src/autolinks/__tests__/autolinks.test.ts | 66 ++++++++++++---- src/autolinks/autolinks.utils.ts | 94 ++++++++++++++++++++--- 2 files changed, 134 insertions(+), 26 deletions(-) diff --git a/src/autolinks/__tests__/autolinks.test.ts b/src/autolinks/__tests__/autolinks.test.ts index 0132db8acc583..f58f89b88d7c8 100644 --- a/src/autolinks/__tests__/autolinks.test.ts +++ b/src/autolinks/__tests__/autolinks.test.ts @@ -13,7 +13,7 @@ const mockRefSets = (prefixes: string[] = ['']): RefSet[] => ignoreCase: false, prefix: prefix, title: 'test', - url: 'test/', + url: '', description: 'test', }, ], @@ -25,27 +25,65 @@ function assertAutolinks(actual: Map, expected: Array) suite('Autolinks Test Suite', () => { test('Branch name autolinks', () => { - assertAutolinks(getBranchAutolinks('123', mockRefSets()), ['test/123']); - assertAutolinks(getBranchAutolinks('feature/123', mockRefSets()), ['test/123']); - assertAutolinks(getBranchAutolinks('feature/PRE-123', mockRefSets()), ['test/123']); - assertAutolinks(getBranchAutolinks('123.2', mockRefSets()), ['test/123', 'test/2']); + assertAutolinks(getBranchAutolinks('123', mockRefSets()), ['123']); + assertAutolinks(getBranchAutolinks('feature/123', mockRefSets()), ['123']); + assertAutolinks(getBranchAutolinks('feature/PRE-123', mockRefSets()), ['123']); + assertAutolinks(getBranchAutolinks('123.2', mockRefSets()), ['123']); assertAutolinks(getBranchAutolinks('123', mockRefSets(['PRE-'])), []); assertAutolinks(getBranchAutolinks('feature/123', mockRefSets(['PRE-'])), []); - assertAutolinks(getBranchAutolinks('feature/2-fa/123', mockRefSets([''])), ['test/123', 'test/2']); - assertAutolinks(getBranchAutolinks('feature/2-fa/123', mockRefSets([''])), ['test/123', 'test/2']); - // incorrectly solved cat worths to compare the blocks length so that the less block size (without possible link) is more likely a link - assertAutolinks(getBranchAutolinks('feature/2-fa/3', mockRefSets([''])), ['test/2', 'test/3']); - assertAutolinks(getBranchAutolinks('feature/PRE-123', mockRefSets(['PRE-'])), ['test/123']); - assertAutolinks(getBranchAutolinks('feature/PRE-123.2', mockRefSets(['PRE-'])), ['test/123']); - assertAutolinks(getBranchAutolinks('feature/3-123-PRE-123', mockRefSets(['PRE-'])), ['test/123']); + assertAutolinks(getBranchAutolinks('feature/2-fa/123', mockRefSets([''])), ['123', '2']); + assertAutolinks(getBranchAutolinks('feature/2-fa/123', mockRefSets([''])), ['123', '2']); + assertAutolinks(getBranchAutolinks('feature/2-fa/3', mockRefSets([''])), ['3', '2']); + assertAutolinks(getBranchAutolinks('feature/PRE-123', mockRefSets(['PRE-'])), ['123']); + assertAutolinks(getBranchAutolinks('feature/PRE-123.2', mockRefSets(['PRE-'])), ['123']); + assertAutolinks(getBranchAutolinks('feature/3-123-PRE-123', mockRefSets(['PRE-'])), ['123']); assertAutolinks( getBranchAutolinks('feature/3-123-PRE-123', mockRefSets(['', 'PRE-'])), - ['test/123', 'test/3'], + ['123', '3'], ); }); test('Commit message autolinks', () => { - assertAutolinks(getAutolinks('test message 123 sd', mockRefSets()), ['test/123']); + assertAutolinks(getAutolinks('test message 123 sd', mockRefSets()), ['123']); + }); + + /** + * 16.1.1^ - improved branch name autolinks matching + */ + test('Improved branch name autolinks matching', () => { + // skip branch names chunks matching '^release(?=(-(?))$` or other release-like values + // skip pair in case of double chunk + assertAutolinks(getBranchAutolinks('folder/release/16/issue-1', mockRefSets([''])), ['1']); + assertAutolinks(getBranchAutolinks('folder/release/16.1/issue-1', mockRefSets([''])), ['1']); + assertAutolinks(getBranchAutolinks('folder/release/16.1.1/1', mockRefSets([''])), ['1']); + // skip one in case of single chunk + assertAutolinks(getBranchAutolinks('folder/release-16/1', mockRefSets([''])), ['1']); + assertAutolinks(getBranchAutolinks('folder/release-16.1/1', mockRefSets([''])), ['1']); + assertAutolinks(getBranchAutolinks('folder/release-16.1.2/1', mockRefSets([''])), ['1']); + + /** + * Added chunk matching logic for non-prefixed numbers: + * - XX - is more likely issue number + * - XX.XX - is less likely issue number, but still possible + * - XX.XX.XX - is more likely not issue number + */ + assertAutolinks(getBranchAutolinks('some-issue-in-release-2024', mockRefSets([''])), ['2024']); + assertAutolinks(getBranchAutolinks('some-issue-in-release-2024.1', mockRefSets([''])), ['2024']); + assertAutolinks(getBranchAutolinks('some-issue-in-release-2024.1.1', mockRefSets([''])), []); + + assertAutolinks(getBranchAutolinks('folder/release-notes-16-1', mockRefSets([''])), ['16']); + assertAutolinks(getBranchAutolinks('folder/16-1-release-notes', mockRefSets([''])), ['16']); + + // considered the distance from the edges of the chunk as a priority sign + assertAutolinks(getBranchAutolinks('folder/16-content-1-content', mockRefSets([''])), ['16', '1']); + assertAutolinks(getBranchAutolinks('folder/content-1-content-16', mockRefSets([''])), ['16', '1']); + + // the chunk that is more close to the end is more likely actual issue number + assertAutolinks(getBranchAutolinks('1-epic-folder/10-issue/100-subissue', mockRefSets([''])), [ + '100', + '10', + '1', + ]); }); }); diff --git a/src/autolinks/autolinks.utils.ts b/src/autolinks/autolinks.utils.ts index 71dc07d0003e5..f75941e2228b5 100644 --- a/src/autolinks/autolinks.utils.ts +++ b/src/autolinks/autolinks.utils.ts @@ -2,6 +2,7 @@ import { IssueIntegrationId } from '../constants.integrations'; import type { IssueOrPullRequest } from '../git/models/issue'; import type { ProviderReference } from '../git/models/remoteProvider'; import type { ResourceDescriptor } from '../plus/integrations/integration'; +import { flatMap } from '../system/iterable'; import { escapeMarkdown } from '../system/markdown'; import type { MaybePausedResult } from '../system/promise'; import { encodeHtmlWeak, escapeRegex } from '../system/string'; @@ -30,6 +31,7 @@ export interface Autolink extends AutolinkReference { provider?: ProviderReference; id: string; index?: number; + priority?: string; tokenize?: | (( @@ -129,16 +131,24 @@ export type RefSet = [ * @returns non-0 result that means a probability of the autolink `b` is more relevant of the autolink `a` */ function compareAutolinks(a: Autolink, b: Autolink): number { + if (b.prefix.length - a.prefix.length) { + return b.prefix.length - a.prefix.length; + } + if (a.priority || b.priority) { + if ((b.priority ?? '') > (a.priority ?? '')) { + return 1; + } + if ((b.priority ?? '') < (a.priority ?? '')) { + return -1; + } + return 0; + } // consider that if the number is in the start, it's the most relevant link if (b.index === 0) return 1; if (a.index === 0) return -1; // maybe it worths to use some weight function instead. - return ( - b.prefix.length - a.prefix.length || - b.id.length - a.id.length || - (b.index != null && a.index != null ? -(b.index - a.index) : 0) - ); + return b.id.length - a.id.length || (b.index != null && a.index != null ? -(b.index - a.index) : 0); } function ensureCachedRegex( @@ -173,12 +183,17 @@ function ensureCachedRegex(ref: CacheableAutolinkReference, outputFormat: 'html' `(^|\\s|\\(|\\[|\\{)(${escapeRegex(ref.prefix)}(${ref.alphanumeric ? '\\w' : '\\d'}+))\\b`, ref.ignoreCase ? 'gi' : 'g', ); - ref.branchNameRegex = new RegExp( - `(^|\\-|_|\\.|\\/)(?${ref.prefix})(?${ - ref.alphanumeric ? '\\w' : '\\d' - }+)(?=$|\\-|_|\\.|\\/)`, - 'gi', - ); + if (!ref.prefix && !ref.alphanumeric) { + ref.branchNameRegex = + /(?^|\/|-|_)(?(?\d+)(((-|\.|_)\d+){0,1}))(?$|\/|-|_)/gi; + } else { + ref.branchNameRegex = new RegExp( + `(^|\\-|_|\\.|\\/)(?${ref.prefix})(?${ + ref.alphanumeric ? '\\w' : '\\d' + }+)(?=$|\\-|_|\\.|\\/)`, + 'gi', + ); + } } return true; @@ -230,6 +245,22 @@ export function getAutolinks(message: string, refsets: Readonly) { return autolinks; } +function calculatePriority( + input: string, + issueKey: string, + numberGroup: string, + index: number, + chunkIndex: number = 0, +): string { + const edgeDistance = Math.min(index, input.length - index + numberGroup.length - 1); + const isSingleNumber = issueKey === numberGroup; + return ` + ${String.fromCharCode('a'.charCodeAt(0) + chunkIndex)}: + ${String.fromCharCode('a'.charCodeAt(0) - edgeDistance)}: + ${String.fromCharCode('a'.charCodeAt(0) + Number(isSingleNumber))} + `; +} + export function getBranchAutolinks(branchName: string, refsets: Readonly) { const autolinks = new Map(); @@ -246,7 +277,30 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly(); + let skip = false; + const matches = flatMap(chunks, chunk => { + const releaseMatch = /^release(s?)((?-[\d.-]+)?)$/gm.exec(chunk); + if (releaseMatch) { + if (!releaseMatch.groups?.releaseNum) { + skip = true; + } + return []; + } + if (skip) { + skip = false; + return []; + } + const match = chunk.matchAll(ref.branchNameRegex); + chunkMap.set(chunk, chunkIndex++); + return match; + }); do { match = matches.next(); if (!match.value?.groups) break; @@ -259,12 +313,28 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly Date: Tue, 24 Dec 2024 15:13:53 +0700 Subject: [PATCH 2/3] Improves branch autolinks --- ThirdPartyNotices.txt | 30 +++++- package.json | 1 + pnpm-lock.yaml | 3 + src/autolinks/__tests__/autolinks.test.ts | 123 +++++++++++++++------- src/autolinks/autolinks.utils.ts | 112 ++++++++++++-------- 5 files changed, 182 insertions(+), 87 deletions(-) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 860199840847e..7c2a10698019a 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -32,7 +32,8 @@ This project incorporates components from the projects listed below. 27. react version 16.8.4 (https://github.com/facebook/react) 28. signal-utils version 0.20.0 (https://github.com/proposal-signals/signal-utils) 29. slug version 10.0.0 (https://github.com/Trott/slug) -30. sortablejs version 1.15.0 (https://github.com/SortableJS/Sortable) +30. slugify version 1.6.6 (https://github.com/simov/slugify) +31. sortablejs version 1.15.0 (https://github.com/SortableJS/Sortable) %% @gk-nzaytsev/fast-string-truncated-width NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -2219,6 +2220,33 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF slug NOTICES AND INFORMATION +%% slugify NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Simeon Velichkov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +========================================= +END OF slugify NOTICES AND INFORMATION + %% sortablejs NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License diff --git a/package.json b/package.json index 299cd48e1f275..a80d71a06b75b 100644 --- a/package.json +++ b/package.json @@ -20030,6 +20030,7 @@ "react-dom": "16.8.4", "signal-utils": "0.20.0", "slug": "10.0.0", + "slugify": "^1.6.6", "sortablejs": "1.15.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a605c267b14f6..b133cb85c89d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: slug: specifier: 10.0.0 version: 10.0.0 + slugify: + specifier: ^1.6.6 + version: 1.6.6 sortablejs: specifier: 1.15.0 version: 1.15.0 diff --git a/src/autolinks/__tests__/autolinks.test.ts b/src/autolinks/__tests__/autolinks.test.ts index f58f89b88d7c8..7f845ac4f090d 100644 --- a/src/autolinks/__tests__/autolinks.test.ts +++ b/src/autolinks/__tests__/autolinks.test.ts @@ -2,9 +2,9 @@ import * as assert from 'assert'; import { suite, test } from 'mocha'; import { map } from '../../system/iterable'; import type { Autolink, RefSet } from '../autolinks.utils'; -import { getAutolinks, getBranchAutolinks } from '../autolinks.utils'; +import { calculatePriority, getAutolinks, getBranchAutolinks } from '../autolinks.utils'; -const mockRefSets = (prefixes: string[] = ['']): RefSet[] => +const mockRefSets = (prefixes: string[] = [''], title = 'test'): RefSet[] => prefixes.map(prefix => [ { domain: 'test', icon: '1', id: '1', name: 'test' }, [ @@ -12,78 +12,121 @@ const mockRefSets = (prefixes: string[] = ['']): RefSet[] => alphanumeric: false, ignoreCase: false, prefix: prefix, - title: 'test', + title: title, url: '', description: 'test', }, ], ]); -function assertAutolinks(actual: Map, expected: Array): void { - assert.deepEqual([...map(actual.values(), x => x.url)], expected); +function assertAutolinks(actual: Map, expected: Array, message: string): void { + assert.deepEqual([...map(actual.values(), x => x.url)], expected, message); } suite('Autolinks Test Suite', () => { test('Branch name autolinks', () => { - assertAutolinks(getBranchAutolinks('123', mockRefSets()), ['123']); - assertAutolinks(getBranchAutolinks('feature/123', mockRefSets()), ['123']); - assertAutolinks(getBranchAutolinks('feature/PRE-123', mockRefSets()), ['123']); - assertAutolinks(getBranchAutolinks('123.2', mockRefSets()), ['123']); - assertAutolinks(getBranchAutolinks('123', mockRefSets(['PRE-'])), []); - assertAutolinks(getBranchAutolinks('feature/123', mockRefSets(['PRE-'])), []); - assertAutolinks(getBranchAutolinks('feature/2-fa/123', mockRefSets([''])), ['123', '2']); - assertAutolinks(getBranchAutolinks('feature/2-fa/123', mockRefSets([''])), ['123', '2']); - assertAutolinks(getBranchAutolinks('feature/2-fa/3', mockRefSets([''])), ['3', '2']); - assertAutolinks(getBranchAutolinks('feature/PRE-123', mockRefSets(['PRE-'])), ['123']); - assertAutolinks(getBranchAutolinks('feature/PRE-123.2', mockRefSets(['PRE-'])), ['123']); - assertAutolinks(getBranchAutolinks('feature/3-123-PRE-123', mockRefSets(['PRE-'])), ['123']); + assertAutolinks(getBranchAutolinks('123', mockRefSets()), ['123'], 'test-1'); + assertAutolinks(getBranchAutolinks('feature/123', mockRefSets()), ['123'], 'test-2'); + assertAutolinks(getBranchAutolinks('feature/PRE-123', mockRefSets()), ['123'], 'test-3'); + assertAutolinks(getBranchAutolinks('123.2', mockRefSets()), ['123'], 'test-4'); + assertAutolinks(getBranchAutolinks('123', mockRefSets(['PRE-'])), [], 'test-5'); + assertAutolinks(getBranchAutolinks('feature/123', mockRefSets(['PRE-'])), [], 'test-6'); + assertAutolinks(getBranchAutolinks('feature/2-fa/123', mockRefSets([''])), ['123', '2'], 'test-7'); + assertAutolinks(getBranchAutolinks('feature/2-fa/123', mockRefSets([''])), ['123', '2'], 'test-8'); + assertAutolinks(getBranchAutolinks('feature/2-fa/3', mockRefSets([''])), ['3', '2'], 'test-9'); + assertAutolinks(getBranchAutolinks('feature/PRE-123', mockRefSets(['PRE-'])), ['123'], 'test-10'); + assertAutolinks(getBranchAutolinks('feature/PRE-123.2', mockRefSets(['PRE-'])), ['123'], 'test-11'); + assertAutolinks(getBranchAutolinks('feature/3-123-PRE-123', mockRefSets(['PRE-'])), ['123'], 'test-12'); assertAutolinks( getBranchAutolinks('feature/3-123-PRE-123', mockRefSets(['', 'PRE-'])), ['123', '3'], + 'test-13', ); }); test('Commit message autolinks', () => { - assertAutolinks(getAutolinks('test message 123 sd', mockRefSets()), ['123']); + assertAutolinks(getAutolinks('test message 123 sd', mockRefSets()), ['123'], 'test-14'); }); + test('Test autolink priority comparation', () => { + assert.equal(calculatePriority('1', 0, '1', 1) > calculatePriority('1', 0, '1', 0), true, 'test-15'); + assert.equal(calculatePriority('1', 0, '1', 2) > calculatePriority('1', 1, '1', 0), true, 'test-16'); + assert.equal(calculatePriority('1', 0, '1', 2) > calculatePriority('1', 0, '1.1', 2), true, 'test-17'); + assert.equal(calculatePriority('2', 0, '2', 2) > calculatePriority('1', 0, '1', 2), true, 'test-19'); + }); /** * 16.1.1^ - improved branch name autolinks matching */ test('Improved branch name autolinks matching', () => { // skip branch names chunks matching '^release(?=(-(?))$` or other release-like values // skip pair in case of double chunk - assertAutolinks(getBranchAutolinks('folder/release/16/issue-1', mockRefSets([''])), ['1']); - assertAutolinks(getBranchAutolinks('folder/release/16.1/issue-1', mockRefSets([''])), ['1']); - assertAutolinks(getBranchAutolinks('folder/release/16.1.1/1', mockRefSets([''])), ['1']); - // skip one in case of single chunk - assertAutolinks(getBranchAutolinks('folder/release-16/1', mockRefSets([''])), ['1']); - assertAutolinks(getBranchAutolinks('folder/release-16.1/1', mockRefSets([''])), ['1']); - assertAutolinks(getBranchAutolinks('folder/release-16.1.2/1', mockRefSets([''])), ['1']); + assertAutolinks(getBranchAutolinks('folder/release/16/issue-1', mockRefSets([''])), ['1'], 'test-20'); + assertAutolinks(getBranchAutolinks('folder/release/16.1/issue-1', mockRefSets([''])), ['1'], 'test-21'); + assertAutolinks(getBranchAutolinks('folder/release/16.1.1/1', mockRefSets([''])), ['1'], 'test-22'); + assertAutolinks(getBranchAutolinks('release-2024', mockRefSets([''])), [], 'test-23'); + assertAutolinks(getBranchAutolinks('v-2024', mockRefSets([''])), [], 'test-24'); + assertAutolinks(getBranchAutolinks('v2024', mockRefSets([''])), [], 'test-25'); + // cannot be definitely handled + assertAutolinks(getBranchAutolinks('some-issue-in-release-2024', mockRefSets([''])), ['2024'], 'test-26'); + assertAutolinks(getBranchAutolinks('folder/release-notes-16-1', mockRefSets([''])), ['16'], 'test-27'); + assertAutolinks(getBranchAutolinks('folder/16-1-release-notes', mockRefSets([''])), ['16'], 'test-28'); + // skip next in case of single chunk + assertAutolinks(getBranchAutolinks('folder/release-16/1', mockRefSets([''])), ['1'], 'test-29'); + assertAutolinks(getBranchAutolinks('folder/release-16.1/1', mockRefSets([''])), ['1'], 'test-30'); + assertAutolinks(getBranchAutolinks('folder/release-16.1.2/1', mockRefSets([''])), ['1'], 'test-31'); /** * Added chunk matching logic for non-prefixed numbers: * - XX - is more likely issue number * - XX.XX - is less likely issue number, but still possible - * - XX.XX.XX - is more likely not issue number + * - XX.XX.XX - is more likely not issue number: seems like a date or version number */ - assertAutolinks(getBranchAutolinks('some-issue-in-release-2024', mockRefSets([''])), ['2024']); - assertAutolinks(getBranchAutolinks('some-issue-in-release-2024.1', mockRefSets([''])), ['2024']); - assertAutolinks(getBranchAutolinks('some-issue-in-release-2024.1.1', mockRefSets([''])), []); + assertAutolinks(getBranchAutolinks('issue-2024', mockRefSets([''])), ['2024'], 'test-32'); + assertAutolinks(getBranchAutolinks('issue-2024.1', mockRefSets([''])), ['2024'], 'test-33'); + assertAutolinks(getBranchAutolinks('issue-2024.1.1', mockRefSets([''])), [], 'test-34'); - assertAutolinks(getBranchAutolinks('folder/release-notes-16-1', mockRefSets([''])), ['16']); - assertAutolinks(getBranchAutolinks('folder/16-1-release-notes', mockRefSets([''])), ['16']); + // the chunk that is more close to the end is more likely actual issue number + assertAutolinks( + getBranchAutolinks('1-epic-folder/10-issue/100-subissue', mockRefSets([''])), + ['100', '10', '1'], + 'test-35', + ); - // considered the distance from the edges of the chunk as a priority sign - assertAutolinks(getBranchAutolinks('folder/16-content-1-content', mockRefSets([''])), ['16', '1']); - assertAutolinks(getBranchAutolinks('folder/content-1-content-16', mockRefSets([''])), ['16', '1']); + // ignore numbers from title + assertAutolinks( + getBranchAutolinks('folder/100-content-content-16', mockRefSets([''], '100-content-content')), + ['16'], + 'test-36', + ); + assertAutolinks( + getBranchAutolinks('folder/100-content-content-16', mockRefSets([''], 'content-content-16')), + ['100'], + 'test-37', + ); - // the chunk that is more close to the end is more likely actual issue number - assertAutolinks(getBranchAutolinks('1-epic-folder/10-issue/100-subissue', mockRefSets([''])), [ - '100', - '10', - '1', - ]); + // consider edge distance and issue key length to sort + assertAutolinks( + getBranchAutolinks('2-some-issue-in-release-2024', mockRefSets([''])), + ['2024', '2'], + 'test-38', + ); + assertAutolinks( + getBranchAutolinks('2024-some-issue-in-release-2', mockRefSets([''])), + ['2024', '2'], + 'test-39', + ); + assertAutolinks( + getBranchAutolinks('some-2-issue-in-release-2024', mockRefSets([''])), + ['2024', '2'], + 'test-40', + ); + assertAutolinks( + getBranchAutolinks('4048-issue-in-release-2024.1', mockRefSets([''])), + ['4048', '2024'], + 'test-41', + ); + // less numbers - more likely issue key + assertAutolinks(getBranchAutolinks('1-issue-in-release-2024.1', mockRefSets([''])), ['1', '2024'], 'test-42'); }); }); diff --git a/src/autolinks/autolinks.utils.ts b/src/autolinks/autolinks.utils.ts index f75941e2228b5..6edc3d736e1ab 100644 --- a/src/autolinks/autolinks.utils.ts +++ b/src/autolinks/autolinks.utils.ts @@ -1,8 +1,9 @@ +import slugify from 'slugify'; import { IssueIntegrationId } from '../constants.integrations'; import type { IssueOrPullRequest } from '../git/models/issue'; import type { ProviderReference } from '../git/models/remoteProvider'; import type { ResourceDescriptor } from '../plus/integrations/integration'; -import { flatMap } from '../system/iterable'; +import { flatMap, forEach } from '../system/iterable'; import { escapeMarkdown } from '../system/markdown'; import type { MaybePausedResult } from '../system/promise'; import { encodeHtmlWeak, escapeRegex } from '../system/string'; @@ -117,7 +118,7 @@ export function isDynamic(ref: AutolinkReference | DynamicAutolinkReference): re return !('prefix' in ref) && !('url' in ref); } -function isCacheable(ref: AutolinkReference | DynamicAutolinkReference): ref is CacheableAutolinkReference { +export function isCacheable(ref: AutolinkReference | DynamicAutolinkReference): ref is CacheableAutolinkReference { return 'prefix' in ref && ref.prefix != null && 'url' in ref && ref.url != null; } @@ -131,9 +132,11 @@ export type RefSet = [ * @returns non-0 result that means a probability of the autolink `b` is more relevant of the autolink `a` */ function compareAutolinks(a: Autolink, b: Autolink): number { + // believe that link with prefix is definitely more relevant that just a number if (b.prefix.length - a.prefix.length) { return b.prefix.length - a.prefix.length; } + // if custom priority provided, let's consider it first if (a.priority || b.priority) { if ((b.priority ?? '') > (a.priority ?? '')) { return 1; @@ -141,7 +144,6 @@ function compareAutolinks(a: Autolink, b: Autolink): number { if ((b.priority ?? '') < (a.priority ?? '')) { return -1; } - return 0; } // consider that if the number is in the start, it's the most relevant link if (b.index === 0) return 1; @@ -184,6 +186,7 @@ function ensureCachedRegex(ref: CacheableAutolinkReference, outputFormat: 'html' ref.ignoreCase ? 'gi' : 'g', ); if (!ref.prefix && !ref.alphanumeric) { + // use a different regex for non-prefixed refs ref.branchNameRegex = /(?^|\/|-|_)(?(?\d+)(((-|\.|_)\d+){0,1}))(?$|\/|-|_)/gi; } else { @@ -245,19 +248,19 @@ export function getAutolinks(message: string, refsets: Readonly) { return autolinks; } -function calculatePriority( - input: string, +/** returns lexicographic priority value ready to sort */ +export function calculatePriority( issueKey: string, + edgeDistance: number, numberGroup: string, - index: number, chunkIndex: number = 0, ): string { - const edgeDistance = Math.min(index, input.length - index + numberGroup.length - 1); const isSingleNumber = issueKey === numberGroup; return ` - ${String.fromCharCode('a'.charCodeAt(0) + chunkIndex)}: - ${String.fromCharCode('a'.charCodeAt(0) - edgeDistance)}: - ${String.fromCharCode('a'.charCodeAt(0) + Number(isSingleNumber))} + ${String.fromCharCode('a'.charCodeAt(0) + chunkIndex)}:\ + ${String.fromCharCode('a'.charCodeAt(0) - edgeDistance)}:\ + ${String.fromCharCode('a'.charCodeAt(0) + Number(isSingleNumber))}:\ + ${String.fromCharCode('a'.charCodeAt(0) + Number(issueKey))} `; } @@ -265,7 +268,6 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly(); let match; - let num; for (const [provider, refs] of refsets) { for (const ref of refs) { if ( @@ -278,16 +280,19 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly(); + const chunkIndexMap = new Map(); let skip = false; - const matches = flatMap(chunks, chunk => { - const releaseMatch = /^release(s?)((?-[\d.-]+)?)$/gm.exec(chunk); + // know chunk indexes, skip release-like chunks or chunk pairs like release-1 or release/1 + let matches: IterableIterator | undefined = flatMap(chunks, chunk => { + const releaseMatch = /^(v|ver?|versions?|releases?)((?[\d.-]+)?)$/gm.exec(chunk); if (releaseMatch) { + // number in the next chunk should be ignored if (!releaseMatch.groups?.releaseNum) { skip = true; } @@ -298,48 +303,63 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly[] = []; + /** indicates that we should remove any matched link from the map */ + let unwanted = false; do { - match = matches.next(); - if (!match.value?.groups) break; + match = matches?.next(); + if (match?.done && refTitlesMatches.length) { + // check ref titles on unwanted matches + matches = refTitlesMatches.shift(); + unwanted = true; + continue; + } + if (!match?.value?.groups) break; - num = match?.value?.groups.issueKeyNumber; + const { issueKeyNumber: issueKey, numberChunk = issueKey } = match.value.groups; + const input = match.value.input; let index = match.value.index; - const linkUrl = ref.url?.replace(numRegex, num); + const entryEdgeDistance = Math.min(index, input.length - index - numberChunk.length - 1); + + const linkUrl = ref.url?.replace(numRegex, issueKey); // strange case (I would say synthetic), but if we parse the link twice, use the most relevant of them const existingIndex = autolinks.get(linkUrl)?.index; if (existingIndex != null) { index = Math.min(index, existingIndex); } - console.log( - JSON.stringify(match.value), - match.value.groups.numberChunk, - match.value.groups.numberChunkBeginning, - match.value.input, - match.value.groups.issueKeyNumber, - ); - autolinks.set(linkUrl, { - ...ref, - provider: provider, - id: num, - index: index, - url: linkUrl, - priority: nonPrefixedRef - ? calculatePriority( - match.value.input, - num, - match.value.groups.numberChunk, - index, - chunkMap.get(match.value.input), - ) - : undefined, - title: ref.title?.replace(numRegex, num), - description: ref.description?.replace(numRegex, num), - descriptor: ref.descriptor, - }); - } while (!match.done); + + // fill refTitlesMatches for non-prefixed refs + if (!unwanted && nonPrefixedRef && ref.title) { + refTitlesMatches.push(slugify(ref.title).matchAll(ref.branchNameRegex)); + } + + if (!unwanted) { + autolinks.set(linkUrl, { + ...ref, + provider: provider, + id: issueKey, + index: index, + url: linkUrl, + priority: nonPrefixedRef + ? calculatePriority( + issueKey, + entryEdgeDistance, + match.value.groups.numberChunk, + chunkIndexMap.get(match.value.input), + ) + : undefined, + title: ref.title?.replace(numRegex, issueKey), + description: ref.description?.replace(numRegex, issueKey), + descriptor: ref.descriptor, + }); + } else { + autolinks.delete(linkUrl); + } + } while (true); } } From 66a9fd12ad526de0cefb6881b7487f9fbacc0d25 Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Wed, 25 Dec 2024 10:18:11 +0700 Subject: [PATCH 3/3] Fixes package versions by default --- .npmrc | 1 + package.json | 4 ++-- pnpm-lock.yaml | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000..31dcbb1a68356 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-prefix = '' diff --git a/package.json b/package.json index a80d71a06b75b..fa83d3ec12fdd 100644 --- a/package.json +++ b/package.json @@ -20030,7 +20030,7 @@ "react-dom": "16.8.4", "signal-utils": "0.20.0", "slug": "10.0.0", - "slugify": "^1.6.6", + "slugify": "1.6.6", "sortablejs": "1.15.0" }, "devDependencies": { @@ -20048,7 +20048,7 @@ "@types/sortablejs": "1.15.8", "@types/vscode": "1.82.0", "@typescript-eslint/parser": "8.18.0", - "@vscode/test-cli": "^0.0.10", + "@vscode/test-cli": "0.0.10", "@vscode/test-electron": "2.4.1", "@vscode/test-web": "0.0.65", "@vscode/vsce": "3.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b133cb85c89d1..2b3ecbf5fd370 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,7 +108,7 @@ importers: specifier: 10.0.0 version: 10.0.0 slugify: - specifier: ^1.6.6 + specifier: 1.6.6 version: 1.6.6 sortablejs: specifier: 1.15.0 @@ -157,7 +157,7 @@ importers: specifier: 8.18.0 version: 8.18.0(eslint@9.16.0(jiti@2.4.0))(typescript@5.7.2) '@vscode/test-cli': - specifier: ^0.0.10 + specifier: 0.0.10 version: 0.0.10 '@vscode/test-electron': specifier: 2.4.1