From a342b94836cff1310dd9e03eba4dddb1fe58d091 Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Fri, 20 Dec 2024 12:06:58 +0700 Subject: [PATCH] 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