Skip to content

Commit

Permalink
Improves branch autolinks experiments
Browse files Browse the repository at this point in the history
  • Loading branch information
nzaytsev committed Dec 20, 2024
1 parent 03229d2 commit a342b94
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 26 deletions.
66 changes: 52 additions & 14 deletions src/autolinks/__tests__/autolinks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const mockRefSets = (prefixes: string[] = ['']): RefSet[] =>
ignoreCase: false,
prefix: prefix,
title: 'test',
url: 'test/<num>',
url: '<num>',
description: 'test',
},
],
Expand All @@ -25,27 +25,65 @@ function assertAutolinks(actual: Map<string, Autolink>, expected: Array<string>)

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(?=(-(?<number-chunk>))$` 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',
]);
});
});
94 changes: 82 additions & 12 deletions src/autolinks/autolinks.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -30,6 +31,7 @@ export interface Autolink extends AutolinkReference {
provider?: ProviderReference;
id: string;
index?: number;
priority?: string;

tokenize?:
| ((
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
`(^|\\-|_|\\.|\\/)(?<prefix>${ref.prefix})(?<issueKeyNumber>${
ref.alphanumeric ? '\\w' : '\\d'
}+)(?=$|\\-|_|\\.|\\/)`,
'gi',
);
if (!ref.prefix && !ref.alphanumeric) {
ref.branchNameRegex =
/(?<numberChunkBeginning>^|\/|-|_)(?<numberChunk>(?<issueKeyNumber>\d+)(((-|\.|_)\d+){0,1}))(?<numberChunkEnding>$|\/|-|_)/gi;
} else {
ref.branchNameRegex = new RegExp(
`(^|\\-|_|\\.|\\/)(?<prefix>${ref.prefix})(?<issueKeyNumber>${
ref.alphanumeric ? '\\w' : '\\d'
}+)(?=$|\\-|_|\\.|\\/)`,
'gi',
);
}
}

return true;
Expand Down Expand Up @@ -230,6 +245,22 @@ export function getAutolinks(message: string, refsets: Readonly<RefSet[]>) {
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<RefSet[]>) {
const autolinks = new Map<string, Autolink>();

Expand All @@ -246,7 +277,30 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly<RefSet[
}

ensureCachedRegex(ref, 'plaintext');
const matches = branchName.matchAll(ref.branchNameRegex);
let chunks = [branchName];
const nonPrefixedRef = !ref.prefix && !ref.alphanumeric;
if (nonPrefixedRef) {
chunks = branchName.split('/');
}
let chunkIndex = 0;
const chunkMap = new Map<string, number>();
let skip = false;
const matches = flatMap(chunks, chunk => {
const releaseMatch = /^release(s?)((?<releaseNum>-[\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;
Expand All @@ -259,12 +313,28 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly<RefSet[
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,
Expand Down

0 comments on commit a342b94

Please sign in to comment.