Skip to content

Commit 325e059

Browse files
authored
Fix different behavior in status check pattern matching with double stars (go-gitea#35474)
Drop the minimatch dependency, use our own glob compiler. Fix go-gitea#35473
1 parent 866c636 commit 325e059

File tree

6 files changed

+325
-6
lines changed

6 files changed

+325
-6
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
"jquery": "3.7.1",
3838
"katex": "0.16.22",
3939
"mermaid": "11.11.0",
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: 23 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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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[], caseDataMap: 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 caseDataMap: 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+
caseDataMap[key] = value;
21+
if (key.startsWith('pattern_')) caseNameMap[key.substring('pattern_'.length)] = true;
22+
}
23+
return {caseNames: Object.keys(caseNameMap), caseDataMap};
24+
}
25+
26+
function loadGlobGolangCases() {
27+
// https://github.com/gobwas/glob/blob/master/glob_test.go
28+
function glob(matched: boolean, pattern: string, input: string, separators: string = '') {
29+
return {matched, pattern, input, separators};
30+
}
31+
return [
32+
glob(true, '* ?at * eyes', 'my cat has very bright eyes'),
33+
34+
glob(true, '', ''),
35+
glob(false, '', 'b'),
36+
37+
glob(true, '*ä', 'åä'),
38+
glob(true, 'abc', 'abc'),
39+
glob(true, 'a*c', 'abc'),
40+
glob(true, 'a*c', 'a12345c'),
41+
glob(true, 'a?c', 'a1c'),
42+
glob(true, 'a.b', 'a.b', '.'),
43+
glob(true, 'a.*', 'a.b', '.'),
44+
glob(true, 'a.**', 'a.b.c', '.'),
45+
glob(true, 'a.?.c', 'a.b.c', '.'),
46+
glob(true, 'a.?.?', 'a.b.c', '.'),
47+
glob(true, '?at', 'cat'),
48+
glob(true, '?at', 'fat'),
49+
glob(true, '*', 'abc'),
50+
glob(true, `\\*`, '*'),
51+
glob(true, '**', 'a.b.c', '.'),
52+
53+
glob(false, '?at', 'at'),
54+
glob(false, '?at', 'fat', 'f'),
55+
glob(false, 'a.*', 'a.b.c', '.'),
56+
glob(false, 'a.?.c', 'a.bb.c', '.'),
57+
glob(false, '*', 'a.b.c', '.'),
58+
59+
glob(true, '*test', 'this is a test'),
60+
glob(true, 'this*', 'this is a test'),
61+
glob(true, '*is *', 'this is a test'),
62+
glob(true, '*is*a*', 'this is a test'),
63+
glob(true, '**test**', 'this is a test'),
64+
glob(true, '**is**a***test*', 'this is a test'),
65+
66+
glob(false, '*is', 'this is a test'),
67+
glob(false, '*no*', 'this is a test'),
68+
glob(true, '[!a]*', 'this is a test3'),
69+
70+
glob(true, '*abc', 'abcabc'),
71+
glob(true, '**abc', 'abcabc'),
72+
glob(true, '???', 'abc'),
73+
glob(true, '?*?', 'abc'),
74+
glob(true, '?*?', 'ac'),
75+
glob(false, 'sta', 'stagnation'),
76+
glob(true, 'sta*', 'stagnation'),
77+
glob(false, 'sta?', 'stagnation'),
78+
glob(false, 'sta?n', 'stagnation'),
79+
80+
glob(true, '{abc,def}ghi', 'defghi'),
81+
glob(true, '{abc,abcd}a', 'abcda'),
82+
glob(true, '{a,ab}{bc,f}', 'abc'),
83+
glob(true, '{*,**}{a,b}', 'ab'),
84+
glob(false, '{*,**}{a,b}', 'ac'),
85+
86+
glob(true, '/{rate,[a-z][a-z][a-z]}*', '/rate'),
87+
glob(true, '/{rate,[0-9][0-9][0-9]}*', '/rate'),
88+
glob(true, '/{rate,[a-z][a-z][a-z]}*', '/usd'),
89+
90+
glob(true, '{*.google.*,*.yandex.*}', 'www.google.com', '.'),
91+
glob(true, '{*.google.*,*.yandex.*}', 'www.yandex.com', '.'),
92+
glob(false, '{*.google.*,*.yandex.*}', 'yandex.com', '.'),
93+
glob(false, '{*.google.*,*.yandex.*}', 'google.com', '.'),
94+
95+
glob(true, '{*.google.*,yandex.*}', 'www.google.com', '.'),
96+
glob(true, '{*.google.*,yandex.*}', 'yandex.com', '.'),
97+
glob(false, '{*.google.*,yandex.*}', 'www.yandex.com', '.'),
98+
glob(false, '{*.google.*,yandex.*}', 'google.com', '.'),
99+
100+
glob(true, '*//{,*.}example.com', 'https://www.example.com'),
101+
glob(true, '*//{,*.}example.com', 'http://example.com'),
102+
glob(false, '*//{,*.}example.com', 'http://example.com.net'),
103+
];
104+
}
105+
106+
test('GlobCompiler', async () => {
107+
const {caseNames, caseDataMap} = await loadGlobTestData();
108+
expect(caseNames.length).toBe(10); // should have 10 test cases
109+
for (const caseName of caseNames) {
110+
const pattern = caseDataMap[`pattern_${caseName}`];
111+
const regexp = caseDataMap[`regexp_${caseName}`];
112+
expect(globCompile(pattern).regexpPattern).toBe(regexp);
113+
}
114+
115+
const golangCases = loadGlobGolangCases();
116+
expect(golangCases.length).toBe(60);
117+
for (const c of golangCases) {
118+
const compiled = globCompile(c.pattern, c.separators);
119+
const msg = `pattern: ${c.pattern}, input: ${c.input}, separators: ${c.separators || '(none)'}, compiled: ${compiled.regexpPattern}`;
120+
// eslint-disable-next-line @vitest/valid-expect -- Unlike Jest, Vitest supports a message as the second argument
121+
expect(compiled.regexp.test(c.input), msg).toBe(c.matched);
122+
}
123+
124+
// then our cases
125+
expect(globCompile('*/**/x').regexpPattern).toBe('^.*/.*/x$');
126+
expect(globCompile('*/**/x', '/').regexpPattern).toBe('^[^/]*/.*/x$');
127+
expect(globCompile('[a-b][^-\\]]', '/').regexpPattern).toBe('^[a-b][^-\\]]$');
128+
expect(globCompile('.+^$()|', '/').regexpPattern).toBe('^\\.\\+\\^\\$\\(\\)\\|$');
129+
});

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"

0 commit comments

Comments
 (0)