package workflowpattern import ( "fmt" "regexp" "strings" ) type WorkflowPattern struct { Pattern string Negative bool Regex *regexp.Regexp } func CompilePattern(rawpattern string) (*WorkflowPattern, error) { negative := false pattern := rawpattern if strings.HasPrefix(rawpattern, "!") { negative = true pattern = rawpattern[1:] } rpattern, err := PatternToRegex(pattern) if err != nil { return nil, err } regex, err := regexp.Compile(rpattern) if err != nil { return nil, err } return &WorkflowPattern{ Pattern: pattern, Negative: negative, Regex: regex, }, nil } //nolint:gocyclo func PatternToRegex(pattern string) (string, error) { var rpattern strings.Builder rpattern.WriteString("^") pos := 0 errors := map[int]string{} for pos < len(pattern) { switch pattern[pos] { case '*': if pos+1 < len(pattern) && pattern[pos+1] == '*' { if pos+2 < len(pattern) && pattern[pos+2] == '/' { rpattern.WriteString("(.+/)?") pos += 3 } else { rpattern.WriteString(".*") pos += 2 } } else { rpattern.WriteString("[^/]*") pos++ } case '+', '?': if pos > 0 { rpattern.WriteByte(pattern[pos]) } else { rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) } pos++ case '[': rpattern.WriteByte(pattern[pos]) pos++ if pos < len(pattern) && pattern[pos] == ']' { errors[pos] = "Unexpected empty brackets '[]'" pos++ break } validChar := func(a, b, test byte) bool { return test >= a && test <= b } startPos := pos for pos < len(pattern) && pattern[pos] != ']' { switch pattern[pos] { case '-': if pos <= startPos || pos+1 >= len(pattern) { errors[pos] = "Invalid range" pos++ break } validRange := func(a, b byte) bool { return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1] } if !validRange('A', 'z') && !validRange('0', '9') { errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9" pos++ break } rpattern.WriteString(pattern[pos : pos+2]) pos += 2 default: if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) { errors[pos] = "Ranges can only include a-z, A-Z and 0-9" pos++ break } rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) pos++ } } if pos >= len(pattern) || pattern[pos] != ']' { errors[pos] = "Missing closing bracket ']' after '['" pos++ } rpattern.WriteString("]") pos++ case '\\': if pos+1 >= len(pattern) { errors[pos] = "Missing symbol after \\" pos++ break } rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]}))) pos += 2 default: rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) pos++ } } if len(errors) > 0 { var errorMessage strings.Builder for position, err := range errors { if errorMessage.Len() > 0 { errorMessage.WriteString(", ") } errorMessage.WriteString(fmt.Sprintf("Position: %d Error: %s", position, err)) } return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String()) } rpattern.WriteString("$") return rpattern.String(), nil } func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) { ret := []*WorkflowPattern{} for _, pattern := range patterns { cp, err := CompilePattern(pattern) if err != nil { return nil, err } ret = append(ret, cp) } return ret, nil } // returns true if the workflow should be skipped paths/branches func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { if len(sequence) == 0 { return false } for _, file := range input { matched := false for _, item := range sequence { if item.Regex.MatchString(file) { pattern := item.Pattern if item.Negative { matched = false traceWriter.Info("%s excluded by pattern %s", file, pattern) } else { matched = true traceWriter.Info("%s included by pattern %s", file, pattern) } } } if matched { return false } } return true } // returns true if the workflow should be skipped paths-ignore/branches-ignore func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { if len(sequence) == 0 { return false } for _, file := range input { matched := false for _, item := range sequence { if item.Regex.MatchString(file) == !item.Negative { pattern := item.Pattern traceWriter.Info("%s ignored by pattern %s", file, pattern) matched = true break } } if !matched { return false } } return true }