Skip to content

Commit d0e9a63

Browse files
committed
fix
1 parent 84812e4 commit d0e9a63

File tree

6 files changed

+234
-6
lines changed

6 files changed

+234
-6
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
"katex": "0.16.22",
3838
"mermaid": "11.11.0",
3939
"mini-css-extract-plugin": "2.9.4",
40-
"minimatch": "10.0.3",
4140
"monaco-editor": "0.53.0",
4241
"monaco-editor-webpack-plugin": "7.1.0",
4342
"online-3d-viewer": "0.16.0",

pnpm-lock.yaml

Lines changed: 19 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web_src/js/features/repo-settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import {minimatch} from 'minimatch';
21
import {createMonaco} from './codeeditor.ts';
32
import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts';
43
import {POST} from '../modules/fetch.ts';
54
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
65
import {fomanticQuery} from '../modules/fomantic/base.ts';
6+
import {globMatch} from '../utils/glob.ts';
77

88
const {appSubUrl, csrfToken} = window.config;
99

@@ -108,7 +108,7 @@ function initRepoSettingsBranches() {
108108
let matched = false;
109109
const statusCheck = el.getAttribute('data-status-check');
110110
for (const pattern of validPatterns) {
111-
if (minimatch(statusCheck, pattern, {noext: true})) { // https://github.com/go-gitea/gitea/issues/33121 disable extended glob syntax
111+
if (globMatch(statusCheck, pattern, '/')) {
112112
matched = true;
113113
break;
114114
}

web_src/js/utils/glob.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {readFile} from 'node:fs/promises';
2+
import * as path from 'node:path';
3+
import {globCompile} from './glob.ts';
4+
5+
async function loadGlobTestData(): Promise<{caseNames: string[], kvMap: Record<string, string>}> {
6+
const fileContent = await readFile(path.join(import.meta.dirname, 'glob.test.txt'), 'utf8');
7+
const fileLines = fileContent.split('\n');
8+
const kvMap: Record<string, string> = {};
9+
const caseNameMap: Record<string, boolean> = {};
10+
for (let line of fileLines) {
11+
line = line.trim();
12+
if (!line || line.startsWith('#')) continue;
13+
const parts = line.split('=', 2);
14+
if (parts.length !== 2) throw new Error(`Invalid test case line: ${line}`);
15+
16+
const key = parts[0].trim();
17+
let value = parts[1].trim();
18+
value = value.substring(1, value.length - 1); // remove quotes
19+
value = value.replace(/\\\\/g, '\\').replaceAll(/\\\//g, '/');
20+
kvMap[key] = value;
21+
if (key.startsWith('pattern_')) caseNameMap[key.substring('pattern_'.length)] = true;
22+
}
23+
return {caseNames: Object.keys(caseNameMap), kvMap};
24+
}
25+
26+
test('GlobCompiler', async () => {
27+
const {caseNames, kvMap} = await loadGlobTestData();
28+
expect(caseNames.length).toEqual(10); // should have 10 test cases
29+
for (const caseName of caseNames) {
30+
const pattern = kvMap[`pattern_${caseName}`];
31+
const regexp = kvMap[`regexp_${caseName}`];
32+
expect(globCompile(pattern).regexpPattern).toEqual(regexp);
33+
}
34+
35+
// then our cases
36+
expect(globCompile('*/**/x').regexpPattern).toEqual('^.*/.*/x$');
37+
expect(globCompile('*/**/x', '/').regexpPattern).toEqual('^[^/]*/.*/x$');
38+
expect(globCompile('[a-b][^-\\]]', '/').regexpPattern).toEqual('^[a-b][^-\\]]$');
39+
expect(globCompile('.+^$()|', '/').regexpPattern).toEqual('^\\.\\+\\^\\$\\(\\)\\|$');
40+
});

web_src/js/utils/glob.test.txt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# test cases are from https://github.com/gobwas/glob/blob/master/glob_test.go
2+
3+
pattern_all = "[a-z][!a-x]*cat*[h][!b]*eyes*"
4+
regexp_all = `^[a-z][^a-x].*cat.*[h][^b].*eyes.*$`
5+
fixture_all_match = "my cat has very bright eyes"
6+
fixture_all_mismatch = "my dog has very bright eyes"
7+
8+
pattern_plain = "google.com"
9+
regexp_plain = `^google\.com$`
10+
fixture_plain_match = "google.com"
11+
fixture_plain_mismatch = "gobwas.com"
12+
13+
pattern_multiple = "https://*.google.*"
14+
regexp_multiple = `^https:\/\/.*\.google\..*$`
15+
fixture_multiple_match = "https://account.google.com"
16+
fixture_multiple_mismatch = "https://google.com"
17+
18+
pattern_alternatives = "{https://*.google.*,*yandex.*,*yahoo.*,*mail.ru}"
19+
regexp_alternatives = `^(https:\/\/.*\.google\..*|.*yandex\..*|.*yahoo\..*|.*mail\.ru)$`
20+
fixture_alternatives_match = "http://yahoo.com"
21+
fixture_alternatives_mismatch = "http://google.com"
22+
23+
pattern_alternatives_suffix = "{https://*gobwas.com,http://exclude.gobwas.com}"
24+
regexp_alternatives_suffix = `^(https:\/\/.*gobwas\.com|http://exclude\.gobwas\.com)$`
25+
fixture_alternatives_suffix_first_match = "https://safe.gobwas.com"
26+
fixture_alternatives_suffix_first_mismatch = "http://safe.gobwas.com"
27+
fixture_alternatives_suffix_second = "http://exclude.gobwas.com"
28+
29+
pattern_prefix = "abc*"
30+
regexp_prefix = `^abc.*$`
31+
pattern_suffix = "*def"
32+
regexp_suffix = `^.*def$`
33+
pattern_prefix_suffix = "ab*ef"
34+
regexp_prefix_suffix = `^ab.*ef$`
35+
fixture_prefix_suffix_match = "abcdef"
36+
fixture_prefix_suffix_mismatch = "af"
37+
38+
pattern_alternatives_combine_lite = "{abc*def,abc?def,abc[zte]def}"
39+
regexp_alternatives_combine_lite = `^(abc.*def|abc.def|abc[zte]def)$`
40+
fixture_alternatives_combine_lite = "abczdef"
41+
42+
pattern_alternatives_combine_hard = "{abc*[a-c]def,abc?[d-g]def,abc[zte]?def}"
43+
regexp_alternatives_combine_hard = `^(abc.*[a-c]def|abc.[d-g]def|abc[zte].def)$`
44+
fixture_alternatives_combine_hard = "abczqdef"

web_src/js/utils/glob.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Reference: https://github.com/gobwas/glob/blob/master/glob.go
2+
//
3+
// Compile creates Glob for given pattern and strings (if any present after pattern) as separators.
4+
// The pattern syntax is:
5+
//
6+
// pattern:
7+
// { term }
8+
//
9+
// term:
10+
// `*` matches any sequence of non-separator characters
11+
// `**` matches any sequence of characters
12+
// `?` matches any single non-separator character
13+
// `[` [ `!` ] { character-range } `]`
14+
// character class (must be non-empty)
15+
// `{` pattern-list `}`
16+
// pattern alternatives
17+
// c matches character c (c != `*`, `**`, `?`, `\`, `[`, `{`, `}`)
18+
// `\` c matches character c
19+
//
20+
// character-range:
21+
// c matches character c (c != `\\`, `-`, `]`)
22+
// `\` c matches character c
23+
// lo `-` hi matches character c for lo <= c <= hi
24+
//
25+
// pattern-list:
26+
// pattern { `,` pattern }
27+
// comma-separated (without spaces) patterns
28+
//
29+
30+
class GlobCompiler {
31+
nonSeparatorChars: string;
32+
globPattern: string;
33+
regexpPattern: string;
34+
regexp: RegExp;
35+
pos: number = 0;
36+
37+
#compileChars(): string {
38+
let result = '';
39+
if (this.globPattern[this.pos] === '!') {
40+
this.pos++;
41+
result += '^';
42+
}
43+
while (this.pos < this.globPattern.length) {
44+
const c = this.globPattern[this.pos];
45+
this.pos++;
46+
if (c === ']') {
47+
return `[${result}]`;
48+
}
49+
if (c === '\\') {
50+
if (this.pos >= this.globPattern.length) {
51+
throw new Error('Unterminated character class escape');
52+
}
53+
this.pos++;
54+
result += `\\${this.globPattern[this.pos]}`;
55+
} else {
56+
result += c;
57+
}
58+
}
59+
throw new Error('Unterminated character class');
60+
}
61+
62+
#compile(subPattern: boolean = false): string {
63+
let result = '';
64+
let hasSubGroups = false;
65+
while (this.pos < this.globPattern.length) {
66+
const c = this.globPattern[this.pos];
67+
this.pos++;
68+
if (subPattern && c === '}') {
69+
return result;
70+
}
71+
switch (c) {
72+
case '*':
73+
if (this.globPattern[this.pos] !== '*') {
74+
result += `${this.nonSeparatorChars}*`; // match any sequence of non-separator characters
75+
} else {
76+
this.pos++;
77+
result += '.*'; // match any sequence of characters
78+
}
79+
break;
80+
case '?':
81+
result += this.nonSeparatorChars; // match any single non-separator character
82+
break;
83+
case '[':
84+
result += this.#compileChars();
85+
break;
86+
case '{':
87+
result += this.#compile(true);
88+
hasSubGroups = true;
89+
break;
90+
case ',':
91+
result += subPattern ? '|' : ',';
92+
break;
93+
case '\\':
94+
if (this.pos >= this.globPattern.length) {
95+
throw new Error('No character to escape');
96+
}
97+
this.pos++;
98+
result += `\\${this.globPattern[this.pos]}`;
99+
break;
100+
case '.': case '+': case '^': case '$': case '(': case ')': case '|':
101+
result += `\\${c}`; // escape regexp special characters
102+
break;
103+
default:
104+
result += c;
105+
}
106+
}
107+
return hasSubGroups ? `^(${result})$` : `^${result}$`;
108+
}
109+
110+
constructor(pattern: string, separators: string = '') {
111+
const escapedSeparators = separators.replaceAll(/[\^\]\-\\]/g, '\\$&');
112+
this.nonSeparatorChars = escapedSeparators ? `[^${escapedSeparators}]` : '.';
113+
this.globPattern = pattern;
114+
this.regexpPattern = this.#compile();
115+
this.regexp = new RegExp(`^${this.regexpPattern}$`);
116+
}
117+
}
118+
119+
export function globCompile(pattern: string, separators: string = ''): GlobCompiler {
120+
return new GlobCompiler(pattern, separators);
121+
}
122+
123+
export function globMatch(str: string, pattern: string, separators: string = ''): boolean {
124+
try {
125+
return globCompile(pattern, separators).regexp.test(str);
126+
} catch {
127+
return false;
128+
}
129+
}

0 commit comments

Comments
 (0)