@@ -427,6 +427,17 @@ export interface UserEnvVar extends UserEnvVarValue {
427427}
428428
429429export namespace UserEnvVar {
430+ export const DELIMITER = "/" ;
431+ export const WILDCARD_ASTERISK = "*" ;
432+ // This wildcard is only allowed on the last segment, and matches arbitrary segments to the right
433+ export const WILDCARD_DOUBLE_ASTERISK = "**" ;
434+ const WILDCARD_SHARP = "#" ; // TODO(gpl) Where does this come from? Bc we have/had patterns as part of URLs somewhere, maybe...?
435+ const MIN_PATTERN_SEGMENTS = 2 ;
436+
437+ function isWildcard ( c : string ) : boolean {
438+ return c === WILDCARD_ASTERISK || c === WILDCARD_SHARP ;
439+ }
440+
430441 /**
431442 * @param variable
432443 * @returns Either a string containing an error message or undefined.
@@ -452,8 +463,8 @@ export namespace UserEnvVar {
452463 if ( pattern . trim ( ) === "" ) {
453464 return "Scope must not be empty." ;
454465 }
455- const split = pattern . split ( "/" ) ;
456- if ( split . length < 2 ) {
466+ const split = splitRepositoryPattern ( pattern ) ;
467+ if ( split . length < MIN_PATTERN_SEGMENTS ) {
457468 return "A scope must use the form 'organization/repo'." ;
458469 }
459470 for ( const name of split ) {
@@ -466,55 +477,111 @@ export namespace UserEnvVar {
466477 return undefined ;
467478 }
468479
469- // DEPRECATED: Use ProjectEnvVar instead of repositoryPattern - https://github.com/gitpod-com/gitpod/issues/5322
470480 export function normalizeRepoPattern ( pattern : string ) {
471481 return pattern . toLocaleLowerCase ( ) ;
472482 }
473483
474- // DEPRECATED: Use ProjectEnvVar instead of repositoryPattern - https://github.com/gitpod-com/gitpod/issues/5322
475- export function score ( value : UserEnvVarValue ) : number {
476- // We use a score to enforce precedence:
477- // value/value = 0
478- // value/* = 1
479- // */value = 2
480- // */* = 3
481- // #/# = 4 (used for env vars passed through the URL)
482- // the lower the score, the higher the precedence.
483- const [ ownerPattern , repoPattern ] = splitRepositoryPattern ( value . repositoryPattern ) ;
484- let score = 0 ;
485- if ( repoPattern == "*" ) {
486- score += 1 ;
487- }
488- if ( ownerPattern == "*" ) {
489- score += 2 ;
490- }
491- if ( ownerPattern == "#" || repoPattern == "#" ) {
492- score = 4 ;
493- }
494- return score ;
484+ function splitRepositoryPattern ( pattern : string ) : string [ ] {
485+ return pattern . split ( DELIMITER ) ;
495486 }
496487
497- // DEPRECATED: Use ProjectEnvVar instead of repositoryPattern - https://github.com/gitpod-com/gitpod/issues/5322
498- export function filter < T extends UserEnvVarValue > ( vars : T [ ] , owner : string , repo : string ) : T [ ] {
499- let result = vars . filter ( ( e ) => {
500- const [ ownerPattern , repoPattern ] = splitRepositoryPattern ( e . repositoryPattern ) ;
501- if ( ownerPattern !== "*" && ownerPattern !== "#" && ! ! owner && ownerPattern !== owner . toLocaleLowerCase ( ) ) {
488+ function joinRepositoryPattern ( ...parts : string [ ] ) : string {
489+ return parts . join ( DELIMITER ) ;
490+ }
491+
492+ /**
493+ * Matches the given EnvVar pattern against the fully qualified name of the repository:
494+ * - GitHub: "repo/owner"
495+ * - GitLab: "some/nested/repo" (up to MAX_PATTERN_SEGMENTS deep)
496+ * @param pattern
497+ * @param repoFqn
498+ * @returns True if the pattern matches that fully qualified name
499+ */
500+ export function matchEnvVarPattern ( pattern : string , repoFqn : string ) : boolean {
501+ const fqnSegments = splitRepositoryPattern ( repoFqn ) ;
502+ const patternSegments = splitRepositoryPattern ( pattern ) ;
503+ if ( patternSegments . length < MIN_PATTERN_SEGMENTS ) {
504+ // Technically not a problem, but we should not allow this for safety reasons.
505+ // And because we hisotrically never allowed this (GitHub would always require at least "*/*") we are just keeping old constraints.
506+ // Note: Most importantly this guards against arbitrary wildcard matches
507+ return false ;
508+ }
509+
510+ function isWildcardMatch ( patternSegment : string , isLastSegment : boolean ) : boolean {
511+ if ( isWildcard ( patternSegment ) ) {
512+ return true ;
513+ }
514+ return isLastSegment && patternSegment === WILDCARD_DOUBLE_ASTERISK ;
515+ }
516+ let i = 0 ;
517+ for ( ; i < patternSegments . length ; i ++ ) {
518+ if ( i >= fqnSegments . length ) {
502519 return false ;
503520 }
504- if ( repoPattern !== "*" && repoPattern !== "#" && ! ! repo && repoPattern !== repo . toLocaleLowerCase ( ) ) {
521+ const fqnSegment = fqnSegments [ i ] ;
522+ const patternSegment = patternSegments [ i ] ;
523+ const isLastSegment = patternSegments . length === i + 1 ;
524+ if ( ! isWildcardMatch ( patternSegment , isLastSegment ) && patternSegment !== fqnSegment . toLocaleLowerCase ( ) ) {
505525 return false ;
506526 }
507- return true ;
508- } ) ;
509-
527+ }
528+ if ( fqnSegments . length > i ) {
529+ // Special case: "**" as last segment matches arbitrary # of segments to the right
530+ if ( patternSegments [ i - 1 ] === WILDCARD_DOUBLE_ASTERISK ) {
531+ return true ;
532+ }
533+ return false ;
534+ }
535+ return true ;
536+ }
537+
538+ // Matches the following patterns for "some/nested/repo", ordered by highest score:
539+ // - exact: some/nested/repo ()
540+ // - partial:
541+ // - some/nested/*
542+ // - some/*
543+ // - generic:
544+ // - */*/*
545+ // - */*
546+ // Does NOT match:
547+ // - */repo (number of parts does not match)
548+ // cmp. test cases in env-var-service.spec.ts
549+ export function filter < T extends UserEnvVarValue > ( vars : T [ ] , owner : string , repo : string ) : T [ ] {
550+ const matchedEnvVars = vars . filter ( ( e ) =>
551+ matchEnvVarPattern ( e . repositoryPattern , joinRepositoryPattern ( owner , repo ) ) ,
552+ ) ;
510553 const resmap = new Map < string , T [ ] > ( ) ;
511- result . forEach ( ( e ) => {
554+ matchedEnvVars . forEach ( ( e ) => {
512555 const l = resmap . get ( e . name ) || [ ] ;
513556 l . push ( e ) ;
514557 resmap . set ( e . name , l ) ;
515558 } ) ;
516559
517- result = [ ] ;
560+ // In cases of multiple matches per env var: find the best match
561+ // Best match is the most specific pattern, where the left-most segment is most significant
562+ function scoreMatchedEnvVar ( e : T ) : number {
563+ function valueSegment ( segment : string ) : number {
564+ switch ( segment ) {
565+ case WILDCARD_ASTERISK :
566+ return 2 ;
567+ case WILDCARD_SHARP :
568+ return 2 ;
569+ case WILDCARD_DOUBLE_ASTERISK :
570+ return 1 ;
571+ default :
572+ return 3 ;
573+ }
574+ }
575+ const patternSegments = splitRepositoryPattern ( e . repositoryPattern ) ;
576+ let result = 0 ;
577+ for ( const segment of patternSegments ) {
578+ // We can assume the pattern matches from context, so we just need to check whether it's a wildcard or not
579+ const val = valueSegment ( segment ) ;
580+ result = result * 10 + val ;
581+ }
582+ return result ;
583+ }
584+ const result = [ ] ;
518585 for ( const name of resmap . keys ( ) ) {
519586 const candidates = resmap . get ( name ) ;
520587 if ( ! candidates ) {
@@ -527,12 +594,12 @@ export namespace UserEnvVar {
527594 continue ;
528595 }
529596
530- let minscore = 10 ;
531- let bestCandidate : T | undefined ;
532- for ( const e of candidates ) {
533- const score = UserEnvVar . score ( e ) ;
534- if ( ! bestCandidate || score < minscore ) {
535- minscore = score ;
597+ let bestCandidate = candidates [ 0 ] ;
598+ let bestScore = scoreMatchedEnvVar ( bestCandidate ) ;
599+ for ( const e of candidates . slice ( 1 ) ) {
600+ const score = scoreMatchedEnvVar ( e ) ;
601+ if ( score > bestScore ) {
602+ bestScore = score ;
536603 bestCandidate = e ;
537604 }
538605 }
@@ -541,14 +608,6 @@ export namespace UserEnvVar {
541608
542609 return result ;
543610 }
544-
545- // DEPRECATED: Use ProjectEnvVar instead of repositoryPattern - https://github.com/gitpod-com/gitpod/issues/5322
546- export function splitRepositoryPattern ( repositoryPattern : string ) : string [ ] {
547- const patterns = repositoryPattern . split ( "/" ) ;
548- const repoPattern = patterns . slice ( 1 ) . join ( "/" ) ;
549- const ownerPattern = patterns [ 0 ] ;
550- return [ ownerPattern , repoPattern ] ;
551- }
552611}
553612
554613export interface SSHPublicKeyValue {
0 commit comments