mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18:25 +00:00 
			
		
		
		
	Fix different behavior in status check pattern matching with double stars (#35474)
Drop the minimatch dependency, use our own glob compiler. Fix #35473
This commit is contained in:
		
							
								
								
									
										129
									
								
								web_src/js/utils/glob.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								web_src/js/utils/glob.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
			
		||||
import {readFile} from 'node:fs/promises';
 | 
			
		||||
import * as path from 'node:path';
 | 
			
		||||
import {globCompile} from './glob.ts';
 | 
			
		||||
 | 
			
		||||
async function loadGlobTestData(): Promise<{caseNames: string[], caseDataMap: Record<string, string>}> {
 | 
			
		||||
  const fileContent = await readFile(path.join(import.meta.dirname, 'glob.test.txt'), 'utf8');
 | 
			
		||||
  const fileLines = fileContent.split('\n');
 | 
			
		||||
  const caseDataMap: Record<string, string> = {};
 | 
			
		||||
  const caseNameMap: Record<string, boolean> = {};
 | 
			
		||||
  for (let line of fileLines) {
 | 
			
		||||
    line = line.trim();
 | 
			
		||||
    if (!line || line.startsWith('#')) continue;
 | 
			
		||||
    const parts = line.split('=', 2);
 | 
			
		||||
    if (parts.length !== 2) throw new Error(`Invalid test case line: ${line}`);
 | 
			
		||||
 | 
			
		||||
    const key = parts[0].trim();
 | 
			
		||||
    let value = parts[1].trim();
 | 
			
		||||
    value = value.substring(1, value.length - 1); // remove quotes
 | 
			
		||||
    value = value.replace(/\\\\/g, '\\').replaceAll(/\\\//g, '/');
 | 
			
		||||
    caseDataMap[key] = value;
 | 
			
		||||
    if (key.startsWith('pattern_')) caseNameMap[key.substring('pattern_'.length)] = true;
 | 
			
		||||
  }
 | 
			
		||||
  return {caseNames: Object.keys(caseNameMap), caseDataMap};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function loadGlobGolangCases() {
 | 
			
		||||
  // https://github.com/gobwas/glob/blob/master/glob_test.go
 | 
			
		||||
  function glob(matched: boolean, pattern: string, input: string, separators: string = '') {
 | 
			
		||||
    return {matched, pattern, input, separators};
 | 
			
		||||
  }
 | 
			
		||||
  return [
 | 
			
		||||
    glob(true, '* ?at * eyes', 'my cat has very bright eyes'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '', ''),
 | 
			
		||||
    glob(false, '', 'b'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '*ä', 'åä'),
 | 
			
		||||
    glob(true, 'abc', 'abc'),
 | 
			
		||||
    glob(true, 'a*c', 'abc'),
 | 
			
		||||
    glob(true, 'a*c', 'a12345c'),
 | 
			
		||||
    glob(true, 'a?c', 'a1c'),
 | 
			
		||||
    glob(true, 'a.b', 'a.b', '.'),
 | 
			
		||||
    glob(true, 'a.*', 'a.b', '.'),
 | 
			
		||||
    glob(true, 'a.**', 'a.b.c', '.'),
 | 
			
		||||
    glob(true, 'a.?.c', 'a.b.c', '.'),
 | 
			
		||||
    glob(true, 'a.?.?', 'a.b.c', '.'),
 | 
			
		||||
    glob(true, '?at', 'cat'),
 | 
			
		||||
    glob(true, '?at', 'fat'),
 | 
			
		||||
    glob(true, '*', 'abc'),
 | 
			
		||||
    glob(true, `\\*`, '*'),
 | 
			
		||||
    glob(true, '**', 'a.b.c', '.'),
 | 
			
		||||
 | 
			
		||||
    glob(false, '?at', 'at'),
 | 
			
		||||
    glob(false, '?at', 'fat', 'f'),
 | 
			
		||||
    glob(false, 'a.*', 'a.b.c', '.'),
 | 
			
		||||
    glob(false, 'a.?.c', 'a.bb.c', '.'),
 | 
			
		||||
    glob(false, '*', 'a.b.c', '.'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '*test', 'this is a test'),
 | 
			
		||||
    glob(true, 'this*', 'this is a test'),
 | 
			
		||||
    glob(true, '*is *', 'this is a test'),
 | 
			
		||||
    glob(true, '*is*a*', 'this is a test'),
 | 
			
		||||
    glob(true, '**test**', 'this is a test'),
 | 
			
		||||
    glob(true, '**is**a***test*', 'this is a test'),
 | 
			
		||||
 | 
			
		||||
    glob(false, '*is', 'this is a test'),
 | 
			
		||||
    glob(false, '*no*', 'this is a test'),
 | 
			
		||||
    glob(true, '[!a]*', 'this is a test3'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '*abc', 'abcabc'),
 | 
			
		||||
    glob(true, '**abc', 'abcabc'),
 | 
			
		||||
    glob(true, '???', 'abc'),
 | 
			
		||||
    glob(true, '?*?', 'abc'),
 | 
			
		||||
    glob(true, '?*?', 'ac'),
 | 
			
		||||
    glob(false, 'sta', 'stagnation'),
 | 
			
		||||
    glob(true, 'sta*', 'stagnation'),
 | 
			
		||||
    glob(false, 'sta?', 'stagnation'),
 | 
			
		||||
    glob(false, 'sta?n', 'stagnation'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '{abc,def}ghi', 'defghi'),
 | 
			
		||||
    glob(true, '{abc,abcd}a', 'abcda'),
 | 
			
		||||
    glob(true, '{a,ab}{bc,f}', 'abc'),
 | 
			
		||||
    glob(true, '{*,**}{a,b}', 'ab'),
 | 
			
		||||
    glob(false, '{*,**}{a,b}', 'ac'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '/{rate,[a-z][a-z][a-z]}*', '/rate'),
 | 
			
		||||
    glob(true, '/{rate,[0-9][0-9][0-9]}*', '/rate'),
 | 
			
		||||
    glob(true, '/{rate,[a-z][a-z][a-z]}*', '/usd'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '{*.google.*,*.yandex.*}', 'www.google.com', '.'),
 | 
			
		||||
    glob(true, '{*.google.*,*.yandex.*}', 'www.yandex.com', '.'),
 | 
			
		||||
    glob(false, '{*.google.*,*.yandex.*}', 'yandex.com', '.'),
 | 
			
		||||
    glob(false, '{*.google.*,*.yandex.*}', 'google.com', '.'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '{*.google.*,yandex.*}', 'www.google.com', '.'),
 | 
			
		||||
    glob(true, '{*.google.*,yandex.*}', 'yandex.com', '.'),
 | 
			
		||||
    glob(false, '{*.google.*,yandex.*}', 'www.yandex.com', '.'),
 | 
			
		||||
    glob(false, '{*.google.*,yandex.*}', 'google.com', '.'),
 | 
			
		||||
 | 
			
		||||
    glob(true, '*//{,*.}example.com', 'https://www.example.com'),
 | 
			
		||||
    glob(true, '*//{,*.}example.com', 'http://example.com'),
 | 
			
		||||
    glob(false, '*//{,*.}example.com', 'http://example.com.net'),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test('GlobCompiler', async () => {
 | 
			
		||||
  const {caseNames, caseDataMap} = await loadGlobTestData();
 | 
			
		||||
  expect(caseNames.length).toBe(10); // should have 10 test cases
 | 
			
		||||
  for (const caseName of caseNames) {
 | 
			
		||||
    const pattern = caseDataMap[`pattern_${caseName}`];
 | 
			
		||||
    const regexp = caseDataMap[`regexp_${caseName}`];
 | 
			
		||||
    expect(globCompile(pattern).regexpPattern).toBe(regexp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const golangCases = loadGlobGolangCases();
 | 
			
		||||
  expect(golangCases.length).toBe(60);
 | 
			
		||||
  for (const c of golangCases) {
 | 
			
		||||
    const compiled = globCompile(c.pattern, c.separators);
 | 
			
		||||
    const msg = `pattern: ${c.pattern}, input: ${c.input}, separators: ${c.separators || '(none)'}, compiled: ${compiled.regexpPattern}`;
 | 
			
		||||
    // eslint-disable-next-line @vitest/valid-expect -- Unlike Jest, Vitest supports a message as the second argument
 | 
			
		||||
    expect(compiled.regexp.test(c.input), msg).toBe(c.matched);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // then our cases
 | 
			
		||||
  expect(globCompile('*/**/x').regexpPattern).toBe('^.*/.*/x$');
 | 
			
		||||
  expect(globCompile('*/**/x', '/').regexpPattern).toBe('^[^/]*/.*/x$');
 | 
			
		||||
  expect(globCompile('[a-b][^-\\]]', '/').regexpPattern).toBe('^[a-b][^-\\]]$');
 | 
			
		||||
  expect(globCompile('.+^$()|', '/').regexpPattern).toBe('^\\.\\+\\^\\$\\(\\)\\|$');
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										44
									
								
								web_src/js/utils/glob.test.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web_src/js/utils/glob.test.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
# test cases are from https://github.com/gobwas/glob/blob/master/glob_test.go
 | 
			
		||||
 | 
			
		||||
pattern_all          = "[a-z][!a-x]*cat*[h][!b]*eyes*"
 | 
			
		||||
regexp_all           = `^[a-z][^a-x].*cat.*[h][^b].*eyes.*$`
 | 
			
		||||
fixture_all_match    = "my cat has very bright eyes"
 | 
			
		||||
fixture_all_mismatch = "my dog has very bright eyes"
 | 
			
		||||
 | 
			
		||||
pattern_plain          = "google.com"
 | 
			
		||||
regexp_plain           = `^google\.com$`
 | 
			
		||||
fixture_plain_match    = "google.com"
 | 
			
		||||
fixture_plain_mismatch = "gobwas.com"
 | 
			
		||||
 | 
			
		||||
pattern_multiple          = "https://*.google.*"
 | 
			
		||||
regexp_multiple           = `^https:\/\/.*\.google\..*$`
 | 
			
		||||
fixture_multiple_match    = "https://account.google.com"
 | 
			
		||||
fixture_multiple_mismatch = "https://google.com"
 | 
			
		||||
 | 
			
		||||
pattern_alternatives          = "{https://*.google.*,*yandex.*,*yahoo.*,*mail.ru}"
 | 
			
		||||
regexp_alternatives           = `^(https:\/\/.*\.google\..*|.*yandex\..*|.*yahoo\..*|.*mail\.ru)$`
 | 
			
		||||
fixture_alternatives_match    = "http://yahoo.com"
 | 
			
		||||
fixture_alternatives_mismatch = "http://google.com"
 | 
			
		||||
 | 
			
		||||
pattern_alternatives_suffix                = "{https://*gobwas.com,http://exclude.gobwas.com}"
 | 
			
		||||
regexp_alternatives_suffix                 = `^(https:\/\/.*gobwas\.com|http://exclude\.gobwas\.com)$`
 | 
			
		||||
fixture_alternatives_suffix_first_match    = "https://safe.gobwas.com"
 | 
			
		||||
fixture_alternatives_suffix_first_mismatch = "http://safe.gobwas.com"
 | 
			
		||||
fixture_alternatives_suffix_second         = "http://exclude.gobwas.com"
 | 
			
		||||
 | 
			
		||||
pattern_prefix                 = "abc*"
 | 
			
		||||
regexp_prefix                  = `^abc.*$`
 | 
			
		||||
pattern_suffix                 = "*def"
 | 
			
		||||
regexp_suffix                  = `^.*def$`
 | 
			
		||||
pattern_prefix_suffix          = "ab*ef"
 | 
			
		||||
regexp_prefix_suffix           = `^ab.*ef$`
 | 
			
		||||
fixture_prefix_suffix_match    = "abcdef"
 | 
			
		||||
fixture_prefix_suffix_mismatch = "af"
 | 
			
		||||
 | 
			
		||||
pattern_alternatives_combine_lite = "{abc*def,abc?def,abc[zte]def}"
 | 
			
		||||
regexp_alternatives_combine_lite  = `^(abc.*def|abc.def|abc[zte]def)$`
 | 
			
		||||
fixture_alternatives_combine_lite = "abczdef"
 | 
			
		||||
 | 
			
		||||
pattern_alternatives_combine_hard = "{abc*[a-c]def,abc?[d-g]def,abc[zte]?def}"
 | 
			
		||||
regexp_alternatives_combine_hard  = `^(abc.*[a-c]def|abc.[d-g]def|abc[zte].def)$`
 | 
			
		||||
fixture_alternatives_combine_hard = "abczqdef"
 | 
			
		||||
							
								
								
									
										127
									
								
								web_src/js/utils/glob.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								web_src/js/utils/glob.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
			
		||||
// Reference: https://github.com/gobwas/glob/blob/master/glob.go
 | 
			
		||||
//
 | 
			
		||||
// Compile creates Glob for given pattern and strings (if any present after pattern) as separators.
 | 
			
		||||
// The pattern syntax is:
 | 
			
		||||
//
 | 
			
		||||
//    pattern:
 | 
			
		||||
//        { term }
 | 
			
		||||
//
 | 
			
		||||
//    term:
 | 
			
		||||
//        `*`         matches any sequence of non-separator characters
 | 
			
		||||
//        `**`        matches any sequence of characters
 | 
			
		||||
//        `?`         matches any single non-separator character
 | 
			
		||||
//        `[` [ `!` ] { character-range } `]`
 | 
			
		||||
//                    character class (must be non-empty)
 | 
			
		||||
//        `{` pattern-list `}`
 | 
			
		||||
//                    pattern alternatives
 | 
			
		||||
//        c           matches character c (c != `*`, `**`, `?`, `\`, `[`, `{`, `}`)
 | 
			
		||||
//        `\` c       matches character c
 | 
			
		||||
//
 | 
			
		||||
//    character-range:
 | 
			
		||||
//        c           matches character c (c != `\\`, `-`, `]`)
 | 
			
		||||
//        `\` c       matches character c
 | 
			
		||||
//        lo `-` hi   matches character c for lo <= c <= hi
 | 
			
		||||
//
 | 
			
		||||
//    pattern-list:
 | 
			
		||||
//        pattern { `,` pattern }
 | 
			
		||||
//                    comma-separated (without spaces) patterns
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
class GlobCompiler {
 | 
			
		||||
  nonSeparatorChars: string;
 | 
			
		||||
  globPattern: string;
 | 
			
		||||
  regexpPattern: string;
 | 
			
		||||
  regexp: RegExp;
 | 
			
		||||
  pos: number = 0;
 | 
			
		||||
 | 
			
		||||
  #compileChars(): string {
 | 
			
		||||
    let result = '';
 | 
			
		||||
    if (this.globPattern[this.pos] === '!') {
 | 
			
		||||
      this.pos++;
 | 
			
		||||
      result += '^';
 | 
			
		||||
    }
 | 
			
		||||
    while (this.pos < this.globPattern.length) {
 | 
			
		||||
      const c = this.globPattern[this.pos];
 | 
			
		||||
      this.pos++;
 | 
			
		||||
      if (c === ']') {
 | 
			
		||||
        return `[${result}]`;
 | 
			
		||||
      }
 | 
			
		||||
      if (c === '\\') {
 | 
			
		||||
        if (this.pos >= this.globPattern.length) {
 | 
			
		||||
          throw new Error('Unterminated character class escape');
 | 
			
		||||
        }
 | 
			
		||||
        this.pos++;
 | 
			
		||||
        result += `\\${this.globPattern[this.pos]}`;
 | 
			
		||||
      } else {
 | 
			
		||||
        result += c;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    throw new Error('Unterminated character class');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #compile(subPattern: boolean = false): string {
 | 
			
		||||
    let result = '';
 | 
			
		||||
    while (this.pos < this.globPattern.length) {
 | 
			
		||||
      const c = this.globPattern[this.pos];
 | 
			
		||||
      this.pos++;
 | 
			
		||||
      if (subPattern && c === '}') {
 | 
			
		||||
        return `(${result})`;
 | 
			
		||||
      }
 | 
			
		||||
      switch (c) {
 | 
			
		||||
        case '*':
 | 
			
		||||
          if (this.globPattern[this.pos] !== '*') {
 | 
			
		||||
            result += `${this.nonSeparatorChars}*`; // match any sequence of non-separator characters
 | 
			
		||||
          } else {
 | 
			
		||||
            this.pos++;
 | 
			
		||||
            result += '.*'; // match any sequence of characters
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case '?':
 | 
			
		||||
          result += this.nonSeparatorChars; // match any single non-separator character
 | 
			
		||||
          break;
 | 
			
		||||
        case '[':
 | 
			
		||||
          result += this.#compileChars();
 | 
			
		||||
          break;
 | 
			
		||||
        case '{':
 | 
			
		||||
          result += this.#compile(true);
 | 
			
		||||
          break;
 | 
			
		||||
        case ',':
 | 
			
		||||
          result += subPattern ? '|' : ',';
 | 
			
		||||
          break;
 | 
			
		||||
        case '\\':
 | 
			
		||||
          if (this.pos >= this.globPattern.length) {
 | 
			
		||||
            throw new Error('No character to escape');
 | 
			
		||||
          }
 | 
			
		||||
          result += `\\${this.globPattern[this.pos]}`;
 | 
			
		||||
          this.pos++;
 | 
			
		||||
          break;
 | 
			
		||||
        case '.': case '+': case '^': case '$': case '(': case ')': case '|':
 | 
			
		||||
          result += `\\${c}`; // escape regexp special characters
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          result += c;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(pattern: string, separators: string = '') {
 | 
			
		||||
    const escapedSeparators = separators.replaceAll(/[\^\]\-\\]/g, '\\$&');
 | 
			
		||||
    this.nonSeparatorChars = escapedSeparators ? `[^${escapedSeparators}]` : '.';
 | 
			
		||||
    this.globPattern = pattern;
 | 
			
		||||
    this.regexpPattern = `^${this.#compile()}$`;
 | 
			
		||||
    this.regexp = new RegExp(`^${this.regexpPattern}$`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function globCompile(pattern: string, separators: string = ''): GlobCompiler {
 | 
			
		||||
  return new GlobCompiler(pattern, separators);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function globMatch(str: string, pattern: string, separators: string = ''): boolean {
 | 
			
		||||
  try {
 | 
			
		||||
    return globCompile(pattern, separators).regexp.test(str);
 | 
			
		||||
  } catch {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user